What you’re tracking
Fixed at startup
Add / remove
Atomic move
Per WH threshold
Analogy: two warehouses, one shared ledger rule
Transferring stock A→B while someone else does B→A is two people each holding one warehouse key and waiting for the other — deadlock. Agree to always pick up keys in warehouse-id order first.
Clarifying questions → locked requirements
- Warehouses? → Fixed set at init (no dynamic warehouse CRUD).
- Low-stock? → Per product per warehouse; pluggable callback, not email wiring.
- Negative stock? → Reject — remove/transfer must fail cleanly.
- Concurrency? → Thread-safe from the start (receive + fulfill race).
- Out of scope? → Catalog, orders/payments, persistence.
Entities
- InventoryManager — public API; map warehouseId → Warehouse.
- Warehouse — inventory map, alert configs, no-negative rule.
- AlertConfig — threshold + listener value object.
- AlertListener — Observer interface
onLowStock(warehouseId, productId, qty). - Not entities: Product (external key), Order, Payment.
Class design
class InventoryManager:
- warehouses: dict[str, Warehouse]
+ InventoryManager(warehouse_ids)
+ add_stock(wh, product, qty) -> None
+ remove_stock(wh, product, qty) -> bool
+ transfer(product, from_wh, to_wh, qty) -> bool
+ warehouses_with_availability(product, qty) -> list[str]
+ set_low_stock_alert(wh, product, threshold, listener) -> None
class Warehouse:
- id: str
- inventory: dict[str, int]
- alert_configs: dict[str, list[AlertConfig]]
+ add_stock / remove_stock / get_stock / check_availability
+ set_low_stock_alert
- _alerts_to_fire(product, prev, new) -> list # collect under lock
class AlertConfig:
- threshold: int
- listener: AlertListener
class AlertListener(Protocol):
def on_low_stock(self, warehouse_id, product_id, current_qty) -> None: ...
class InventoryManager {
Map<String, Warehouse> warehouses;
InventoryManager(List<String> warehouseIds) { /* ... */ }
void addStock(String wh, String product, int qty) { /* ... */ }
boolean removeStock(String wh, String product, int qty) { /* ... */ return false; }
boolean transfer(String product, String fromWh, String toWh, int qty) { /* ... */ return false; }
List<String> warehousesWithAvailability(String product, int qty) { /* ... */ return List.of(); }
void setLowStockAlert(String wh, String product, int threshold, AlertListener listener) { /* ... */ }
}
class Warehouse {
String id;
Map<String, Integer> inventory;
Map<String, List<AlertConfig>> alertConfigs;
void addStock(String product, int qty) { /* ... */ }
boolean removeStock(String product, int qty) { /* ... */ return false; }
int getStock(String product) { /* ... */ return 0; }
boolean checkAvailability(String product, int qty) { /* ... */ return false; }
void setLowStockAlert(String product, int threshold, AlertListener listener) { /* ... */ }
// collect under lock
List<Object[]> alertsToFire(String product, int prev, int neu) { /* ... */ return List.of(); }
}
class AlertConfig {
int threshold;
AlertListener listener;
}
interface AlertListener {
void onLowStock(String warehouseId, String productId, int currentQty);
}
InventoryManager — especially transfer
def transfer(self, product: str, from_id: str, to_id: str, qty: int) -> bool:
if qty <= 0 or from_id == to_id:
return False
src, dst = self.warehouses.get(from_id), self.warehouses.get(to_id)
if src is None or dst is None:
return False
# Consistent lock order → no deadlock A↔B
first, second = (src, dst) if from_id < to_id else (dst, src)
with first._lock:
with second._lock:
if not src.remove_stock(product, qty): # reentrant lock assumed
return False
dst.add_stock(product, qty)
return True
boolean transfer(String product, String fromId, String toId, int qty) {
if (qty <= 0 || fromId.equals(toId)) return false;
Warehouse src = warehouses.get(fromId), dst = warehouses.get(toId);
if (src == null || dst == null) return false;
// Consistent lock order → no deadlock A↔B
Warehouse first = fromId.compareTo(toId) < 0 ? src : dst;
Warehouse second = fromId.compareTo(toId) < 0 ? dst : src;
first.lock.lock();
try {
second.lock.lock();
try {
if (!src.removeStock(product, qty)) return false; // reentrant lock assumed
dst.addStock(product, qty);
return true;
} finally {
second.lock.unlock();
}
} finally {
first.lock.unlock();
}
}
Warehouse — locks, alerts outside critical section
prev >= threshold and new < threshold) — no spam while already low; recovers when stock climbs above then drops again. No hasFired flag needed.def add_stock(self, product: str, qty: int) -> None:
if qty <= 0:
raise ValueError("qty")
to_fire = []
with self._lock:
prev = self.inventory.get(product, 0)
new = prev + qty
self.inventory[product] = new
to_fire = self._alerts_to_fire(product, prev, new)
for listener, p, q in to_fire: # OUTSIDE lock — no I/O under mutex
listener.on_low_stock(self.id, p, q)
def remove_stock(self, product: str, qty: int) -> bool:
if qty <= 0:
return False
to_fire = []
with self._lock:
prev = self.inventory.get(product, 0)
if prev < qty:
return False
new = prev - qty
self.inventory[product] = new
to_fire = self._alerts_to_fire(product, prev, new)
for listener, p, q in to_fire:
listener.on_low_stock(self.id, p, q)
return True
def _alerts_to_fire(self, product, prev, new):
out = []
for cfg in self.alert_configs.get(product, []):
if prev >= cfg.threshold and new < cfg.threshold:
out.append((cfg.listener, product, new))
return out
void addStock(String product, int qty) {
if (qty <= 0) throw new IllegalArgumentException("qty");
List<Object[]> toFire = List.of();
lock.lock();
try {
int prev = inventory.getOrDefault(product, 0);
int neu = prev + qty;
inventory.put(product, neu);
toFire = alertsToFire(product, prev, neu);
} finally {
lock.unlock();
}
for (Object[] t : toFire) // OUTSIDE lock — no I/O under mutex
((AlertListener) t[0]).onLowStock(id, (String) t[1], (int) t[2]);
}
boolean removeStock(String product, int qty) {
if (qty <= 0) return false;
List<Object[]> toFire = List.of();
lock.lock();
try {
int prev = inventory.getOrDefault(product, 0);
if (prev < qty) return false;
int neu = prev - qty;
inventory.put(product, neu);
toFire = alertsToFire(product, prev, neu);
} finally {
lock.unlock();
}
for (Object[] t : toFire)
((AlertListener) t[0]).onLowStock(id, (String) t[1], (int) t[2]);
return true;
}
List<Object[]> alertsToFire(String product, int prev, int neu) {
List<Object[]> out = new ArrayList<>();
for (AlertConfig cfg : alertConfigs.getOrDefault(product, List.of())) {
if (prev >= cfg.threshold && neu < cfg.threshold)
out.add(new Object[]{ cfg.listener, product, neu });
}
return out;
}
Verification
- Alert crossing — 15→9 fires (threshold 10); 9→7 silent; 7→22 silent; 22→9 fires again.
- Atomic transfer — EAST 7 → WEST 0, transfer 5 → EAST 2 / WEST 5; both locked for the whole move.
- No negative — transfer 10 from EAST(2) fails; state unchanged; availability query returns only warehouses with enough stock.
- Happy: Warehouse A has 10; transfer 4 to B → A=6,B+=4; total constant.
- Failure: transfer 50 when A has 6 → reject; allocate more than available → reject.
- Concurrency: allocate and transfer on same SKU — ordered locks prevent deadlock and negative stock.
Extensibility
reserved separately; available = on_hand - reserved. reserve → confirm → release with TTL + background expiry — same shape as movie seat holds. Prevents “filled the form then OOS.”Transfer as an InventoryHolder (same add/remove/get interface as Warehouse). initiate removes from source into Transfer; complete moves Transfer → destination. Stock always accounted for; in-transit not sellable.Common interview pitfalls
These mistakes show up constantly on this prompt. Name the trap, then show the fix in your design — don’t wait for the interviewer to catch you.
- Single integer stock with no warehouse dimension — transfers become hand-waves.
- Negative stock possible because decrement isn’t atomic with the check.
- Observer/low-stock alerts bolted on before the transfer invariant works.
- Transfer that decrements source after incrementing dest (or vice versa) without a transaction narrative.
- SKU vs WarehouseStock confusion — who owns quantity?
- Ignoring idempotency of restock/allocate under retries.
Interview script (say this)
Read this once out loud before a mock. It’s the spine of a strong answer — not a script to recite robotically.
- Clarify SKUs, multi-warehouse, allocate/reserve vs commit, transfer, low-stock threshold.
- v1: InventoryManager + Warehouse; quantities per (warehouse, sku); atomic transfer.
- APIs: add_stock, allocate, transfer, get_availability.
- Invariants: never negative; transfer conserves total unless damage/shrink write-off.
- Optional Observer for low-stock — after correctness.
- Trace: transfer 5 units A→B; concurrent allocate on source.
- Lock strategy: lock manager or ordered warehouse locks to avoid deadlock.
Extra verification traces
Walk these three traces on the board. If you can narrate them cleanly, your implementation section usually follows.
def transfer(self, sku, src, dst, qty):
for wh_id in sorted([src, dst]):
self.warehouses[wh_id].lock.acquire()
try:
self.warehouses[src].remove(sku, qty) # raises if insufficient
self.warehouses[dst].add(sku, qty)
finally:
for wh_id in sorted([src, dst], reverse=True):
self.warehouses[wh_id].lock.release()
void transfer(String sku, String src, String dst, int qty) {
List<String> order = Arrays.asList(src, dst);
Collections.sort(order);
for (String whId : order) warehouses.get(whId).lock.lock();
try {
warehouses.get(src).remove(sku, qty); // throws if insufficient
warehouses.get(dst).add(sku, qty);
} finally {
Collections.reverse(order);
for (String whId : order) warehouses.get(whId).lock.unlock();
}
}
Staff-level follow-ups
At staff+, they twist the prompt. Answer in one sentence that names the seam — don’t redesign the whole board.
- Reservations for orders? — RESERVED vs ON_HAND buckets; expire reservations like seat holds.
- Multi-region? — Shard by warehouse; transfers become async jobs with outbox.
- Lot/expiry tracking? — StockLayer under WarehouseStock; allocate FEFO strategy.
- Audit? — Append-only StockLedger entries for every mutation.
Complete solution: stock + transfer
from threading import Lock
from typing import Callable, Optional
class Warehouse:
def __init__(self, wid: str):
self.id = wid
self.stock: dict[str, int] = {}
self.lock = Lock()
self.alerts: dict[str, tuple[int, Callable]] = {}
def add(self, sku: str, n: int) -> None:
with self.lock:
self.stock[sku] = self.stock.get(sku, 0) + n
self._maybe_alert(sku)
def remove(self, sku: str, n: int) -> bool:
with self.lock:
if self.stock.get(sku, 0) < n:
return False
self.stock[sku] -= n
self._maybe_alert(sku)
return True
def _maybe_alert(self, sku: str) -> None:
if sku in self.alerts:
thr, cb = self.alerts[sku]
if self.stock.get(sku, 0) <= thr:
cb(self.id, sku, self.stock.get(sku, 0))
class InventoryManager:
def __init__(self, warehouses: list[Warehouse]):
self.wh = {w.id: w for w in warehouses}
def transfer(self, src: str, dst: str, sku: str, n: int) -> bool:
a, b = self.wh[src], self.wh[dst]
first, second = (a, b) if a.id <= b.id else (b, a)
with first.lock:
with second.lock:
if a.stock.get(sku, 0) < n:
return False
a.stock[sku] -= n
b.stock[sku] = b.stock.get(sku, 0) + n
return True
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
@FunctionalInterface
interface LowStockCallback {
void onLowStock(String warehouseId, String sku, int qty);
}
class Warehouse {
String id;
Map<String, Integer> stock = new HashMap<>();
final ReentrantLock lock = new ReentrantLock();
Map<String, Object[]> alerts = new HashMap<>(); // sku → {threshold, callback}
Warehouse(String wid) { this.id = wid; }
void add(String sku, int n) {
lock.lock();
try {
stock.put(sku, stock.getOrDefault(sku, 0) + n);
maybeAlert(sku);
} finally {
lock.unlock();
}
}
boolean remove(String sku, int n) {
lock.lock();
try {
if (stock.getOrDefault(sku, 0) < n) return false;
stock.put(sku, stock.get(sku) - n);
maybeAlert(sku);
return true;
} finally {
lock.unlock();
}
}
void maybeAlert(String sku) {
if (!alerts.containsKey(sku)) return;
Object[] a = alerts.get(sku);
int thr = (int) a[0];
LowStockCallback cb = (LowStockCallback) a[1];
int qty = stock.getOrDefault(sku, 0);
if (qty <= thr) cb.onLowStock(id, sku, qty);
}
}
class InventoryManager {
Map<String, Warehouse> wh = new HashMap<>();
InventoryManager(List<Warehouse> warehouses) {
for (Warehouse w : warehouses) wh.put(w.id, w);
}
boolean transfer(String src, String dst, String sku, int n) {
Warehouse a = wh.get(src), b = wh.get(dst);
Warehouse first = a.id.compareTo(b.id) <= 0 ? a : b;
Warehouse second = a.id.compareTo(b.id) <= 0 ? b : a;
first.lock.lock();
try {
second.lock.lock();
try {
if (a.stock.getOrDefault(sku, 0) < n) return false;
a.stock.put(sku, a.stock.get(sku) - n);
b.stock.put(sku, b.stock.getOrDefault(sku, 0) + n);
return true;
} finally {
second.lock.unlock();
}
} finally {
first.lock.unlock();
}
}
}
Concurrency cases
- Oversell — two remove(1) on qty=1 without lock → both succeed.
- Transfer deadlock — A→B and B→A; fix with ordered locks.
- Alert callback — invoke outside lock if callback is slow/re-entrant into Inventory.