All case studies

Client work · Ever.Ag, through Deegloo

Faster reads for the office. Safer workflows in the field.

Ever.Ag runs a supply-chain platform for the North American dairy industry. Between February and September 2026 I owned two pieces of it: the API read path behind client statements and operational apps, and the mobile workflow field staff use to record custody.

  • Full Stack Engineer at Deegloo
  • Feb – Sep 2026
  • Dairy supply chain
faster API responses in production, from logs and APM traces
~70%
endpoints moved to the new read path, no consumer broken
9
custody workflows folded into one offline-first subsystem
51

Who used what I built.

Ever.Ag’s platform sits between dairy producers, haulers, plants and the operations teams that coordinate them. The APIs in this story produced the weekly and monthly statements clients receive, and fed four operational applications used by producers, farm owners, field staff and dairy operations teams.

I worked as an engineer at Deegloo, embedded with the client’s product and engineering teams. On the backend that meant Java and Spring services, PostgreSQL data design, integration work and production monitoring. On the frontend it meant the React and Ionic field app. I owned both initiatives below from diagnosis through rollout.

Why the old read path got slow.

The statement and operations endpoints all read from the live transactional schema. Each request loaded a deeply nested aggregate, paginated it in application memory and recomputed derived values for every record. At realistic data volumes it was slow, and risky enough that the service carried an explicit out-of-memory guard. As more applications started reading the same data, the problem compounded.

Rather than tune the existing queries, I moved reads onto a purpose-built read model. The response payload is stored in PostgreSQL as JSONB, next to indexed scalar columns and small filter tables for the fields people search and sort on. The transactional schema stays the source of truth: Debezium captures its changes and a RabbitMQ pipeline refreshes the affected rows. Each stored payload carries a version, so the shape can evolve and an administrator can trigger a controlled rebuild instead of a migration.

The read model costs storage, and the refresh pipeline is one more thing to operate. In exchange the hot path became bounded: a request runs three SQL statements regardless of how deep the underlying aggregate is, and a regression test asserts that count so the shape cannot quietly grow back.

The read path, before and after
  1. 01Request
  2. 02Load transactional graph
  3. 03Assemble and page in memory
  4. 04Response
  1. 01Change event
  2. 02Refresh versioned read model
  3. 03Bounded read
  4. 04Response

Moving nine endpoints without breaking their consumers.

The endpoints had public consumers I could not schedule a cut-over with. I introduced the new response behind media-type versioning on the existing URLs: a client asking for the old representation kept getting it, and one asking for the new representation got the read model. Nine public and internal endpoints moved over this way, one at a time.

Each request could also be routed back to the old path by a feature flag, without a deployment. That made the rollout reversible per endpoint and let me compare both paths on real traffic.

The measurement came from production. I compared request logs and APM traces before and after the rollout across the response-time distribution, not a single average. Responses were approximately 70% faster.

Five copies of one workflow, each failing differently.

Field staff use the Ionic app to capture custody records, often with poor or no connectivity, and often with the operating system killing the app between steps. Five workflows needed that capability, and each had grown its own copy of the state and persistence logic.

The copies drifted. An edit could be saved as a new record, a deleted record could reappear after sync, and completed work could vanish when the app was terminated mid-flow. Every fix landed in one copy and left the other four unchanged.

I replaced the copies with a single subsystem the workflows share. Its draft model has two tiers: confirmed values that mirror what the server knows, and pending changes layered on top. Every record gets a stable local identity the moment it is created, so an edit stays an edit even before the server has assigned an id. Drafts persist per workflow in IndexedDB, which is what survives an app termination.

Recovery needed two sets of rules. Online, the app reconciles against server state and the server wins. Offline, the local draft wins, because it may be the only copy of work a person has already done in the field. Both rules live in the shared layer now, so no workflow can get them subtly wrong on its own.

The mobile repository had no unit-test harness, so I could not lean on tests here the way I could on the backend. Correctness came from diagnostics in the persisted state, guards at the boundaries, and end-to-end validation on devices.

The condition to design for is someone losing signal, finishing the job anyway, and having the app killed mid-step. The work has to be there when they open it again.

What the choices cost.

  • Denormalisation bought predictable reads at the price of a second copy of the data and a refresh pipeline that has to be kept honest.
  • Media-type versioning kept the URLs stable, but two representations live side by side until the last consumer migrates.
  • Two recovery rules are more to explain than one. They are documented in the shared layer, where the next engineer will actually read them.

React 18 · TypeScript · Ionic · Capacitor · Redux · IndexedDB · Java 21 · Spring Boot 3 · PostgreSQL JSONB · Debezium · RabbitMQ · Micrometer · Dynatrace

Need someone who can own the interface and the system behind it?