Design an Inventory Management System
SKUs, warehouses, stock movements, and reorder rules with an event-driven low-stock notifier.
Problem Statement
Design the object model for an Inventory Management System that tracks products across warehouses, records on-hand and reserved quantities, and protects stock while orders are being checked out.
The core interview challenge is the correctness boundary: two order threads must not both decrement the same available units. The design should also send low-stock notifications exactly when a product crosses its threshold and keep replenishment policy pluggable instead of hard-coded.
Business context
Inventory systems sit behind shopping carts, food delivery, warehouses, and marketplaces. A small race can oversell a SKU, break fulfillment promises, and create expensive support issues. Interviewers use this problem to evaluate whether you can model domain invariants, isolate persistence with a Repository, apply Observer for threshold events, and use Strategy for reorder decisions.
Functional Requirements
Register products with SKU, name, and a low-stock threshold.
Register warehouses and keep stock independently per SKU and warehouse.
Add physical stock to a warehouse after receiving inventory.
Reserve available stock for an order without overselling.
Release previously reserved stock when an order is cancelled or payment fails.
Ship reserved stock by decrementing on-hand inventory after fulfillment.
Notify observers when available stock crosses below the product threshold.
Generate replenishment recommendations through a pluggable strategy.
Non-Functional Requirements
Concurrency safety
The check of available quantity and the reserved or shipped mutation must happen under one lock per stock item.
Extensibility
New reorder policies, alert sinks, and repository implementations should be additive classes, not edits to reservation flow.
Consistency of invariants
reserved must never exceed onHand, and available = onHand - reserved must never be negative.
Low latency hot path
Reserve and release should be O(1) for a known SKU and warehouse, with synchronization scoped to that stock item.
Auditable operations
Production implementations should attach order ids, actor ids, and timestamps to stock movements for reconciliation.
Requirement Clarification
QIs stock tracked globally or per warehouse?
Track stock per SKU + warehouse. A product can be available in one warehouse and low in another, so the stock item is the aggregate for that pair.
QWhat is the difference between reserve, release, and ship?
Reserve moves units from available to reserved for an order. Release returns reserved units to available. Ship consumes reserved units by decrementing physical on-hand stock.
QWhen should low-stock observers be notified?
Notify when available stock crosses from at or above the threshold to below it. This prevents repeated alerts for every later operation while the SKU is already low.
QDo we need purchase orders or supplier integration?
Not in the base design. The system returns a ReorderRecommendation from a strategy; a supplier adapter can consume that recommendation later.
QIs persistence required for the interview implementation?
Use an in-memory Repository for clarity, but keep the service dependent on the Repository interface so SQL, Redis, or event-sourced storage can replace it.
UML Class Diagram
Sequence Diagram
Entity Identification
Product
Immutable SKU metadata: identifier, display name, and the low-stock threshold used by alerting and replenishment.
Warehouse
Immutable location metadata. It scopes inventory so the same SKU can have separate availability in different facilities.
StockItem
Aggregate for one Product in one Warehouse. Owns onHand, reserved, and the lock that protects every mutation.
InventoryService
Application facade. Validates requests, loads stock items from the Repository, coordinates reserve, release, and ship operations, and triggers low-stock observers.
InventoryRepository
Persistence seam for products, warehouses, and stock items. The sample uses an in-memory implementation; production can replace it with durable storage.
ReplenishmentStrategy
Policy interface that decides whether and how many units to reorder when stock becomes low.
InventoryObserver
Observer interface for low-stock events. Email, Slack, purchase-order creation, and metrics can subscribe independently.
Design Patterns Used
InventoryObserver decouples stock changes from alert delivery. The service publishes a low-stock event and does not care whether the subscriber sends email, updates metrics, or creates a purchase order.
ReplenishmentStrategy keeps reorder logic pluggable. Fixed quantity, top-up-to-target, supplier lead-time, and demand-forecast policies all implement the same method.
InventoryRepository hides storage details from the service. Reservation logic depends on an interface while the demo uses InMemoryInventoryRepository.
InventoryService.getInstance gives small applications one shared inventory facade. The design still exposes an isolated factory for tests and demos that need fresh state.
Step-by-Step Design
1Model stock at SKU and warehouse granularity
Use StockItem as the aggregate root for one product in one warehouse. Global product state is not enough because reservations and thresholds are location-specific.
2Represent availability as a derived value
Store onHand and reserved; compute available stock instead of storing it separately. That removes a third value that can drift out of sync.
public int getAvailable() { return onHand - reserved; }3Make reserve an atomic check-then-update
The race is between checking available units and increasing reserved units. Put both operations under the same stock-item lock.
lock.lock(); try { if (availableLocked() < quantity) { return ReservationResult.rejected(snapshotLocked(), "Not enough stock"); } reserved += quantity; return ReservationResult.accepted(before, snapshotLocked()); } finally { lock.unlock(); }4Separate reserve, release, and ship
Reserve protects stock for checkout, release cancels the hold, and ship decrements physical inventory after fulfillment. Keeping these verbs separate prevents order-state ambiguity.
5Notify only on threshold crossing
Compare before and after snapshots. Observers should hear about a transition into low stock, not every operation while already low.
if (before.getAvailable() >= after.getLowStockThreshold() && after.getAvailable() < after.getLowStockThreshold()) { ReorderRecommendation recommendation = replenishmentStrategy.recommend(after); for (InventoryObserver observer : observers) { observer.onLowStock(after, recommendation); } }6Keep policies and storage behind interfaces
The service depends on InventoryRepository, InventoryObserver, and ReplenishmentStrategy. This keeps persistence, notifications, and reorder math replaceable.
Complete Java Implementation
Explanation of Every Class
Product
Immutable SKU definition. Equality is based on sku, and lowStockThreshold drives alerting and replenishment decisions.
Warehouse
Immutable facility definition. Keeping warehouse identity separate from product identity lets the same SKU have independent stock in each location.
StockItem
The concurrency-safe aggregate. It owns onHand, reserved, StockSnapshot, StockChange, and ReservationResult; reserve, release, and ship all hold the same ReentrantLock while checking and mutating state.
ReplenishmentStrategy
The reorder policy seam. The file includes fixed-quantity and top-up-to-target strategies plus ReorderRecommendation, so the service can ask for a recommendation without knowing the formula.
InventoryObserver
Observer contract for threshold breaches. The console observer is deliberately simple, but email, Slack, metrics, or purchase-order adapters would implement the same method.
InventoryService
Singleton facade and orchestration layer. It registers catalog data, loads stock through InventoryRepository, calls StockItem for atomic mutations, and notifies observers after threshold crossings.
Main
Executable demo. Two threads each try to reserve six units from ten on-hand units; the stock-item lock allows one reservation and cleanly rejects the other, while the winner triggers a low-stock notification.
Dry Run
Sample input
SKU BOOK-001 in warehouse WH-BLR has onHand 10, reserved 0, and low-stock threshold 5. Two order threads each try to reserve 6 units.
| Step | ORDER-1 | ORDER-2 | onHand | reserved | Outcome |
|---|---|---|---|---|---|
| 1 | starts reserve(6) | starts reserve(6) | 10 | 0 | Both requests target the same stock item |
| 2 | acquires item lock | waits for item lock | 10 | 0 | ORDER-1 checks available = 10 |
| 3 | reserved becomes 6 | still waiting | 10 | 6 | Available drops to 4, crossing threshold 5 |
| 4 | observer notified | acquires item lock | 10 | 6 | Strategy recommends reorder before ORDER-2 checks |
| 5 | ships reserved 6 | reserve(6) rejected | 4 | 0 | Only 4 units remain, so oversell is prevented |
The important transition is Step 3: available stock moves from 10 to 4 inside the lock, so the second order can only see the committed result and the observer fires once.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| register product or warehouse | O(1) | O(1) | Hash-map insert in the in-memory Repository. |
| add stock | O(1) | O(1) | Lookup the stock item and update onHand under its lock. |
| reserve stock | O(1 + O) | O(1) | O observers are notified only when the threshold is crossed; the stock mutation itself is O(1). |
| release stock | O(1) | O(1) | Decrease reserved quantity under the stock-item lock. |
| snapshot all stock | O(N) | O(N) | N stock items are copied into immutable snapshots. |
The design intentionally locks only one StockItem for reserve, release, and ship. That keeps unrelated SKUs and warehouses independent while preserving the invariant for the hot aggregate.
Extensibility
New replenishment policy
Implement ReplenishmentStrategy for lead-time demand, seasonal buffers, supplier minimum order quantities, or machine-learning forecasts.
New notification channel
Implement InventoryObserver for email, Slack, PagerDuty, metrics, or automatic purchase-order creation without editing stock mutation logic.
Durable persistence
Replace InMemoryInventoryRepository with a SQL repository that uses row locks or optimistic version checks around the stock item.
Multi-warehouse allocation
Add an allocation strategy that chooses the warehouse based on nearest location, shipping cost, or stock age before calling reserve.
Inventory movement ledger
Record every add, reserve, release, and ship operation as an immutable movement event for audits and reconciliation.
Alternative Designs
Database row lock as the concurrency boundary
Instead of an in-memory ReentrantLock, store each stock item in a table and reserve with a transaction that locks the SKU and warehouse row.
Tradeoffs
Works across multiple service instances and survives restarts, but adds database latency and requires careful transaction tuning.
Event-sourced inventory ledger
Append stock movement events and derive current availability from snapshots plus later events.
Tradeoffs
Excellent auditability and replay, but more complex reads and harder real-time reservation guarantees unless projections are strongly consistent.
Reservation tokens with expiry
Create reservation records with expiration times and a sweeper that automatically releases stock after checkout timeouts.
Tradeoffs
Matches e-commerce checkout behavior, but introduces background processing and order-state coordination.
Common Mistakes
- ×
Checking available stock and then updating reserved stock in separate unsynchronized steps.
- ×
Treating available as a stored field, which can drift from onHand - reserved.
- ×
Sending a low-stock alert on every reserve while the item is already below threshold.
- ×
Hard-coding reorder quantity inside InventoryService instead of using Strategy.
- ×
Letting the service manipulate maps directly instead of depending on a Repository interface.
- ×
Mixing reserved and shipped states so cancelled orders accidentally reduce physical stock.
- ×
Using one global lock for every SKU and warehouse, which makes unrelated reservations block each other.
Follow-up Interview Questions
QHow would you make this safe across multiple app servers?
Move the concurrency boundary into the Repository with a database transaction, row lock, or compare-and-swap version field on the stock item.
QShould low stock be based on on-hand or available stock?
For order promise accuracy, use available = onHand - reserved. A product can have high on-hand inventory but still be unavailable because most units are reserved.
QHow do you prevent abandoned carts from holding stock forever?
Attach an expiry time to reservations and run a sweeper or scheduled job that releases expired holds through the same releaseStock path.
QHow would you support choosing the best warehouse for an order?
Add a WarehouseAllocationStrategy before reservation. It can consider customer location, shipping cost, stock age, and warehouse load, then reserve against the chosen stock item.
QWhat happens if an observer fails?
In production, observer delivery should be isolated through retries or an outbox. The stock mutation should remain committed even if email or Slack delivery fails.
Production Considerations
Transactions and locking
Use row-level locks, optimistic versions, or atomic conditional updates so multiple service instances cannot oversell the same SKU and warehouse.
Outbox for notifications
Persist low-stock events in the same transaction as the stock change, then deliver them asynchronously to observers for reliable retries.
Idempotency
Accept an order id and operation id so repeated reserve, release, or ship requests do not double-count after client retries.
Audit and reconciliation
Maintain an inventory movement ledger and reconcile it against warehouse scans, supplier receipts, and shipment confirmations.
Metrics
Track reservation failures, low-stock events, observer latency, reorder recommendation volume, and stock drift by warehouse.
What Interviewers Look For
Did the candidate identify SKU + warehouse as the stock aggregate?
Is the check-then-reserve mutation protected by a single lock or transaction?
Are reserve, release, and ship separate verbs with clear invariants?
Does low-stock alerting fire on threshold crossing rather than every update?
Are Repository, Observer, Strategy, and Singleton used for real seams rather than decoration?
Can the design evolve to multi-instance persistence and reservation expiry?
Quiz
0/5 answered
1.Why does **StockItem.reserve** lock around both the availability check and the reserved update?
2.Why is available stock computed as **onHand - reserved**?
3.When should **InventoryObserver** subscribers be notified?
4.What does **ReplenishmentStrategy** make easy to change?
5.What role does **InventoryRepository** play?
Practice Variants
Add reservation expiry
IntermediateCreate reservation records with expiry timestamps and a sweeper that releases expired holds through releaseStock.
Add warehouse allocation strategy
AdvancedChoose the best warehouse for an order based on customer location, shipping cost, and current availability before reserving stock.
Add purchase-order generation
IntermediateTurn low-stock recommendations into purchase orders through an observer, then track supplier receipts back into addStock.
Flashcards
Cheat Sheet
Entities: Product, Warehouse, StockItem, InventoryService, InventoryRepository, InventoryObserver, ReplenishmentStrategy.
Core invariant: available stock is derived as onHand - reserved; reserve must not let available go negative.
Concurrency: lock one StockItem during reserve, release, add, and ship. In production, move the lock to a durable transaction boundary.
Patterns: Observer for low-stock notifications, Strategy for reorder policy, Repository for persistence, Singleton for the shared service facade.
Flows: add stock = increase onHand; reserve = increase reserved; release = decrease reserved; ship = decrease reserved and onHand.
Alert rule: notify only when available crosses from at or above threshold to below threshold, then ask the strategy for a recommendation.
References
- BookPatterns of Enterprise Application Architecture — Repository — Martin Fowler
- BookEffective Java — Synchronization and concurrency guidance — Joshua Bloch
- DocsRefactoring Guru — Observer Pattern
- DocsRefactoring Guru — Strategy Pattern