The system design round is not a trivia quiz. The interviewer hands you a vague prompt ("design a URL shortener") and grades how you drive it: whether you clarify before drawing, quantify before choosing, and name the tradeoff of every component out loud. The classic problems repeat, and each one is really a test of two or three primitives underneath. This list comes from the rubric behind our system design module and real loop reports.
At a glance, the problems and what each one is actually probing:
| Classic problem | What it looks like | The primitive it tests |
|---|---|---|
| URL shortener | read-heavy, key generation | caching + DB scaling |
| News feed | fan-out on write vs read | queues + NoSQL modeling |
| Rate limiter | distributed counter | consistency |
| Any of them at 10x | where the bottleneck moves | scaling + resilience |
Start with the method, not the boxes
The first move is never a database name. It is clarifying scope: which features are in (functional requirements), and what the latency, availability, consistency, and scale targets are (non-functional). Only then do you quantify. Convert daily volume to QPS by dividing by 86,400, size storage as bytes/row times rows/day times retention, and apply a peak multiplier because you design for the spike, not the average.
A URL shortener at 100M writes/day is roughly 1,160 write QPS but around 11,600 read QPS at a 10:1 ratio, and the read path saturates first. That single number decides your architecture: a cache in front of reads is the highest-leverage first move, not sharding, not a queue. Every component you draw should trace back to a requirement you can point at ("this cache exists because reads are 10:1").
Follow-up "Walk me through your estimate." If you designed for the daily average and never multiplied for peak, the design is already under-provisioned. Interviewers watch for the multiplier specifically.
Study System design method and scaling and load balancing.
The classic problems
URL shortener: it is a caching problem wearing a database costume
The tension is key generation. Base62 of a distributed unique ID gives no collisions, but a single monotonic sequence is a write bottleneck and the codes are enumerable, leaking your volume. Hash-of-URL is fixed length but forces you to detect and resolve collisions on write. A key-generation service that pre-mints keys sidesteps both. With 62^7 close to 3.5 trillion codes, seven characters is plenty.
Then the read path. Every lookup is a primary-key hit, so a read-through cache absorbs the 11,600 read QPS and the database barely notices.
Trap the 301 vs 302 redirect. A 301 permanent redirect lets the browser cache it and cuts your service load, but it also kills per-click analytics because subsequent clicks never reach you. A 302 keeps every click hitting the service so you can count them. Pick based on whether analytics is a requirement, and say why.
Study caching strategies for the read path and database scaling for storage growth.
News feed: fan-out on write vs fan-out on read
Fan-out on write pushes a new post into every follower's feed at post time. Reads are then trivial, but a celebrity with 50M followers triggers 50M writes per post. Fan-out on read pulls and merges each follower's followees at read time, cheap to write and expensive to read. The senior answer is the hybrid: push for normal accounts, pull for high-fan-out ones, and merge at read.
Async fan-out belongs on a queue, not the request path. The write returns fast, and a worker does the fan-out out of band.
Study queues and async processing and NoSQL data models for the feed store.
Rate limiter: the boundary trap
The naive fixed-window counter allows double the limit across a window edge:
# WRONG: fixed window. Limit 100/min, but a client can send
# 100 at 00:00:59 and 100 more at 00:01:00 -> 200 in one second.
count = redis.incr(f"rl:{user}:{minute}")
if count > 100:
reject()
# RIGHT: sliding window (or token bucket) smooths the boundary.
# Token bucket: refill R tokens/sec up to burst B; each request
# takes one token; empty bucket -> reject.
The counter itself lives in Redis so every app node sees the same value. That makes correctness a distributed-counting question: without an atomic increment, two nodes read the same count and both allow the request.
Study distributed systems fundamentals for why the shared counter needs atomicity.
The primitives the problem drags in
Every classic problem eventually surfaces a deeper question. These are where middles and seniors separate.
"Your replica is serving stale data. Why?"
Read replicas scale reads by fanning them out from the primary, but async replication means a replica can lag. Right after a user writes, their own read may hit a replica that has not applied the change yet: the read-your-writes anomaly. Fixes include routing that user's reads to the primary for a window, or waiting for the replica to reach the write position.
Push further and you hit sharding, where cross-shard pagination is a real correctness trap:
-- WRONG: pushing OFFSET to each shard returns the wrong page.
-- Global offset 100 is NOT offset 100 on every shard.
SELECT * FROM posts ORDER BY created_at LIMIT 20 OFFSET 100; -- per shard
-- RIGHT: keyset/cursor pagination, mergeable across shards.
SELECT * FROM posts WHERE created_at < :cursor
ORDER BY created_at DESC LIMIT 20;
Study database scaling.
"Would you split this into microservices?"
For a small team on a new product, no. You cannot draw good boundaries before you understand the domain, and every split adds a network hop and a deploy-coordination cost. Boundaries follow business capability (Orders, Payments, Inventory), never technical layers. Each service owns its data privately, and once data lives in separate databases you cannot use one ACID transaction across them. That is where the saga pattern comes in: a sequence of local transactions with compensating actions to undo committed steps.
Trap claiming a saga is atomic and isolated. Sagas give ACD, not ACID. Intermediate state is visible before the saga finishes, so you must name a countermeasure (a semantic lock, a pending status) for the missing isolation.
Study microservices vs monolith.
"It is 3am and the design is down. What do you look at?"
A design that cannot be debugged is incomplete. Scope the blast radius with SLI dashboards, localize with a distributed trace, then confirm with logs joined by trace id. Alert on user-facing symptoms (error rate, latency), not causes like CPU at 80%. Mention that high-cardinality metric labels (user_id, raw path) will OOM your time-series database, and you sound like you have run one in production.
Study observability.
The 10x follow-up
After the cache absorbs reads, the bottleneck migrates to write throughput or a hot key. Retries between services compound the problem: if A calls B calls C and each retries three times, C sees up to 9x load exactly when it is already failing. The fixes are a retry budget, backoff with jitter, and a circuit breaker that fails fast instead of piling threads onto a dead dependency.
Study resilience patterns.
Red flags diving into boxes before clarifying requirements; treating every system as needing HA, sharding, and six nines; asserting "use Redis" without tying it to a stated requirement; and claiming exactly-once delivery or strong consistency everywhere. Any of these caps the score no matter how fluent the rest sounds.
The free notes run from the method through every primitive above. Start with the system design module, and the diagnostic will tell you which band you clear today.