Inventory Management — Implementation¶
Multi-warehouse stock with a two-phase reserve → commit / release flow, all-or-nothing multi-line reservations, per-item locking that cannot oversell or deadlock, and pluggable allocation and reorder policies. Single file, stdlib only, Python 3.10+.
🧭 The model in one picture¶
Product ──1:N── InventoryItem ──N:1── Warehouse (one item per (sku, warehouse))
on_hand, reserved, available = on_hand - reserved
Reservation(order_id, lines[(sku, warehouse, qty)], expires_at, status)
ACTIVE ──commit──▶ COMMITTED (on_hand -= q, reserved -= q, SHIP movement)
│──release──▶ RELEASED (reserved -= q)
└──TTL──────▶ EXPIRED (reserved -= q; sweeper or lazily on commit)
Movement ledger: append-only, one row per physical change to on_hand
PurchaseOrder: OPEN ──▶ RECEIVED | CANCELLED
Invariant, per item, always true while its lock is free: 0 <= reserved <= on_hand.
🔑 Key design decisions¶
| Decision | Why |
|---|---|
| Reserve and commit are separate steps | Checkout needs a promise ("these units are yours for 15 minutes") before payment succeeds. Decrementing on_hand at checkout would make the physical count wrong; not tracking promises at all oversells. |
available is computed, never stored |
One less field to keep in sync. Everything that promises stock checks available; everything that ships consumes reserved. |
| Reservations have identity and a TTL | Commit and release act on the reservation, not "some quantity of SKU X", so a cancel can never release someone else's units. TTL bounds how long abandoned carts hold stock. |
One lock per InventoryItem, acquired in sorted (sku, warehouse_id) order |
Two orders for different SKUs run in parallel. A multi-line order locks all candidate items up front, so the check (_plan) and the act (reserve) happen under the same locks: no check-then-act race. Global lock ordering rules out deadlock (e.g. two opposite transfers). |
| Plan, then mutate | _plan raises InsufficientStockError before any item is touched, so a failed multi-line reservation leaves nothing behind. No compensation logic needed. |
Idempotent on order_id |
A client retry after a timeout returns the original reservation. Same order_id with different lines raises IdempotencyConflictError (the same rule Stripe applies to reused idempotency keys); a concurrent duplicate in flight also gets a conflict instead of a second reservation. |
| Commit checks the TTL itself | The sweeper is periodic, so an expired-but-not-yet-swept reservation must not be committable. commit expires it lazily and raises ReservationExpiredError. Both paths take reservation.lock, so commit and the sweeper cannot both win. |
| Transfers and write-offs only touch available units | You cannot move stock already promised to a customer. transfer_stock locks source and destination together, so a failure leaves both unchanged (the original version debited the source and then crashed on an unknown destination). |
| Inventory position = available + open PO quantity | Reorder decisions use position, not on-hand. Counting open POs is what stops check_reorder() from raising a duplicate PO every time it runs. |
Decimal for money, ManualClock for time |
No float rounding in valuation; tests and demo are deterministic. |
🧩 Where to extend¶
| Extension point | Built-in implementations | Typical interview "now add X" |
|---|---|---|
AllocationStrategy.allocate(qty, stock) |
SingleWarehouseFirstAllocation (default: fewest shipments), GreedySplitAllocation |
Nearest warehouse to the customer, cheapest shipping, keep safety stock per warehouse. It receives an immutable StockView snapshot taken under the locks, so it stays a pure function and is trivially unit-testable. |
ReorderPolicy.reorder_quantity(ctx) |
FixedReorderPolicy ((s, Q) policy), DemandBasedReorderPolicy (lead time + safety days, cover days) |
EOQ, seasonality, supplier minimums. Gets a ReorderContext with position and average daily demand computed from SHIP movements only (transfers are not demand). |
MovementType + _record |
RECEIVE, SHIP, TRANSFER_OUT/IN, ADJUST | Returns (RMA), damage write-offs, lot/expiry tracking. |
Not built, deliberately: partial commit (ship some lines of a reservation), backorders, lot/serial tracking, warehouse capacity. Each is a small addition to the structures above.
🔒 Concurrency walkthrough¶
| Lock | Guards | Held while acquiring |
|---|---|---|
_registry_lock |
product / warehouse / item dicts | nothing |
InventoryItem.lock |
that item's on_hand / reserved |
other item locks, in sorted order only; then _ledger_lock or _res_lock briefly |
Reservation.lock |
that reservation's status transition | item locks |
_res_lock |
reservation dicts, in-flight order ids | nothing |
_po_lock |
purchase orders | one item lock at a time; _ledger_lock |
The acquisition order is always Reservation.lock → item locks (sorted) → _res_lock / _ledger_lock, so there is no cycle. reserve() takes item locks and then _res_lock; commit() takes the reservation lock and then item locks; nothing takes a reservation lock while holding an item lock.
Reads such as available(sku) lock each item in turn, so the total is not a single atomic snapshot across warehouses. That is fine for display; anything that acts on availability goes through reserve().
💻 Full source¶
"""
Inventory Management System - Low Level Design
==============================================
Multi-warehouse stock with a two-phase reserve -> commit/release flow.
on_hand physical units on the shelf
reserved units promised to orders that have not shipped yet
available on_hand - reserved (what we may still promise)
Invariant, per (sku, warehouse), held under that item's lock:
0 <= reserved <= on_hand
Order flow:
reserve(order_id, {sku: qty}) -> Reservation (ACTIVE, has a TTL)
commit(reservation_id) -> stock leaves the building (on_hand and reserved drop)
release(reservation_id) -> order cancelled, units become available again
expire_reservations() -> sweeper releases ACTIVE reservations past their TTL
Concurrency model: one lock per InventoryItem. Any operation touching several
items acquires their locks in a global sorted order (sku, warehouse_id), so two
requests can never deadlock and a multi-line reservation is all-or-nothing.
Extension points (ABCs): AllocationStrategy (which warehouse fills a line) and
ReorderPolicy (when and how much to replenish).
Stdlib only, Python 3.10+.
"""
from __future__ import annotations
import itertools
import math
import threading
import time
from abc import ABC, abstractmethod
from contextlib import ExitStack
from dataclasses import dataclass, field
from decimal import Decimal
from enum import Enum
from typing import Callable, Dict, List, Mapping, Optional, Sequence, Tuple
Clock = Callable[[], float] # seconds since epoch; injectable for tests
SECONDS_PER_DAY = 86_400
# ---------------------------------------------------------------- errors
class InventoryError(Exception):
"""Base class for domain errors."""
class NotFoundError(InventoryError):
pass
class InsufficientStockError(InventoryError):
def __init__(self, sku: str, requested: int, available: int):
super().__init__(f"{sku}: requested {requested}, available {available}")
self.sku, self.requested, self.available = sku, requested, available
class InvalidStateError(InventoryError):
"""An operation is not allowed in the object's current state."""
class ReservationExpiredError(InvalidStateError):
pass
class IdempotencyConflictError(InventoryError):
"""Same order_id reused with different lines, or a concurrent duplicate is in flight."""
# ---------------------------------------------------------------- enums
class MovementType(Enum):
RECEIVE = "receive" # goods in (PO receipt, manual receipt)
SHIP = "ship" # committed reservation leaves the warehouse
TRANSFER_OUT = "transfer_out"
TRANSFER_IN = "transfer_in"
ADJUST = "adjust" # cycle-count correction, damage, shrinkage
class ReservationStatus(Enum):
ACTIVE = "active"
COMMITTED = "committed"
RELEASED = "released"
EXPIRED = "expired"
class POStatus(Enum):
OPEN = "open"
RECEIVED = "received"
CANCELLED = "cancelled"
# ---------------------------------------------------------------- value objects
@dataclass(frozen=True)
class Product:
sku: str
name: str
unit_price: Decimal
reorder_level: int = 10 # reorder when inventory position <= this
reorder_quantity: int = 50 # default / minimum order size
@dataclass(frozen=True)
class Warehouse:
warehouse_id: str
name: str
priority: int = 0 # lower = preferred (e.g. closer to customers, cheaper to ship)
@dataclass(frozen=True)
class Movement:
"""Immutable ledger entry. Only physical changes to on_hand are recorded."""
movement_id: int
sku: str
warehouse_id: str
type: MovementType
delta: int # signed change to on_hand
on_hand_after: int
reference: str
at: float
@dataclass(frozen=True)
class ReservationLine:
sku: str
warehouse_id: str
quantity: int
@dataclass(frozen=True)
class StockView:
"""What an AllocationStrategy is allowed to see (read under the item locks)."""
warehouse_id: str
priority: int
available: int
# ---------------------------------------------------------------- stock per (sku, warehouse)
class InventoryItem:
"""Stock of one product at one warehouse. All mutators require self.lock to be held."""
def __init__(self, product: Product, warehouse: Warehouse):
self.product = product
self.warehouse = warehouse
self.lock = threading.Lock()
self._on_hand = 0
self._reserved = 0
@property
def key(self) -> Tuple[str, str]:
return (self.product.sku, self.warehouse.warehouse_id)
@property
def on_hand(self) -> int:
return self._on_hand
@property
def reserved(self) -> int:
return self._reserved
@property
def available(self) -> int:
return self._on_hand - self._reserved
def receive(self, qty: int) -> None:
self._on_hand += qty
def correct(self, delta: int) -> None:
"""Cycle-count correction; the caller guarantees on_hand stays >= reserved."""
if self._on_hand + delta < self._reserved:
raise InvalidStateError(f"{self.key}: correction would drop on_hand below reserved")
self._on_hand += delta
def reserve(self, qty: int) -> None:
if qty > self.available:
raise InsufficientStockError(self.product.sku, qty, self.available)
self._reserved += qty
def release(self, qty: int) -> None:
if qty > self._reserved: # would mean a bookkeeping bug: fail loudly, never clamp
raise InvalidStateError(f"{self.key}: releasing {qty} but only {self._reserved} reserved")
self._reserved -= qty
def ship_reserved(self, qty: int) -> None:
"""Commit: units that were reserved physically leave."""
if qty > self._reserved:
raise InvalidStateError(f"{self.key}: shipping {qty} but only {self._reserved} reserved")
self._reserved -= qty
self._on_hand -= qty
def remove_available(self, qty: int) -> None:
"""Take unreserved units (transfer out, write-off). Never touches promised stock."""
if qty > self.available:
raise InsufficientStockError(self.product.sku, qty, self.available)
self._on_hand -= qty
def __repr__(self) -> str:
return (f"InventoryItem({self.product.sku}@{self.warehouse.warehouse_id}: "
f"on_hand={self._on_hand}, reserved={self._reserved})")
# ---------------------------------------------------------------- reservations & POs
@dataclass
class Reservation:
reservation_id: str
order_id: str
lines: Tuple[ReservationLine, ...]
expires_at: float
status: ReservationStatus = ReservationStatus.ACTIVE
lock: threading.Lock = field(default_factory=threading.Lock, repr=False, compare=False)
def requested(self) -> Dict[str, int]:
"""Lines collapsed back to {sku: qty}; used for idempotency comparison."""
out: Dict[str, int] = {}
for line in self.lines:
out[line.sku] = out.get(line.sku, 0) + line.quantity
return out
@dataclass
class PurchaseOrder:
po_id: str
sku: str
warehouse_id: str
quantity: int
status: POStatus = POStatus.OPEN
# ---------------------------------------------------------------- strategies
class AllocationStrategy(ABC):
"""Decides which warehouses fill a line. Pure function of the snapshot it is given."""
@abstractmethod
def allocate(self, qty: int, stock: Sequence[StockView]) -> Optional[List[Tuple[str, int]]]:
"""Return [(warehouse_id, qty), ...] summing to qty, or None if it cannot be filled."""
class GreedySplitAllocation(AllocationStrategy):
"""Fill from the most preferred warehouse first, spilling over to the next."""
def allocate(self, qty: int, stock: Sequence[StockView]) -> Optional[List[Tuple[str, int]]]:
plan, remaining = [], qty
for s in sorted(stock, key=lambda s: (s.priority, s.warehouse_id)):
take = min(remaining, s.available)
if take > 0:
plan.append((s.warehouse_id, take))
remaining -= take
if remaining == 0:
return plan
return None
class SingleWarehouseFirstAllocation(AllocationStrategy):
"""Prefer one warehouse that can fill the whole line (one shipment); split only if none can."""
def __init__(self) -> None:
self._fallback = GreedySplitAllocation()
def allocate(self, qty: int, stock: Sequence[StockView]) -> Optional[List[Tuple[str, int]]]:
for s in sorted(stock, key=lambda s: (s.priority, s.warehouse_id)):
if s.available >= qty:
return [(s.warehouse_id, qty)]
return self._fallback.allocate(qty, stock)
@dataclass(frozen=True)
class ReorderContext:
product: Product
warehouse_id: str
position: int # available + quantity on open POs
avg_daily_demand: float # shipped units / day over the lookback window
class ReorderPolicy(ABC):
@abstractmethod
def reorder_quantity(self, ctx: ReorderContext) -> int:
"""Units to order now; 0 means no reorder."""
class FixedReorderPolicy(ReorderPolicy):
"""Classic (s, Q): when position <= reorder_level, order reorder_quantity."""
def reorder_quantity(self, ctx: ReorderContext) -> int:
return ctx.product.reorder_quantity if ctx.position <= ctx.product.reorder_level else 0
class DemandBasedReorderPolicy(ReorderPolicy):
"""Reorder point = demand over (lead time + safety days); order enough for cover_days."""
def __init__(self, lead_time_days: int = 7, safety_days: int = 3, cover_days: int = 14):
self._lead, self._safety, self._cover = lead_time_days, safety_days, cover_days
def reorder_quantity(self, ctx: ReorderContext) -> int:
reorder_point = max(ctx.product.reorder_level,
math.ceil(ctx.avg_daily_demand * (self._lead + self._safety)))
if ctx.position > reorder_point:
return 0
target = math.ceil(ctx.avg_daily_demand * self._cover)
return max(ctx.product.reorder_quantity, target - ctx.position)
# ---------------------------------------------------------------- service
class InventoryService:
def __init__(self,
allocation: Optional[AllocationStrategy] = None,
reorder_policy: Optional[ReorderPolicy] = None,
reservation_ttl_s: float = 15 * 60,
demand_lookback_days: int = 30,
clock: Clock = time.time):
self._allocation = allocation or SingleWarehouseFirstAllocation()
self._reorder_policy = reorder_policy or FixedReorderPolicy()
self._ttl = reservation_ttl_s
self._lookback_days = demand_lookback_days
self._clock = clock
# Catalog + item registry. _registry_lock guards these dicts, not item contents.
self._registry_lock = threading.Lock()
self._products: Dict[str, Product] = {}
self._warehouses: Dict[str, Warehouse] = {}
self._items: Dict[Tuple[str, str], InventoryItem] = {}
# Reservations. _res_lock guards the dicts; each Reservation has its own lock for transitions.
self._res_lock = threading.Lock()
self._reservations: Dict[str, Reservation] = {}
self._by_order: Dict[str, Reservation] = {}
self._in_flight_orders: set[str] = set()
self._po_lock = threading.Lock()
self._purchase_orders: Dict[str, PurchaseOrder] = {}
self._ledger_lock = threading.Lock()
self._ledger: List[Movement] = [] # append-only; in production this is a DB table
self._ids = itertools.count(1)
# ---- catalog -------------------------------------------------------
def add_product(self, product: Product) -> Product:
if product.unit_price < 0:
raise ValueError("unit_price must be >= 0")
with self._registry_lock:
if product.sku in self._products:
raise InventoryError(f"duplicate sku {product.sku}")
self._products[product.sku] = product
return product
def add_warehouse(self, warehouse: Warehouse) -> Warehouse:
with self._registry_lock:
if warehouse.warehouse_id in self._warehouses:
raise InventoryError(f"duplicate warehouse {warehouse.warehouse_id}")
self._warehouses[warehouse.warehouse_id] = warehouse
return warehouse
# ---- stock in / out ------------------------------------------------
def receive_stock(self, sku: str, warehouse_id: str, qty: int, reference: str = "") -> InventoryItem:
_require_positive(qty)
item = self._get_or_create_item(sku, warehouse_id)
with item.lock:
item.receive(qty)
self._record(item, MovementType.RECEIVE, qty, reference)
return item
def adjust_stock(self, sku: str, warehouse_id: str, counted_on_hand: int, reason: str) -> int:
"""Cycle count: set on_hand to what was physically counted. Returns the delta.
Refuses to drop below what is already promised; in production this raises an
incident instead, because those reservations are now oversold.
"""
if counted_on_hand < 0:
raise ValueError("counted_on_hand must be >= 0")
item = self._get_item(sku, warehouse_id)
with item.lock:
if counted_on_hand < item.reserved:
raise InsufficientStockError(sku, item.reserved, counted_on_hand)
delta = counted_on_hand - item.on_hand
if delta:
item.correct(delta)
self._record(item, MovementType.ADJUST, delta, reason)
return delta
def transfer_stock(self, sku: str, from_wh: str, to_wh: str, qty: int) -> None:
"""Atomic move of *available* units. Both items are locked in key order, so either
both sides change or neither does, and two opposite transfers cannot deadlock."""
_require_positive(qty)
if from_wh == to_wh:
raise ValueError("source and destination are the same warehouse")
src = self._get_item(sku, from_wh)
dst = self._get_or_create_item(sku, to_wh) # validates to_wh before anything moves
with self._lock_all([src, dst]):
src.remove_available(qty)
dst.receive(qty)
ref = f"transfer {from_wh}->{to_wh}"
self._record(src, MovementType.TRANSFER_OUT, -qty, ref)
self._record(dst, MovementType.TRANSFER_IN, qty, ref)
# ---- reservations: reserve -> commit | release | expire -------------
def reserve(self, order_id: str, request: Mapping[str, int]) -> Reservation:
"""All-or-nothing reservation of every line, possibly split across warehouses.
Idempotent on order_id: a retry with the same lines returns the original reservation.
"""
if not request:
raise ValueError("empty request")
for qty in request.values():
_require_positive(qty)
wanted = dict(request)
with self._res_lock:
existing = self._by_order.get(order_id)
if existing is not None:
if existing.requested() != wanted:
raise IdempotencyConflictError(f"order {order_id} already reserved with different lines")
return existing
if order_id in self._in_flight_orders:
raise IdempotencyConflictError(f"order {order_id} is already being reserved")
self._in_flight_orders.add(order_id)
try:
items = self._items_for_skus(wanted.keys())
with self._lock_all(items):
lines = self._plan(wanted, items) # raises before any mutation
by_key = {i.key: i for i in items}
for line in lines:
by_key[(line.sku, line.warehouse_id)].reserve(line.quantity)
reservation = Reservation(f"RSV-{next(self._ids)}", order_id, tuple(lines),
expires_at=self._clock() + self._ttl)
with self._res_lock:
self._reservations[reservation.reservation_id] = reservation
self._by_order[order_id] = reservation
return reservation
finally:
with self._res_lock:
self._in_flight_orders.discard(order_id)
def commit(self, reservation_id: str, reference: str = "") -> Reservation:
"""Order shipped/paid: reserved units physically leave. ACTIVE -> COMMITTED only."""
res = self._get_reservation(reservation_id)
with res.lock:
if res.status is ReservationStatus.COMMITTED:
return res # idempotent retry
if res.status is ReservationStatus.ACTIVE and self._clock() >= res.expires_at:
self._release_locked(res, ReservationStatus.EXPIRED)
if res.status is ReservationStatus.EXPIRED:
raise ReservationExpiredError(f"{reservation_id} expired")
if res.status is not ReservationStatus.ACTIVE:
raise InvalidStateError(f"{reservation_id} is {res.status.value}")
items = {l: self._get_item(l.sku, l.warehouse_id) for l in res.lines}
with self._lock_all(list(items.values())):
for line, item in items.items():
item.ship_reserved(line.quantity)
self._record(item, MovementType.SHIP, -line.quantity, reference or res.order_id)
res.status = ReservationStatus.COMMITTED
return res
def release(self, reservation_id: str) -> Reservation:
"""Order cancelled: give units back. Idempotent; refuses to un-ship a committed order."""
res = self._get_reservation(reservation_id)
with res.lock:
if res.status in (ReservationStatus.RELEASED, ReservationStatus.EXPIRED):
return res
if res.status is ReservationStatus.COMMITTED:
raise InvalidStateError(f"{reservation_id} already committed; use a return flow")
self._release_locked(res, ReservationStatus.RELEASED)
return res
def expire_reservations(self) -> List[Reservation]:
"""Sweeper: release ACTIVE reservations whose TTL has passed."""
now = self._clock()
with self._res_lock:
candidates = [r for r in self._reservations.values()
if r.status is ReservationStatus.ACTIVE and r.expires_at <= now]
expired = []
for res in candidates:
with res.lock: # re-check: a commit may have won the race since the snapshot
if res.status is ReservationStatus.ACTIVE:
self._release_locked(res, ReservationStatus.EXPIRED)
expired.append(res)
return expired
# ---- replenishment ---------------------------------------------------
def check_reorder(self) -> List[PurchaseOrder]:
"""Raise POs where the policy says so. Open POs count toward the inventory position,
so calling this repeatedly does not create duplicate orders."""
created = []
with self._po_lock:
for item in self._all_items():
with item.lock:
available = item.available
sku, wh = item.key
on_order = sum(po.quantity for po in self._purchase_orders.values()
if po.status is POStatus.OPEN and (po.sku, po.warehouse_id) == item.key)
ctx = ReorderContext(item.product, wh, available + on_order, self._avg_daily_demand(sku, wh))
qty = self._reorder_policy.reorder_quantity(ctx)
if qty > 0:
po = PurchaseOrder(f"PO-{next(self._ids)}", sku, wh, qty)
self._purchase_orders[po.po_id] = po
created.append(po)
return created
def receive_purchase_order(self, po_id: str) -> PurchaseOrder:
with self._po_lock:
po = self._purchase_orders.get(po_id)
if po is None:
raise NotFoundError(po_id)
if po.status is not POStatus.OPEN:
raise InvalidStateError(f"{po_id} is {po.status.value}") # no double receipt
po.status = POStatus.RECEIVED
self.receive_stock(po.sku, po.warehouse_id, po.quantity, reference=po_id)
return po
def cancel_purchase_order(self, po_id: str) -> PurchaseOrder:
with self._po_lock:
po = self._purchase_orders.get(po_id)
if po is None:
raise NotFoundError(po_id)
if po.status is not POStatus.OPEN:
raise InvalidStateError(f"{po_id} is {po.status.value}")
po.status = POStatus.CANCELLED
return po
# ---- queries -----------------------------------------------------------
def stock(self, sku: str, warehouse_id: str) -> Tuple[int, int, int]:
"""(on_hand, reserved, available), read consistently under the item lock."""
item = self._get_item(sku, warehouse_id)
with item.lock:
return item.on_hand, item.reserved, item.available
def available(self, sku: str) -> int:
"""Total available across warehouses (each item read under its own lock)."""
total = 0
for item in self._items_for_skus([sku]):
with item.lock:
total += item.available
return total
def inventory_value(self) -> Decimal:
total = Decimal("0")
for item in self._all_items():
with item.lock:
total += item.product.unit_price * item.on_hand
return total
def movements(self, sku: Optional[str] = None) -> List[Movement]:
with self._ledger_lock:
return [m for m in self._ledger if sku is None or m.sku == sku]
# ---- internals -----------------------------------------------------------
def _plan(self, wanted: Mapping[str, int], items: Sequence[InventoryItem]) -> List[ReservationLine]:
lines: List[ReservationLine] = []
for sku, qty in sorted(wanted.items()):
views = [StockView(i.warehouse.warehouse_id, i.warehouse.priority, i.available)
for i in items if i.product.sku == sku]
plan = self._allocation.allocate(qty, views)
if plan is None:
raise InsufficientStockError(sku, qty, sum(v.available for v in views))
lines.extend(ReservationLine(sku, wh, q) for wh, q in plan)
return lines
def _release_locked(self, res: Reservation, final: ReservationStatus) -> None:
"""Caller holds res.lock and has checked res is ACTIVE."""
items = {l: self._get_item(l.sku, l.warehouse_id) for l in res.lines}
with self._lock_all(list(items.values())):
for line, item in items.items():
item.release(line.quantity)
res.status = final
@staticmethod
def _lock_all(items: Sequence[InventoryItem]) -> ExitStack:
"""Acquire item locks in a single global order (sorted, de-duplicated) to rule out deadlock."""
stack = ExitStack()
for item in sorted({i.key: i for i in items}.values(), key=lambda i: i.key):
stack.enter_context(item.lock)
return stack
def _record(self, item: InventoryItem, mtype: MovementType, delta: int, reference: str) -> None:
with self._ledger_lock:
self._ledger.append(Movement(next(self._ids), item.product.sku, item.warehouse.warehouse_id,
mtype, delta, item.on_hand, reference, self._clock()))
def _avg_daily_demand(self, sku: str, warehouse_id: str) -> float:
since = self._clock() - self._lookback_days * SECONDS_PER_DAY
with self._ledger_lock:
shipped = sum(-m.delta for m in self._ledger
if m.type is MovementType.SHIP and m.sku == sku
and m.warehouse_id == warehouse_id and m.at >= since)
return shipped / self._lookback_days
def _get_or_create_item(self, sku: str, warehouse_id: str) -> InventoryItem:
with self._registry_lock:
product = self._products.get(sku)
warehouse = self._warehouses.get(warehouse_id)
if product is None or warehouse is None:
raise NotFoundError(f"unknown sku {sku!r} or warehouse {warehouse_id!r}")
item = self._items.get((sku, warehouse_id))
if item is None:
item = self._items[(sku, warehouse_id)] = InventoryItem(product, warehouse)
return item
def _get_item(self, sku: str, warehouse_id: str) -> InventoryItem:
with self._registry_lock:
item = self._items.get((sku, warehouse_id))
if item is None:
raise NotFoundError(f"no stock record for {sku!r} at {warehouse_id!r}")
return item
def _items_for_skus(self, skus) -> List[InventoryItem]:
skus = set(skus)
with self._registry_lock:
unknown = skus - self._products.keys()
if unknown:
raise NotFoundError(f"unknown sku(s) {sorted(unknown)}")
return [i for k, i in self._items.items() if k[0] in skus]
def _all_items(self) -> List[InventoryItem]:
with self._registry_lock:
return sorted(self._items.values(), key=lambda i: i.key)
def _get_reservation(self, reservation_id: str) -> Reservation:
with self._res_lock:
res = self._reservations.get(reservation_id)
if res is None:
raise NotFoundError(reservation_id)
return res
def _require_positive(qty: int) -> None:
if not isinstance(qty, int) or isinstance(qty, bool) or qty <= 0:
raise ValueError(f"quantity must be a positive int, got {qty!r}")
class ManualClock:
"""Deterministic clock for the demo and tests."""
def __init__(self, start: float = 1_700_000_000.0):
self.now = start
def __call__(self) -> float:
return self.now
def advance(self, seconds: float) -> None:
self.now += seconds
# ---------------------------------------------------------------- demo
def demo() -> None:
clock = ManualClock()
inv = InventoryService(reservation_ttl_s=600, clock=clock)
inv.add_product(Product("LAP-1", "Laptop", Decimal("1200.00"), reorder_level=5, reorder_quantity=20))
inv.add_product(Product("PHN-1", "Phone", Decimal("799.99"), reorder_level=10, reorder_quantity=30))
inv.add_warehouse(Warehouse("BLR", "Bangalore", priority=0))
inv.add_warehouse(Warehouse("CCU", "Kolkata", priority=1))
inv.receive_stock("LAP-1", "BLR", 8)
inv.receive_stock("LAP-1", "CCU", 6)
inv.receive_stock("PHN-1", "BLR", 40)
print("Laptops available:", inv.available("LAP-1")) # 14
# 1. Multi-line order, laptops split across warehouses because BLR alone has only 8.
r1 = inv.reserve("order-1", {"LAP-1": 10, "PHN-1": 2})
print("order-1 lines:", [(l.sku, l.warehouse_id, l.quantity) for l in r1.lines])
assert inv.reserve("order-1", {"LAP-1": 10, "PHN-1": 2}) is r1 # idempotent retry
# 2. All-or-nothing: not enough laptops left, nothing gets reserved.
try:
inv.reserve("order-2", {"PHN-1": 1, "LAP-1": 5})
except InsufficientStockError as e:
print("order-2 rejected:", e)
assert inv.stock("PHN-1", "BLR") == (40, 2, 38)
# 3. Commit order-1: on_hand drops, reservation consumed.
inv.commit(r1.reservation_id)
print("after commit LAP-1@BLR/CCU:", inv.stock("LAP-1", "BLR"), inv.stock("LAP-1", "CCU"))
# 4. A reservation that is never paid for expires and gives its stock back.
r3 = inv.reserve("order-3", {"PHN-1": 5})
clock.advance(601)
print("expired:", [r.order_id for r in inv.expire_reservations()], "->", inv.stock("PHN-1", "BLR"))
# 5. Transfer, then reorder (open POs count toward position, so a second check adds nothing).
inv.transfer_stock("PHN-1", "BLR", "CCU", 10)
pos = inv.check_reorder()
print("POs:", [(p.po_id, p.sku, p.warehouse_id, p.quantity) for p in pos])
assert inv.check_reorder() == []
for po in pos:
inv.receive_purchase_order(po.po_id)
print("inventory value:", inv.inventory_value())
print("ledger:", len(inv.movements()), "movements;", r3.status.value)
if __name__ == "__main__":
demo()
▶️ How to Run¶
cd python-low-level-design/inventory-management
python3 inventory_management.py # deterministic demo
python3 -m unittest test_inventory_management -v # 25 tests, including concurrency
The tests cover reserve/commit/release/expire, all-or-nothing multi-line orders, split allocation, idempotent retries, transfers that cannot lose stock, cycle-count adjustments, duplicate-PO prevention, demand-based reorder, and four concurrency cases: 40 threads racing for 50 units (exactly 25 succeed), opposite transfers (no deadlock, stock conserved), commit vs. expiry sweeper (exactly one winner), and concurrent duplicate order_id (reserved once).