Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,052visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsMachine Coding Exercises
✓ FreeIntermediate· 8 min read

Design an Inventory Management System

Warehouse/Product/StockItem/Supplier/Order classes, stock reservation during checkout reusing the seat-lock pattern, and low-stock alerting as an Observer hook.

Published September 23, 2026


Design an Inventory Management System

Core classes

class Warehouse { String location; Map<String, StockItem> stockByProductId; }
class Product { String id; String name; int lowStockThreshold; }
class StockItem { Product product; int quantityOnHand; int quantityReserved; } // reserved != on-hand — see below
class Supplier { String name; List<Product> suppliedProducts; }
class Order { List<OrderLine> lines; OrderStatus status; }

Advertisement

Stock reservation, reusing the seat-lock pattern

class InventoryService {
    boolean reserveStock(String productId, int quantity, Warehouse warehouse) {
        StockItem item = warehouse.stockByProductId.get(productId);
        synchronized (item) { // per-item lock, same principle as per-spot/per-seat locking elsewhere in this course
            int available = item.quantityOnHand - item.quantityReserved;
            if (available < quantity) return false;
            item.quantityReserved += quantity; // reserved, not yet deducted from on-hand
            return true;
        }
    }

    void confirmReservation(String productId, int quantity, Warehouse warehouse) {
        StockItem item = warehouse.stockByProductId.get(productId);
        synchronized (item) {
            item.quantityOnHand -= quantity;
            item.quantityReserved -= quantity;
        }
    }

    void releaseReservation(String productId, int quantity, Warehouse warehouse) {
        synchronized (warehouse.stockByProductId.get(productId)) {
            warehouse.stockByProductId.get(productId).quantityReserved -= quantity; // order abandoned/expired — release the hold
        }
    }
}

Tracking quantityReserved separately from quantityOnHand is the key modeling decision — it's structurally identical to Movie Ticket Booking System's HELD vs BOOKED seat states and Parking Lot's spot-claiming: a reservation temporarily removes stock from what's available to other orders without yet committing to a permanent deduction, so an abandoned checkout can cleanly release the hold via releaseReservation() rather than needing to "add back" a quantity that was never actually removed from quantityOnHand in the first place.

Low-stock alerting as an Observer hook, not tightly coupled logic

interface LowStockListener { void onLowStock(Product product, int currentQuantity); }

class InventoryService {
    private final List<LowStockListener> listeners = new CopyOnWriteArrayList<>();

    void confirmReservation(String productId, int quantity, Warehouse warehouse) {
        StockItem item = warehouse.stockByProductId.get(productId);
        synchronized (item) {
            item.quantityOnHand -= quantity;
            item.quantityReserved -= quantity;
            if (item.quantityOnHand < item.product.lowStockThreshold) {
                listeners.forEach(l -> l.onLowStock(item.product, item.quantityOnHand)); // notify, don't decide what happens next
            }
        }
    }
}

InventoryService doesn't know or care what happens on low stock — sending a supplier reorder email, paging an ops team, updating a dashboard — it just fires the event (Observer Pattern) and lets registered listeners decide. Coupling reorder-email logic directly into confirmReservation() would violate Single Responsibility (inventory tracking and notification delivery are genuinely separate concerns) and make adding a second reaction (e.g. also updating a dashboard) require editing InventoryService itself rather than just registering a second listener.

Reservations with expiry and an audit trail

The core design distinguishes quantityOnHand from quantityReserved. Two gaps remain: a reservation from an abandoned checkout must eventually be released, and every stock change should be explainable after the fact. Both are solved by treating reservations and movements as records, not just counters:

record Reservation(String id, String orderId, String productId, String warehouseId,
                   int quantity, Instant expiresAt) {}

enum MovementType { RECEIVED, RESERVED, RELEASED, SHIPPED, ADJUSTED, RETURNED }

record StockMovement(String productId, String warehouseId, MovementType type, int quantity,
                     String reference, Instant at) {}

class InventoryService {
    private final Map<String, StockItem> stock;                 // key: warehouseId + ":" + productId
    private final Map<String, Reservation> reservations = new ConcurrentHashMap<>();
    private final List<StockMovement> ledger;                   // append-only
    private final Clock clock;

    Optional<Reservation> reserve(String orderId, String warehouseId, String productId, int qty) {
        StockItem item = stock.get(warehouseId + ":" + productId);
        synchronized (item) {
            if (item.available() < qty) return Optional.empty();   // available = onHand - reserved
            item.quantityReserved += qty;
        }
        Reservation r = new Reservation(UUID.randomUUID().toString(), orderId, productId, warehouseId,
                                        qty, clock.instant().plus(Duration.ofMinutes(15)));
        reservations.put(r.id(), r);
        ledger.add(new StockMovement(productId, warehouseId, MovementType.RESERVED, qty, orderId, clock.instant()));
        return Optional.of(r);
    }

    /** Called when the order is paid: stock physically leaves. */
    boolean commit(String reservationId) {
        Reservation r = reservations.remove(reservationId);
        if (r == null) return false;                               // already expired or released
        StockItem item = stock.get(r.warehouseId() + ":" + r.productId());
        synchronized (item) {
            item.quantityReserved -= r.quantity();
            item.quantityOnHand  -= r.quantity();
        }
        ledger.add(new StockMovement(r.productId(), r.warehouseId(), MovementType.SHIPPED, r.quantity(), r.orderId(), clock.instant()));
        return true;
    }

