bigonotes
CS ConceptsLLD

Inventory Management

Design a single-warehouse inventory that adds products, adjusts stock without going negative, and raises low-stock alerts.

4 min readLast updated LLD

Inventory management looks like CRUD, but the interview is really about invariants: stock never goes negative, every product has a count and a threshold, and a low-stock alert fires when it should. Keeping product catalog data separate from stock levels makes those rules easy to enforce in one place.

Requirements#

Keep the scope to one warehouse and a handful of operations:

  1. Store products of different types and prices.
  2. Add a new product with an initial quantity and a low-stock threshold.
  3. Add stock and remove stock for a product.
  4. Check a product's current quantity.
  5. Raise an alert when stock drops below its threshold.

Actor: an admin.

Clarifications: many products, one warehouse; failure cases are unknown products, non-positive quantities, and removing more than is in stock.

Out of scope: UI, concurrency, persistence, orders.

Core entities#

Three classes split catalog data from stock levels:

Entity Holds Responsibility
Product id, name, description, price Catalog data (rarely changes)
Stock count: product_id → int, threshold: product_id → int Quantities and their invariants
Inventory products map, a Stock Public API; validates input; triggers alerts

Relationships#

Inventory is the orchestrator:

  • Inventory → Product (holds the catalog).
  • Inventory → Stock (composition).
  • Stock refers to products by id only, so it stays a simple, testable data structure.

Key flows#

Each flow validates input first, then lets Stock enforce the invariant.

Add product: reject duplicates → store the product → create its count and threshold in Stock.

Remove stock:

  1. Validate the product and quantity.
  2. Call Stock.adjust(id, -n), which refuses to go below zero.
  3. If the new count is below threshold, raise a low-stock alert.

Add stock: validate → Stock.adjust(id, +n).

Code#

A straightforward Python version:

from typing import Dict, Optional


class Product:
    def __init__(self, product_id: str, name: str, description: str,
                 price: float):
        self.id = product_id
        self.name = name
        self.description = description
        self.price = price


class Stock:
    def __init__(self):
        self.count: Dict[str, int] = {}
        self.threshold: Dict[str, int] = {}

    def add(self, product_id: str, count: int, threshold: int) -> bool:
        if product_id in self.count:
            return False
        self.count[product_id] = count
        self.threshold[product_id] = threshold
        return True

    def adjust(self, product_id: str, delta: int) -> bool:
        if product_id not in self.count:
            return False
        if self.count[product_id] + delta < 0:  # invariant: never negative
            return False
        self.count[product_id] += delta
        return True

    def get(self, product_id: str) -> Optional[int]:
        return self.count.get(product_id)

    def is_low(self, product_id: str) -> bool:
        return (product_id in self.count
                and self.count[product_id] < self.threshold[product_id])


class Inventory:
    def __init__(self):
        self.products: Dict[str, Product] = {}
        self.stock = Stock()

    def add_product(self, product: Product, count: int,
                    threshold: int) -> bool:
        if product.id in self.products or count < 0 or threshold < 0:
            return False
        self.products[product.id] = product
        return self.stock.add(product.id, count, threshold)

    def add_stock(self, product_id: str, count: int) -> bool:
        if product_id not in self.products or count <= 0:
            return False
        return self.stock.adjust(product_id, count)

    def remove_stock(self, product_id: str, count: int) -> bool:
        if product_id not in self.products or count <= 0:
            return False
        if not self.stock.adjust(product_id, -count):  # rejects over-removal
            return False
        if self.stock.is_low(product_id):
            self._alert_low_stock(product_id)
        return True

    def check_stock(self, product_id: str) -> Optional[int]:
        if product_id not in self.products:
            return None
        return self.stock.get(product_id)

    def _alert_low_stock(self, product_id: str) -> None:
        print(f"LOW STOCK: {product_id}")  # stand-in for a real notifier


if __name__ == "__main__":
    inv = Inventory()
    widget = Product("1", "Widget", "small widget", 20.0)
    gadget = Product("2", "Gadget", "big gadget", 35.0)
    print(inv.add_product(widget, 10, 5))  # True
    print(inv.add_product(gadget, 15, 5))  # True

    print(inv.check_stock("1"))       # 10
    print(inv.remove_stock("1", 7))   # prints LOW STOCK: 1, then True
    print(inv.check_stock("1"))       # 3
    print(inv.remove_stock("1", 99))  # False: would go negative
    print(inv.check_stock("1"))       # 3
    print(inv.add_stock("2", 10))     # True
    print(inv.check_stock("2"))       # 25

Extensions and trade-offs#

These are the usual follow-up questions:

  • Two dicts vs a StockItem object: parallel count and threshold maps are quick to write but must be kept in sync. A StockItem(count, threshold) per product keeps them together and is easier to extend (reserved quantity, location).
  • Pluggable alerts: replace the print with an AlertListener interface (email, Slack, reorder service) and notify all listeners (Observer). Debounce so you don't alert on every removal below threshold.
  • Multiple warehouses: key stock by (warehouse_id, product_id), or give each Warehouse its own Stock; add transfer between warehouses as remove + add in one transaction.
  • Reservations: for orders, track reserved separately from on_hand so items in carts aren't double-sold.
  • Concurrency: adjust is check-then-act; two removals at once could both pass the "won't go negative" check. Use a per-product lock or an atomic database update (UPDATE ... SET count = count - n WHERE count >= n).
  • Audit log: record every adjustment with a reason, so counts can be reconciled.

Key takeaways#

  • Separate catalog data (Product) from changing quantities (Stock).
  • Enforce the "never negative" invariant in one place.
  • Check the threshold after every removal and alert when stock is below it.
  • Concurrency and multi-warehouse are the standard follow-ups.

Found a mistake? Tell me on LinkedIn ↗

Further reading#