System Design Interviews, Step by Step · The Method: How to Run a System Design Interview
The Building Blocks Every Design Uses
Load balancers, caches, SQL and NoSQL databases, replication, sharding, queues, object storage and CDNs: what each one is for, when to add it, and what it costs. Most designs are these blocks arranged differently.
The problem
Interview designs look different on the surface (chat, feeds, payments), but they're built from the same dozen parts. If you know what each part fixes and what it costs, you can assemble a reasonable design for a system you've never seen.
Real-world analogy
A restaurant scales the same way a web system does. A host at the door spreads guests across tables (load balancer). Popular dishes are prepped ahead (cache). Orders go on a ticket rail so cooks work at their own pace (queue). The pantry is the source of truth (database), the walk-in freezer holds bulk goods (object storage), and a second location opens across town (replication across regions).
The idea
| Block | What it does | Add it when | Cost / catch |
|---|---|---|---|
| Load balancer | Spreads requests across identical servers; removes unhealthy ones | More than one app server | Servers must be stateless (sessions in a shared store) |
| Cache (Redis, Memcached) | Keeps hot data in memory, ~100× faster than a DB | Read-heavy data that's read far more than it changes | Stale data; invalidation is hard; a cold cache after restarts |
| SQL database (PostgreSQL, MySQL) | Tables, joins, transactions, constraints | Default choice; money, orders, anything relational | Harder to scale writes beyond one machine |
| NoSQL (key-value, document, wide-column) | Simple access by key at huge scale | Massive write volume, flexible schema, access by key only | Limited joins and transactions; you design around queries |
| Read replicas | Copies of the DB that serve reads | Reads overload the primary | Replication lag: a read may miss a write made a moment ago |
| Sharding | Splits data across databases by a key (user id, region) | Writes or data size exceed one machine | Cross-shard queries, rebalancing, hot shards |
| Message queue (Kafka, RabbitMQ, SQS) | Hands work to background workers; absorbs spikes | Slow work (emails, video), fan-out, decoupling services | Eventual consistency; retries mean duplicates (need idempotency) |
| Object storage (S3-style) | Cheap, durable storage for files and blobs | Images, video, backups, large documents | Not for queries; access via keys/URLs |
| CDN | Serves static and media files from servers near users | Global users, heavy static or media traffic | Cache invalidation, cost per GB |
| Rate limiter | Caps requests per user/IP | Public APIs, login, expensive endpoints | Choosing limits; distributed counting |
| Search index (Elasticsearch, OpenSearch) | Full-text and fuzzy search | Searching text at scale | Another copy to keep in sync |
Two ideas run through all of them:
- Consistency vs availability. When the network splits, a system must choose between refusing some requests (consistent) or answering with possibly stale data (available). Payments lean consistent; likes and view counts lean available.
- Sync vs async. Do the minimum while the user waits; push everything else (emails, thumbnails, analytics) to a queue.
The code
Sharding needs a way to map a key to a server that doesn't reshuffle everything when you add one. Plain hash(key) % servers moves most keys when the server count changes; consistent hashing moves only a small share. This simulation compares the two.
import bisect
import hashlib
def h(text):
return int(hashlib.md5(text.encode()).hexdigest(), 16)
def modulo_owner(key, n):
return h(key) % n
class Ring:
def __init__(self, servers, vnodes=100):
self.points = sorted((h(f"{s}#{i}"), s) for s in servers for i in range(vnodes))
self.hashes = [p for p, _ in self.points]
def owner(self, key):
i = bisect.bisect(self.hashes, h(key)) % len(self.points) # first point clockwise
return self.points[i][1]
keys = [f"user:{i}" for i in range(20_000)]
before = {k: modulo_owner(k, 4) for k in keys}
after = {k: modulo_owner(k, 5) for k in keys}
moved = sum(before[k] != after[k] for k in keys)
print(f"modulo: {moved / len(keys):.0%} of keys move")
ring4 = Ring(["db1", "db2", "db3", "db4"])
ring5 = Ring(["db1", "db2", "db3", "db4", "db5"])
moved = sum(ring4.owner(k) != ring5.owner(k) for k in keys)
print(f"consistent hashing: {moved / len(keys):.0%} of keys move (ideal: 20%)")
Output
modulo: 79% of keys move consistent hashing: 20% of keys move (ideal: 20%)
With modulo, about 80% of keys land on a different server, which means a cache full of misses or a huge data migration. Consistent hashing moves roughly the one fifth that the new server should take over.
Common mistakes
- Adding every block to every design. Each block is an answer to a specific problem; name the problem.
- "NoSQL because it scales." Choose by access pattern and consistency needs. Many huge systems run on sharded SQL.
- Caching without an invalidation story. Say how cached data gets updated or expires.
- Queues without idempotency. Messages are delivered at least once; consumers must handle duplicates.
- Stateful app servers. Keep sessions and uploads out of server memory so the load balancer can send any request anywhere.
Key points
- Most designs are the same blocks: load balancer, cache, SQL/NoSQL, replicas, shards, queue, object storage, CDN.
- Add a block only to fix a named bottleneck, and state its cost.
- Read-heavy → cache + replicas + CDN; write-heavy → sharding + queues.
- Consistent hashing keeps resharding and cache misses small.
Practice questions
When would you add read replicas, and what problem do they introduce?
Answer
When reads overload the primary database. Replicas lag slightly behind the primary, so a user may not see their own write immediately; fix by reading from the primary for a short time after a user writes, or for that user's own data.
Why is consistent hashing preferred over hash(key) % n for sharding or cache clusters?
Answer
With modulo, changing n remaps most keys (about (n−1)/n of them), causing a mass cache miss or data migration. Consistent hashing places servers on a ring, so adding or removing one only moves the keys in its section, about 1/n of them.
Open in the app to mark lessons complete, take notes and track your progress.