    /** Scheduled every minute: return expired holds to available stock. */
    void releaseExpired() {
        Instant now = clock.instant();
        reservations.values().stream().filter(r -> !r.expiresAt().isAfter(now)).toList()
                .forEach(r -> release(r.id()));
    }

    void release(String reservationId) {
        Reservation r = reservations.remove(reservationId);        // remove() makes release idempotent
        if (r == null) return;
        StockItem item = stock.get(r.warehouseId() + ":" + r.productId());
        synchronized (item) { item.quantityReserved -= r.quantity(); }
        ledger.add(new StockMovement(r.productId(), r.warehouseId(), MovementType.RELEASED, r.quantity(), r.orderId(), clock.instant()));
    }
}

What this adds:

  • Expiry answers "what if the order is never confirmed?": stock returns automatically after 15 minutes.
  • Idempotent commit and release: reservations.remove() guarantees each reservation is committed or released exactly once, even if a late payment confirmation races with the expiry job. commit returning false tells the payment flow to re-reserve or refund.
  • The ledger (an append-only list of movements) makes every number explainable: on-hand stock is the sum of its movements. That's invaluable when a warehouse count disagrees with the system. Accounting systems work the same way.

Allocating across warehouses

When an order line can be fulfilled from several warehouses, choosing one is a policy, so make it a Strategy:

interface AllocationStrategy {
    List<Allocation> allocate(String productId, int qty, Address shipTo, List<StockItem> candidates);
}

class NearestFirstAllocation implements AllocationStrategy {
    public List<Allocation> allocate(String productId, int qty, Address shipTo, List<StockItem> candidates) {
        List<Allocation> result = new ArrayList<>();
        int remaining = qty;
        for (StockItem s : candidates.stream()
                .sorted(Comparator.comparingDouble(s -> distance(s.warehouse().location(), shipTo))).toList()) {
            int take = Math.min(remaining, s.available());
            if (take > 0) { result.add(new Allocation(s.warehouse().id(), take)); remaining -= take; }
            if (remaining == 0) return result;
        }
        return List.of();                                  // can't fully allocate: backorder or reject
    }
}

Alternatives: prefer a single warehouse even if it's farther (one shipment instead of two), or prefer warehouses with excess stock. The service reserves each allocation in turn, and releases all of them if any fails, as an all-or-nothing hold.

Reordering

The low-stock observer triggers replenishment. A simple, standard rule is the reorder point:

reorderPoint = averageDailyDemand × supplierLeadTimeDays + safetyStock

When available stock drops below the reorder point, create a purchase order for a quantity that brings stock back up to a target level. Guard against duplicate purchase orders by checking for an open one first, because the low-stock event may fire many times while stock stays low.

Follow-up questions this topic invites — and their answers

Q: What happens if reserveStock() succeeds but the order is never confirmed or explicitly released (e.g. the application crashes)? A: Same failure mode as any hold-with-expiry pattern in this course — a reservation needs a timeout/expiry mechanism (see Movie Ticket Booking System's scheduleExpiry discussion) so an abandoned reservation doesn't permanently lock stock away; a durable, sweep-based expiry (not just an in-process timer) matters even more here since inventory holds can be longer-lived than a seat-booking flow.

Q: Why lock per-StockItem rather than per-Warehouse? A: The same fine-grained-locking argument as Parking Lot — Implementation: locking the whole warehouse would serialize reservations across every unrelated product, while per-item locking only contends when two orders target the exact same product, letting unrelated products' reservations proceed fully in parallel.

Q: How would this design handle a single order needing multiple products from potentially different warehouses? A: Each product's reservation would need its own reserveStock() call, and the order as a whole should only confirm once every line's reservation succeeds (an all-or-nothing multi-item commit) — the same atomicity concern as Airline Booking's multi-leg itinerary, applied to order lines instead of flight legs.

Q: Does firing the low-stock event synchronously inside the synchronized block risk anything? A: Yes — a slow or blocking listener would hold the StockItem's lock for the listener's entire execution time, blocking other threads trying to reserve/release that same product. A safer design dispatches listener notifications asynchronously (via an executor, see ExecutorService & Thread Pools) after releasing the lock, rather than synchronously inside the critical section.

Q: Why keep an append-only ledger of stock movements instead of only current quantities? A: Quantities alone can't explain themselves. A ledger records every receipt, reservation, shipment and adjustment, with its reference, so discrepancies can be traced, reports can be rebuilt, and the current quantity can always be recomputed and verified.

Q: What happens if payment confirmation arrives after the reservation has expired? A: commit finds no reservation and returns false. The order flow should then try to reserve again, if stock is still available, or refund and notify the customer. It must never ship stock that was already released to someone else.

Q: How do you avoid creating duplicate purchase orders when stock stays low? A: Make replenishment idempotent per product and warehouse: before creating a purchase order, check for an open one, and include incoming quantities in the "available + on order" figure that's compared with the reorder point.

Advertisement

Previous

Design a Card Game Framework

Next

Restaurant Management System

AI Tutor

Lesson: Design an Inventory Management System

Quick actions

AI responses can be inaccurate. Verify critical information.