Concurrency & low-level design problems (LRU, rate limiter, parking lot)¶
At a glance
Priority: P0 · Complexity: ⅗ · Est. time: 4 h · Phase: 6 · Prereqs: Linked list (LRU), Heap, Sliding window You're done when: you can implement, in Python and in under 30 minutes each, a thread-safe LRU, a token-bucket and sliding-window rate limiter, a bounded blocking queue, an in-memory KV store with TTL, a pub-sub bus, and a parking-lot / elevator model, and explain the lock granularity, deadlock and fairness trade-offs of each.
Why it matters¶
Staff-level loops usually include an LLD/OOD round ("design a parking lot", "design an LRU cache with TTL") and sometimes a concurrency follow-up ("now make it thread-safe", "now 10k QPS"). These rounds test what pure DSA rounds don't: clear APIs, invariants, state machines, extensibility (Strategy/State/Observer), and correctness under concurrent access. Since you already build production systems, the trap is over-engineering. Do the smallest correct design, name the invariants, then extend on request.
Core concepts¶
The LLD interview loop (35-45 min)¶
flowchart LR
A[1. Clarify requirements<br/>actors, use cases, scale, concurrency?] --> B[2. Entities + relationships<br/>nouns to classes, verbs to methods]
B --> C[3. Public API first<br/>signatures + invariants]
C --> D[4. Core algorithm/data structure<br/>complexity of each op]
D --> E[5. Concurrency + failure<br/>locks, atomicity, time source]
E --> F[6. Extensions<br/>strategies, persistence, distribution]
| Principle | In practice |
|---|---|
| Requirements first | "Single process or distributed? Reads vs writes? Eviction policy fixed or pluggable?" |
| Keep entities small | One class, one reason to change; avoid inheritance pyramids |
| Strategy for varying behaviour | Eviction policy, pricing, elevator scheduling, rate-limit algorithm |
| State pattern for lifecycles | Elevator (idle/moving/doors), parking ticket (issued/paid/exited) |
| Observer for notifications | Pub-sub, event hooks |
| Inject the clock | time_fn=time.monotonic makes TTL and rate-limit tests deterministic |
| Say the invariant | "len(map) == len(list) and size <= capacity after every public call" |
Python concurrency facts (as of September 2026)¶
| Topic | Fact |
|---|---|
| GIL | Standard CPython 3.14 still has the GIL. The free-threaded build (3.14t, PEP 779 supported) runs threads in parallel, so compound operations (check then act) need real locks in both. |
| Atomicity | Single bytecode-level operations like list.append and dict[key] = v are atomic in CPython, but x += 1 and if k in d: d[k] ... are not. Always lock compound operations. |
| Threading primitives | Lock, RLock, Condition, Semaphore, Event, Barrier, queue.Queue (already thread-safe) |
| asyncio | asyncio.Lock/Semaphore/Queue are for coroutines only (not thread-safe). Single-threaded event loops make check-then-act safe between awaits. |
| Time | Use time.monotonic() for intervals (immune to wall-clock jumps). Use wall time only for absolute timestamps. |
Problem 1: LRU cache (single-thread, then thread-safe)¶
Single-thread with OrderedDict (the 10-line answer). See Linked list for the explicit DLL version.
from collections import OrderedDict
import threading
class LRUCache:
"""Thread-safe LRU. Invariant: len(self._d) <= capacity after every public call."""
def __init__(self, capacity: int):
if capacity <= 0:
raise ValueError("capacity must be positive")
self._cap = capacity
self._d: OrderedDict = OrderedDict()
self._lock = threading.Lock()
self.hits = self.misses = self.evictions = 0
def get(self, key, default=None):
with self._lock:
if key in self._d:
self._d.move_to_end(key) # most recent at the end
self.hits += 1
return self._d[key]
self.misses += 1
return default
def put(self, key, value):
with self._lock:
if key in self._d:
self._d.move_to_end(key)
self._d[key] = value
if len(self._d) > self._cap:
self._d.popitem(last=False) # evict least recent
self.evictions += 1
Scaling notes: one lock serialises all ops, so shard into N LRUs by hash(key) % N (approximate LRU), or use CLOCK to avoid write-on-read. In free-threaded Python, per-shard locks matter more.
Problem 2: rate limiters (token bucket and sliding window)¶
import time, threading
from collections import deque
class TokenBucket:
"""Allows bursts up to `capacity`, sustained rate `rate` tokens/sec."""
def __init__(self, rate: float, capacity: float, time_fn=time.monotonic):
self.rate, self.capacity, self._time = rate, capacity, time_fn
self.tokens, self.last = capacity, time_fn()
self._lock = threading.Lock()
def allow(self, cost: float = 1.0) -> bool:
with self._lock:
now = self._time()
self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
self.last = now
if self.tokens >= cost:
self.tokens -= cost
return True
return False
class SlidingWindowLog:
"""Exact: at most `limit` requests in any window of `window` seconds. O(limit) memory per key."""
def __init__(self, limit: int, window: float, time_fn=time.monotonic):
self.limit, self.window, self._time = limit, window, time_fn
self.q: deque = deque()
self._lock = threading.Lock()
def allow(self) -> bool:
with self._lock:
now = self._time()
while self.q and self.q[0] <= now - self.window:
self.q.popleft()
if len(self.q) < self.limit:
self.q.append(now)
return True
return False
| Algorithm | Memory | Accuracy | Bursts | Use |
|---|---|---|---|---|
| Fixed window counter | O(1) | boundary double-burst (up to 2x) | yes | Cheap, coarse |
| Sliding window log | O(limit) | exact | no | Small limits, audit |
| Sliding window counter | O(1) | approx (about 1% error) | smooth | Gateways at scale |
| Token bucket | O(1) | exact average rate | up to capacity | APIs (AWS, Stripe style) |
| Leaky bucket | O(1) | smooth output | no | Traffic shaping |
Distributed: Redis Lua script for atomic refill-and-take, keyed per client, clock from Redis TIME. See the system-design track's rate limiting page for depth.
Problem 3: thread-safe bounded blocking queue¶
import threading
from collections import deque
class BoundedBlockingQueue:
def __init__(self, capacity: int):
self._q, self._cap = deque(), capacity
lock = threading.Lock()
self._not_full = threading.Condition(lock) # two Conditions, ONE lock
self._not_empty = threading.Condition(lock)
def put(self, item, timeout=None) -> bool:
with self._not_full:
if not self._not_full.wait_for(lambda: len(self._q) < self._cap, timeout):
return False
self._q.append(item)
self._not_empty.notify()
return True
def get(self, timeout=None):
with self._not_empty:
if not self._not_empty.wait_for(lambda: len(self._q) > 0, timeout):
raise TimeoutError
item = self._q.popleft()
self._not_full.notify()
return item
Points to say: wait_for re-checks the predicate (guards against spurious wakeups), and both conditions share one lock so state changes are atomic. In production use queue.Queue(maxsize=n). Add close/shutdown semantics and backpressure metrics if asked.
Problem 4: in-memory KV store with TTL¶
import heapq, threading, time
class TTLStore:
"""Lazy expiry on read + heap-driven sweeper. Time source injectable for tests."""
def __init__(self, time_fn=time.monotonic):
self._d = {} # key -> (value, expires_at or None)
self._heap = [] # (expires_at, key) entries, may be stale
self._lock = threading.RLock()
self._time = time_fn
def set(self, key, value, ttl=None):
with self._lock:
exp = self._time() + ttl if ttl is not None else None
self._d[key] = (value, exp)
if exp is not None:
heapq.heappush(self._heap, (exp, key))
def get(self, key, default=None):
with self._lock:
item = self._d.get(key)
if item is None:
return default
value, exp = item
if exp is not None and exp <= self._time():
del self._d[key] # lazy expiry
return default
return value
def sweep(self, max_items=1000):
"""Call from a background thread. Amortised O(log n) per expired key."""
with self._lock:
now, n = self._time(), 0
while self._heap and self._heap[0][0] <= now and n < max_items:
exp, key = heapq.heappop(self._heap)
item = self._d.get(key)
if item and item[1] == exp: # skip stale heap entries
del self._d[key]
n += 1
Design points: lazy plus active expiry (as Redis does: lazy on access plus sampled sweeps), stale heap entries after overwrites (lazy deletion), monotonic clock, bounded sweep to cap lock hold time. Extension: LRU eviction when full, per-key locks or sharding, persistence (WAL plus snapshots).
Problem 5: pub-sub (in-process)¶
import queue, threading
from collections import defaultdict
class PubSub:
"""At-most-once, per-subscriber bounded queue: slow subscribers cannot block publishers."""
def __init__(self):
self._subs = defaultdict(list) # topic -> [Subscriber]
self._lock = threading.RLock()
def subscribe(self, topic, handler, max_pending=1000):
sub = Subscriber(handler, max_pending)
with self._lock:
self._subs[topic].append(sub)
return sub
def publish(self, topic, msg):
with self._lock:
subs = list(self._subs[topic]) # snapshot; do not call handlers under the lock
for s in subs:
s.offer(msg)
class Subscriber:
def __init__(self, handler, max_pending):
self._q = queue.Queue(maxsize=max_pending)
self.dropped = 0
threading.Thread(target=self._run, args=(handler,), daemon=True).start()
def offer(self, msg):
try:
self._q.put_nowait(msg)
except queue.Full:
self.dropped += 1 # policy: drop newest; alternatives: drop oldest / block
def _run(self, handler):
while True:
handler(self._q.get())
Discuss delivery semantics (at-most/at-least/exactly-once), ordering per topic, back-pressure policy, handler exceptions (isolate), unsubscribe, and replay (a log-based design like Kafka).
Problem 6: parking lot (OOD)¶
from dataclasses import dataclass, field
from enum import Enum
import itertools, threading
class SpotSize(Enum):
MOTORCYCLE = 1
COMPACT = 2
LARGE = 3
class VehicleType(Enum):
MOTORCYCLE = SpotSize.MOTORCYCLE
CAR = SpotSize.COMPACT
TRUCK = SpotSize.LARGE
@dataclass
class Ticket:
id: int
plate: str
spot: "Spot"
entry_time: float
@dataclass
class Spot:
id: str
size: SpotSize
occupied: bool = False
class ParkingLot:
"""Strategy for allocation: smallest fitting spot. Thread-safe via one lock (a spot is a critical resource)."""
def __init__(self, spots, clock, pricing):
self._free = {s: [sp for sp in spots if sp.size == s] for s in SpotSize}
self._active = {} # ticket id -> Ticket
self._ids = itertools.count(1)
self._lock, self._clock, self._pricing = threading.Lock(), clock, pricing
def park(self, plate, vtype: VehicleType):
need = vtype.value
with self._lock:
for size in SpotSize: # smallest fitting first (enum order)
if size.value >= need.value and self._free[size]:
spot = self._free[size].pop()
spot.occupied = True
t = Ticket(next(self._ids), plate, spot, self._clock())
self._active[t.id] = t
return t
return None # lot full: caller decides (queue / reject)
def leave(self, ticket_id):
with self._lock:
t = self._active.pop(ticket_id) # KeyError: unknown/duplicate exit
t.spot.occupied = False
self._free[t.spot.size].append(t.spot)
return self._pricing(t.entry_time, self._clock())
Design points: allocation strategy (nearest to entrance means a heap per size), pricing as an injected Strategy, multi-level and multi-entrance, reservation and payment as states on the ticket, capacity display as a read-only view. Concurrency: contention is per lot, so shard by level if needed. Persist tickets so a crash doesn't lose active cars.
Problem 7: elevator (State + scheduling Strategy)¶
from enum import Enum
import heapq
class Dir(Enum):
UP = 1
DOWN = -1
IDLE = 0
class Elevator:
"""SCAN (LOOK) algorithm: keep going in the current direction while requests exist ahead, then reverse."""
def __init__(self, floors):
self.floor, self.dir = 0, Dir.IDLE
self.up, self.down = [], [] # min-heap of floors above; max-heap (negated) below
def request(self, floor):
if floor > self.floor:
heapq.heappush(self.up, floor)
elif floor < self.floor:
heapq.heappush(self.down, -floor)
# floor == self.floor: open doors immediately (omitted)
def step(self):
if self.dir in (Dir.UP, Dir.IDLE) and self.up:
self.dir, self.floor = Dir.UP, heapq.heappop(self.up)
elif self.down:
self.dir, self.floor = Dir.DOWN, -heapq.heappop(self.down)
elif self.up:
self.dir, self.floor = Dir.UP, heapq.heappop(self.up)
else:
self.dir = Dir.IDLE
return self.floor
Extensions: multiple elevators with a dispatcher (choose the cost-minimising car: distance, direction match, load), door and overload states, requests as (floor, direction) hall calls versus cabin buttons, priorities (fire mode), and testing with a simulated clock. Say why SCAN beats FCFS (avoids thrash and starvation).
Classic concurrency hazards (say these by name)¶
| Hazard | Example | Fix |
|---|---|---|
| Race (check-then-act) | if k not in d: d[k] = expensive() |
Lock around the compound op, or setdefault/atomic primitive |
| Deadlock | Two locks acquired in different orders (Dining Philosophers) | Global lock ordering; timeouts; try-lock |
| Lost wakeup / spurious wakeup | cond.wait() without a predicate loop |
wait_for(predicate) |
| Starvation | Writers starved by readers | Fair locks / queueing |
| Lock convoy / contention | One global lock in a hot path | Sharding, lock-free reads, batching |
| Holding a lock while calling out | Invoking user callbacks under a lock | Snapshot under lock, call outside |
| Non-monotonic clocks | TTL using time.time() |
time.monotonic() |
flowchart TD
A[Shared mutable state?] -- no --> B[Immutable / message passing / queue.Queue]
A -- yes --> C{Compound invariant across fields?}
C -- yes --> D[One lock guarding the whole invariant]
C -- no --> E[Atomic single op is enough]
D --> F{Hot path contention?}
F -- yes --> G[Shard by key, read-copy-update, or approximate policy]
F -- no --> H[Keep the simple lock]
D --> I{Multiple locks?}
I -- yes --> J[Define a global lock order]
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| Python threading docs | docs | Lock, Condition, Semaphore, Barrier semantics | intermediate | free |
| Python queue docs | docs | Thread-safe queues, task_done/join |
intermediate | free |
| Python asyncio synchronization | docs | Coroutine locks, semaphores, events | intermediate | free |
| concurrent.futures docs | docs | ThreadPoolExecutor / ProcessPoolExecutor patterns | intermediate | free |
| Free-threaded Python guide |
docs | What changes without the GIL (3.14t) | advanced | free |
| Super Fast Python |
article | Practical, well-benchmarked threading/asyncio/multiprocessing recipes | intermediate | freemium |
| The Little Book of Semaphores (Downey) |
book | Classic synchronisation puzzles with solutions | advanced | free |
| Python Concurrency with asyncio (Fowler) | book | Depth on the asyncio model | advanced | paid |
| awesome-low-level-design |
repo | Curated LLD problems with code and diagrams | intermediate | free |
| Refactoring Guru: Design patterns | article | Clear Strategy/State/Observer explanations | intermediate | free |
| David Beazley | article/courses | Deep GIL and concurrency understanding | advanced | free/paid |
Hands-on lab¶
Part A: build (3 h). Implement the seven components above in lld/ with pytest and an injectable clock. Required tests:
- LRU: a 10-thread stress test (100k ops) never exceeds capacity, and hit/miss counters are consistent with a single-threaded oracle for a deterministic key sequence.
- Token bucket: with a fake clock, verify the burst then the sustained rate exactly. Sliding-window log: verify the boundary case (request at t=0 and t=window).
- Bounded queue: N producers and M consumers, verify no loss or duplication, and that
putblocks at capacity. Add a deliberately broken version (ifinstead ofwait_for) and reproduce a failure. - TTL store: expiry with a fake clock, overwrite-then-expire (stale heap entry), sweep bound.
- Pub-sub: a slow subscriber does not slow the publisher, and drops are counted.
- Parking lot: a full-lot rejection and the smallest-fit rule. Elevator: request sequence gives the SCAN order.
Part B: LeetCode concurrency and design set (time-box 25-35 min each; log in the tracker):
| # | LC | Problem | Difficulty | Set | Key insight |
|---|---|---|---|---|---|
| 1 | 1114 | Print in Order | Easy | NC250+ | threading.Event / Barrier ordering. |
| 2 | 359 | Logger Rate Limiter | Easy | NC250+ | Map msg -> next allowed timestamp. (Premium) |
| 3 | 1115 | Print FooBar Alternately | Medium | NC250+ | Two semaphores ping-pong. |
| 4 | 1116 | Print Zero Even Odd | Medium | NC250+ | Three semaphores; zero thread hands off by parity. |
| 5 | 1117 | Building H2O | Medium | NC250+ | Semaphores (2 H, 1 O) + Barrier(3). |
| 6 | 1195 | Fizz Buzz Multithreaded | Medium | NC250+ | Condition variable with a shared counter predicate. |
| 7 | 1226 | The Dining Philosophers | Medium | NC250+ | Global lock order (lower fork first) breaks circular wait. |
| 8 | 1188 | Design Bounded Blocking Queue | Medium | NC250+ | Lock + two Conditions (not_full, not_empty). (Premium) |
| 9 | 1242 | Web Crawler Multithreaded | Medium | NC250+ | ThreadPoolExecutor + locked visited set. (Premium) |
| 10 | 362 | Design Hit Counter | Medium | NC250+ | Circular buffer of 300 (ts, count) slots. (Premium) |
| 11 | 380 | Insert Delete GetRandom O(1) | Medium | NC250+ | Array + index map; swap-with-last delete. |
| 12 | 1472 | Design Browser History | Medium | NC250+ | Array + cursor + logical end. |
| 13 | 146 | LRU Cache | Medium | NC150 | Hash map + DLL; see linked-list page. |
| 14 | 460 | LFU Cache | Hard | NC250+ | freq -> OrderedDict buckets + minFreq pointer. |
Expected output: a repo with passing tests, a one-page note per component listing invariants, lock strategy and the scaling next step.
Questions¶
L1 — Recall¶
Q1. Is dict[key] = value thread-safe in CPython? Is d[k] += 1?
Answer
A single dict.__setitem__ is atomic in CPython (and in the free-threaded build it is protected by per-object locks). d[k] += 1 is a read-modify-write across several bytecodes, so it is not atomic, and two threads can lose an update. Compound operations need a lock (or collections.Counter under a lock, or per-thread counters merged later).
Q2. Why must Condition.wait() be in a loop or use wait_for(predicate)?
Answer
Wakeups can be spurious, and another thread may consume the condition between the notify and your reacquiring the lock. The predicate must be rechecked after waking while holding the lock.
Q3. Token bucket vs leaky bucket vs sliding window: one-line differences?
Answer
Token bucket: allows bursts up to capacity at an average rate. Leaky bucket: smooths output to a constant rate (a queue). Sliding window: strict count over any trailing time window (log = exact, counter = approximate).
L2 — Apply¶
Q4. Make an existing LRU thread-safe with minimal contention. What do you do?
Answer
Start with one lock around get and put (correct, simple). Measure. If contended, shard: N independent LRU instances selected by hash(key) % N, each with its own lock. The trade-off is approximate global recency and capacity per shard (cap/N). Avoid a RWLock because get mutates recency order. Other options are CLOCK (a hit sets a bit, with no list move) or buffering recency updates.
Q5. Implement Print in Order (LC 1114) and Print FooBar Alternately (LC 1115).
Answer
import threading
class Foo: # 1114
def __init__(self):
self.a, self.b = threading.Event(), threading.Event()
def first(self, f): f(); self.a.set()
def second(self, f): self.a.wait(); f(); self.b.set()
def third(self, f): self.b.wait(); f()
class FooBar: # 1115
def __init__(self, n):
self.n = n
self.foo_ok = threading.Semaphore(1)
self.bar_ok = threading.Semaphore(0)
def foo(self, f):
for _ in range(self.n):
self.foo_ok.acquire(); f(); self.bar_ok.release()
def bar(self, f):
for _ in range(self.n):
self.bar_ok.acquire(); f(); self.foo_ok.release()
Q6. Fix the deadlock: transfer(a, b) locks a then b, while another thread calls transfer(b, a).
Answer
Impose a global order on lock acquisition, for example by account id:
def transfer(a, b, amt):
first, second = (a, b) if a.id < b.id else (b, a)
with first.lock:
with second.lock:
a.balance -= amt; b.balance += amt
try_acquire with timeout and backoff, or a message-passing design with a single owner per account.
L3 — Design & trade-offs¶
Q7. Threads vs asyncio vs multiprocessing for an I/O-heavy rate-limited crawler in Python?
Answer
I/O-bound with thousands of connections: asyncio (low overhead per task, explicit cancellation, Semaphore for concurrency limits, a token bucket per host). Threads: simpler with blocking libraries, and a ThreadPoolExecutor scales to hundreds. Multiprocessing (or free-threaded 3.14t threads) only when CPU-bound parsing dominates. Mixed: asyncio for fetching plus a process pool for parsing. Bring up politeness (robots.txt, per-host limits), retries with jitter, and backpressure via bounded queues.
Q8. In-memory TTL cache: lazy expiry only vs active sweeper vs timer per key?
Answer
Lazy only: zero overhead, but expired-and-never-read keys leak memory. Timer per key: precise but expensive at millions of keys (timer wheel mitigates). Heap or timer-wheel sweeper: bounded memory, an O(log n) or O(1) cost per key, and a background thread with a bounded work budget. Redis combines lazy expiry with sampled active expiry. Choose lazy plus a bounded sweeper for most services.
Q9. Design choice: one lock for the whole KV store vs striped locks vs lock-free structures?
Answer
Single lock: simplest and correct, and fine up to modest QPS in Python (the GIL already serialises bytecode). Striped locks (N shards): near-linear scaling for independent keys, but multi-key operations (transactions, scans, size) need to acquire multiple stripes in a global order. Lock-free/CAS structures: complexity and ABA/memory-reclamation issues. In Python, prefer striping or per-key Lock maps for free-threaded builds, and measure with a realistic key distribution (hot keys defeat striping).
L4 — Staff-level ambiguity¶
Q10. 'Make this in-process rate limiter work across 50 service instances.' Walk through options and pick.
Answer
Options: (1) Central Redis token bucket via a Lua script: exact, one hop of latency (~1 ms), a Redis SPOF/hot-key risk. (2) Local limiters with limit/N per instance: no dependencies, but wrong under skewed traffic. (3) Hybrid: local token leases, where each instance grabs a batch of tokens (say 100) from Redis and spends them locally. That cuts Redis calls by ~100x, with slight over- or under-admission. (4) Sidecar/gateway enforcement (Envoy global rate limit service). Pick (3) or (4) at high QPS, and (1) when exactness matters (billing quotas). Address failure: fail-open vs fail-closed per API class, clock skew, and observability (429 rates, limiter latency). Communicate the SLO-driven decision and the migration plan (shadow mode first, log-only, then enforce).
Q11. Your LRU cache in a payments service showed a rare stale-read bug under load. How do you investigate and prevent recurrence?
Answer
Reproduce with a stress test plus randomised interleavings (threading with forced yields, or a deterministic simulation). Look for check-then-act windows (get then put by callers, which the cache can't make atomic), invalidation ordering (write-through vs write-behind, a race between DB write and cache set), and clock/TTL issues. Fixes: get_or_load with per-key single-flight to stop stampedes, versioned values or compare-and-set, and short TTLs for correctness-critical data. Prevention: an invariant checker in tests, chaos/latency injection, metrics on staleness, and a design-doc rule ("cache is never the source of truth for balances"). Write a blameless postmortem with a regression test.
Q12. How would you test concurrent code deterministically?
Answer
Inject seams: clock, executor and scheduler. Use barriers/events to force specific interleavings ("thread A pauses after check, thread B runs"), stress tests with many iterations and randomised sleeps, invariants checked after each run, and tools like pytest-repeat or hypothesis stateful tests. For asyncio, control the event loop (asyncio.sleep(0) yields, or a fake clock). Also run under the free-threaded build in CI to surface latent races the GIL hid.
Real-world use cases¶
- API gateways: distributed token buckets (per-key quotas), with 429 and
Retry-After. - Caching layers: application LRU/TTL caches in front of reference-data services (port codes, tariffs), with single-flight loading.
- Work queues and pipelines: bounded queues for backpressure between ingest and processing stages.
- Operational systems: yard/berth allocation (parking-lot style resource assignment) and lift/crane scheduling (elevator-style SCAN heuristics).
- Event distribution: in-process pub-sub as a domain event bus, evolving into Kafka topics.
Pitfalls & anti-patterns¶
- Adding locks without stating the invariant they protect.
- Calling user callbacks while holding a lock.
- Using wall-clock time for intervals.
- Building an unbounded queue (memory bomb) instead of a bounded one with a policy.
- Over-engineering with class hierarchies before requirements are clear.
- Reading the GIL as a guarantee of correctness.
Checklist¶
- I built and tested all seven components with an injectable clock
- I can explain deadlock, lost wakeup and check-then-act races with code
- I can scale each component (shard, lease, approximate) and name the trade-off
- I answered all L3 questions out loud in < 3 min each