Inventory Management System Low Level Design is a core problem asked at Amazon, Flipkart, and Walmart. It covers stock reservation, multi-warehouse transfers, audit trail, and auto-reorder. This guide covers the complete Inventory Management LLD with Java code, class diagram, and interview FAQ.
Why Interviewers Ask Inventory Management LLD
Inventory systems must handle concurrent stock updates without corruption. Interviewers want to see:
- Can you model stock reservation (soft lock before purchase) vs actual deduction?
- Do you use Command pattern to create an auditable history of every stock change?
- Can you handle multi-warehouse operations — transfers and allocation strategies?
- Do you use Observer pattern to trigger auto-reorder when stock falls below threshold?
- Can you prevent overselling with optimistic locking or atomic operations?
Functional Requirements
- Track stock quantity per product per warehouse
- Reserve stock for an order (soft lock) — prevents overselling during checkout
- Confirm reservation on payment — permanently deducts stock
- Cancel reservation — releases the soft lock
- Transfer stock between warehouses
- Auto-reorder when stock falls below the reorder point
- Audit trail — every stock change is logged with who did it and why
- Admin can adjust stock manually (receive new shipment, write-off damaged goods)
Non-Functional Requirements
- Stock updates must be atomic — concurrent order placements must not oversell
- Audit trail must be immutable — no record can be deleted or modified
- Adding a new stock change type (e.g., DAMAGED_WRITE_OFF) must not change existing code
- Auto-reorder must trigger within 1 minute of stock dropping below threshold
Core Entities — Inventory Management LLD Class Design
- Product — id, name, sku, category, reorderPoint, reorderQuantity
- Warehouse — id, name, location, capacity
- InventoryItem — product, warehouse, quantityOnHand, quantityReserved
- StockReservation — id, orderId, product, warehouse, quantity, status, expiresAt
- StockMovement — Command; encapsulates a stock change with reason and metadata
- AuditLog — immutable record of every StockMovement
- ReorderObserver — notified when stock falls below reorderPoint
- InventoryService — reserve, confirm, cancel, transfer, adjust
Text-Based Class Diagram
Product +-- id, name, sku: String +-- reorderPoint: int (trigger auto-reorder below this) +-- reorderQuantity: int Warehouse +-- id, name, location: String InventoryItem +-- product: Product, warehouse: Warehouse +-- quantityOnHand: int (physical stock) +-- quantityReserved: int (soft-locked for pending orders) +-- quantityAvailable(): int // onHand - reserved StockReservation +-- id, orderId: String +-- product: Product, warehouse: Warehouse +-- quantity: int +-- status: ReservationStatus (ACTIVE/CONFIRMED/CANCELLED/EXPIRED) +-- createdAt, expiresAt: LocalDateTime StockMovement (Command) +-- execute(): void +-- undo(): void (for rollback scenarios) AuditLog +-- id, timestamp: LocalDateTime +-- productId, warehouseId: String +-- movementType: MovementType +-- quantityChange: int +-- referenceId: String (orderId, transferId, etc.) +-- performedBy: String
Command Pattern — Stock Movements
public interface StockMovement {
void execute();
MovementType getType();
}
// Inbound: new stock received
public class StockReceiveMovement implements StockMovement {
private final InventoryItem item;
private final int quantity;
private final String referenceId; // purchase order ID
@Override
public void execute() {
item.setQuantityOnHand(item.getQuantityOnHand() + quantity);
}
@Override
public MovementType getType() { return MovementType.RECEIVED; }
}
// Outbound: confirmed order deduction
public class StockDeductMovement implements StockMovement {
private final InventoryItem item;
private final int quantity;
private final String orderId;
@Override
public void execute() {
if (item.getQuantityOnHand() < quantity)
throw new InsufficientStockException(item.getProduct().getSku());
item.setQuantityOnHand(item.getQuantityOnHand() - quantity);
item.setQuantityReserved(item.getQuantityReserved() - quantity); // release reservation
}
@Override
public MovementType getType() { return MovementType.SOLD; }
}
// Transfer between warehouses
public class StockTransferMovement implements StockMovement {
private final InventoryItem source;
private final InventoryItem destination;
private final int quantity;
@Override
public void execute() {
if (source.getQuantityAvailable() < quantity)
throw new InsufficientStockException("Source warehouse cannot fulfill transfer");
source.setQuantityOnHand(source.getQuantityOnHand() - quantity);
destination.setQuantityOnHand(destination.getQuantityOnHand() + quantity);
}
@Override
public MovementType getType() { return MovementType.TRANSFERRED; }
}InventoryService — Reservation and Confirmation
public class InventoryService {
private final InventoryItemRepository itemRepo;
private final ReservationRepository reservationRepo;
private final AuditLogRepository auditLogRepo;
private final List<InventoryObserver> observers = new ArrayList<>();
public void addObserver(InventoryObserver observer) { observers.add(observer); }
public StockReservation reserve(String orderId, String productId, String warehouseId, int quantity) {
InventoryItem item = itemRepo.findByProductAndWarehouse(productId, warehouseId);
synchronized (item) {
if (item.getQuantityAvailable() < quantity)
throw new InsufficientStockException("Not enough available stock");
item.setQuantityReserved(item.getQuantityReserved() + quantity);
itemRepo.save(item);
}
StockReservation reservation = new StockReservation(UUID.randomUUID().toString(),
orderId, item.getProduct(), item.getWarehouse(), quantity,
ReservationStatus.ACTIVE, LocalDateTime.now(), LocalDateTime.now().plusMinutes(30));
return reservationRepo.save(reservation);
}
public void confirmReservation(String reservationId) {
StockReservation res = reservationRepo.findById(reservationId);
if (res.getStatus() != ReservationStatus.ACTIVE || res.isExpired())
throw new InvalidReservationException("Reservation is not active");
InventoryItem item = itemRepo.findByProductAndWarehouse(
res.getProduct().getId(), res.getWarehouse().getId());
StockDeductMovement movement = new StockDeductMovement(item, res.getQuantity(), res.getOrderId());
executeAndAudit(movement, item, res.getOrderId());
res.setStatus(ReservationStatus.CONFIRMED);
reservationRepo.save(res);
notifyObservers(item);
}
public void cancelReservation(String reservationId) {
StockReservation res = reservationRepo.findById(reservationId);
if (res.getStatus() == ReservationStatus.CONFIRMED)
throw new InvalidReservationException("Confirmed reservations cannot be cancelled");
InventoryItem item = itemRepo.findByProductAndWarehouse(
res.getProduct().getId(), res.getWarehouse().getId());
item.setQuantityReserved(item.getQuantityReserved() - res.getQuantity());
itemRepo.save(item);
res.setStatus(ReservationStatus.CANCELLED);
reservationRepo.save(res);
}
private void executeAndAudit(StockMovement movement, InventoryItem item, String referenceId) {
movement.execute();
itemRepo.save(item);
auditLogRepo.save(new AuditLog(UUID.randomUUID().toString(), LocalDateTime.now(),
item.getProduct().getId(), item.getWarehouse().getId(),
movement.getType(), -item.getQuantityOnHand(), referenceId, getCurrentUser()));
}
private void notifyObservers(InventoryItem item) {
if (item.getQuantityOnHand() <= item.getProduct().getReorderPoint()) {
observers.forEach(obs -> obs.onLowStock(item));
}
}
}Key Design Decisions
- Reserve (soft lock) before deduct: quantityReserved tracks stock committed to pending orders. quantityAvailable = onHand - reserved. This prevents overselling during the window between checkout and payment confirmation.
- Command pattern for all stock movements: Every change — receive, sell, transfer, write-off — is a Command. The audit log records the command type, not just the quantity delta. This makes the history human-readable and supports undo for admin corrections.
- Synchronized on InventoryItem: Concurrent order reservations for the same product in the same warehouse race on the availability check. Synchronizing on the specific item object prevents double-allocation without a global lock.
- Observer for auto-reorder: AutoReorderObserver calls the purchase management system when stock drops below reorderPoint. This decouples inventory from procurement — adding an email alert is a new observer class only.
Common Follow-Up Questions
- "How do you handle expired reservations?" — A scheduled job runs every minute and finds ACTIVE reservations where expiresAt is in the past. For each, it cancels the reservation and releases the quantityReserved back to the item. Alternatively, expire lazily on the next availability check.
- "How do you choose which warehouse fulfills an order?" — Add a WarehouseAllocationStrategy (Strategy pattern): NearestWarehouse (Haversine from delivery address), HighestStock (maximize utilization), or RoundRobin. InventoryService calls the strategy to pick the warehouse before reserving.
- "How do you handle stock discrepancies from physical counts?" — Admin triggers a StockAdjustMovement with the counted quantity and the reason (CYCLE_COUNT). The movement sets quantityOnHand to the audited value and logs the delta. The audit trail shows the discrepancy and who resolved it.
FAQ — Inventory Management System Low Level Design
What design patterns are used in Inventory Management LLD?
The primary patterns are Command (StockMovement — Receive, Deduct, Transfer),Observer (AutoReorderObserver on low stock), and Strategy(warehouse allocation). The Command pattern is the most important — it makes the audit trail a natural byproduct of business operations.
What is the difference between reserved stock and on-hand stock?
On-hand stock is the physical quantity in the warehouse. Reserved stock is the portion of on-hand stock committed to pending orders but not yet shipped. Available stock = on-hand minus reserved. Users can only order from available stock. This prevents overselling during the checkout-to-payment window.
How do you prevent overselling in an inventory system?
Synchronize the availability check and reservation increment as a single atomic operation. In the database, use SELECT FOR UPDATE to lock the row, check availability, and update quantityReserved in one transaction. The Java-level synchronized block is for in-process concurrency; the DB lock handles multi-node deployments.
How do you design an audit trail for inventory changes?
Every StockMovement writes an immutable AuditLog entry with: timestamp, product, warehouse, movement type, quantity change, reference ID (orderId or transferId), and the user who initiated it. AuditLog records are insert-only — no update or delete operations are permitted. This gives a complete, tamper-evident history.