System Design Casebook
Learn system design the way interviews and production outages teach it. Start with one server, add each component only when a number forces you to, and defend every choice with an estimate. Every system design opens with a short animated video of it at work, tradeoffs marked, and every topic gives you a diagram to step through or a simulator to break.
- Part IFoundationsCore definitions, request anatomy, percentiles, availability, queueing, data modelsBasic
- Part IIMethodThe 45 minute plan, envelope math, latency numbersBasic
- Part IIIScalingFrom one box to millions of users in nine stepsBasic
- Part IVBuilding blocksLoad balancers, caches, hashing, CAP, queues, rate limitsIntermediate
- Part VCase studiesFourteen full designs, URL shortener to payments, plus a use case mapBasic to advanced
- Part VIReviewFifty most asked questions, a tradeoff sheet and a quizMixed
Foundations
The vocabulary and the handful of basic facts every design discussion assumes. Learn these first; every later part builds on them.
Core definitions
The terms that come up in every design conversation, one or two lines each. Search them, filter by area, and follow a link to see the idea at work elsewhere in the guide.
Anatomy of a request
Before your code runs, a request pays for a name lookup and handshakes, and each one costs round trips. Knowing where the time goes explains why CDNs, connection reuse and HTTP/3 exist.
- Round trip time (RTT)
- Time for a packet to reach the other side and come back. Distance sets the floor: light in fiber covers about 200 km per millisecond.
- DNS
- Turns a name like example.com into an IP address. Answers are cached by browsers, operating systems and resolvers for the record’s TTL.
- TCP handshake
- SYN, SYN-ACK, ACK: one round trip before any data can flow.
- TLS handshake
- Agrees on keys to encrypt the connection: two round trips with TLS 1.2, one with TLS 1.3.
- Time to first byte (TTFB)
- From starting the request until the first byte of the response arrives. Setup, one round trip and server work all count.
- Connection reuse
- Keeping a connection open (keep-alive, pooling) so later requests skip DNS and both handshakes.
Simulator: where the time goes
- Connection setup
- 0 ms
- Time to first byte
- 0 ms
- Total
- 0 ms
Assumes the resolver already has the DNS answer cached, 60 ms of server work and a 200 KB response over a 50 Mbps link.
How a name becomes an address
Latency, throughput and percentiles
Latency is how long one request takes; throughput is how many you finish per second. Averages hide the slow requests users actually feel, so serious systems are measured in percentiles.
- Latency
- Time for one operation, measured end to end. Usually reported in milliseconds.
- Throughput
- Work completed per unit of time: requests per second, messages per second, MB per second.
- Bandwidth
- The maximum rate a link can carry. Throughput is what you actually get through it.
- Percentile
- p99 = 250 ms means 99% of requests finished within 250 ms and 1 in 100 took longer. p50 is the median.
- Tail latency
- The slow end of the distribution, p99 and beyond. It grows in importance as one user action fans out into many calls.
- Mean
- The average. A few very slow requests drag it far above what a typical user sees, so it describes nobody.
Simulator: 10,000 response times
- p50, median
- 0
- Mean
- 0
- p95
- 0
- p99
- 0
Why the tail matters more than it looks
Availability and the nines
Availability is the share of time a system does its job. Each extra nine is ten times harder to reach, and every component you chain together brings its own failures.
- Availability
- Uptime as a share of total time, such as 99.9%. Measured over a window, often 30 days.
- MTBF
- Mean time between failures: how long things usually run before breaking.
- MTTR
- Mean time to recovery: how long it takes to get back up once broken.
- Single point of failure
- A component whose failure stops the whole system.
- Redundancy
- Extra copies of a component so one can fail without an outage.
- SLI, SLO, SLA
- What you measure, the target you set for it, and the contract you sign with customers.
Formula availability = MTBF ÷ (MTBF + MTTR) Example fails once a month (720 h), 1 h to recover → 720 ÷ 721 = 99.86% Faster same failures, recover in 6 minutes → 720 ÷ 720.1 = 99.99%
Recovering faster buys nines as surely as failing less often, and it is usually cheaper: good alerts, quick rollbacks and automatic failover.
Downtime allowed per year
Simulator: chains and copies
- Each stage
- 0
- Whole system
- 0
- Downtime per year
- 0
Components in a chain multiply: all must be up. Copies in parallel fail only if every copy fails at once. This assumes independent failures; shared power, bad deploys and bugs often break copies together, so real gains are smaller.
Load, utilization and queueing
A server at 50% utilization feels fast; at 95% the same server feels broken. Queueing explains why, and Little’s law turns traffic into the number of requests in flight.
- Arrival rate (λ)
- How many requests arrive per second.
- Service time (S)
- How long one request takes when nothing else is waiting.
- Utilization (ρ)
- The share of time a resource is busy: arrival rate × service time, for one server.
- Little’s law
- Requests in the system = arrival rate × time each one spends there. L = λ × W, true for any stable system.
- Saturation
- Work arrives faster than it can be served; queues and latency grow without limit.
- Headroom
- Spare capacity kept free for spikes and failures, often 30 to 40% at peak.
Simulator: the hockey stick
- Average response time
- 0
- Slower than an idle server
- 0
- Requests in the system
- 0
One server, random arrivals, 10 ms of work per request: response time = S ÷ (1 − ρ). Real systems differ in detail but share the hockey stick, which is why capacity is planned for about 60 to 70% utilization at peak.
Calculator: Little’s law
- Requests in flight
- 400
Data models and storage
Every database is built around one shape of data and a few access patterns. Pick the shape that matches how you read, and the rest of the design gets easier.
- Transaction
- A group of reads and writes that succeed or fail as one unit.
- ACID
- Atomic (all or nothing), consistent (rules hold), isolated (as if one at a time), durable (survives crashes).
- Index
- A sorted structure, usually a B-tree, that finds rows without scanning the table. Speeds reads, slows writes.
- OLTP and OLAP
- Many small transactional reads and writes, versus large analytical scans over history.
- Schema
- The declared structure of the data. Enforced on write in relational databases, interpreted on read in many NoSQL stores.
- Write-ahead log
- Changes are appended to a log before being applied, so a crash can be replayed. Replication and change data capture read the same log.
The same customer and orders, six ways
- Shape
- Good at
- Weak at
- Examples
Rows or columns on disk
| Storage type | What it is | Use it for | Examples |
|---|---|---|---|
| Block | A raw disk volume attached to one machine | Database files, low latency random reads and writes | AWS EBS, local SSD |
| File | Folders and files shared over the network | Shared home directories, legacy apps | NFS, AWS EFS |
| Object | A flat namespace of immutable blobs fetched by key over HTTP | Images, video, backups, data lakes; cheap and very durable | Amazon S3, Google Cloud Storage |
Communication patterns
How two parties exchange messages decides latency, server load and what happens when one side is slow. Five patterns cover nearly every design.
- Synchronous
- The caller waits for the answer. Simple, but the caller is only as fast and available as whatever it calls.
- Asynchronous
- The caller hands off work, often through a queue, and moves on. Decouples the two sides at the cost of eventual results.
- Pull and push
- Pull: the client asks for news. Push: the server sends it when it happens.
- Webhook
- An HTTP callback: you give a service a URL and it calls you when an event happens.
- RPC
- Calling a function on another machine as if it were local, as gRPC does with typed contracts.
- Fan-out
- One request or event that triggers many downstream calls or messages.
Five ways to get news from a server
How it behaves
Use it for, and what it costs
| Style | Format | Strength | Typical place |
|---|---|---|---|
| REST | JSON over HTTP, resources and verbs | Universal, cacheable, easy to debug | Public and mobile APIs |
| gRPC | Protobuf over HTTP/2 | Typed contracts, streaming, compact and fast | Service to service |
| GraphQL | One endpoint, client picks fields | No over-fetching, one round trip for nested data | Apps with many different screens |
| Message queue or log | Messages on a broker | Decoupling, buffering, replay | Background work, events between services |
Units, sizes and transfer times
Most estimation mistakes are unit mistakes: bits for bytes, or a thousand for 1,024. These are the numbers worth knowing by heart.
- Bit and byte
- A byte is 8 bits. Network speeds are quoted in bits (Mbps), file sizes in bytes (MB), so divide by 8.
- KB and KiB
- KB is 1,000 bytes; KiB is 1,024. For estimates the 2.4% difference does not matter; for disk sizes it adds up.
- Throughput in practice
- Protocol overhead and congestion usually leave 70 to 90% of the link rate for your data.
Powers of two
| Power | Value | Remember it as |
|---|---|---|
| 210 | 1,024 | about a thousand (KiB) |
| 216 | 65,536 | ports, 16 bit IDs |
| 220 | ≈ 1.05M | about a million (MiB) |
| 230 | ≈ 1.07B | about a billion (GiB) |
| 232 | ≈ 4.3B | all IPv4 addresses, 32 bit IDs |
| 240 | ≈ 1.1T | about a trillion (TiB) |
| 264 | ≈ 1.8 × 1019 | 64 bit IDs never run out |
Sizes to remember
| Thing | Size |
|---|---|
| ASCII character | 1 B |
| Integer or timestamp | 4 or 8 B |
| UUID | 16 B |
| Short post with metadata | ≈ 1 KB |
| Phone photo, compressed | 2 to 5 MB |
| 1 minute of 1080p video at 5 Mbps | ≈ 37.5 MB |
| 1 hour of 4K video at 15 Mbps | ≈ 6.8 GB |
Calculator: how long does it take to move?
Method
A repeatable way to attack any prompt, and the numbers you need to reason about scale out loud.
The 45 minute plan
A system design interview is a conversation about tradeoffs under a clock. The same five moves work for every prompt in this guide, and each case study in Part V follows them in order.
- 1
Clarify requirements
Agree on users, features, scale and what matters most. Leave with 3 to 5 functional and 3 to 5 non-functional requirements written down.
“Read heavy or write heavy? Is a few seconds of staleness acceptable?”
- 2
Estimate
QPS, peak QPS, storage and bandwidth, rounded hard. You want the order of magnitude that decides the design, not precision.
“35K reads per second is more than one database should take, so we need a cache.”
- 3
High level design
API, data model, and one diagram of the request path. Get the interviewer to agree before you add detail.
“That’s the happy path end to end. Which part should we go deeper on?”
- 4
Deep dive
Two or three hard parts: the hot path, the data that grows, the failure that hurts. Show options, pick one, and say what it costs.
“Push fan-out breaks for celebrities, so I’d switch them to pull.”
- 5
Wrap up
Recap, name the bottlenecks, what you would monitor, and what changes at 10× traffic.
“At 10× the fan-out workers go first. I’d alert on queue lag.”
Mistakes that cost the most
- Drawing boxes before the requirements are agreed.
- Naming a product instead of the property you need. Say “a durable, ordered, replayable log” first, then “Kafka”.
- Presenting one option. Interviewers want the alternative you rejected and why.
- Ignoring failure. For every box, know what happens when it dies or a call to it times out.
- Designing for planet scale when the numbers say 50 requests per second.
Back of the envelope math
Estimates exist to answer one question: does this fit on one machine, or do we need a cache, replicas, partitions or a CDN? Round aggressively and say your assumptions out loud.
- 1 day
- 86,400 s ≈ 105 s
- 1 month
- ≈ 2.6M s
- 1M requests / day
- ≈ 12 per second
- Peak traffic
- ≈ 2 to 3× average
- KB, MB, GB, TB, PB
- 103, 106, 109, 1012, 1015 B
- ID, timestamp
- 8 B each
Estimator
- Write QPS
- 0
- Read QPS
- 0
- Peak read QPS
- 0
- New data per day
- 0
- Total storage
- 0
- Write bandwidth
- 0
What these numbers imply
For full worked examples, see how five guides estimate the URL shortener, with a calculator preloaded with each guide’s assumptions.
Thresholds are rules of thumb for reasoning out loud: a tuned relational database handles a few thousand writes and tens of thousands of simple reads per second; one Redis node handles roughly 100K simple operations per second.
Latency numbers
Design decisions come from ratios. Memory is about 160 times faster than a random SSD read, and a request across an ocean costs as much time as a million memory lookups.
Log scale: each gridline is 10× the one before. Values are rounded orders of magnitude for 2020s hardware, not benchmarks.
What to take from it
- Avoid chatty calls. One round trip inside a data center (about 0.5 ms) costs as much as 5,000 memory reads. Batch requests and fetch in bulk.
- Keep hot data in memory. That is the whole case for caches like Redis.
- Read sequentially. Reading 1 MB in sequence from SSD is cheaper than 4 random 4 KB reads combined. Log structured storage (Kafka, LSM trees) exploits this.
- Put data near users. Crossing an ocean costs about 150 ms per round trip, which is why CDNs and multi-region deployments exist.
- Compress before sending. Compressing 1 KB takes microseconds; sending it across a WAN takes milliseconds.
Scaling
Every large system started as one box. Each step below exists because a specific limit was hit.
One server to millions of users
Step through nine changes that take a web app from prototype to a system serving millions. Each step fixes one bottleneck and introduces a new cost. Naming both is exactly what interviewers listen for.
One server does everything
Why this step
What it costs
Tip: the left and right arrow keys step through the diagram while the buttons have focus.
Building blocks
The six components that appear in nearly every design. Learn what each one buys you and what it breaks.
Load balancing
A load balancer takes every request on one public address and forwards it to a healthy server behind it. It gives you horizontal scaling (add servers) and failover (stop routing to a server that fails its health check).
Layer 4 or layer 7
Layer 4 balancers route TCP or UDP connections by IP and port. Very fast and protocol agnostic, but blind to URLs and headers.
Layer 7 balancers speak HTTP. They route /api and /images to different pools, terminate TLS and retry safe requests. Examples: NGINX, Envoy, HAProxy, AWS ALB.
Keeping it reliable
Health checks call an endpoint such as /healthz every few seconds and take failing servers out of rotation.
The balancer must not become the new single point of failure. Run an active and standby pair that share a floating IP, or several balancers behind DNS.
| Algorithm | How it picks | Good for | Watch out for |
|---|---|---|---|
| Round robin | Next server in order | Identical servers, short uniform requests | Ignores load, so slow servers pile up work |
| Weighted round robin | Round robin, bigger servers get more turns | Mixed hardware | Weights are static |
| Least connections | Server with the fewest open requests | Long or uneven requests: uploads, WebSockets | Balancer nodes need to share counts |
| Power of two choices | Pick two at random, use the less loaded | Large fleets with many balancers | Near optimal, rarely a problem |
| Hash of IP or key | Same client or key always hits the same server | Caches, sticky sessions | Skewed keys; use consistent hashing so resizing moves few keys |
Simulator: four servers, two new requests every tick
Caching
A cache keeps a copy of hot data in memory so most reads skip the slow path. It works because real traffic is skewed: a small share of keys receives most of the requests.
Caches exist at every layer: the browser, the CDN, the API gateway, an application cache such as Redis or Memcached, and the database’s own buffer pool. In interviews “add a cache” usually means an application cache, and the follow up is always the same: how does data get in, and how does stale data get out?
Eviction policies
- LRU evicts the least recently used key. The default almost everywhere.
- LFU evicts the least frequently used. Better when popularity is stable.
- TTL expires keys after a fixed time. Your safety net against stale data.
Failure modes to name
- Stampede. A hot key expires and thousands of requests hit the database at once. Let one request refresh while others wait, and add random jitter to TTLs.
- Hot key. One key overloads one cache node. Replicate it under suffixes (
key#1tokey#8) or keep a small local cache per server. - Stale reads. Update the database first, then delete the cache key.
Simulator: an LRU cache
- Hits
- 0
- Misses
- 0
- Hit ratio
- 0%
Skewed traffic sends half the requests to keys A and B, the way real users hammer a few popular items. Compare its hit ratio with uniform traffic across eight keys: the same small cache is far more useful when access is skewed.
Consistent hashing
When data is spread over N servers, you need a rule that maps each key to a server. The obvious rule, hash(key) mod N, remaps almost every key when N changes. For a cache cluster that means a sudden flood of misses; for a database, a huge migration.
The ring
Hash servers and keys onto the same circle of values. A key belongs to the first server found moving clockwise. Adding a server only takes keys from one neighbor; removing one hands its keys to the next. On average only 1/N of the keys move.
Virtual nodes
With one position per server the arcs are uneven, so some servers own far more keys. Give each server many positions (virtual nodes) and the load evens out; when a server leaves, its keys spread across all the others. Used by Dynamo, Cassandra, Riak and most sharded caches.
- Keys moved on the ring
- None yet
- With hash mod N
- None yet
Keys per server (dashed line is a perfectly even share)
Add a server and compare the two counters. Then switch to 64 virtual positions per server and watch the bars even out. You can also click any key dot on the ring.
CAP and consistency
When a network partition splits a replicated system, each side must choose: refuse requests it cannot answer correctly (consistency) or answer with what it has (availability). Partitions happen in every real network, so the practical question is which one you give up while it lasts.
Choose consistency (CP) when
A wrong answer is worse than no answer: account balances, inventory reservations, leader election, unique usernames. Examples: ZooKeeper, etcd, Spanner, a single primary relational database.
Choose availability (AP) when
A slightly old answer is fine and downtime is not: shopping carts, like counts, feeds, DNS. Examples: Cassandra and DynamoDB in their default modes.
PACELC adds the everyday case: even with no partition, waiting for replicas to confirm (consistency) costs latency. Most tuning happens here, not during rare partitions.
Simulator: two replicas and a network cable
Try it: cut the network, write, then read, in both modes. Then heal the network and see what an AP system has to reconcile.
Consistency models, strongest to weakest
| Model | Guarantee | Example use |
|---|---|---|
| Strong (linearizable) | Every read sees the latest completed write | Payments, locks, counters that must not go negative |
| Read your writes | You always see your own updates; others may lag | Editing your profile, posting a comment |
| Monotonic reads | You never see time go backwards across reads | Pinning a user to one replica |
| Eventual | Replicas converge if writes stop | Like counts, view counters, DNS |
Queues and streams
A queue sits between a producer and a consumer so neither waits for the other. It absorbs bursts, lets each side scale on its own, and keeps work safe when a consumer crashes.
Two shapes
Message queue (RabbitMQ, SQS): each message goes to one consumer and is deleted once acknowledged. Good for jobs.
Log or stream (Kafka, Kinesis, Pulsar): messages are kept for days in ordered partitions. Many consumer groups read at their own offset and can replay history. Good for events that several systems need.
Delivery guarantees
At most once may lose messages. At least once may deliver duplicates and is the usual default. Exactly once in practice means at least once delivery plus an idempotent consumer that ignores message IDs it has already processed.
Ordering holds only within a partition, so partition by the key whose order matters, such as order_id. After N failed attempts, park a message in a dead letter queue instead of blocking the partition.
Simulator: a traffic spike hits the queue
- Peak backlog
- 0
- Longest wait
- 0 s
- Backlog cleared
- n/a
Rate limiting
A rate limiter caps how many requests a client can make in a window. It protects services from abuse and from well meaning clients stuck in retry loops, and it keeps costs predictable.
Simulator: token bucket
- Allowed (200)
- 0
- Rejected (429)
- 0
- Retry-After hint
- 0 s
Each request spends one token; tokens drip back at the refill rate. A full bucket lets a burst through, then the limiter settles to the refill rate. That is why Stripe and AWS use this algorithm: it allows short bursts while enforcing an average.
| Algorithm | How it works | Strength | Weakness |
|---|---|---|---|
| Token bucket | Tokens refill at rate r up to capacity b; each request takes one | Allows bursts, tiny memory | Two parameters to tune |
| Leaky bucket | Requests queue and drain at a fixed rate | Smooth, steady output | Bursts wait or are dropped |
| Fixed window | Counter per minute, reset at the boundary | Simplest to build | 2× the limit can pass around a boundary |
| Sliding window log | Store each request timestamp, count the last 60 s | Exact | Memory grows with traffic |
| Sliding window counter | Blend current and previous window counts by overlap | Accurate enough, cheap | Approximate |
Where it runs
Put the limiter in the API gateway or as middleware, keyed by user ID, API key or IP. Counters live in Redis so every gateway node sees the same numbers; wrap check and decrement in a Lua script (or use INCR with EXPIRE) so two nodes cannot both spend the last token. Rejected calls get 429 Too Many Requests with a Retry-After header, and well behaved clients back off exponentially.
Case studies
Fourteen prompts that come up again and again, in order of difficulty. Each opens with a short video of the system at work, then follows the 45 minute plan: requirements, estimates, API and data model, the design, and the deep dives interviewers push on.
Use case map
The design prompts that come up again and again, what each one is really testing, and where to practice it here. Most prompts are a new costume on a handful of core problems.
| Prompt | Level | The core problem | Key techniques | Practice it |
|---|---|---|---|---|
| URL shortener | Basic | Read heavy lookups by key | Base62 IDs, cache, 302 redirects | V.1 |
| Rate limiter | Basic | Shared counters at low latency | Token bucket, Redis with atomic scripts | IV.6 |
| Unique ID generator | Basic | Ordering without coordination | Snowflake IDs, time sortable keys | V.3 |
| News feed | Intermediate | Fan-out cost | Push, pull and hybrid; feed cache of IDs | V.2 |
| Chat or messaging | Intermediate | Stateful connections, ordering, offline users | WebSockets, inbox queues, presence | V.3 |
| Notification system | Intermediate | Many channels, flaky providers | Per channel queues, dedupe, retries, dead letters | V.4 |
| Search autocomplete | Intermediate | Prefix lookups in milliseconds | Trie with top K per node, offline rebuild | V.5 |
| Web crawler | Intermediate | Politeness and dedupe at scale | URL frontier, Bloom filter, SimHash | V.6 |
| Nearby places | Intermediate | Two dimensional search | Geohash, quadtree, nine cell query | V.7 |
| Distributed cache | Intermediate | Hot keys and resizing | Consistent hashing, LRU, replication | IV.2, IV.3 |
| Message queue | Intermediate | Durability with ordering | Partitioned log, offsets, consumer groups | IV.5 |
| Leaderboard or top K | Intermediate | Ranking a changing set | Redis sorted sets; count-min sketch for streams | VI.1 |
| Ticket or hotel booking | Advanced | Contention on scarce inventory | Conditional updates, holds with expiry, waiting room | V.8 |
| Ride sharing | Advanced | Location firehose plus exclusive matching | In memory geo index, compare and set | V.9 |
| Video streaming | Advanced | Bandwidth and transcoding | Segment DAG, adaptive bitrate, CDN | V.10 |
| File sync | Advanced | Send only what changed, never lose data | Content hashed blocks, versioned commits | V.11 |
| Collaborative editor | Advanced | Concurrent edits must converge | OT or CRDT, one server per document | V.12 |
| Key value store | Advanced | Partitioning with high availability | Consistent hashing, quorums, vector clocks | V.13 |
| Payment system | Advanced | Correctness over unreliable networks | Idempotency keys, ledger, reconciliation | V.14 |
| Metrics and monitoring | Advanced | Huge volumes of time series writes | Time series storage, downsampling, push or pull collection | VI.1 |
| Ad click aggregation | Advanced | Exact counts in near real time | Stream processing with windows, idempotent sinks, reconciliation | IV.5 |
Spot the families: booking, payments and ride matching are all contention problems; feeds, chat and notifications are fan-out problems; autocomplete, crawlers and nearby search are indexing problems.
URL shortener
The classic warm up. A small surface area that still tests estimation, ID generation, caching and the difference between a 301 and a 302.
Design a service like TinyURL: given a long URL, return a short one that redirects to it.
Functional requirements
- Create a short URL for a long one, with optional custom alias and expiry.
- Visiting the short URL redirects to the long one.
- Optional: click counts per link.
Non-functional requirements
- Redirects are fast, a few milliseconds at the server.
- Highly available: a dead short link is a broken page on someone else’s site.
- Codes are never reused and are hard to enumerate.
Estimates
New URLs 100M per month ÷ 2.6M s ≈ 40 writes/s Redirects 100 reads per write ≈ 4,000 reads/s, peak ≈ 8,000 Total URLs 100M × 12 months × 10 years = 12B Storage 12B × 500 B ≈ 6 TB Code space 62⁶ ≈ 56.8B, 62⁷ ≈ 3.5T → 7 characters gives plenty of headroom
Calculator: run the numbers yourself
- Writes per second
- Redirects per second
- Peak redirects per second
- Storage
- Shortest code
- Cache, 80/20 method
What the numbers decide
Numbers used in these calculations
| Number | Value | Used for |
|---|---|---|
| Seconds in a day, a month | 86,400 ≈ 105; ≈ 2.5M | Turning daily or monthly volume into per second rates |
| Memory read | ≈ 100 ns | Why hot redirects are served from a cache |
| SSD read, HDD read | ≈ 0.1 ms, ≈ 10 ms | Memory is about 1,000× faster than SSD |
| SSD random reads | ≈ 100,000 IOPS | Peak reads beyond this cannot come from one disk |
| HDD random reads | 100 to 200 IOPS | Why nobody serves hot reads from spinning disks |
| One Redis instance | 100,000+ ops/s | Cache nodes needed at peak; the global ID counter |
| Network latency: same AZ, across AZs, across regions | < 1 ms; 1 to 2 ms; 50 to 150 ms | The 100 ms redirect budget; why a CDN helps distant users |
| Network bandwidth per instance | 25 Gbps standard, 50 to 100 high end | Bandwidth is never the limit for 500 byte rows |
| One large machine | 512 GiB RAM, 128 vCPUs; up to 24 TB RAM; up to 60 TB local SSD | Whether data and cache fit on one box |
The point of these numbers is that modern machines are huge. Rules like “split the database at 100 GB” are long outdated, and sharding before the numbers demand it is a common interview mistake. At 500 GB, a URL shortener fits comfortably on one Postgres server; replicate it for availability, and shard only when throughput or size truly requires it.
API
POST /api/v1/urls
{ "longUrl": "https://example.com/a/long/path",
"alias": null, "expiresAt": null }
→ 201 { "shortUrl": "https://sho.rt/zn9edcu" }
GET /zn9edcu
→ 302 Found
Location: https://example.com/a/long/pathData model
urls code CHAR(7) PRIMARY KEY long_url TEXT owner_id BIGINT created_at TIMESTAMP expires_at TIMESTAMP NULL
Every read is a lookup by primary key, so a key value store (DynamoDB, Cassandra) or a relational table both work.
Deep dive: generating the short code
| Approach | How | Pros | Cons |
|---|---|---|---|
| Hash and truncate | Hash the long URL (MD5, SHA-256), keep the first 7 base62 characters | Same URL gives the same code; no coordination | Collisions: check the store, rehash with a salt; an extra read on every write |
| Base62 of a unique ID Common pick | Each server leases a block of IDs (say 1,000) from a coordinator, encodes each ID in base62 | No collisions, no extra reads, simple | Sequential codes are guessable unless you shuffle the bits |
| Key generation service | Pre-generate random codes offline; servers claim batches | Random and collision free | One more service; claimed keys are lost if a server crashes |
Lab: shorten a URL both ways
Base62 of the ID
| Number | ÷ 62 | Remainder | Char |
|---|
Divide by 62 until nothing is left, then read the remainders from the bottom up. Alphabet: 0 to 9, a to z, A to Z.
Hash and truncate
Deep dive: 301 or 302
A 301 Moved Permanently is cached by the browser, so repeat visits never reach you. Less load, but you lose click analytics and cannot change or expire the link for that visitor. A 302 Found sends every click through your servers: more load, complete analytics. Most commercial shorteners use 302 because analytics is the product.
Deep dive: scaling reads
Redirects outnumber writes 100 to 1, and clicks concentrate on recent and viral links. Cache code → long URL in Redis with LRU eviction. If the hottest 20% of a month’s links take most of the clicks, that is 20M × 500 B = 10 GB, which fits in one Redis node. Add read replicas behind the cache, and for extreme traffic answer redirects at the CDN edge.
What the interviewer probes next
How do custom aliases work?
Same table, same unique key. Check availability on create and reserve words like api and login. To keep aliases from colliding with generated codes, generated codes are always exactly 7 characters and aliases must be another length, or both go through the same uniqueness check.
How do expired links get cleaned up?
Lazily on read (return 410 Gone when expired) plus a low priority job that deletes expired rows in batches. Never recycle codes: an old link printed in a book would suddenly point at a stranger’s page.
How do you count clicks without slowing redirects?
Redirect first, then publish a click event to a queue such as Kafka. Stream processors aggregate counts per link per minute into an analytics store. The redirect never waits on analytics.
How do you stop abuse?
Rate limit creation per user and IP, check new URLs against malware and phishing lists such as Google Safe Browsing, and require an account for high volume.
News feed
The central question is when to do the work: when a post is written, or when a feed is read. The answer changes with follower count, which is why every real feed is a hybrid.
Design the home timeline for a social network: users follow others and see their posts, newest first.
Functional requirements
- Publish a post with text and media.
- Follow and unfollow users.
- View a paginated feed of posts from followed users.
Non-functional requirements
- Feed loads in under 200 ms.
- A new post may take a few seconds to appear (eventual consistency).
- Read heavy and highly available.
Estimates
Feed reads 300M DAU × 10 opens ÷ 86,400 s ≈ 35,000 reads/s New posts 300M DAU × 2 posts ÷ 86,400 s ≈ 7,000 posts/s Fan-out 7,000 posts/s × 200 followers ≈ 1.4M feed inserts/s if we push Feed cache 300M users × 500 post IDs × 8 B ≈ 1.2 TB of IDs in memory
API
POST /v1/posts
{ "text": "...", "mediaIds": ["m_81"] }
GET /v1/feed?limit=20&cursor=1783229482342
→ 200 { "posts": [...],
"nextCursor": "1783228190031" }Data model
posts post_id PK, author_id, text,
media_ids, created_at
follows follower_id, followee_id
feeds user_id → [post_id, ...]
Redis list, capped at ~500Deep dive: push or pull
Calculator: what one post and one feed open cost
| Push (on write) | Pull (on read) | Hybrid | |
|---|---|---|---|
| Writes when this author posts | |||
| Lookups when a reader opens the feed | |||
| Time until every follower can see it (1M inserts/s) |
Hybrid is the standard answer: push for ordinary accounts, and for accounts above a threshold (say 100K followers) skip fan-out. At read time the feed service merges the precomputed feed with fresh posts from the few celebrities the reader follows. Skip fan-out to users inactive for 30 days too, and build their feed by pull when they come back.
Deep dive: pagination and ranking
Paginate with a cursor (the last post ID seen), never with OFFSET. New posts arriving at the top shift every offset, so page two by offset repeats items; a cursor stays stable. Ranked feeds fetch a few hundred candidates, score them by affinity, recency and engagement, and cache the ranked page for a few minutes.
What the interviewer probes next
A user follows someone new. When do old posts appear?
Backfill asynchronously: fetch the new followee’s recent posts and merge them into the follower’s cached feed. Unfollow is handled by filtering at read time and cleaning the cache lazily.
The feed cache node dies. Now what?
The feed cache is derived data. Rebuild a user’s feed on demand by pulling recent posts from their followees, then resume pushing. Replicate the cache so a single node loss rarely forces this.
How are deleted or edited posts handled?
Feeds store IDs only. Hydration reads the current post, and deleted posts are filtered out at read time, so nothing needs to be rewritten across millions of feeds.
How do media uploads fit in?
Clients upload directly to object storage with a pre-signed URL, a worker creates thumbnails, and the CDN serves them. Posts keep only media IDs.
Chat system
Chat introduces stateful servers: millions of open connections that must find each other. The design turns on how a message travels from one socket to another, and what happens when the other side is offline.
Design a chat app like WhatsApp or Messenger: one to one and small group chats, online status, and sync across devices.
Functional requirements
- One to one and group chats of up to 100 members.
- Delivery and read receipts, online presence.
- Offline users get push notifications and catch up on reconnect.
- History syncs across a user’s devices.
Non-functional requirements
- Low delivery latency.
- Messages ordered within a conversation.
- Durable: an acknowledged message is never lost.
Estimates
Messages 50M DAU × 40 per day ÷ 86,400 s ≈ 23,000 messages/s Storage 2B messages/day × 100 B ≈ 200 GB/day, ≈ 73 TB/year Connections 10M concurrent ÷ 100K per server ≈ 100 chat servers
How clients stay connected
| Technique | How it works | Tradeoff |
|---|---|---|
| Short polling | Client asks “anything new?” every few seconds | Simple, but most responses are empty and waste capacity |
| Long polling | Server holds the request open until a message arrives or a timeout | Fewer empty replies; still one request per message and constant reconnects |
| WebSocket Pick | One persistent, two way connection over TCP | Real time both ways; servers become stateful and need reconnect handling |
Use WebSockets for messages only. Login, profiles, group management and media uploads stay on ordinary stateless HTTP services behind a load balancer.
Step through: Alice sends Bob a message
Deep dive: ordering with Snowflake IDs
Client clocks drift, so ordering comes from IDs the server assigns. A Snowflake ID packs a timestamp into the high bits, so sorting by ID sorts by time, and each generator can mint thousands of IDs per millisecond with no coordination.
- Timestamp, 41 bits: milliseconds since a custom epoch, enough for about 69 years.
- Machine, 10 bits: up to 1,024 generators.
- Sequence, 12 bits: 4,096 IDs per millisecond per generator, reset every millisecond.
- ID
- n/a
- Time
- n/a
- Machine
- n/a
- Sequence
- n/a
Deep dive: storage, presence, groups
Message store
Chat is write heavy and reads are mostly recent. A wide column store (Cassandra, HBase, ScyllaDB) partitioned by conversation_id and sorted by message_id serves “latest 50 messages” from one partition. Discord runs this model on ScyllaDB, and Facebook Messenger ran it on HBase for years.
Multi-device sync
Each device keeps a cursor: the largest message ID it has. On reconnect it asks for everything after that ID, which also covers offline delivery.
Presence
Clients send a heartbeat every 5 seconds. A user is marked offline only after 30 seconds of silence, so a train through a tunnel does not flicker their status. Changes are published to friends over pub/sub; in large groups, presence is fetched only when someone opens the member list.
Groups
Small groups copy each message into every member’s inbox queue. Large channels store once and let members pull, like celebrities in the news feed.
What the interviewer probes next
A chat server crashes. What happens to its users?
Their sockets drop, and clients reconnect with backoff and jitter; service discovery (ZooKeeper, etcd) hands them another server. Messages were persisted before delivery, and clients resend anything unacknowledged with the same client message ID, which the server deduplicates.
Two people in a group send at the same moment. Who comes first?
Whoever the server assigns the lower ID. Using one ID source per conversation gives every member the same order; device timestamps are only for display.
How does end to end encryption change the design?
Keys live only on devices (the Signal protocol), so servers store and forward ciphertext. Server side search and message previews in push notifications no longer work unless the device decrypts them locally.
How do you deploy without dropping 10M connections?
Drain servers one at a time: stop accepting new sockets, ask connected clients to reconnect gradually, then restart. The load balancer must support long lived connections and not cut them on a fixed timeout.
Notification system
Many services, three channels, unreliable third party providers. The design is about keeping channels isolated from each other, retrying without spamming, and respecting what each user asked for.
Design a system that sends push notifications, SMS and email on behalf of every team in the company.
Functional requirements
- Send through push (iOS, Android), SMS and email.
- Respect per user preferences, opt-outs and quiet hours.
- Immediate, scheduled and bulk campaign sends from templates.
- Track sent, delivered and opened.
Non-functional requirements
- Urgent messages such as login codes arrive within seconds.
- No lost notifications, and no duplicates the user can see.
- A slow provider in one channel never delays the others.
Estimates
Daily volume 10M push + 1M SMS + 5M email = 16M per day, ≈ 185/s Campaign peak 10M pushes in 10 minutes ≈ 17,000/s Log storage 16M × 500 B × 365 days ≈ 2.9 TB per year
API
POST /v1/notifications
{ "eventId": "evt_8812", "userId": 42,
"template": "payment_succeeded",
"params": { "amount": "€50" },
"channels": ["push", "email"],
"priority": "high" }
→ 202 Accepted { "notificationId": "ntf_19a" }Data model
devices user_id, token, platform
preferences user_id, category, channel,
enabled, quiet_hours
notifications id PK, event_id UNIQUE,
user_id, channel, status,
sent_at, opened_atDeep dive: delivery you can trust
- Persist first. Write the notification row, then enqueue it. If a worker crashes, a sweeper finds rows stuck in
PENDINGand re-enqueues them. - Deduplicate by event ID. The unique constraint on
event_idmeans a service that publishes the same event twice still produces one notification. - One queue per channel, with priority lanes. Login codes and payment alerts never wait behind a ten million user marketing campaign.
- Retry with exponential backoff and jitter, within each provider’s rate limits. After a fixed number of attempts, move the message to a dead letter queue and alert.
- Close the loop. Providers report delivery through callbacks; the app reports opens. Both update the status row.
| Channel | Provider examples | Watch out for |
|---|---|---|
| Push | APNs (Apple), FCM (Google) | Device tokens go stale. Delete a token when the provider reports the app as unregistered. |
| SMS | Twilio, Vonage | Cost per message, country specific sender rules, and abuse such as SMS pumping fraud. |
| Amazon SES, SendGrid | Bounces and spam complaints damage your sender reputation. Marketing email needs an unsubscribe link. |
Deep dive: not annoying people
Check preferences at send time, not when the message is queued, because a user may opt out in between. Cap marketing messages per user per day, hold non-urgent messages during quiet hours in the user’s own time zone, and bundle bursts into digests: “12 people liked your post” instead of twelve pings.
What the interviewer probes next
How do you send a campaign to 10M users without breaking anything?
A scheduler expands the audience in batches of a few thousand and feeds the low priority lane. Workers throttle to each provider’s quota. Transactional messages use their own lane and workers, so they are unaffected.
A login code must arrive fast. How?
A dedicated high priority queue with reserved workers, a short expiry (a code that arrives after it expires is useless), and a fallback from push to SMS if no delivery receipt arrives within a few seconds.
The same event is published twice. Does the user get two pushes?
No. The second insert violates the unique event_id and is dropped. As a second guard, the app ignores notification IDs it has already shown.
How do you know a notification was opened?
The app reports opens with the notification ID; emails use a tracking pixel and wrapped links. Events stream into analytics and update the status row.
Search autocomplete
Every keystroke is a request, which makes this one of the busiest endpoints a company runs. The trick is to do the expensive ranking offline and make serving a plain memory lookup.
Design the suggestions that appear under a search box as the user types.
Functional requirements
- Return the top 5 completions for a prefix, ranked by popularity.
- Trending searches appear within about an hour.
- Offensive and blocked terms never appear.
Non-functional requirements
- Under 100 ms end to end, so the server budget is about 20 ms.
- Highly available; if suggestions fail, search still works.
- Freshness of minutes to hours is fine.
Estimates
Requests 10M DAU × 10 searches × 20 keystrokes = 2B per day, ≈ 23,000/s Peak ≈ 2× ≈ 50,000/s Prefix index ≈ 1B prefixes × 5 IDs × 8 B ≈ 40 GB plus keys, so shard it
API
GET /v1/suggest?q=tw&lang=en
→ 200 Cache-Control: max-age=3600
{ "suggestions": ["twitter", "twitch",
"twilight", "twins", "tweetdeck"] }What each trie node stores
prefix top 5 completions, precomputed t twitter, twitch, tiktok, target, translate tw twitter, twitch, twilight, twins, tweetdeck twi twitch, twilight, twins, twitter, twine
Storing the top five at every node is what makes serving fast: a lookup walks down as many nodes as the prefix has characters and returns the stored list, with no need to search the subtree below.
Deep dive: where suggestions live
| Approach | Serving | Updating | Fits when |
|---|---|---|---|
| Trie in memory, top 5 per node Common pick | Walk the prefix, return the list | Rebuilt offline and swapped in | The classic interview answer |
| Prefix → top 5 in a key value store | One GET per keystroke | Batch write every prefix | Simple to shard and cache, more storage |
| Search engine completion suggester | In memory automaton with fuzzy matching | Near real time indexing | You already run Elasticsearch or OpenSearch and need typo tolerance |
Deep dive: the data pipeline
Search logs flow into a stream. A batch job counts queries over the last few weeks with older counts weighted less; a streaming job counts the last hour to catch trending terms. The scores are merged, a builder produces a new trie, writes it to object storage, and serving nodes load it and swap it in atomically. Reads never wait for writes.
Deep dive: cutting the load
- Debounce in the client: send only after a short pause in typing.
- Cache responses in the browser, and at the CDN for short prefixes, which are the hottest and identical for everyone.
- Shard by prefix range using a shard map balanced by traffic, because far more queries start with “s” than with “x”.
What the interviewer probes next
A harmful term starts trending. How do you remove it right now?
Filter results against a blocklist at serving time, before returning them. The next rebuild removes the term from the trie itself.
How would you personalize suggestions?
Blend the global top results with the user’s own recent searches, kept on the device or in a small per user store, and merge them in the client or at the edge.
How do you handle typos?
Fuzzy matching by edit distance. It is much more expensive, so run it only when the exact prefix returns few results; completion suggesters in search engines support it directly.
What about other languages?
Build one trie per language or region, normalize Unicode, and for languages typed through an input method, such as Chinese pinyin, index the typed form as well as the final characters.
Web crawler
A crawler is a loop: take a URL, fetch it, extract its links, repeat. At a billion pages the hard parts are staying polite to every website, never fetching the same thing twice, and surviving the strange corners of the web.
Design a web crawler that collects a billion pages a month for a search engine.
Functional requirements
- Start from seed URLs and follow links.
- Store page content for indexing.
- Recrawl pages according to how often they change.
- Obey robots.txt.
Non-functional requirements
- Politeness: never overload any single website.
- Scales to hundreds of fetcher machines.
- Robust to broken HTML, slow servers and crawler traps.
- Extensible to new content types such as PDFs.
Estimates
Fetch rate 1B pages ÷ 2.6M s per month ≈ 385 pages/s, peak ≈ 800 Storage 1B × 500 KB = 500 TB per month, ≈ 30 PB in 5 years URL seen 10B URLs in a Bloom filter at 1% ≈ 12 GB of memory
Deep dive: the URL frontier
Deep dive: never twice
Same URL
Normalize first: lowercase the host, drop fragments, sort query parameters, strip tracking parameters. Then check a Bloom filter of URLs already seen. A false positive means a page is occasionally skipped, which is acceptable; it never causes a duplicate fetch.
Same content
Mirrors and session parameters serve identical pages under many URLs. A checksum of the content catches exact copies; SimHash catches near duplicates, such as the same article with a different ad block.
| Trap | What happens | Defense |
|---|---|---|
| Endless calendars | “Next month” links forever | Maximum depth and pages per host |
| Session IDs in URLs | Every visit looks like a new page | Strip known parameters, content dedupe |
| Generated link farms | Pages that link to ever new pages | URL length limits, per host budgets |
| Slow servers | Fetchers sit idle waiting | Tight timeouts; one host per back queue isolates it |
What the interviewer probes next
How do you decide when to recrawl a page?
Estimate each page’s change rate from past crawls: news front pages hourly, documentation monthly. Use conditional requests with If-Modified-Since or ETag, so unchanged pages return a 304 with no body.
What about pages built with JavaScript?
Send them to a separate render queue served by headless browsers. Rendering costs far more than fetching, so do it only when the static HTML lacks the content.
How are hosts split across crawler machines?
Hash the hostname to a machine with consistent hashing. One machine then owns all politeness state for a host, and adding machines moves only some hosts.
DNS turns out to be the bottleneck. Why, and what helps?
Each lookup can take tens to hundreds of milliseconds and many resolvers are single threaded. Run a caching resolver on every crawler machine and respect record TTLs.
Nearby places
A read heavy search problem with a twist: databases index one dimension well, and location has two. The design is mostly about the geospatial index.
Design the backend for “restaurants near me” as in Yelp or Google Maps: given a location and a radius, return nearby businesses.
Functional requirements
- Search by location and radius: 0.5, 1, 2, 5 or 20 km.
- View business details.
- Owners add and update businesses; changes may appear the next day.
Non-functional requirements
- Low search latency.
- Handles lunch and dinner peaks.
- User locations are private and not stored longer than needed.
Estimates
Searches 100M DAU × 5 searches ÷ 86,400 s ≈ 5,800 QPS, peak ≈ 15,000 Businesses 200M × 1 KB ≈ 200 GB Geo index 200M × (6 B geohash + 8 B ID) ≈ 3 GB, fits in memory
Why a plain index fails
SELECT id FROM business WHERE lat BETWEEN 48.853 AND 48.863 AND lng BETWEEN 2.284 AND 2.305;
An index on latitude returns a thin strip around the whole planet, and one on longitude returns another. The database has to intersect two huge sets. What we need is a way to map two dimensions to one so that points close on the map stay close in the index.
Geohash does this. Split the world in half by longitude and write 1 if the point is in the upper half, 0 if lower. Then split by latitude, then longitude again, alternating. Every 5 bits become one base32 character, and each extra character makes the cell 32 times smaller. Nearby points usually share a prefix, so “everything in this cell” becomes a prefix lookup.
Explorer: encode a location
Geohash
Bits, 5 per character (blue longitude, red latitude)
First splits
| Axis | Range | Midpoint | Bit |
|---|
- Cell size here
- Suggested precision
The boundary problem
Two cafés across a street can sit in different cells with no shared prefix. So never search only the user’s cell: query the cell and its eight neighbors, compute exact distances for the candidates, drop those outside the radius and sort. If too few results come back, drop one character of precision and search again.
| Index | Idea | Strength | Weakness |
|---|---|---|---|
| Geohash | Fixed grid, cell ID is a string prefix | Works in any database or Redis (GEOSEARCH); simple to shard and cache | Fixed cell size regardless of density |
| Quadtree | In memory tree; split any cell holding more than K places into four | Adapts to density: small cells in Manhattan, huge ones in a desert | Built at startup, harder to update live |
| S2 or H3 | Hierarchical cells on a sphere (S2 squares on a Hilbert curve, Uber’s H3 hexagons) | Covers any shape well; good for geofences | More complex to explain and operate |
What the interviewer probes next
What changes for moving objects, like ride share drivers?
Locations update every few seconds, so it becomes write heavy. Keep driver positions in memory (Redis geo sets or an in memory grid keyed by cell) with short TTLs, and never write every update to the main database.
Should we shard the geo index?
Not by default. At 3 GB it fits in memory on every replica, and sharding by region creates hot shards in big cities. Replicate it and shard only the business table, by business ID.
How do you cache searches when every user is at a different point?
Cache by cell, not by coordinates: key is the geohash at the precision for the radius, value is the list of business IDs. Every user in the same cell shares the entry.
Ticket booking
Thousands of people want the same few seats in the same second. The design is about contention: exactly one buyer gets each seat, holds expire cleanly, and the on-sale spike never takes the site down.
Design Ticketmaster: users find an event, choose seats and buy tickets.
Functional requirements
- Browse events and see a seat map with availability.
- Hold seats for a few minutes during checkout.
- Pay and receive tickets.
- No seat is ever sold twice.
Non-functional requirements
- Strong consistency for seat state.
- Survive on-sale spikes of millions of users.
- Fair access; bots kept out.
- Browsing may be a few seconds stale.
Estimates
Normal day 1M tickets per day ≈ 12 per second Big on-sale 2M fans for 50,000 seats = 40 fans per seat Seat map 2M fans refreshing every 5 s = 400,000 reads/s, so cache and push
Data model
events event_id, venue_id, on_sale_at
seats event_id, seat_id, section, price,
status FREE | HELD | SOLD,
held_by, hold_expires_at, version
bookings booking_id, user_id, event_id,
seat_ids, payment_id, statusThe one statement that matters
UPDATE seats
SET status = 'HELD', held_by = :user,
hold_expires_at = now() + interval '10 min'
WHERE event_id = :event AND seat_id = :seat
AND (status = 'FREE' OR
(status = 'HELD' AND hold_expires_at < now()));
/* 1 row changed: you hold it.
0 rows: someone else does. */Deep dive: preventing double booking
| Approach | How | Tradeoff |
|---|---|---|
| Pessimistic lock | SELECT … FOR UPDATE inside a transaction | Simple; contenders queue on the row lock |
| Optimistic lock | Read version, then UPDATE … WHERE version = :v | No locks; under heavy contention most attempts fail and retry |
| Conditional update Common pick | One UPDATE with the status check in WHERE | Atomic, one round trip, no retry loop |
| Redis hold with expiry | SET seat:A12 user NX PX 600000 | Fast holds off the database; the database must still enforce the final sale |
Deep dive: surviving the on-sale
- Virtual waiting room. Fans get a place in line and a signed admission token; the booking service only accepts requests with a valid token and admits at the rate it can serve.
- Seat map from cache, changes pushed. Serve the map from a cache refreshed every second or two and push seat changes over server-sent events. The hold step is authoritative, so a slightly stale map is harmless.
- Split the hot event. One event cannot be sharded by event ID; partition its seats by section instead.
What the interviewer probes next
The hold expires while the fan is paying. Now what?
Confirmation re-checks the hold. Extend it once when payment starts; if it was lost anyway and the charge succeeded, refund automatically and tell the fan clearly.
Why not keep seat state in application memory?
There are many application servers and their memory is not shared. One authority, the database or a single Redis key per seat, must arbitrate.
How do you stop bots from buying everything?
Verified fan presales, per account and per card limits, a challenge at waiting room entry, and device fingerprinting.
How does this change for hotels or flights?
Same core: conditional updates on inventory counts, holds with expiry and idempotent confirmation. Airlines also deliberately overbook, which is a business rule layered on top.
Ride sharing
Two hard problems in one: absorbing a firehose of GPS updates, and matching each rider to exactly one nearby driver within seconds.
Design Uber: a rider requests a ride, the system finds a nearby driver, and both follow the trip live on a map.
Functional requirements
- Drivers share their location while online.
- Riders see nearby cars and an arrival estimate.
- Request, match, accept; live tracking; fare at the end.
Non-functional requirements
- Match within a few seconds.
- Never assign one driver to two riders.
- Absorb hundreds of thousands of location updates per second.
- A failure in one region does not affect others.
Estimates
Location 1M drivers online × 1 update per 4 s = 250,000 writes/s Ingest 250,000 × 100 B ≈ 25 MB/s Ride asks 10M rides per day ≈ 115/s, peak ≈ 600/s
API
PUT /v1/drivers/me/location
{ lat, lng, heading, ts }
POST /v1/rides { pickup, dropoff }
→ 202 { rideId }
WS /v1/rides/{id}/stream
→ driver position, ETADriver state machine
AVAILABLE → OFFERED → ON_TRIP → AVAILABLE
│
└ timeout or decline → AVAILABLE
/* every transition is a compare and set */
UPDATE drivers SET status = 'OFFERED'
WHERE id = :d AND status = 'AVAILABLE';Deep dive: where locations live
| Option | Verdict |
|---|---|
| A database row per driver, updated every 4 s | 250,000 writes per second of data that is stale in seconds. The wrong tool. |
Redis geo set per city (GEOADD, GEOSEARCH) | In memory and simple; a solid answer for most cities. |
| In memory grid of cells (geohash, or H3 hexagons) sharded by city | What large platforms build. H3 was created at Uber for exactly this. |
Location history still matters for fares, safety and analytics, so updates are also written to a stream such as Kafka and stored in bulk, never row by row.
Deep dive: matching
Matching asks the index for available drivers in the rider’s cell and its neighbors, then ranks them by estimated time of arrival from a routing engine on the road graph, not by straight-line distance: the closest car by air may be across a river. The best candidate gets an offer with a timeout of 10 to 15 seconds; on timeout or decline, the next candidate gets it. The compare and set on driver status guarantees no driver is offered two rides at once.
Deep dive: live tracking
During a trip, the driver app keeps sending locations; the location service publishes them on a channel for that trip, and the rider’s WebSocket receives them. Updates every few seconds are enough, because the client animates smoothly between points.
What the interviewer probes next
The driver’s phone loses signal mid-trip.
The app buffers GPS points and uploads them on reconnect; the fare is computed from the full path afterwards. The rider sees the last known position with a reconnecting notice.
How does surge pricing work?
A stream job counts ride requests and available drivers per cell per minute. When demand far exceeds supply, a multiplier applies, smoothed over time so prices do not flicker.
The matching service crashes in the middle of an offer.
The ride request and its state are persisted, so another instance resumes it. Offers carry timeouts, and the driver compare and set prevents a double assignment during the handover.
How is the system partitioned?
By city or region: location index, matching and trip services all run per region, so a busy Saturday in one city cannot slow another, and an outage stays local.
Video streaming
Uploads are heavy but rare; views are light but constant and enormous. Most decisions trade compute at upload time for bandwidth at view time, and bandwidth is the bill that matters.
Design YouTube: creators upload videos, and hundreds of millions of people watch them on any device and any connection.
Functional requirements
- Upload large files reliably.
- Transcode into formats for every device and network.
- Stream with fast start and few pauses.
- Metadata, thumbnails, view counts.
Non-functional requirements
- Playback starts quickly and rarely rebuffers.
- Highly available, and originals are never lost.
- Cost efficient: egress dominates the bill.
Estimates (assumptions)
Uploads 1M videos per day × 300 MB = 300 TB per day of originals Views 1B views per day × 100 MB = 100 PB per day Egress 100 PB ÷ 86,400 s ≈ 1.2 TB/s, about 9 Tbps Verdict bandwidth dwarfs everything, so the CDN is the core of the design
API
POST /v1/uploads → { uploadId, chunkUrls[] } PUT {chunkUrl} 8 MB chunk, resumable POST /v1/uploads/{id}/complete GET /v1/videos/{id}/master.m3u8 → list of renditions
Deep dive: the transcoding pipeline
| Rendition | Typical bitrate, H.264 | Used for |
|---|---|---|
| 240p | ≈ 0.4 Mbps | Weak mobile networks |
| 480p | ≈ 1.2 Mbps | Phones on cellular |
| 720p | ≈ 2.5 Mbps | Laptops, good mobile |
| 1080p | ≈ 5 Mbps | Desktop, TV |
| 2160p (4K) | ≈ 15 Mbps | Large TVs on fast broadband |
Bitrates vary a lot with content and encoder; treat these as orders of magnitude.
Deep dive: adaptive bitrate streaming
Each rendition is cut into segments of a few seconds and listed in a manifest (HLS or DASH). The player downloads segments one at a time, measures throughput and how much video is buffered, and picks the quality of the next segment accordingly. That is why video drops to a blurrier picture instead of freezing when the train enters a tunnel.
Deep dive: spending to save
Newer codecs such as VP9 and AV1 need noticeably less bandwidth for the same quality but cost far more to encode. Spend that compute on popular videos, where every saved byte is multiplied by millions of views; keep the long tail on cheaper encodes. Push popular videos to edge caches ahead of demand; Netflix goes further and places its own cache servers inside internet providers’ networks.
What the interviewer probes next
How do you count billions of views?
Players send events to a stream; a job aggregates them per video per minute and updates counters in batches. Show an approximate count live and reconcile the exact one later, deduplicating by session.
What changes for live streaming?
There is no time for a big transcoding graph. Ingest over RTMP or SRT, transcode a few renditions in real time, and use segments of one or two seconds (low latency HLS). The CDN still does the fan-out.
How do you resume where the viewer stopped?
The player reports its position every few seconds to a per user key value store, read on the next play.
How do you detect copyrighted uploads?
Fingerprint the audio and video during processing and compare against a reference database, then block, monetize or track according to the owner’s policy.
File sync
Users expect every file on every device, always, and never a lost edit. Splitting files into blocks named by their content makes uploads small, storage deduplicated, and sync a matter of comparing lists of hashes.
Design Dropbox or Google Drive: files placed in a folder appear on all of a user’s devices and can be shared with others.
Functional requirements
- Upload and download files of any size.
- Automatic sync across devices.
- Version history and restore.
- Share files and folders.
Non-functional requirements
- Durability above all: never lose a file.
- Sync quickly while sending as few bytes as possible.
- Metadata is strongly consistent.
- Hundreds of petabytes.
Estimates
Commits 500M file changes per day ≈ 5,800 commits/s Storage 100M users × 5 GB = 500 PB before deduplication Upload a typical edit changes one 4 MB block, not the whole file
API
POST /v1/blocks/check { hashes: [...] }
→ which hashes the server is missing
PUT /v1/blocks/{hash} one 4 MB block
POST /v1/files/commit
{ path, baseVersion, blockHashes: [...] }
GET /v1/changes?cursor=c_9812 long pollData model
files file_id, namespace_id, path,
latest_version
versions file_id, version, block_hashes[],
size, device_id, created_at
blocks hash PK, storage_key, ref_count
journal namespace_id, seq, file_id, versionDeep dive: blocks and deduplication
Split each file into blocks of about 4 MB and name every block by its SHA-256 hash. Before uploading, the client asks which hashes the server already has and sends only the missing ones. The same block stored by a million users is kept once. Fixed size blocks have one weakness: inserting a byte near the start shifts every boundary. Content defined chunking places boundaries with a rolling hash, so they move with the content and an insert changes only one or two blocks.
Deep dive: the commit protocol
- Upload missing blocks to object storage.
- Commit the new version with its block list and the
baseVersionit was edited from. - The server accepts only if
baseVersionis still the latest, appends to the folder’s journal and bumpslatest_version, all in one transaction. - If another device committed first, the commit is rejected and the client saves its edit as a conflicted copy instead of overwriting.
Metadata lives in a relational database sharded by namespace (a user’s root or a shared folder), so each folder’s journal is on one shard and strictly ordered. Devices remember their position in the journal as a cursor and long poll for anything newer.
What the interviewer probes next
A 2 GB upload fails at 80%.
Blocks already uploaded stay on the server. The client asks again which hashes are missing and resumes from there.
How do deletes free up space?
Deletes are soft and kept in version history for a period. A garbage collector removes blocks whose reference count has reached zero.
A shared folder has 10,000 members.
The folder is its own namespace with one journal. Members’ devices follow that journal, and change notifications fan out through pub/sub.
Why not store files in the database?
Large blobs bloat the database and its backups. Object storage is far cheaper per byte and built for durability; Amazon S3 is designed for eleven nines.
Collaborative editor
Several people type into the same paragraph at once, and everyone must end up with exactly the same text. That convergence guarantee is the whole problem; the rest is routing every editor of a document to the same place.
Design Google Docs: several users edit one document at the same time and see each other’s changes and cursors live.
Functional requirements
- Real time co-editing of rich text.
- Collaborators’ cursors and selections.
- Comments and version history.
- Offline edits merge on reconnect.
Non-functional requirements
- Remote edits appear within about 100 ms in the same region.
- All copies converge to the same text.
- No acknowledged keystroke is lost.
Estimates (assumptions)
Peak load 1M open documents × 2 editors × 1 op/s ≈ 2M operations/s Servers ≈ 10,000 active documents per server ≈ 100+ collaboration servers Op log ≈ 50B ops per day × 100 B ≈ 5 TB per day before compaction
Data model
documents doc_id, title, owner,
snapshot_version
snapshots doc_id, version, content
operations doc_id, version, author,
op, created_at
append only, version + 1 eachAn operation on the wire
{ "doc": "d_71", "baseVersion": 4182,
"op": { "insert": "X", "at": 1 },
"clientOpId": "c9-0012" }
→ { "ack": "c9-0012", "version": 4183 }Deep dive: OT or CRDT
| Operational transformation (OT) | CRDT | |
|---|---|---|
| How it converges | A server orders operations and transforms each against concurrent ones it already applied | Every character gets a unique ID; merges are commutative, so any order gives the same result |
| Server | Required, one authority per document | Optional; peers can sync directly |
| Offline editing | Possible, but long divergence is hard | Natural fit |
| Overhead | Small operations | More metadata: IDs per character, deletion markers |
| Examples | Google Docs | Yjs, Automerge, many local-first apps |
Deep dive: one document, one server
Transformation needs a single ordering point, so every editor of a document must reach the same collaboration server. Route by doc_id with consistent hashing and a lookup service. That server holds the document in memory, assigns each operation the next version, appends it to the log, and broadcasts it. A client sends each operation with the version it was based on; if the server has moved on, it transforms the operation before applying it.
Cursors and selections are presence data: broadcast a few times per second, never persisted.
What the interviewer probes next
The server for a document crashes.
Another server claims the doc_id, loads the latest snapshot plus the operations after it, and clients reconnect. Each client resends unacknowledged operations with their base version and client operation ID, and the server transforms them and drops any it already applied.
How does version history work?
Replay operations from the nearest snapshot to reach any version. For the history panel, group operations into sessions by author and time.
Why not just lock the paragraph being edited?
It is simpler but feels broken: people expect to type anywhere. OT and CRDTs give lock-free editing.
What about very large documents?
Split them into blocks or pages, load lazily, and scope operations to blocks so a change in chapter 9 never touches chapter 1.
Distributed key value store
The advanced classic, modeled on Amazon’s Dynamo paper. Every technique from Part IV shows up here, combined, and the interviewer will ask what each one costs.
Design a key value store like Dynamo or Cassandra: put(key, value) and get(key) across hundreds of nodes, always writable.
Functional requirements
putandgetfor values under 10 KB.- Tunable consistency per request.
Non-functional requirements
- Petabytes of data, horizontal scaling, no downtime to add nodes.
- High availability: writes succeed even during failures.
- Low latency at the 99th percentile.
| Problem | Technique | What it gives you |
|---|---|---|
| Data larger than one machine | Consistent hashing with virtual nodes | Even spread; adding a node moves only its share |
| Node loss | Replicate to the next N distinct nodes clockwise | Copies on separate machines, racks and zones |
| Freshness against latency | Quorums N, R, W | Choose per request |
| Concurrent writes to one key | Vector clocks or last write wins | Detect conflicts, then resolve them |
| Temporary failure | Sloppy quorum and hinted handoff | Writes succeed while a replica is down |
| Permanent failure, drift | Anti-entropy with Merkle trees | Replicas resync, moving only what differs |
| Knowing who is alive | Gossip protocol | No central monitor; news spreads in O(log N) rounds |
| Fast writes | LSM tree: commit log, memtable, SSTables | Sequential disk writes only |
Partitioning and replication use exactly the ring from IV.3: pick a key there to see its three replicas.
Simulator: quorum reads and writes
- Consistency
- Writes survive
- Reads survive
Deep dive: conflicts and vector clocks
With W below N, two clients can update the same key on different replicas at once. A vector clock is a list of [server, counter] pairs attached to each version. If one version’s counters are all greater or equal, it descends from the other and wins. If neither dominates, the writes were concurrent and the system keeps both for the client to merge.
Deep dive: failures
Detecting them
Every node keeps a membership list with heartbeat counters and gossips it to a few random peers each second. If a node’s counter stops rising for a while, peers mark it down. News reaches every node in about log N rounds, with no single monitor to fail.
Surviving them
Hinted handoff: if a replica is down, the write goes to the next healthy node with a note naming the real owner, and is handed back when it returns. Merkle trees let two replicas compare hashes of key ranges and sync only the branches that differ.
What the interviewer probes next
Why not just use last write wins?
It is simple but silently discards one of two concurrent writes, and clock skew between servers makes the choice arbitrary. Fine for caches and full overwrites; wrong for carts or counters where both updates should survive.
How does a read find the latest version?
The coordinator reads R replicas and returns the newest version, or all concurrent ones. Replicas that returned stale data are fixed in the background (read repair).
How do you add a node with no downtime?
The new node takes its virtual positions on the ring, streams the key ranges it now owns from their current owners, then starts serving. Only neighboring ranges move.
How do you place replicas across data centers?
Walk the ring but skip nodes in a rack or zone that already holds a copy, so N replicas land in N failure domains. Cassandra calls this a network topology strategy.
Payment system
Low throughput, zero tolerance. Here the hard part is not scale but correctness across unreliable networks: never charge twice, never lose money, and prove it with a ledger.
Design the payment backend for a marketplace: customers pay, sellers get paid out, and the books always balance.
Functional requirements
- Pay-in: charge the customer through a payment service provider (PSP) such as Stripe or Adyen.
- Pay-out to sellers, refunds.
- Reconciliation with the PSP.
Non-functional requirements
- Correctness first: no double charges, no lost payments.
- Auditable history of every movement of money.
- Card data never stored by us (PCI DSS scope).
Estimates
Payments 1M per day ÷ 86,400 s ≈ 12 per second Peak Black Friday, 10× ≈ 120 per second Verdict one database handles this; spend the design effort on correctness
The flow
- Checkout sends a payment request with an idempotency key, for example the order ID. The payment service stores the order as
NOT_STARTED. - The executor calls the PSP. Card details never touch our servers: the PSP’s hosted page or SDK turns them into a token.
- The PSP answers, or calls our webhook later, and the order moves to
SUCCESSorFAILED. - On success, the wallet balance and ledger entries are written in one transaction, or through the outbox pattern if they live in different services.
- Nightly, reconciliation matches the PSP settlement file line by line against the ledger and flags every mismatch.
Simulator: a timeout, a retry, and the idempotency key
Charges at the PSP
- Charges
- 0
- Buyer charged
- €0
Idempotency table
| Key | Stored result |
|---|---|
| Empty | |
A timeout does not mean the charge failed. The server may have finished the work and only the response was lost. Retrying is necessary (at least once), and the idempotency key makes the retry harmless (at most once). Together they give the exactly once effect.
Deep dive: the double entry ledger
| Entry | Account | Debit | Credit |
|---|---|---|---|
| 1 | Buyer, card via PSP | €50.00 | |
| 2 | Seller payable | €45.00 | |
| 3 | Platform fee revenue | €5.00 | |
| Total | €50.00 | €50.00 |
Every transaction debits and credits the same total, so the sum of all entries is always zero. Entries are append only: a refund is a new pair of entries, never an edit. If the books ever fail to balance, you can trace exactly which transaction broke them.
Deep dive: retries and distributed steps
- Retry with exponential backoff and jitter: about 1, 2, 4, 8 seconds, each randomized so thousands of clients do not retry in lockstep. Then stop and alert.
- Check before retrying an unknown outcome: query the PSP for the payment’s status using the same key.
- Outbox pattern: write the state change and the event to publish in the same database transaction; a relay publishes the event afterwards. No “saved but never announced” gaps.
- Saga for multi-step flows: each step has a compensating action (a refund undoes a charge) instead of a distributed lock.
- Store money as integers in minor units (cents) with a currency code. Floating point cannot represent 0.10 exactly.
What the interviewer probes next
The PSP is down. What do customers see?
Route to a secondary PSP where contracts allow, protected by a circuit breaker so failing calls stop quickly. If none is available, fail fast with a clear error; never leave a payment in an unknown state without a follow up job to resolve it.
How are sellers paid out?
A daily batch sends pay-outs through the PSP or a bank, with the same idempotency keys and ledger entries. Funds are held for a period so refunds and chargebacks can be covered.
What does reconciliation do with a mismatch?
Classify it. Known patterns (a delayed webhook) are fixed automatically; unknown ones go to a finance queue for manual review. The point is that no discrepancy goes unnoticed for more than a day.
What is a chargeback, and how does the ledger handle it?
The customer disputes the charge with their bank, which pulls the money back. The ledger records reversing entries against the seller’s balance, which may go negative until the next pay-out settles it.
Review
The questions that come up in almost every interview, the tradeoffs behind them, and a quiz to check what stuck.
Most asked questions
The conceptual questions that come up again and again, with answers short enough to say out loud. Starred ones come up most often. Use the flashcards to practice answering before you peek.
Tradeoff sheet
Interviews reward the sentence “I chose A over B because of this requirement, and it costs us this.” Here are the pairs you will use most.
| Choice | Pick the first when | Pick the second when |
|---|---|---|
| Scale up or scale out | Traffic is modest and simplicity wins | You need failover or you have hit hardware limits |
| SQL or NoSQL | Relations, joins, transactions, evolving queries | Simple access by key at huge scale, flexible schema |
| Strong or eventual consistency | Money, inventory, uniqueness | Feeds, counters, carts, anything that tolerates seconds of lag |
| Push or pull fan-out | Most authors have modest audiences | Few authors with enormous audiences |
| Synchronous call or queue | The caller needs the result now | Work is slow, bursty or can finish later |
| Cache-aside or write-through | Read heavy; misses are acceptable | Reads must see fresh data right after writes |
| Normalize or denormalize | Writes dominate and data must not drift | Reads dominate and joins are too slow |
| Hash or range sharding | Even spread, point lookups | Range scans such as “orders from last week” |
| 301 or 302 redirect | You want minimal load | You need analytics or changeable links |
| Long polling or WebSocket | Occasional updates, simple infrastructure | Real time, two way, high frequency |
Self check quiz
Fifteen questions that come straight from the case studies. Each answer explains why, so a wrong pick is still useful.