LLD-019 · DESIGN PRACTICE
Inventory and Order Fulfillment
Separate on-hand, reserved and shipped quantities; make allocation, expiry and partial fulfillment preserve ownership.
Local Java 21 reference verified: 29 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design a local multi-warehouse inventory service that reserves every order line atomically, supports partial shipments, expires or releases only unshipped allocations and audits stock adjustments. Explain the exact transaction boundary before discussing distributed warehouses or carrier integration.
Before you start
- Immutable quantity snapshots and integer arithmetic
- Atomic compound operations and ownership
- Idempotency fingerprints and optimistic versions
- Expiry clocks and fulfillment state machines
Scope and explicit decisions
- One in-memory inventory owner serializes commands. Warehouse/SKU keys and opening quantities are fixed; stock adjustments change existing keys. No database, real warehouse devices, carrier calls, returns, backorders, payment integration or cross-process transaction is implemented.
- Quantities are nonnegative long integers; requested quantities and TTL are positive. There are no fractional units, lots, serial numbers or unit conversions. checked arithmetic rejects quantity/deadline overflow where applicable. Version/identity counters are finite; exhaustion requires retiring the local instance rather than wrapping counters.
- reserve receives order identity, SKU demand, TTL and operation key. It plans every line before changing allocations, using warehouse-name order for deterministic selection. This policy is reproducible, not optimal for distance, shipping cost, fairness or number of packages.
- onHand is physical stock still at a warehouse; reserved is the active unshipped portion; available=onHand-reserved. Reservation.original stays historical, remaining tracks still-held units and shipped tracks locally accepted shipments. Shipped units leave both onHand and reserved together.
- Time is a trusted nondecreasing caller input. Time-bearing commands/read stock advance authoritative expiry. At now>=expiresAt an ACTIVE reservation expires and releases remaining quantities before a fresh shipment can proceed. get returns the last materialized snapshot and does not advance time.
- reserve and ship have typed global operation-key fingerprints. Matching retries resolve the same reservation and return its current view, including later shipment/expiry changes; they do not allocate or ship again. Different inputs or operation kinds under the same key reject.
- A rejected allocation does not partially reserve order lines. Advancing time may still expire older reservations before a new command fails; failure is not a promise that unrelated expiry facts remain frozen.
- Partial shipment consumes only requested keys/quantities belonging to that reservation. Release/expiry clears remaining allocations only. Already shipped units are never restored by cancellation; returns require a separate inspected receipt and adjustment contract.
- adjust requires the current stock version, a nonblank reason and a result onHand>=reserved. Allocation, release, shipment and adjustment advance stock versions. Audit records cover adjustments; this is not a complete durable movement journal. Histories and operation keys are retained without compaction.
Worked examples
Input: w1:A=2,w2:A=3,w1:B=1. Reserve A=4,B=1.
Outcome: Allocation takes w1:A=2,w2:A=2,w1:B=1; physical onHand stays unchanged and reserved rises.
Greedy warehouse order is deterministic. All three allocation entries commit together.
Input: Reserve five units, ship two, then release the remaining reservation.
Outcome: onHand fell by two; reserved returns to zero. Only the unshipped three become available again.
Cancellation releases a promise; it is not a return receipt for already shipped goods.
Your design attempt
Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.
Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.
Run program · Java 21
No unsaved changes · 208/20000
Self-review
Checks apply to this visit. Self-review is not a passed submission or verified mastery.
Try before revealing
Hint 1
Where is each promised unit owned?
Hint 2
Which quantities may cancellation safely release after partial shipping?
Hint 3
What happens at exactly expiresAt, including a retry of an older successful shipment?
Requirements and invariants
R1 — Reserve all requested SKU quantities or reserve none of the new order. reserve protects I1.
R2 — A shipment cannot exceed its reservation remaining quantities or consume twice under a retry key. ship protects I2.
R3 — Release and expiry restore availability only for unshipped units. release protects I3.
R4 — Stock adjustments reject stale intent and cannot invalidate active reservations. adjust protects I1.
I1: For every warehouse/SKU: 0<=reserved<=onHand, and reserved equals the sum of remaining quantities on ACTIVE reservations.
I2: For each allocation while ACTIVE, original=remaining+shipped; a successful shipment reduces onHand and reserved equally and advances shipped exactly once per operation identity.
I3: COMPLETED, RELEASED and EXPIRED reservations hold no remaining units. Release/expiry never adds already shipped units back to onHand.
Derive the design
A SKU counter cannot explain ownership
A single SKU→quantity map can prevent some oversells but cannot answer which warehouse owns a promise, which order may release it, or whether cancelled units already shipped. Add warehouse/SKU stock and a reservation allocation map, then derive available stock rather than decrementing physical stock on reservation. Plan every order line before committing, because compensating a partially applied local reservation is needlessly harder than validating it first.
Alternative: A database implementation can lock selected stock rows in a stable order or use version-conditional updates inside one transaction. That preserves local allocation semantics; independent warehouse services require a saga/partial-reservation contract instead of claiming one monitor spans processes.
Define units and warehouse ownership
Clarify integer quantities and whether onHand includes reservations. Write available=onHand-reserved and tie every reserved unit to an ACTIVE reservation.
Choose the allocation transaction boundary
Resolve retry identity and expiry, prepare every SKU allocation, then commit holds once. This avoids exposing a partially accepted local order.
Model partial progress explicitly
Original is historical; remaining is still held; shipped is accepted outbound progress. Terminal release clears remaining and does not pretend that original=remaining+shipped after released units have left the reservation.
Separate physical and local evidence
ship records a trusted local shipment fact. Real pick/pack/carrier execution requires external command identity, unknown outcomes, outbox/inbox and reconciliation; a local counter is not proof that a truck departed.
Responsibilities and ownership
Inventory
Owns: Stock, active allocation accounting, operation identities, clock and adjustment history
Methods: reserve, ship, release, adjust, stock, get, invariantHolds
Local transaction owner; no physical shipment or database I/O.
Key / Stock
Owns: Warehouse/SKU identity and quantity/version snapshot
Methods: Validated records and snapshot accessors
SKU totals do not replace warehouse ownership.
Reservation
Owns: Historical original, remaining, shipped, expiry, version and state
Methods: Immutable copied-map accessors
Snapshot is evidence, not a mutable allocation handle.
ReserveRequest / ShipRequest
Owns: Typed command fingerprint and correlated reservation identity
Methods: Private operation lookup
An order ID alone does not make every later shipment the same operation.
Warehouse-first order is a policy, not a shipping-cost optimization.
- Demand + operation identity
↓ Trusted monotonic time
Expire / plan every SKU↓ No partial new allocation
Commit all reservation holds↓ Physical stock unchanged
Immutable current reservation
A partial shipment can leave ACTIVE remaining units; terminal reservations hold none.
Retain shipped history
Never restore shipped units
Data model, schema and relationships
SKU describes the item; warehouse_stock owns quantity at one warehouse. An order reservation splits into warehouse allocations, and shipments consume only selected allocated quantities.
Implementation boundary: Local keys/allocation maps implement conservation and current-view retries. Order/SKU/catalog/line tables explain durable fulfillment modeling; real picking, returns and carrier facts remain adapters.
These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Entity definitions and field contracts
sku
Sellable integer-unit item.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | SKU identity |
name | text · VALUE | Catalog label |
Code mapping: Demand SKU keys.
- Unit conversion, lots and serial tracking require additional scope.
warehouse
Physical inventory location.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Warehouse identity |
name | text · VALUE | Display label |
Code mapping: Key.warehouse.
- Inventory authority must be explicit per location.
warehouse_stock
Physical quantity and active promises.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Stock-row identity |
warehouse_id | text · FK | Location · warehouse.id |
sku_id | text · FK | Item · sku.id |
on_hand | bigint · VALUE | Still physically present |
reserved | bigint · VALUE | Active remaining allocation sum |
version | bigint · VALUE | Adjustment guard |
Code mapping: Stock/held/version maps.
- Unique(warehouse_id,sku_id); 0<=reserved<=on_hand.
fulfillment_order
Customer demand owner.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Order identity |
status | enum · VALUE | Business lifecycle |
Code mapping: Order identity input.
- An order may need several reservation attempts; command identity must distinguish them.
inventory_reservation
All-or-nothing promise lifecycle.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Reservation identity |
order_id | text · FK | Demand owner · fulfillment_order.id |
expires_at | timestamp · VALUE | Promise deadline |
state | enum · VALUE | ACTIVE COMPLETED RELEASED EXPIRED |
Code mapping: Reservation.
- Terminal reservation holds no remaining units.
allocation
Warehouse-specific reserved line.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Allocation identity |
reservation_id | uuid · FK | Promise owner · inventory_reservation.id |
stock_id | uuid · FK | Physical source · warehouse_stock.id |
original | bigint · VALUE | Historical allocation |
remaining | bigint · VALUE | Still held |
shipped | bigint · VALUE | Accepted outbound quantity |
Code mapping: Reservation maps.
- Unique(reservation_id,stock_id); ACTIVE original=remaining+shipped. Terminal release needs released/expired accounting if full conservation is journaled.
shipment_line
Deduplicated outbound evidence.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Shipment-line identity |
allocation_id | uuid · FK | Consumed promise · allocation.id |
shipment_key | text · VALUE | Logical shipment command |
quantity | bigint · VALUE | Accepted shipped units |
request_fingerprint | json · VALUE | Full reservation/quantities command meaning must agree for all lines under one key |
Code mapping: Ship operation history; full shipment journal extension.
- Unique(shipment_key,allocation_id) with full typed command fingerprint; quantity cannot exceed owned remaining.
stock_adjustment
Audited physical correction.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Adjustment identity |
stock_id | uuid · FK | Corrected source · warehouse_stock.id |
delta | bigint · VALUE | Signed exact correction |
reason | text · VALUE | Audit reason |
result_version | bigint · VALUE | Accepted stock version |
Code mapping: Adjustment audit list.
- Cannot reduce on_hand below reserved; expected version protects stale intent.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| warehouse → warehouse_stock holds stock | 1 parent per child; 0..* children per parent | warehouse_stock.warehouse_id → warehouse.idOne location holds quantities for many SKUs. |
| sku → warehouse_stock is stocked | 1 parent per child; 0..* children per parent | warehouse_stock.sku_id → sku.idWarehouse/SKU pair owns physical units. |
| fulfillment_order → inventory_reservation requests promises | 1 parent per child; 0..* children per parent | inventory_reservation.order_id → fulfillment_order.idA stable operation key determines whether a retry is the same reservation. |
| inventory_reservation → allocation allocates | 1 parent per child; 0..* children per parent | allocation.reservation_id → inventory_reservation.idOne order promise can span SKUs and warehouses atomically in this owner. |
| warehouse_stock → allocation backs promises | 1 parent per child; 0..* children per parent | allocation.stock_id → warehouse_stock.idSum of ACTIVE remaining allocations equals reserved. |
| allocation → shipment_line is shipped | 1 parent per child; 0..* children per parent | shipment_line.allocation_id → allocation.idPartial shipment creates progress without discarding remaining ownership. |
| warehouse_stock → stock_adjustment is corrected | 1 parent per child; 0..* children per parent | stock_adjustment.stock_id → warehouse_stock.idAdjustments are explicit audited facts, distinct from cancellation. |
Transaction boundaries
- Reserve: prepare every SKU allocation, lock chosen stock rows in stable order, revalidate availability and commit reservation/all allocations/held deltas atomically.
- Ship: resolve command identity, advance expiry, validate all remaining quantities, decrement on_hand and reserved together and store evidence.
- Release/expire: clear only ACTIVE remaining quantities. Returns need a new inspected receipt, not resurrection of original allocation.
Indexes and uniqueness
- Unique warehouse/SKU and reservation/stock; index ACTIVE reservation(expires_at).
- Index allocation(stock_id,reservation_id) for reconciliation; unique scoped shipment operation and fingerprint.
Schema traps and failure cases
- Independent warehouse commits cannot claim global all-or-nothing order acceptance without a coordinating transaction/saga contract.
- After shipping two of five, cancellation releases three; restoring all five invents stock.
Trace the records through one operation
- Order O needs SKU X7; allocation takes W1/X5 and W2/X2.
- Shipment consumes W1/X3, leaving W1 on_hand/reserved both lower by three.
- Expiry releases only remaining allocations; duplicate shipment key cannot decrement again.
Method contracts
reserve(order,demand,ttl,operationKey,now) → Reservation
Validate immutable demand, resolve a matching retry, expire old allocations, plan all lines, then commit all new holds. Fresh deadline is checked now+ttl; retries retain the original deadline.
ship(reservationId,quantities,operationKey,now) → Reservation
At the exact expiry boundary fresh shipment rejects. Matching retry returns current reservation without another decrement. Validate every requested key/amount before committing any shipment quantity.
release(reservationId,now) → Reservation
ACTIVE becomes RELEASED after clearing remaining holds. Other terminal states return unchanged; shipped stock is not restored.
adjust(key,delta,reason,expectedVersion,now) → Stock
Advance expiry, then check current version and exact result quantity. A stale version or quantity below held stock rejects; accepted adjustment records reason/result version.
stock(key,now); get(reservationId); invariantHolds()
stock advances global expiry; get observes last materialized reservation without a clock advance. invariantHolds independently recomputes active reserved totals from reservations.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Quantity ownership and conservation
- what
- Separate physical stock from promises and accepted shipments.
- when
- An order may wait, partially ship or cancel after some units leave.
- why
- One mutable available counter loses the evidence needed to distinguish a promise release from a physical return.
- how
- Stock holds onHand/reserved. Reservation copies original/remaining/shipped. ship decrements onHand and held equally; release/expiry decrements held for remaining only.
Code: Stock, Reservation, ship, close · Invariants: I1, I2, I3
Alternatives, cost and failure example
Alternative: A full movement ledger can derive these projections with stronger audit/rebuild evidence, at the cost of more storage and transaction design.
Cost: Maps and reservation histories grow; this reference does not retain a complete receipt/shipment movement journal.
Counterexample: Reserve five, ship two, cancel, then incorrectly add five back to onHand.
Test: Check partial shipment then release: only three become available and two remain physically gone.
All-or-nothing allocation with deterministic policy
- what
- Prepare a complete candidate allocation before changing authoritative holds.
- when
- One order spans several SKUs and warehouses.
- why
- Committing the first line before discovering another is unavailable strands stock and creates partial promises.
- how
- reserve scans sorted warehouse/SKU keys into a temporary allocation, rejects an unsatisfied line, and applies holds only after every SKU is satisfied.
Code: reserve, onHand · Invariants: I1
Alternatives, cost and failure example
Alternative: Warehouse cost optimization is a separate strategy; ordered greedy allocation is enough to make this reference reproducible.
Cost: For D demanded SKUs and W stock keys, scan cost is O(D×W) plus sorting demand; one monitor serializes unrelated inventory operations.
Counterexample: SKU A exists but SKU B is unavailable; a loop reserves A before returning failure.
Test: A multi-SKU failure leaves all new-order holds absent; competing last-unit reservations yield exactly one owner.
Retry identity versus current state
- what
- Recognize a repeated command before repeating its quantity mutation.
- when
- A reservation or shipment response is lost and the client retries after later changes.
- why
- Checking current ACTIVE state first can reject a legitimate old success or applying the same shipment twice can erase stock.
- how
- Typed ReserveRequest/ShipRequest fingerprints bind operation key to content and result reservation. Matching repeats return current view; changed quantity/type/key meaning rejects.
Code: previous, ReserveRequest, ShipRequest, ship · Invariants: I2
Alternatives, cost and failure example
Alternative: Persist original response snapshots if the API promises byte-for-byte historical replies; current-view retry is an explicit different contract.
Cost: Operation history must survive the retry horizon in a durable system. Process-local retention is neither durable nor bounded.
Counterexample: A successful final shipment becomes COMPLETED; its retry fails ACTIVE validation or decrements stock again.
Test: Repeat a shipment key after completion/expiry, and reject the same key with changed quantities.
Expiry and optimistic adjustment ownership
- what
- Expiry changes reservation eligibility; stock versions detect stale adjustment intent.
- when
- Cleanup races shipping or an operator adjusts stock while reservations change.
- why
- A blind cleanup can release another allocation, and a blind absolute adjustment can consume promised units.
- how
- at expires only ACTIVE identities with expiresAt<=now. close clears only remaining holds. adjust checks version after expiry and validates onHand>=held before writing an audit record.
Code: at, close, adjust · Invariants: I1, I3
Alternatives, cost and failure example
Alternative: A deadline heap improves expiry lookup; transactional conditional state transitions are needed across multiple workers.
Cost: Current at scans retained reservations, so stock reads/commands are O(history) even when no expiry is due. Caller time must be trustworthy.
Counterexample: Ship at the exact deadline after an unrelated stock read already expired the reservation.
Test: Assert exact-boundary rejection, no resurrection of shipped units and stale-version rejection after stock ownership changes.
Read and run the Java reference
Reveal complete Solution.java
Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.
reserve separates planning from mutation
The allocation map is temporary until every line can be satisfied. Failed demand cannot leave a new partial reservation behind.
ship validates all entries before decrement
Each quantity must belong to remaining. Copies of remaining/shipped are prepared, then physical and held stock change together under one owner.
close releases remaining only
Expiry and explicit release share this code path. Historical shipped entries survive terminal reservation state.
adjust checks the post-expiry version
Advancing expiry may itself advance the stock version. The adjustment must use current ownership evidence rather than bypassing those changes.
Complexity and limits
- Let R be retained reservations, W warehouse/SKU keys and D requested SKUs. Time advancement scans O(R), and expiry release additionally visits the remaining allocation entries of expired records.
- Fresh reserve scans O(D×W), plus O(D log D) demand order and allocation-map updates. Stored TreeMap updates cost O(log W); this is a clarity reference, not a large-warehouse optimizer.
- ship validates/updates K requested keys and copies reservation allocation maps; account for those copies plus O(K log W) stock writes and the shared expiry scan.
- Space is O(W+all retained allocations+operation identities+adjustment history). Snapshot copying costs proportional to maps. No compaction, paging, byte quota or independent warehouse parallelism is implemented.
Walk through the actual reference
These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.
Multi-warehouse reservation and partial shipment
Step 1 of 3: Reserve x7,y2
w1:x5,y2; w2:x2 held
Invariant: Every line allocated atomically
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Reserve x7,y2 | w1:x5,y2; w2:x2 held | Every line allocated atomically |
| Ship w1:x3 | w1:x onHand2/reserved2 | Shipment deducts on-hand and reservation together |
| Repeat shipment key | No extra deduction | Shipment identity deduplicates quantity change |
Expiry releases only unshipped quantity
Step 1 of 2: Hold expires at t=10 after partial ship
Remaining holds released; shipped3 stays deducted
Invariant: Expiry cannot resurrect physical stock
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Hold expires at t=10 after partial ship | Remaining holds released; shipped3 stays deducted | Expiry cannot resurrect physical stock |
| Attempt late new shipment | Rejected; reconcile externally if physical shipment already happened | Local reservation authority is explicit |
Mistakes and counterexamples
Decrement physical stock on reserve and again on ship
Failing case: One order consumes the same units twice.
Repair: Reserve changes held; ship changes held and onHand together.
Release original allocation after partial shipment
Failing case: Cancellation recreates units already shipped.
Repair: Release remaining quantities only.
Allocate lines incrementally before knowing all can succeed
Failing case: A missing final SKU strands earlier reservations.
Repair: Plan then commit all lines atomically.
Treat inventory retry keys as arbitrary request labels
Failing case: One key is reused for a different shipment quantity.
Repair: Persist typed immutable request fingerprints and reject mismatches.
Engineer → Senior → Staff
Model stock and atomic reservations.
Executed: multi-warehouse allocation, all-line validation, quantity invariants and immutable snapshots.
Change exercise
Add a warehouse preference policy without changing conservation.
Worked answer guidance
Keep allocation planning separate from commit; inject a stable ordered candidate list, validate all demand, then run the same hold transaction. Compare policy quality separately from correctness.
Interview follow-ups
Why keep onHand and reserved?
They represent physical inventory and promises. Deriving available preserves both facts and makes shipment/release rules explicit.
Is deterministic greedy allocation optimal?
No. It is reproducible and simple; cost, distance, split packages and fairness need explicit policy objectives.
Provenance and scope of evidence
Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.