System Design Thinking - basic
use case: Inventory Management System
A compact mental compile sheet for writing an inventory management system under time pressure. Topic → why → code anchor. Retype from memory; adapt return strings to the prompt.
60-second start
Write this first:
E — Entities: Product, Warehouse, InventoryError*
S — State: catalog{}, warehouses{}, ledger defaultdict(dict), locks defaultdict(Lock)
L — Logic: Guard → Lock → Mutate → Return exact string
P — Parser: strip → split → int() → try/except → "\n".join
When a test fails, check these two first:
int(...)cast on quantities- Extra space / wrong exact output string
1. Mental model
Stock lives at (warehouse, product), not on Product
Why: A product does not “have 50 units.” W1 may hold 30 and W2 may hold 20. Quantity on Product loses location and breaks transfer.
catalog : ProductID → Product(id, name)
warehouses : WarehouseID → Warehouse(id, location)
ledger : WarehouseID → { ProductID → qty:int }
locks : WarehouseID → threading.Lock
{"W1": {"P1": 50, "P2": 10}, "W2": {"P1": 100}}
ESLP compile order
Why: Methods before state → KeyError thrash. Nouns → memory → mutations → IO.
| Meaning | Ask yourself | |
|---|---|---|
| E | Entities + errors | What nouns? What fails? |
| S | State | Where stored? How locked? |
| L | Logic | Guard → Lock → Mutate → Return |
| P | Parser | strip → split → try/catch → exact strings |
2. Entities & exceptions
@dataclass for Product / Warehouse
Why: Clear identity fields; less boilerplate than raw dicts.
from dataclasses import dataclass
@dataclass(slots=True)
class Product:
id: str
name: str
@dataclass(slots=True)
class Warehouse:
id: str
location: str
Drop slots=True if the runtime is old — behavior matters more.
Domain exceptions
Why: One catch in the parser maps to the prompt’s exact error string.
class InventoryError(Exception): pass
class InsufficientStockError(InventoryError): pass
class EntityNotFoundError(InventoryError): pass
class InvalidInputError(InventoryError): pass
class CapacityExceededError(InventoryError): pass
Rewrite exception → output mapping in one place after reading the prompt.
3. State
defaultdict(dict) ledger
Why: First touch of a warehouse auto-creates {}. Fewer nested existence checks.
from collections import defaultdict
import threading
self.catalog = {}
self.warehouses = {}
self.ledger = defaultdict(dict) # w → {p → qty}
self.locks = defaultdict(threading.Lock) # w → Lock
Always .get(p_id, 0) on read
Why: Missing product in a warehouse means 0, not KeyError.
current = self.ledger[w_id].get(p_id, 0)
4. Logic — Guard → Lock → Mutate → Return
Unified modify_stock(w, p, qty)
Why: ADD and REMOVE share one path. Parser passes +qty or -qty.
def modify_stock(self, w_id, p_id, qty: int) -> str:
if w_id not in self.warehouses:
raise EntityNotFoundError("warehouse")
if p_id not in self.catalog:
raise EntityNotFoundError("product")
with self.locks[w_id]:
current = self.ledger[w_id].get(p_id, 0)
new_qty = current + qty
if new_qty < 0:
raise InsufficientStockError("stock")
self.ledger[w_id][p_id] = new_qty
return str(new_qty) # only if prompt wants remaining qty
Return exactly what samples show ("50", not "Remaining: 50").
CRUD existence checks
Why: Hidden cases love duplicate IDs and unknown IDs.
- Add product/warehouse: reject if ID exists (unless prompt says otherwise)
- Stock ops: reject if warehouse or product missing
Per-warehouse locks
Why: Two writers can both read current=1 and both decrement → negative stock. Per-warehouse locks allow concurrent ops on different warehouses.
Deadlock-free transfer
Why: A locks W1→W2 while B locks W2→W1 → deadlock. Always acquire in sorted ID order.
def transfer_stock(self, w_from, w_to, p_id, qty: int) -> str:
if qty <= 0:
raise InvalidInputError("qty")
if w_from not in self.warehouses or w_to not in self.warehouses:
raise EntityNotFoundError("warehouse")
if p_id not in self.catalog:
raise EntityNotFoundError("product")
if w_from == w_to:
# threading.Lock is NOT reentrant — never double-acquire
return "SUCCESS" # or error per prompt
a, b = sorted([w_from, w_to])
with self.locks[a]:
with self.locks[b]:
src = self.ledger[w_from].get(p_id, 0)
if src < qty:
raise InsufficientStockError("stock")
self.ledger[w_from][p_id] = src - qty
self.ledger[w_to][p_id] = self.ledger[w_to].get(p_id, 0) + qty
return "SUCCESS"
5. Parser
Command loop shape
def ArrayChallenge(strArr):
manager = InventoryManager()
out = []
for line in strArr:
line = line.strip()
if not line:
continue
parts = line.split()
cmd = parts[0].upper()
try:
if cmd == "ADD_PRODUCT":
out.append(manager.add_product(parts[1], " ".join(parts[2:])))
elif cmd == "ADD_WAREHOUSE":
out.append(manager.add_warehouse(parts[1], " ".join(parts[2:])))
elif cmd == "ADD_STOCK":
out.append(manager.modify_stock(parts[1], parts[2], int(parts[3])))
elif cmd == "REMOVE_STOCK":
out.append(manager.modify_stock(parts[1], parts[2], -int(parts[3])))
elif cmd == "TRANSFER":
out.append(manager.transfer_stock(
parts[1], parts[2], parts[3], int(parts[4])))
elif cmd == "GET_STOCK":
out.append(manager.get_stock(parts[1], parts[2]))
else:
out.append("ERROR:_UNKNOWN_COMMAND")
except ValueError:
out.append("ERROR:_INVALID_NUMBER_FORMAT")
except IndexError:
out.append("ERROR:_MISSING_ARGUMENTS")
except InventoryError:
out.append("ERROR") # replace with prompt’s exact strings
return "\n".join(out)
Gotchas
| Threat | Defense | Why |
|---|---|---|
" ADD_STOCK " |
strip() |
Leading spaces break the command token |
"P1 Mac Book" |
" ".join(parts[2:]) |
Names are multi-word |
| Verbose returns | Match samples literally | Diff checkers are exact |
| Missing ledger key | .get(p, 0) |
Absence = zero |
| Qty as string | int(parts[k]) |
Need arithmetic |
| Blank lines | if not line: continue |
Empty split crashes |
| Arg order swap | Recheck prompt | wid vs pid first is a common fail |
Stdin fallback
if __name__ == "__main__":
import sys, ast
raw = sys.stdin.read().strip()
if raw.startswith("["):
arr = ast.literal_eval(raw)
else:
arr = raw.splitlines()
print(ArrayChallenge(arr))
6. Curveballs (only if asked)
Capacity
Why: ADD/TRANSFER-in can fail even when the product exists.
total = sum(self.ledger[w_id].values())
if total + qty > warehouse.capacity:
raise CapacityExceededError("capacity")
Atomic multi-item checkout
Why: All lines succeed or none apply.
# 1) validate every (pid, qty) against ledger
# 2) then apply all deductions
# never mutate halfway
Audit / history
Why: Append-only log for HISTORY queries.
self.events.append(("ADD_STOCK", w_id, p_id, qty, new_qty))
FIFO perishable batches
Why: Expiry — remove oldest batch first.
# ledger[w][p] = deque of (expiry, qty)
# REMOVE pops from the left; skip/reject expired per prompt
Reservations
Why: Prevent oversell between order create and ship.
available = on_hand - reserved
reserve → reserved += qty (if available >= qty)
commit → on_hand -= qty; reserved -= qty
release → reserved -= qty
7. Typing anchors (memorize shapes)
import sys, threading, ast
from collections import defaultdict
from dataclasses import dataclass
class InventoryManager:
def __init__(self):
self.catalog = {}
self.warehouses = {}
self.ledger = defaultdict(dict)
self.locks = defaultdict(threading.Lock)
a, b = sorted([w_from, w_to])
parts = line.strip().split(); cmd = parts[0].upper()
name = " ".join(parts[2:])
8. Prompt adaptation (2 minutes)
- Copy sample I/O into a comment
- List commands + argument order
- Write exact success strings
- Write exact error strings
- Implement ESLP
- Sweep: strip,
int,.get, lock sort, same-warehouse, negative qty, unknown IDs
9. Pocket sheet
E: Product, Warehouse, InventoryError*
S: catalog{}, warehouses{}, ledger defaultdict(dict), locks defaultdict(Lock)
L: Guard → Lock → Mutate → Return EXACT string
P: strip → split → upper → int() → try/except → "\n".join
ledger[w][p] = qty
read: ledger[w].get(p, 0)
same-wh transfer: no double-lock
transfer: a,b = sorted([from,to]); lock a then b
fail checklist: int | spaces | .get | strip | join names | lock order
10. Failure taxonomy
| Symptom | Likely cause |
|---|---|
| Almost all fail | Wrong entry function / join separator |
| First command fails | Forgot strip / wrong index |
| Multi-word name wrong | Used parts[2] not join |
| Transfer hangs | Same warehouse double-lock |
| Off-by formatting | Trailing space/newline; ERROR vs ERROR:_... |
| Negative stock | Missing new < 0 guard or missing lock |