How it is built
Architecture overview
A warehouse management system (WMS) turns customer orders into physical work: which stock to take from which bin, which picker or robot does it, in what route, how it is packed and handed to a carrier, and how inbound goods are received, put away and counted. It also owns the inventory record, the system's belief about how many units of each item are at each location and in what state (available, reserved, picked, damaged). Retailers, wholesalers, pharmaceutical distributors, spare-parts businesses and fulfilment centres run the same core loop at very different sizes.
The idea that makes it work is that the inventory record is a ledger of events, and every physical step is a task with a small state machine. Stock never changes by "set quantity to 7"; it changes by a movement (received, reserved, picked, moved, adjusted) that is appended once, with an idempotency key, and the current balance is a fold of the movements. Tasks (pick this line, pack that order) move through states that scanners and robots advance, and planners sit on top: they group orders into waves, assign them to workers, and choose routes, but they never mutate stock directly. This split makes the system recoverable (replay the ledger), auditable (every unit has a history) and parallelisable (planners are pure functions of the current state).
Parts:
- Inventory ledger — movements, balances per location and lot, reservations —
inventory-reservation(no page yet),conditional-writes(no page yet), Event sourcing, Idempotency keys, Optimistic concurrency control,fifo-lot-consumption(no page yet), Hierarchical timing wheel - Order and task lifecycle — states for orders, picks, parcels; recovery after faults —
order-lifecycle-state-machine(no page yet), Hierarchical state machine, FIX protocol - Event backbone — carries movements and state changes between services — Partitioned commit log, Segmented log, Consumer offsets, Primary-backup replication, Write-ahead log, Transactional outbox, Change data capture, Incremental sync cursors, Event deduplication, Zero-copy I/O, Fast hash functions
- Wave and slotting planner — groups orders, assigns stock to bins, balances load — Mixed-integer programming, Cutting planes, Presolve reductions, Linear programming (simplex), Bin packing, K-means, D-ary heap
- Pick routing — orders picks to cut walking distance —
tsp-heuristics(no page yet),local-search-2opt-or-opt(no page yet),minimum-weight-perfect-matching(no page yet), Minimum spanning tree, K-d tree, Union-find, Dijkstra, BFS and DFS - Robot traffic — collision-free paths for mobile robots in aisles —
multi-agent-path-finding(no page yet), A*, Interval tree - Demand and replenishment — forecasts what will be ordered and when to reorder — Time-series forecasting, Exponential smoothing, STL decomposition,
arima(no page yet),croston-method(no page yet), Time-series cross-validation, Kalman filter, L-BFGS, Finite differences, Robust statistics, Evaluation metrics, Loss functions,bootstrap-confidence-intervals(no page yet) - Durability and fault tolerance — keeps the record safe and the service up — CRC32C checksums, fsync and direct I/O, Leases, Fencing tokens, Health checking, Exponential backoff with jitter, Cryptographic hashes (SHA-2, BLAKE3), Slab allocator
ERP / e-shop --orders--> order intake (idempotent) --> order state machine
| allocate
v
receiving --movement--> +-----------------------+ reserve +----------------+
scanners --movement--> | inventory ledger |<---------->| planner: |
robots --movement--> | (events + balances) | | waves, slotting|
+-----------+-----------+ +-------+--------+
| outbox -> commit log | pick tasks
v v
+-----------------------+ +----------------+
| consumers: pick UI, |<-----------| route + robot |
| carrier, billing, BI | tasks | planner |
+-----------------------+ +----------------+
How the parts fit together
Inventory ledger
Two people scanning the last unit of an item must not both get it. The ledger gives each (item, location, lot, state) a balance, and every change is a movement with an Idempotency keys value (the scan id, or order line id plus step), so a scanner that retries after a dropped Wi-Fi packet produces exactly one movement. Event sourcing stores the movements as the source of truth and derives balances from them, which is what lets an auditor ask why a pallet disappeared.
Reserving stock for an order is inventory-reservation (no page yet): a hold with a time limit that is released on timeout, which prevents unpaid or abandoned orders from locking stock forever; expiry is cheap on a Hierarchical timing wheel. The reservation must be an atomic "if available >= n then decrement", so it is written as a conditional operation, conditional-writes (no page yet), or as an Optimistic concurrency control retry loop on the balance row (read version, check, write if version unchanged). OCC wins when conflicts are rare (a popular SKU is the exception); for the hot SKUs the ledger partitions available quantity into slices per zone or per channel so that writers do not collide. Lot-controlled goods (food, medicine) consume stock by fifo-lot-consumption (no page yet): first-expiry-first-out, so the oldest expiry is picked first and recalls can find exactly which orders received a lot.
The contract with everything else: the ledger exposes append movement and reserve/release operations that are idempotent, and publishes every committed movement exactly once to the event backbone.
Order and task lifecycle
Orders and pick tasks are state machines: new, allocated, released, picking, picked, packed, shipped, with side branches for short picks, cancels and returns. The model is order-lifecycle-state-machine (no page yet); a Hierarchical state machine keeps it manageable because "picking" has substates (assigned, at location, confirmed) that share parent reactions, such as "order cancelled" valid anywhere in the parent. Every transition is triggered by an event and is idempotent, so replaying an event does nothing. The graph lists the FIX protocol because its sequence-numbered, resend-on-gap session model is the same discipline that carrier and customer EDI interfaces need; a warehouse speaks an EDI or API dialect instead, with the same needs: per-direction sequence numbers, acknowledgements and replay.
Event backbone
The ledger, order service, pick UI, carrier integration and analytics must agree without sharing one database. A Partitioned commit log carries events: keyed by order or by item so per-key order holds, stored in a Segmented log with Zero-copy I/O sends, replicated with Primary-backup replication on top of a Write-ahead log, and consumed with Consumer offsets. The graph lists a durable job queue as the alternative to a log; it fits "do this one task once" work, but a log lets a new consumer (a new analytics store) replay history, which warehouses need often.
The service that commits a movement must also publish the event, and doing both in separate steps loses events on a crash. The Transactional outbox writes the event row in the same database transaction as the balance change; Change data capture reads that table from the database log and publishes in order, with Incremental sync cursors for a consumer that needs a snapshot first. The graph names two-phase commit as the alternative; it blocks on a failed coordinator, which a warehouse running 24 hours cannot tolerate. Because delivery is at-least-once, consumers use Event deduplication on event ids; Fast hash functions key partitions and dedup tables.
Planner: waves, slotting, bin packing
The planner has three jobs. Slotting decides where each SKU lives: fast movers near the dock, items often ordered together close to each other; K-means on co-occurrence or location vectors proposes clusters. Wave planning decides which orders to release now, so each wave fits worker capacity, carrier cut-off times and dock doors; at small scale a heap of orders by due time (D-ary heap) is enough, at larger scale it is an assignment model for Mixed-integer programming, where Linear programming (simplex) relaxations, Cutting planes and Presolve reductions do the work. Cartonisation decides which items go into which box: Bin packing (first-fit decreasing is the fast heuristic, with an exact check when a box is nearly full) minimises cartons and carrier charges. The contract with the ledger: the planner reads a snapshot of balances and writes reservations, so its plan is only valid as long as reservations commit; a rejected reservation triggers a re-plan for those lines.
Pick routing
A picker walks a list of locations; order matters because walking is most of the labour (the share varies by site). The route is a travelling-salesman problem on the aisle graph. Distances between locations come from Dijkstra (or precomputed all-pairs for a fixed layout) and BFS and DFS for reachability. tsp-heuristics (no page yet) give a first tour (nearest neighbour, or a tree-based tour from a Minimum spanning tree with minimum-weight-perfect-matching (no page yet) in Christofides style), and local-search-2opt-or-opt (no page yet) improves it by swapping segments, which in a warehouse with 10 to 50 picks per tour runs in well under a second (order of). Many layouts have simple structure (parallel aisles), where a dynamic-programming route beats the generic heuristic. A K-d tree finds the nearest candidate pick to the worker's position when locations are coordinates, and Union-find groups locations into zones or merges aisles into connected components.
Robot traffic
Where mobile robots carry shelves or totes, many robots share aisles and intersections. multi-agent-path-finding (no page yet), for example conflict-based search, finds collision-free paths for all robots; the single-robot search inside it is A*, and reservations of (cell, time interval) pairs are kept in an Interval tree so a new path can ask "is this cell free in this window?". The contract with the task layer: robots receive a task and return a progress event; the traffic planner never changes inventory.
Demand and replenishment
Reorder points depend on forecasts of demand per SKU. Time-series forecasting applies; Exponential smoothing and STL decomposition cover regular items, arima (no page yet) autocorrelated ones, and croston-method (no page yet) the many slow movers with mostly zero days. Time-series cross-validation validates by rolling origin, scored with Evaluation metrics and a suitable Loss functions. Robust statistics keeps one big order from setting the reorder point, and bootstrap-confidence-intervals (no page yet) give a range on safety stock. A Kalman filter can fuse noisy counts (RFID reads, weight sensors) into a stock estimate with a variance. L-BFGS fits the models. The graph also lists Finite differences, Explicit Euler and thomas-tridiagonal-solver (no page yet) for simulating flows such as conveyor throughput; they are off the core path.
Durability
The ledger is the asset. Segment writes are checksummed with CRC32C checksums and committed with the rules from fsync and direct I/O. Failover uses Leases for primary ownership and Fencing tokens so that an old primary that wakes late cannot write; Health checking feeds the decision. Clients retry with Exponential backoff with jitter. Content hashes (Cryptographic hashes (SHA-2, BLAKE3)) fingerprint label and document payloads, and a Slab allocator serves fixed-size in-memory records for the hot balance cache.
Data and control flow
One order, from intake to shipment.
- The e-shop sends the order with its id; intake stores it under that id (a repeat returns the stored result).
- The state machine moves it to "allocating"; the planner asks the ledger to reserve each line, preferring stock by lot expiry and location. Short stock produces a backorder or split.
- Reservations commit; outbox events flow to the log; the pick service creates tasks.
- A wave is released; the router orders the picks; the picker's device shows the first location.
- Each scan confirms a pick: the ledger appends a "picked" movement (idempotent), the reservation turns into a move to the pack station.
- Packing runs cartonisation, prints the label, records the parcel; the carrier integration sends the shipment event.
- A "shipped" movement leaves the stock; the order closes. Time is spent in step 4 (human walking) and step 2 (conditional writes on hot items).
One inbound receipt.
- A supplier notice creates an expected receipt; at the dock the clerk scans items and lots.
- Each scan appends a "received" movement into a staging location with the lot and expiry.
- Putaway rules choose a bin by slotting and space (bin packing for pallets); the move is a pair of movements (staging out, bin in), committed together.
- Discrepancies against the notice raise a task, not a silent adjustment.
One cycle count.
- The system picks locations to count (high value, high movement, long unchecked).
- The counter submits the count; if it differs from the balance, an "adjustment" movement with a reason is appended after review.
- Adjustments feed back into the slotting and forecasting data.
Scaling
- Hot SKUs. The first limit is write contention on a few popular balances. Slice available quantity per zone or channel and rebalance in the background.
- Event volume. Scans and robot events grow with throughput. Add partitions (keyed so one order stays in order), batch movements per scan session, and keep consumers separate from the ledger.
- Planner cost. MIP and routing grow quickly with size. Plan by zone, use heuristics first, limit time per wave, and keep exact solvers for slotting that runs overnight.
- Robot count. Path conflicts grow faster than robot count. Plan in short horizons, split the floor into regions with handoff, and fall back to simple priority rules.
- Read load. Dashboards and carrier queries read from replicas built from the log, not from the ledger.
- Multi-site. One ledger per site; a global view built from each site's log. Transfers between sites are a pair of movements joined by an in-transit state.
Failure modes
| Failure | Symptom | Cause | Fix |
|---|---|---|---|
| Phantom stock | System says 3, shelf has 0; pickers walk to empty bins | Missed scans, damage not recorded, misplaced putaway | Cycle counts; short-pick events that trigger a count; reconcile movements |
| Double pick or double ship | Two parcels for one line | Retried scan without idempotency; two tasks for one line | Idempotency keys; one task per line; state machine rejects duplicates |
| Oversold stock | Orders accepted that cannot be fulfilled | Read-then-write reservation without atomicity | Conditional reservation; slicing for hot SKUs |
| Reservation leak | Stock shows reserved but nothing moves | Hold without expiry on abandoned or failed orders | Timeouts; periodic sweeper; alert on old reservations |
| Lost event | Carrier never told about a shipped parcel | Commit and publish as separate steps | Transactional outbox; reconciliation job |
| Duplicate event effect | Stock decremented twice downstream | At-least-once delivery, consumer not idempotent | Event ids and deduplication window |
| Split brain | Two primaries accept writes after a failover | Lease expired, old primary kept running | Fencing tokens checked by the storage layer |
| Robot deadlock | Robots stop in a cycle | Mutual blocking at an intersection | Reservations with time windows; deadlock detection and a re-plan |
| Wrong expiry shipped | Recall hits stock already sent | Lot not tracked per movement | Lot on every movement; FEFO consumption; lot-to-order trace |
| Slow wave planning | Pickers idle waiting for the next wave | Solver runs to optimality on large instances | Time limit with heuristic fallback; plan next wave while the current runs |
Real implementations
Large operators (the Examples line names Amazon fulfilment) run in-house systems that are not public. The open-source items below cover the inventory and order model, and the event and solver building blocks.
| name | what it is | repo | licence | notes |
|---|---|---|---|---|
| OpenBoxes | Open-source warehouse and inventory management for supply chains | openboxes/openboxes | EPL-1.0 | Ledger-style stock movements, lots and expiry |
| Odoo (Community) | ERP with an Inventory app: warehouses, locations, operation types, cycle counts, reordering rules | odoo/odoo | LGPL-3.0 | docs; Enterprise edition is separate |
| Apache OFBiz | Open-source ERP system in Java | apache/ofbiz-framework | Apache-2.0 | Broad business suite |
| Apache Kafka | Distributed partitioned commit log | apache/kafka | Apache-2.0 | Event backbone |
| Debezium | Change data capture from database logs | debezium/debezium | Apache-2.0 | Outbox relay |
| OR-Tools | Optimisation suite: MIP, CP-SAT, routing and bin packing | google/or-tools | Apache-2.0 | Planner and routing; routing docs |
| HiGHS | LP and MIP solver | ERGO-Code/HiGHS | MIT | Planner |
| StatsForecast | Statistical forecasting: automatic ARIMA, ETS, Theta | Nixtla/statsforecast | Apache-2.0 | Replenishment forecasts |
Sources
- Transactional outbox pattern: https://microservices.io/patterns/data/transactional-outbox.html
- Apache Kafka documentation: https://kafka.apache.org/documentation/
- Debezium documentation: https://debezium.io/documentation/
- OR-Tools routing: https://developers.google.com/optimization/routing
- Odoo Inventory documentation: https://www.odoo.com/documentation/latest/applications/inventory_and_mrp/inventory.html
- Hyndman, Athanasopoulos, Forecasting: Principles and Practice, 3rd edition: https://otexts.com/fpp3/
- Croston, J. D., "Forecasting and Stock Control for Intermittent Demands", Operational Research Quarterly, 1972: https://doi.org/10.2307/3007885
- OpenBoxes: https://github.com/openboxes/openboxes