LLD Hub
lldcommand-patternobserver

Inventory Management System Low Level Design — LLD Interview Guide

Design a warehouse inventory system with stock reservation, audit trail, multi-warehouse transfers, and auto reorder. Asked at Amazon, Flipkart SDE interviews.

18 April 2025·8 min read

Practice this problem

Inventory Management System — get AI-scored feedback on your solution

Solve it →

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.

Ready to practice?

Submit your solution and get AI-scored feedback on OOP, SOLID principles, design patterns, and code quality.

Solve Inventory Management System