Case notes

Case note: retiring the 2 a.m. stock sync

TL;DRStop copying a warehouse snapshot into commerce overnight. Publish each stock movement as an event, keep a per-depot availability projection updated in seconds, and compute promise dates against live allocation. Overselling drops and promised dates start to hold. Run the event stream in shadow beside the batch first, then cut over depot by depot.

We replaced a nightly batch stock sync with event-driven availability, and most of the overselling went with it. Instead of copying a warehouse snapshot into the commerce layer at 2 a.m., each stock movement is now published as an event, a per-depot availability projection updates within seconds, and order promise dates are computed against live allocation rather than yesterday's guess. Here is how we did it, and where it bit us.

Why is a nightly stock sync wrong by lunchtime?

The classic route-to-market shape is warehouse management system to middleware to commerce, reconciled once a night. By the time reps and B2B customers place their orders the next morning, the number on screen is already decaying: goods have shipped, returns have landed, breakages have been logged, and one depot has quietly borrowed pallets from its neighbour. The snapshot was accurate at 2 a.m. and fiction by noon.

This is not a niche failure. Independent research on fast-moving consumer goods puts worldwide out-of-stock rates at around 8.3% on average, and a widely cited study of nearly 370,000 records found about 65% of inventory records were inaccurate. Analysts still price the combined cost of overstocks and out-of-stocks at roughly 6.5% of global retail sales — about 1.73 trillion dollars a year, as of September 2025. A brewer's depot network sits squarely inside those numbers.

The operational tell is the apology email. When availability is stale, someone oversells, and the remedy is a human typing 'sorry, that line is short, we will part-ship'. Multiply that across a few hundred routes and the batch sync stops being a data problem and becomes a trust problem with customers.

What does event-driven availability actually mean?

Three moving parts. First, every stock movement — receipt, pick, dispatch, return, adjustment, breakage — is published to the event bus the moment it happens, not aggregated overnight. Second, a per-depot availability projection consumes those events and keeps a running figure of what is genuinely allocatable, refreshed in seconds. Third, when an order is captured, the promise date is computed against that live allocation, so the platform commits to a date it can actually keep.

This is a plain application of event sourcing and read models, as documented by Martin Fowler: the events are the source of truth, and the availability figure is a projection you can rebuild from scratch whenever you doubt it. That disposability matters more than it sounds — see the failure below. Because the promise date is finally trustworthy, order capture can commit to it at the point of sale, which is part of the same effort to close the loop between the rep visit and the next order.

Crucially, the SAP financial core stays where it is. We are not moving inventory valuation or the ledger; we build an operational availability view beside it and feed order capture. If you want the shape of that split, it is in our platform architecture.

How do you retire a nightly stock sync without a big-bang cutover?

Slowly, and with the old job still running as your safety net. The sequence we use:

  1. Instrument the movements first. Publish stock events from the WMS in parallel with the existing batch. You get a live stream and the old snapshot side by side, which lets you measure disagreement without taking any risk.
  2. Build the projection as a read model. Consume the events into a per-depot availability table. Keep it idempotent and replayable, and store the event offset so you can rebuild it from any point.
  3. Run both in shadow for a few weeks. Compare the projection against the 2 a.m. snapshot every morning. Where they disagree, the projection is usually right and the batch is stale — but you need that evidence on paper before anyone trusts it.
  4. Reserve promise dates against live allocation. Move order capture to allocate against the projection at the moment of order, not against last night's number.
  5. Cut over one depot, then fan out. Switch a single low-risk depot to the live view, watch the oversell and promise-date numbers, then expand. Kill the batch job only once every depot is off it.
  6. Keep a daily reconciliation against the financial core. Check operational availability against the ledger stock every day so finance is never surprised at period-end.

The same movement events do double duty elsewhere: they feed returnables, which is how we ended the monthly empties-balance argument. And because a live projection is something a human now owns at 3 a.m., we put it behind one on-call rota for the whole stack rather than a batch team who clocked off long before the errors surfaced.

What did we see, and what went wrong?

Framed as our own monitoring rather than any client's claim, the direction was unambiguous. Across the instrumented depots in the first pilot, over roughly two months, this is what we watched:

Our internal observationNightly batchEvent-driven
Availability freshnessUp to ~24h staleSeconds
Oversell incidents per week, instrumented depotsLow twentiesLow single digits
Orders meeting their promised dateNot reliably measurableAbove 95%

Now the honest part: our first projection quietly went wrong. One depot's WMS re-sent a block of adjustment events after a maintenance window — out of order, with duplicates — and because our early consumer was not fully idempotent it double-counted returns and briefly showed negative availability on two SKUs. Nobody oversold, because the error leaned conservative, but it cost us a fortnight of trust and we had to rebuild the projection from the event log to prove the real number. The lesson was blunt: treat every consumer as if the bus will one day replay and reorder its events, because eventually it will. The one thing that saved us was the disposability of the read model — we replayed it from offset zero overnight and moved on.

The next thing we want to retire is the reconciliation job itself. Once the operational availability view has earned enough trust, the daily check against the financial core can become a subscription rather than a batch: the ledger listens to the same movement events, and 'sync' stops being a noun in the architecture at all. We are not there yet — but the 2 a.m. job is gone, and nobody has asked to bring it back.

Frequently asked questions

Will this work if our inventory valuation lives in SAP?

Yes. The financial core stays in place. We build an operational availability view beside it from stock-movement events and feed order capture; the ledger is untouched. A daily reconciliation keeps finance and operations agreed, so period-end holds no surprises.

How is overselling actually prevented?

Order capture reserves against a live per-depot availability projection at the moment of order, not against last night's snapshot. Because the projection updates within seconds of each movement, the promise date the system commits to is one the depot can genuinely keep.

Do we have to switch every depot at once?

No, and you shouldn't. Publish events in parallel with the batch, run the projection in shadow for a few weeks, then cut over one low-risk depot. Expand as the oversell and promise-date numbers hold, and retire the nightly job only when every depot is off it.

Stuck with exactly this?

BrewOS builds the full route-to-market stack for global brewers — order capture, stock, loyalty, returnables, delivery and analytics on one event bus, run by a team of ~20 engineers. Bring us the feature that’s been stuck the longest and we’ll show you how we’d ship it in days.

Book a 30-minute walkthrough