Design an Inventory Management System
Learning Objectives#
- Model the difference between a product and its stock positions, and see why the stock ledger matters more than the product catalog.
- Design stock operations as events with a before and after, so every unit that leaves a warehouse can be explained.
- Handle the concurrency story that is not optional in this system: two orders decrementing the same product's stock at the same moment.
Introduction#
Inventory management is the case study where the data model is the design and everything else is scaffolding. The catalog, the warehouses, the orders: they all exist to serve one question, which is "how much of product X do we have right now, and where?" The trap is that "right now" is a lie in a busy warehouse. Stock is not a number that exists, it is a number that results from a ledger of events, and the systems that treat the number as ground truth are the systems that ship more than they own. Interviewers ask this because it tests whether you can design the boring foundation correctly, and because it has a real concurrency story that the parking lot and the library could only gesture at: every candidate for an e-commerce or logistics backend will live or die by this exact design in their day job.
Requirements Gathering#
Functional requirements:
- The system maintains a product catalog with SKU, name, and metadata.
- Stock is tracked per product, per warehouse, per location within a warehouse.
- Inbound and outbound movements update stock: receiving, selling, adjusting, and returns.
- The system reports current stock levels and flags products that fall below a reorder threshold.
- Every stock change is recorded as a movement event with quantity, direction, and timestamp.
Non-functional requirements:
- Stock updates must be consistent under concurrency; two simultaneous sells of the last unit must not both succeed.
- Stock queries must be cheap and must reflect all committed movements.
Assumptions to state out loud: no multi-currency costing or inventory valuation (FIFO vs weighted average), no batch or expiry tracking, no reservations separate from actual decrements, and stock counts are integer units, not weights or liters. Cut valuation, cut reservations. The interviewer is testing the movement ledger and the concurrency guard, not your accounting knowledge.
Identifying Core Entities#
The entity list is short, and the two classes carrying the weight are the ones beginners leave out.
| Entity | One-line responsibility |
|---|---|
Product | The catalog entry: SKU, name, and unit definition. |
Warehouse | A named physical location with a set of storage positions. |
StockPosition | The current count of a product at one warehouse location. |
StockMovement | The ledger event: product, warehouse, quantity delta, reason, timestamp. |
StockLedger | The ordered history of movements that lets the system answer "how did we get here?" |
InventoryService | The facade that applies movements atomically and answers stock queries. |
The distinction that matters is between StockPosition, which is the derived current state, and StockMovement, which is the recorded truth. Positions can be rebuilt from movements. Movements cannot be rebuilt from positions, and that asymmetry is the whole design.
Class Design#
Product is metadata, deliberately thin.
StockPosition is the current count. Its single interesting method is apply, which mutates the count by a signed delta and refuses to go negative. The negative guard is the first line of defense against overselling, and it belongs here, next to the data it protects, not in a service method far away.
StockMovement is the ledger event. Every reason, inbound or outbound, gets one. The reason field is what makes a ledger explainable: you can ask "why did we go negative on this SKU?" and the movements answer, each one tagged with RECEIVING, SALE, RETURN, or ADJUSTMENT.
StockLedger is the append-only history. In a real system this is a database table; here it is a list. It has one job, record movements, and it does it so the inventory service can reconstruct any position from scratch if it ever needs to.
InventoryService is where the money operation lives: releaseStock. This is the atomic apply of a negative delta to a position, paired with a ledger record. The synchronized on the position's apply is the concurrency guard, and the order matters: apply the delta, and only if it succeeds, record the movement. If you record first and apply second, a failed apply leaves a phantom ledger entry.
Diagram: movements flow into the append-only ledger, the position is derived from it, and the last-unit race resolves inside a single synchronized apply.
The query side is intentionally boring: read the position map. The history side is where the interview gets interesting, because reconstructing a position from the ledger is the answer to "what if the count is wrong?" and it is the proof that the ledger, not the number, is the source of truth.
Design Patterns Used#
There is no classic GoF pattern at the heart of this system, and the honest answer is to say so. The interesting structure is the append-only ledger, which is an event-sourcing idea in miniature: state is derived from events, events are never deleted. Do not reach for a Decorator to layer overstock checks or a Strategy for allocation policy; the follow-up "where do you enforce the reorder threshold" is answered by a scan of positions and a policy object, not by a pattern. The one pattern-shaped thing worth naming is the Facade in InventoryService, and it is a modest one. If the interviewer pushes for a pattern, name the ledger as the real architectural decision and say that reaching for a pattern here would be manufacturing complexity, which is the blunt truth.
Handling Edge Cases / Concurrency#
This is the concurrency case study of the chapter, so go deep. The race is the last unit: two orders for one remaining unit of a SKU arrive together. Both services call releaseStock, both read the position, both see quantity 1, both subtract, and the count goes negative. The fix is that apply is synchronized and the check and the decrement happen under the same lock, so the second caller observes 0 and its apply returns false. In a real system with a database, the equivalent is an atomic UPDATE stock_positions SET quantity = quantity - 1 WHERE sku = ? AND quantity >= 1, which is the same check-and-decrement made atomic by the database. The candidate who can say "synchronized here, atomic conditional update in the database, same idea" has answered the strongest concurrency question in this chapter.
The second edge is the negative-stock guard. Overselling is a business decision, not a state the system should reach accidentally. The apply guard makes negative stock impossible through normal flows, and if the business ever wants backorders, that is a new field and a deliberate choice, not a bug in the guard.
The third edge is the reconstruction proof. If a count looks wrong, the ledger lets you replay all movements for a SKU and see where the drift came from. In a real system that is SUM(delta) FROM movements WHERE sku = ? grouped by warehouse, and it is the query that auditors run. Name it.
Common Mistakes#
The most common mistake is the product owning its stock. A Product with an int stock field and a decrement() method means two products in two warehouses cannot exist, because the product has one count, and the interviewer's "how much do we have in the east warehouse" question is unanswerable. Stock is keyed by product and warehouse and position, and the product itself must stay metadata.
The second mistake is mutating the count without a ledger record, or recording without mutating. Either half of the pair on its own produces an inventory that drifts from reality, and the drift is invisible until a stocktake finds it. The movement and the mutation are one transaction, in that order.
The third mistake is a naively non-atomic releaseStock. The classic version reads the count, checks it, decrements it, and writes it back in four separate lines, and the last-unit race corrupts it. The candidate who writes that and never mentions the race has built the exact system that oversells at launch.
Interview Perspective#
A weak answer is a catalog and a count. Product with stock, getStock(), setStock(). There is no ledger, no movement, no concurrency story, and the "two orders for the last unit" question is answered with a shrug and a hopeful "we could add a lock." The design has nothing to walk through because nothing can be explained.
A strong answer says "stock is derived from a ledger, positions are the cached result, and the decrement is an atomic check-and-apply." The follow-ups answer themselves. "What if the east warehouse is out and the west warehouse has stock" (the position map is keyed by warehouse, so routing is a lookup, which is exactly why the split exists), "how do you support returns" (a positive delta with a RETURN reason, one ledger entry, no special case), "how do you detect shrinkage" (replay the ledger against a physical count, the difference is the answer). The strongest candidates volunteer the UPDATE ... WHERE quantity >= 1 phrasing unprompted, because they have run the last-unit race and know it by heart.
Knowledge Check#
- Two orders for the one remaining unit of a SKU are processed concurrently. Trace both calls through
releaseStockand explain which order succeeds, which fails, and what the position map and ledger each show afterward. - The inventory for a SKU in a warehouse reads 5, but a physical count finds 3. Describe the two things you need to produce to explain the gap, and what question each of them answers.
- The product catalog and the positions live in separate maps, and the movement carries the SKU rather than the product object. Why does this split make warehouse routing and history reporting possible, and what would break if the product owned its stock count?
Key Takeaways#
- Stock is keyed by product and warehouse and position. The product is metadata, not a count.
- The ledger is the source of truth; the position is the derived cache. Never confuse the two.
- Apply the delta, then record the movement. The order is the correctness.
- The last-unit race is the concurrency story, and the fix is an atomic check-and-decrement,
synchronizedhere, conditional update in the database. - Reconstructing a position from its ledger is the answer to every "why is the count wrong" question, and the query every auditor runs.
What's Next#
Inventory taught you the ledger-and-position split and the atomic decrement. The car rental system reuses the inventory idea, then layers on the thing that changes the arithmetic: time. A car is stock, but a car for this week is a different kind of stock, and calendars replace counts.