Design Drills
Five whiteboard‑ready walkthroughs you can narrate end‑to‑end. Each one follows the same rhythm — requirements & constraints, a back‑of‑envelope estimate, an API sketch, one architecture diagram, how you scale it, how you keep it up, and the tradeoffs you would say out loud. Lead with the numbers, draw the boxes, name the tradeoffs.
The goal of a design drill is not a “correct” diagram — it is to show a repeatable method. Interviewers grade the process: do you clarify scope, quantify load, separate stateless compute from state, scale each tier deliberately, and reason about failure? Start every drill with the framework below, then specialise.
The framework — run this on any question
One loop covers 90% of system‑design prompts. Say each step out loud so the interviewer can follow your reasoning; the numbers you state become the anchors for every later decision.
- Clarify requirements. Split functional (what it must do) from non‑functional (scale, latency SLO, consistency, read/write ratio, availability, retention). Pin down the two or three that dominate the design — e.g. “read‑heavy, p99 < 200 ms, globally available.”
- Back‑of‑envelope. Turn users into QPS (queries per second — avg and peak), then storage, bandwidth/egress, and cache memory. State every assumption; the numbers justify every later choice (sharding, replicas, CDN).
- Define the API. Name the resources and verbs, idempotency semantics, pagination, and auth. The contract exposes the read/write shape and the hot paths.
- High‑level architecture. Draw three tiers: edge (DNS, CDN, gateway) → stateless compute (autoscaled fleet) → data (cache, DB, queue). Keep state out of compute.
- Scale each tier. Compute scales horizontally; reads scale with cache + CDN + replicas; writes scale with sharding/partitioning + async queues. Attack the bottleneck the numbers revealed.
- Reliability. Enumerate failure modes, add redundancy (multi‑AZ), protect every hop with timeouts / retries / circuit breakers, and state the SLO you are defending.
- Tradeoffs. Close by naming what you gave up — strong vs eventual consistency, cost vs latency, simplicity vs flexibility. Seniority = making the tradeoff explicit.
QPS = queries per second — how many requests your system handles each second. It’s the same thing as RPS (requests per second); people use the terms interchangeably. You size hardware for this number, so you always quote two of them:
- Average QPS = total requests per day ÷ 86,400 (the number of seconds in a day). Example: 1M users × 30 requests/day = 30M/day ÷ 86,400 ≈ ~350 QPS.
- Peak QPS = average × a spikiness factor, because traffic bunches up in busy hours instead of arriving evenly. A common rule of thumb is ×3 to ×5, so ~350 avg ≈ ~1.7k QPS at peak.
Always design for the peak, not the average — the average won’t save you during the rush.
“I’ll assume 1M DAU, mostly reads, p99 < 200 ms, and multi‑region availability. That puts us near ~350 RPS average, call it ~1.7k at peak, so a single autoscaled stateless fleet behind a cache and read replicas is plenty — no premature sharding.” The interviewer can now follow every decision back to a number you named.
Drill 1 — an API used by millions of clients
Prompt: design a public API service consumed by 1M+ clients worldwide. Read‑heavy, global, must stay fast and up.
Requirements & constraints
Functional
Serve resource reads/writes over HTTPS; authenticated; consistent contract; global clients on flaky networks.
Non‑functional
1M+ clients, read‑heavy (~95% reads), p99 < 200 ms, ≥ 99.9% availability, elastic to daily peaks.
Back‑of‑envelope (say the arithmetic)
Assume 1M DAU, ~3 sessions/day × 10 requests/session ≈ 30M req/day. Average RPS = 30M ÷ 86,400 s ≈ ~350 RPS; apply a ×5 peak factor → ~1.7k RPS. Payloads ~5 KB → egress ≈ 30M × 5 KB ≈ ~150 GB/day (the CDN absorbs most). Core dataset ~2 KB/user × 1M ≈ ~2 GB hot, growing — comfortably a single primary + replicas, no sharding yet.
API sketch
GET /v1/items?cursor=eyJpZCI6MTB9&limit=50 # cursor pagination, cacheable
GET /v1/items/{id} # ETag + Cache-Control for CDN/edge
POST /v1/items Idempotency-Key: 9f2c… # safe client retries
PUT /v1/items/{id} If-Match: "<etag>" # optimistic concurrency
# Response headers on reads
Cache-Control: public, max-age=60, stale-while-revalidate=300
ETag: "a1b2c3"
Architecture
Scaling
- Stateless horizontal scaling. Sessions/tokens are self‑contained (JWT) so any replica serves any request; autoscale the fleet on CPU/RPS.
- Read path. Cache‑aside in Redis for hot objects, CDN for cacheable GETs (ETag +
stale‑while‑revalidate) — most reads never reach the origin. - DB. Route reads to replicas, writes to the primary; connection pooling to avoid exhausting DB connections. Shard only when a single primary’s write throughput or dataset becomes the bottleneck.
- Multi‑region. Active‑passive first (simple, one write region); go active‑active only when latency or availability demands it — and accept conflict resolution.
Reliability
Redundancy
≥ 2–3 AZs, N replicas per tier, health checks with auto‑replacement — no single point of failure.
Guardrails
Timeouts, bounded retries with jitter, and circuit breakers on every downstream hop.
Degrade gracefully
Serve stale cache, drop non‑essential features, shed load with 429/503 before falling over.
Tradeoffs
| Decision | Option A | Option B | Pick when… |
|---|---|---|---|
| Consistency | Strong correct | Eventual fast/cheap | Reads tolerate staleness → eventual + cache; money/inventory → strong. |
| Region topology | Active‑passive simple | Active‑active complex | Start passive; go active‑active only for global write latency / RTO≈0. |
| Datastore | SQL joins·txns | NoSQL scale·flexible | Relational + transactions → SQL; massive KV/append scale → NoSQL. |
Drill 2 — a routing layer for multiple LLM providers
Prompt: build a service that routes inference requests across OpenAI, Anthropic, Azure and a self‑hosted model — with failover, cost/latency optimisation, streaming, per‑provider quotas and token accounting.
Requirements & constraints
Back‑of‑envelope
Assume 500 RPS of chat completions, avg ~1.2 s provider latency, ~800 tokens per response. In‑flight concurrency ≈ RPS × latency = 500 × 1.2 ≈ ~600 concurrent streams the router must proxy. At ~$X/1k tokens, per‑provider cost accounting and a prompt cache (even 15% hit rate) is real money — routing on cost within a latency SLO pays for itself.
Architecture
Routing policy
- Capability match. Filter to providers/models that satisfy the request (context window, modality, JSON mode, region/compliance).
- Optimise within SLO. Among the eligible set, choose the lowest cost whose recent p95 latency fits the SLO; use weighted load balancing to honour capacity and spread risk.
- Failover. On breaker‑open, timeout or
429, retry on the next‑best provider. Optionally hedge — fire a second request after a delay to cut tail latency — capped to control cost.
Once you’ve proxied SSE/chunked tokens to the client, you can’t silently switch providers mid‑stream. Rule: retry only if zero tokens have been emitted; after the first byte, fail forward (surface the error) rather than restart. Buffer just the first chunk to keep the failover window open a moment longer.
✅ Router pros
- Vendor independence + instant failover on outages/quota.
- Cost + latency optimisation across providers.
- Central place for token accounting, caching, quotas, observability.
🚫 Router cons
- Extra hop = added latency; must be lean and streaming‑aware.
- Router becomes critical path — needs its own HA + breakers.
- Adapters drift as provider APIs change; ongoing maintenance.
Drill 3 — a rate‑limiting system
Prompt: protect an API from abuse and overload with a fair, distributed rate limiter. Pick an algorithm, enforce it across a fleet, and return the right client signals.
Algorithms compared
| Algorithm | Accuracy | Memory | Bursts | Notes |
|---|---|---|---|---|
| Fixed window | low | tiny | Boundary spikes | Simple counter/window; 2× burst at window edges. |
| Sliding window log | exact | heavy | Smooth | Stores every timestamp — precise but costly at scale. |
| Sliding window counter | approx | cheap | Smooth | Recommended default — weighted blend of two windows. |
| Token bucket | good | cheap | Allows bursts ≤ bucket | Recommended for APIs — smooth refill, controlled burst. |
| Leaky bucket | good | cheap | Smooths output | Fixed drain rate — great for shaping downstream load. |
Token bucket — the mechanism
r up to capacity B; a request spends one, or gets 429 when empty. Bursts up to B are allowed.Sliding window — the mechanism
[now − W, now] count toward the limit N.Distributed enforcement (a fleet, not one node)
Per‑node buckets are fast but drift across a fleet — a client hitting different replicas gets N× the intended limit. The accurate answer is a centralised store (Redis) with an atomic read‑modify‑write. The pragmatic answer at very high RPS is hybrid: a permissive local pre‑check to shed obvious abuse cheaply, reconciled asynchronously against the global counter.
-- Redis token-bucket (atomic Lua) — KEYS[1]=bucket key
-- ARGV: rate, capacity, now, requested
local rate = tonumber(ARGV[1]) -- tokens per second
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1]) or capacity
local ts = tonumber(data[2]) or now
-- refill for time elapsed, cap at capacity
local delta = math.max(0, now - ts)
tokens = math.min(capacity, tokens + delta * rate)
local allowed = tokens >= requested
if allowed then tokens = tokens - requested end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', KEYS[1], math.ceil(capacity / rate) + 1)
return { allowed and 1 or 0, tokens } -- 1=allow, remaining tokens
The whole script runs as one atomic operation on the Redis node, so concurrent requests can’t race the read‑modify‑write. A sliding‑window‑counter alternative is even cheaper: INCR a per‑window key and set EXPIRE, then weight the previous window’s count by the elapsed fraction.
Where
Enforce at the edge/gateway so rejected traffic never reaches your services.
Key by
API key / user / IP / tenant — and support cost‑weighted limits (expensive routes spend more tokens).
Respond
429 + Retry‑After + X‑RateLimit‑Limit/Remaining/Reset so clients back off correctly.
| Choice | Local per‑node | Global (Redis) |
|---|---|---|
| Latency | fastest in‑process | +1 RTT network hop |
| Accuracy across fleet | drifts N× | exact |
| Failure risk | None (isolated) | Hotspot / SPOF — needs HA |
| Best use | Cheap pre‑filter | Authoritative limit |
429 with Retry‑After.”Drill 4 — a highly available REST service
Prompt: design a REST service with no single point of failure, targeting 99.99% availability (~52 min/year of downtime).
Availability math (state it)
Serial dependencies multiply: three tiers each at 99.9% ⇒ 0.999³ ≈ 99.7% — worse than any one part. Redundancy improves it: N independent replicas each with availability a give 1 − (1 − a)ᴺ, so two 99% nodes ⇒ 99.99%. Lesson: reduce serial hops, replicate every remaining one.
| Target | Downtime / year | Downtime / month |
|---|---|---|
| 99.9% (three 9s) | ~8.8 h | ~43 min |
| 99.99% (four 9s) | ~52 min | ~4.4 min |
| 99.999% (five 9s) | ~5.3 min | ~26 s |
Architecture — multi‑AZ, no SPOF
Techniques
Eliminate SPOFs
Redundancy at every tier — LBs, replicas, DB nodes — spread across ≥ 2–3 AZs. Health checks auto‑replace unhealthy instances.
Stateless compute + DB failover
No session affinity, so any replica is disposable. Synchronous standby with automatic failover; quorum/consensus (Raft) elects the new leader.
Guardrails + load shedding
Timeouts, bounded retries with jitter, circuit breakers; shed load with 429/503 to protect the core rather than collapse.
Safe deploys + chaos
Rolling / blue‑green / canary releases with fast rollback; regular chaos tests (kill a node, an AZ) to prove failover actually works.
Deploy safety
✅ Blue‑green
- Two full environments; flip traffic instantly, roll back just as fast.
- Simple mental model, near‑zero‑downtime cutover.
- Cost: 2× capacity during the switch; whole‑fleet blast radius if the new version is bad.
🟡 Canary
- Route 1% → 5% → 25% → 100%, watching SLOs at each step.
- Tiny blast radius; catches regressions on real traffic early.
- Slower, needs solid metrics + automation to gate promotion.
Drill 5 — a telemetry pipeline
Prompt: ingest metrics, logs and traces from many services at high volume; power near‑real‑time dashboards and alerting; stay durable under bursts while controlling cost and cardinality.
Requirements & constraints
Architecture
Key concerns
Buffer decouples ingest from query
Kafka/Kinesis absorbs bursts so a slow query store never backs pressure onto producers; processors consume at their own pace.
Sampling
Traces use head (decide at start, cheap) or tail sampling (decide after seeing the full trace — keep the slow/errored ones). Balances fidelity vs cost.
Cardinality & cost
Cap label cardinality (no unbounded user‑id labels), pre‑aggregate/roll‑up, and batch + compress at the edge to cut network and storage.
Delivery & backpressure
At‑least‑once + idempotent writes (dedupe on event id); shed load / drop low‑priority signals when overloaded rather than crash.
Hot (last hours/days, fast SSD, full resolution) → warm (weeks, downsampled) → cold (months+ in object storage, roll‑ups only). Most queries hit hot data; cold is cheap insurance for audits and post‑mortems.
| Tradeoff | Option A | Option B | Guidance |
|---|---|---|---|
| Metrics collection | Pull easy health | Push ephemeral/edge | Pull for stable targets; push for serverless/short‑lived jobs. |
| Trace volume | Full fidelity costly | Sampling cheap | Tail‑sample to keep errors/slow traces without storing everything. |
| Retention | Long $$$ | Tiered balanced | Hot/warm/cold tiers; downsample old data instead of deleting. |
Design cheat sheet
The framework + go‑to numbers
- Loop: clarify → estimate → API → architecture (edge→stateless compute→data) → scale each tier → reliability → tradeoffs.
- Say the numbers: DAU × req/user ÷ 86,400 = avg RPS; ×3–5 for peak; payload × req/day = egress; rows × size = storage.
- Nines: 99.9% ≈ 8.8 h/yr · 99.99% ≈ 52 min/yr · 99.999% ≈ 5 min/yr. Serial hops multiply; redundancy =
1 − (1 − a)ᴺ. - Reads scale with CDN + cache + replicas; writes scale with sharding + async queues; compute scales by staying stateless.
- Rate limit: token bucket for APIs, sliding‑window‑counter for accuracy; make it atomic in Redis; return
429+Retry‑After. - LLM routing: capability → cheapest within latency SLO; breaker per provider; only retry streams before the first token.
- Telemetry: durable buffer decouples ingest from query; split by signal; control cardinality; tail‑sample; tier retention.
- Always mention: the SLO you defend, redundancy that removes SPOFs, and the tradeoff you consciously accepted.