Design Drills
Chapter 5

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.

Million‑client APILLM routerRate limiter HA RESTTelemetry

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.

The design loop — narrate it top to bottom Clarify requirements Estimate QPS · storage Define API contract Architecture edge·compute·data Scale tiers horizontal Reliability SLO · redundancy Tradeoffs name them
The loop you run on every prompt — the same seven moves, different specifics.
  1. 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.”
  2. 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).
  3. Define the API. Name the resources and verbs, idempotency semantics, pagination, and auth. The contract exposes the read/write shape and the hot paths.
  4. High‑level architecture. Draw three tiers: edge (DNS, CDN, gateway) → stateless compute (autoscaled fleet) → data (cache, DB, queue). Keep state out of compute.
  5. 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.
  6. Reliability. Enumerate failure modes, add redundancy (multi‑AZ), protect every hop with timeouts / retries / circuit breakers, and state the SLO you are defending.
  7. Tradeoffs. Close by naming what you gave up — strong vs eventual consistency, cost vs latency, simplicity vs flexibility. Seniority = making the tradeoff explicit.
What is QPS? (a.k.a. RPS)

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.

State the assumptions and the numbers out loud

“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/session30M 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.

1M
DAU
~30M
Requests / day
~350
RPS average
~1.7k
RPS peak (×5)
~150 GB
Egress / day
<200 ms
p99 target

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

EDGE COMPUTE DATA Clients 1M+ global GeoDNS / Anycast nearest region CDN static + cacheable GETs API Gateway TLS · authN · rate limit Stateless service fleet autoscaled · multi‑AZ Redis cache cache‑aside Primary DB writes · source of truth read replicas Queue async work Workers hit enqueue
Clients → GeoDNS/Anycast → CDN → gateway → stateless fleet → cache + primary/replicas + async queue.

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

DecisionOption AOption BPick when…
ConsistencyStrong correctEventual fast/cheapReads tolerate staleness → eventual + cache; money/inventory → strong.
Region topologyActive‑passive simpleActive‑active complexStart passive; go active‑active only for global write latency / RTO≈0.
DatastoreSQL joins·txnsNoSQL scale·flexibleRelational + transactions → SQL; massive KV/append scale → NoSQL.
Say this: “Reads dominate, so I push work to the edge — CDN and Redis cache‑aside — keep the service stateless behind an autoscaled fleet, and route reads to replicas. That holds p99 under 200 ms at ~1.7k peak RPS without sharding; I’d only shard the DB when write throughput actually forces it.”

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

Provider adaptersFailoverCost + latency policy SSE streamingPer‑provider rate limitsToken accounting Idempotency + retriesPrompt cache

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.

4
Providers
~500
RPS
~600
Concurrent streams
~800
Tokens / response

Architecture

Client SSE stream Gateway auth · quota LLM ROUTER Registry / adapters per‑provider API Policy engine capability·cost·latency Health checker probes · SLIs Circuit breakers per provider Weighted LB split traffic Retry + fallback idempotent Token / cost meter accounting Response cache identical prompts Request queue — absorb bursts / smooth to provider limits Provider A · OpenAI healthy Provider B · Anthropic 429 · breaker OPEN ✕ Provider C · Azure fallback target Self‑hosted model cheapest · capacity‑bound failover →
Router picks a provider by capability → lowest cost within the latency SLO; a tripped breaker on B fails over to C.

Routing policy

  1. Capability match. Filter to providers/models that satisfy the request (context window, modality, JSON mode, region/compliance).
  2. 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.
  3. 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.
Streaming can’t be blindly retried

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.
Say this: “The router is capability‑then‑cost within a latency SLO, with a circuit breaker per provider so a 429 or timeout instantly fails over. Streaming is the sharp edge — I only retry before the first token, otherwise I fail forward — and I meter tokens per provider so cost and quota are first‑class SLIs.”

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

AlgorithmAccuracyMemoryBurstsNotes
Fixed windowlowtinyBoundary spikesSimple counter/window; 2× burst at window edges.
Sliding window logexactheavySmoothStores every timestamp — precise but costly at scale.
Sliding window counterapproxcheapSmoothRecommended default — weighted blend of two windows.
Token bucketgoodcheapAllows bursts ≤ bucketRecommended for APIs — smooth refill, controlled burst.
Leaky bucketgoodcheapSmooths outputFixed drain rate — great for shaping downstream load.

Token bucket — the mechanism

Refill · r tokens / sec bucket · capacity B (max burst) tokens Request wants 1 token consume token Allowed · 200 token available Rejected · 429 bucket empty
Tokens refill at rate r up to capacity B; a request spends one, or gets 429 when empty. Bursts up to B are allowed.

Sliding window — the mechanism

window W (last 60 s) · limit N count inside = 5 / N now → past slides with time
The window slides continuously; only requests within [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.

ChoiceLocal per‑nodeGlobal (Redis)
Latencyfastest in‑process+1 RTT network hop
Accuracy across fleetdrifts N×exact
Failure riskNone (isolated)Hotspot / SPOF — needs HA
Best useCheap pre‑filterAuthoritative limit
Say this: “I’d default to a token bucket for APIs — it allows a controlled burst up to the bucket size then smooths to the refill rate. Across a fleet I make it authoritative in Redis with an atomic Lua script, add a cheap local pre‑check for abuse, enforce it at the gateway keyed per API key, and always return 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.

TargetDowntime / yearDowntime / 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

Global DNS / Anycast Load Balancer 1 Load Balancer 2 AVAILABILITY ZONE A AVAILABILITY ZONE B AVAILABILITY ZONE C Stateless replicas DB Primary writes · leader Stateless replicas DB Standby sync · auto‑failover Stateless replicas DB Read replica async · scales reads sync async
Redundant LBs fan into ≥3 AZs of stateless replicas; DB primary + synchronous standby (auto‑failover) + async read replica.

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.
Say this: “Availability is about removing single points of failure and minimising serial hops. I keep compute stateless and replicated across three AZs behind redundant load balancers, run a synchronous DB standby with automatic failover, wrap every hop in timeouts and breakers, and ship with canary + fast rollback. Then I chaos‑test failover so 99.99% is proven, not hoped.”

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

High‑volume ingestNear‑real‑timeDurable BackpressureCardinality controlRetention tiers
1000s
Emitting services
~1M
Events / sec (peak)
3
Signals: metrics·logs·traces
<30 s
Ingest→dashboard lag

Architecture

Services SDK / agent batch + local buffer Collector auth · validate Durable buffer Kafka / Kinesis Stream processors aggregate · sample · roll‑up Metrics → TSDB Prometheus / Cortex Logs → log store Elasticsearch / Loki / S3 Traces → trace store Tempo / Jaeger Query · Dashboards (Grafana) + Alerting decoupled reads · SLO alerts absorbs bursts
Buffer decouples ingest from query: services → collector → Kafka → processors → per‑signal stores → Grafana + alerting.

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.

Retention tiers keep it affordable

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.

TradeoffOption AOption BGuidance
Metrics collectionPull easy healthPush ephemeral/edgePull for stable targets; push for serverless/short‑lived jobs.
Trace volumeFull fidelity costlySampling cheapTail‑sample to keep errors/slow traces without storing everything.
RetentionLong $$$Tiered balancedHot/warm/cold tiers; downsample old data instead of deleting.
Say this: “The move that makes a telemetry pipeline robust is the durable buffer between ingest and query — Kafka absorbs bursts so producers never block. I split storage by signal, control cost with cardinality caps, tail‑sampling and tiered retention, and keep writes idempotent so at‑least‑once delivery doesn’t double‑count.”

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.