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:
- Store products of different types and prices.
- Add a new product with an initial quantity and a low-stock threshold.
- Add stock and remove stock for a product.
- Check a product's current quantity.
- 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).Stockrefers 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:
- Validate the product and quantity.
- Call
Stock.adjust(id, -n), which refuses to go below zero. - 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
StockItemobject: parallelcountandthresholdmaps are quick to write but must be kept in sync. AStockItem(count, threshold)per product keeps them together and is easier to extend (reserved quantity, location). - Pluggable alerts: replace the
printwith anAlertListenerinterface (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 eachWarehouseits ownStock; add transfer between warehouses as remove + add in one transaction. - Reservations: for orders, track
reservedseparately fromon_handso items in carts aren't double-sold. - Concurrency:
adjustis 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.