SDE-2 HLD Handbook
High-level design from first principles to SDE-2 interviews in 17 modules and 16 case studies, practice first: every lesson opens with a question to try, every flow diagram steps through, every case study asks before it tells, and a design tracker brings each design back for spaced review.
Loading your progress.
How this handbook teaches
The goal is not to memorize 16 architectures. The goal is to look at a new prompt, name what makes it hard, and assemble a design from building blocks you understand well enough to defend. So the handbook asks you to think before it explains.
- Try before you read. Every lesson opens with a short question. Answer it on paper first; getting it wrong is what makes the explanation stick.
- Analogy, then mechanism, then trade-off. Each idea starts with what it solves and a plain analogy, then how it works, then what it costs. Depth sits in folded "Go deeper" sections.
- Step through every flow. Diagrams with a step bar reveal one arrow at a time. Predict the next arrow before pressing Next. You should be able to redraw each one from memory.
- Production notes. Green "In production" boxes show how the idea appears in real systems, including payments and data pipelines, so the handbook serves your day job, not only interviews.
- Interview signals. Purple boxes say what interviewers listen for at SDE-2 (L4) and what an L5 answer adds.
- One method everywhere. The decode method turns any prompt into pressures and building blocks in seven steps.
- Live tools. Calculators for estimation and availability, a quorum explorer, a consistent hashing playground, and a token bucket simulator let you test intuitions with numbers.
How to study one module
- Clear the revision queue above at the start of each session: redesign those systems from a blank page.
- Do the warm-up quiz without reading anything. Note which questions you missed.
- For each lesson: answer the "Try it first" question, read the short part, step through the figure, then say out loud what problem the idea solves and when it fails.
- Open "Go deeper" only when a case study or your own question needs it.
- Do the self-check quiz at the end, then tick lessons as studied.
- Tick the mastery checklist only when each item is true with your notes closed.
How to practice a case study
- Set a 45-minute timer and design it yourself on paper or a whiteboard using the interview framework: requirements, estimates, API, data model, diagram, deep dives.
- Then open the case study and compare section by section. Answer each guided deep dive before revealing it.
- Set an honest status at the top of the page. The handbook schedules a review.
- Redesign it from memory when it comes due, faster each time. Then try the related practice prompts in the tracker.
Design statuses and the revision queue
| Status | Meaning | Next review |
|---|---|---|
| Not started | Not attempted | Not scheduled |
| Could not design it | You needed the solution for the core | 2 days |
| Designed with hints | You got there with a few peeks | 4 days |
| Designed independently | Full design in 45 minutes, trade-offs explained | 10 days |
| Needs revision | Did it before, but a redo went badly | 1 day |
| Can teach it | Designed it twice weeks apart, and can answer every follow-up | 30 days |
When a design comes up for review, redesign it without looking at notes, then set the new status or press "Reviewed, same status" to restart its timer.
Where your progress is saved
Lesson ticks, mastery checks, readiness checks, and design statuses save automatically to your account, privately: nobody else who opens this page sees them, and they follow you to other devices. The indicator in the top bar shows where the latest change went. If the page cannot reach your account (for example in a private window), it saves in this browser instead, and the tracker has a backup and restore box.
Scope
This handbook covers what general SDE-2 system design rounds test, in the depth they test it, plus the production knowledge a backend engineer uses daily. It deliberately leaves out: machine learning system design (feature stores and model serving appear only as boxes), cloud certification trivia, and research-level internals (Paxos proofs, CRDT algebra). Low-level design (classes and patterns) belongs to a separate LLD track.
Syllabus and prerequisite map
Seventeen modules in five phases, then sixteen case studies. The order follows dependencies: each phase uses ideas from the one before.
The map
Priority overview
| Module | Why it matters | Interview weight | Time |
|---|---|---|---|
| 1 Systems and requirements | Vocabulary, requirements, trade-offs | Every interview | 2 h |
| 2 Networking | Protocols behind every arrow; real-time choices | Medium | 2.5 h |
| 3 API design | Contracts, pagination, idempotency | High | 2 h |
| 4 Estimation | Turns numbers into decisions | High | 1.5 h |
| 5 Scaling and load balancing | Stateless services, LBs, autoscaling | High | 2 h |
| 6 Caching | The most used scaling tool | Very high | 3 h |
| 7 CDN and object storage | Files, media, uploads | Medium | 1.5 h |
| 8 Databases | Choosing and using the store | Very high | 4 h |
| 9 Replication | Availability and read scaling | High | 2.5 h |
| 10 Sharding | Write and data scale | Very high | 2.5 h |
| 11 Consistency, CAP, consensus | What readers see; coordination | High | 3 h |
| 12 Queues and streams | Async, Kafka, delivery semantics | Very high | 3 h |
| 13 Distributed transactions | Sagas, outbox, idempotency | High (very high for fintech) | 3 h |
| 14 Microservices | Boundaries and communication | Medium | 2 h |
| 15 Reliability and rate limiting | Failure handling, limits, DR | High | 3 h |
| 16 Observability and security | Operating and securing systems | Medium | 2.5 h |
| 17 Building blocks toolkit | IDs, Bloom filters, geo, search, vectors | High | 3 h |
Total: about 45 hours of study, plus about 20 hours for the 16 case studies (45 minutes to design each, then about 30 minutes to compare and note what you missed).
How each module is organized
- Warm-up quiz to expose what you do not know yet.
- Lessons, each opening with a "Try it first" question, then what the idea solves, an analogy, the mechanism, and the trade-offs.
- Figures that step through flows one arrow at a time.
- Go deeper folds for details you need only sometimes.
- In production notes and Interview signal notes.
- Common mistakes, a self-check quiz, and a mastery checklist.
Case studies follow one format: try it first, requirements, estimates, API, data model, architecture figure, guided deep dives, failures, what a strong answer shows at L4 and L5, and follow-up questions.
Module 1: How a system is built
High-level design is deciding which boxes exist, how they talk, where the data lives, and what happens when load grows or a box dies. This module gives you the vocabulary and the habit of turning a vague product into numbers.
Answer each in one line before reading. Reveal and compare. Misses tell you which lesson to read closely.
- A single server runs the app and the database. Deploys cause 2 minutes of downtime. What is the first change?
Reveal
Run 2+ app servers behind a load balancer and deploy one at a time (rolling deploy). The database moves to its own machine so app servers become replaceable.
- The home page is identical for everyone and the database CPU sits at 90%.
Reveal
Cache it. A cache (Redis, or a CDN for whole pages) serves the repeated read from memory, so the database only sees misses.
- Sending a welcome email makes sign-up take 3 seconds.
Reveal
Put the email on a queue and let a worker send it. The user's request returns as soon as the job is enqueued.
- Profile photos are stored on the app servers' disks. A new server has none of them.
Reveal
Store files in an object store (S3, GCS) and serve them through a CDN. App servers keep no local state.
- "The system must be fast." What should you ask?
Reveal
Fast for which action, at what percentile, at what load: for example "feed loads in under 200 ms at p99 for 10K requests per second".
1.1What high-level design is
You are asked to "design Instagram". Write the first three questions you would ask before drawing anything.
Reveal the answer
Good first questions narrow the scope and get numbers:
- Which features are in scope? (post photos, follow, feed; probably not stories, DMs, ads)
- How many users, and how active? (for example 500M daily active users, 2 posts per day each)
- What matters most? (feed latency, never losing a photo, how fresh the feed must be)
Drawing before asking is the most common reason candidates design the wrong system well.
A plan for a system at the level of components: services, data stores, caches, queues, and the network paths between them. It answers: where does each piece of data live, who reads and writes it, and what happens at 10x load or when a machine dies.
City planning versus interior design. HLD decides where the roads, power station, and water tanks go and how big they must be. Low-level design (LLD) decides the rooms inside one building: classes, interfaces, and methods inside one service.
A diagram of components and data flow, the API, the data model, back-of-the-envelope numbers, and the trade-offs you chose with the reason for each.
| High-level design (HLD) | Low-level design (LLD) | |
|---|---|---|
| Unit | Services, databases, queues, regions | Classes, interfaces, methods |
| Questions | Which database? Where is the cache? How do we shard? | Which classes? Which design pattern? What are the method signatures? |
| Failure thinking | Machine, network, region outages | Exceptions, invalid states |
| Output | Architecture diagram, data model, API, capacity estimates | Class diagram, sequence diagram, code |
At work, HLD is the "design doc" stage: before a team builds a new pipeline (say, a statement-generation service), someone writes the components, data flow, failure handling, and capacity plan, and reviewers attack the trade-offs. The interview round is a compressed version of that review.
1.2The building blocks
Name the component you would reach for in each case: (a) many users read the same data, (b) work takes 30 seconds, (c) store 10 TB of videos, (d) spread requests across 20 servers.
Reveal the answer
(a) Cache. (b) Queue plus workers. (c) Object storage plus a CDN. (d) Load balancer. Most designs are these blocks, wired differently.
Almost every design is assembled from about a dozen blocks. Learn what each one is for, and you can read any architecture diagram.
| Block | Its job | Typical technology | Module |
|---|---|---|---|
| Client | Sends requests, renders results | Browser, mobile app, another service | 2 |
| DNS | Turns a name into an IP address | Route 53, Cloud DNS | 2 |
| CDN | Serves static or cacheable content from servers near the user | CloudFront, Akamai, Cloudflare | 7 |
| Load balancer | Spreads requests across healthy servers | NGINX, HAProxy, AWS ALB/NLB, Envoy | 5 |
| API gateway | One front door: auth, rate limits, routing to services | Kong, Apigee, AWS API Gateway | 5, 14 |
| App service | Business logic; stateless so any copy can serve any request | Spring Boot service on Kubernetes | 5 |
| Cache | Keeps hot data in memory to cut latency and DB load | Redis, Memcached | 6 |
| Database | Durable source of truth | PostgreSQL, MySQL, Cassandra, DynamoDB | 8 |
| Object storage | Cheap, durable storage for files and blobs | S3, GCS | 7 |
| Queue / log | Decouples producers from consumers; absorbs bursts | Kafka, RabbitMQ, SQS | 12 |
| Workers | Process background jobs from a queue | Consumer services, batch jobs | 12 |
| Search index | Full-text and faceted search | Elasticsearch, OpenSearch | 17 |
1.3Following one request end to end
A user opens a feed in the app. List every hop the request makes until the first byte comes back.
Reveal the answer
App resolves the domain (DNS, usually cached), opens a TCP + TLS connection to the load balancer, the LB forwards to one app server, the app server checks the cache, on a miss queries the database, builds the response, and sends it back the same way. Images on the page come separately from the CDN.
The rule this figure teaches: keep the synchronous path short. Everything the user waits for adds latency and is a place to fail. Move anything slow or non-essential (emails, thumbnails, analytics) to a queue.
Go deeperWhere the milliseconds go
A rough budget for one dynamic request within a region:
| Hop | Typical cost |
|---|---|
| DNS lookup | 0 ms if cached, 20-120 ms if not |
| TCP + TLS 1.3 handshake | 1-2 round trips; ~30-100 ms on mobile, ~1 ms inside a datacenter |
| Load balancer hop | under 1 ms |
| Redis read | ~0.5-1 ms over the network |
| Indexed SQL query | 1-10 ms |
| Unindexed query on a large table | seconds |
| Cross-region call | 50-150 ms |
Connection reuse (HTTP keep-alive, HTTP/2) removes the handshake cost from all but the first request, which is why clients and services keep long-lived connection pools.
1.4Functional and non-functional requirements
For a URL shortener, write two functional and three non-functional requirements. Put a number in every non-functional one.
Reveal the answer
Functional: create a short URL for a long URL; redirect a short URL to the long one. (Maybe: custom aliases, expiry, click counts.)
Non-functional: 100M new URLs per month; redirect latency under 50 ms at p99; 99.99% availability for redirects; short URLs never collide; links last at least 5 years.
What the system does, written as user actions: "a user can post a photo", "a user sees a feed of people they follow". Keep 3-5 core ones and say out loud what you are leaving out.
How well it must do it, with numbers: scale, latency, availability, consistency, durability, security, cost. NFRs decide the architecture; FRs decide the API.
The NFR checklist to run through in every design:
| Quality | Question to ask | Example answer |
|---|---|---|
| Scale | How many users, requests per second, data per day? | 100M DAU, 50K reads/s peak |
| Read/write ratio | Read-heavy or write-heavy? | 100:1 reads to writes |
| Latency | Which action, at which percentile? | Feed under 200 ms at p99 |
| Availability | How much downtime is acceptable? | 99.99% (about 52 minutes a year) |
| Consistency | Must every reader see the latest write immediately? | Feed can lag 5 s; payments cannot |
| Durability | Can we ever lose data? | Never lose a confirmed payment |
| Security / compliance | PII, payments, regions? | Card data under PCI DSS; data stays in India |
| Cost | Any budget or hardware limit? | Storage dominates; use cold tiers |
Consistency and availability requirements are often different per feature. "Likes can be eventually consistent, but the account balance must be strongly consistent" is a better answer than one global choice.
1.5Quality attributes and their trade-offs
Why can't a system be maximally fast, cheap, always available, and always consistent at once? Pick one pair and explain the tension.
Reveal the answer
Example: consistency vs availability. To guarantee every read sees the latest write, replicas must coordinate; if the network between them breaks, a replica must either refuse requests (lose availability) or answer with possibly stale data (lose consistency). Another: latency vs cost, since lower latency needs more replicas, more cache memory, and servers closer to users.
| Attribute | Meaning | Main tools |
|---|---|---|
| Scalability | Handle more load by adding resources | Stateless services, load balancing, caching, sharding, queues |
| Availability | Fraction of time the system answers | Redundancy, failover, health checks, multi-zone |
| Reliability | Answers correctly, even with faults | Retries, idempotency, replication, testing |
| Latency | Time for one request | Caching, CDN, indexes, fewer hops, async work |
| Throughput | Requests or bytes per second | Horizontal scaling, batching, partitioning |
| Consistency | Readers agree on the latest value | Single leader, quorums, consensus, transactions |
| Durability | Confirmed data survives failures | Replication, write-ahead logs, backups |
| Maintainability | Easy to change and operate | Clear boundaries, observability, automation |
The trade-offs you will name in almost every design:
- Consistency vs availability and latency. Strong consistency needs coordination, which costs round trips and can block during failures (Module 11).
- Latency vs throughput. Batching raises throughput but makes each item wait (Kafka producer batching, DB group commit).
- Read speed vs write speed. Indexes, caches, and denormalized copies make reads fast and writes slower or more complex.
- Simplicity vs scale. A single PostgreSQL primary handles far more than most people assume. Shard only when numbers force you to.
Interviewers at L4 listen for trade-offs said out loud with a reason: "I'll use cache-aside with a 60 s TTL because the profile page tolerates a minute of staleness and reads outnumber writes 100 to 1." A list of technologies without the "because" scores low.
1.6Growing a design from one box
A system on one server is growing fast. In what order would you add: CDN, read replicas, load balancer, separate DB server, cache, sharding? Justify the first two.
Reveal the answer
A common order: separate DB server, then load balancer with 2+ app servers, then cache and read replicas, then CDN and queues, then sharding, then multi-region. Separate the DB first because it lets app servers become stateless and replaceable; add the LB next because it removes the single point of failure and enables zero-downtime deploys. Sharding comes late because it is the most expensive change to undo.
| Stage | Bottleneck it fixes |
|---|---|
| Separate DB | App and DB fight for CPU and memory; one deploy kills both |
| LB + many app servers | One app server is a single point of failure and a CPU ceiling |
| Cache + read replicas | Database reads saturate the primary |
| CDN + queue | Static bytes waste server bandwidth; slow tasks block requests |
| Sharding | Writes or data size exceed one primary |
| Multi-region | Region outages and far-away users |
Do not jump to stage 6 for a system with 1,000 users. Over-engineering is marked down just like under-engineering. Size the design to the numbers you estimated.
1.7Common mistakes
Common mistakes
- Drawing before asking. Five minutes on requirements saves you from designing the wrong system.
- Non-functional requirements without numbers. "Highly available" and "low latency" decide nothing; "99.99%" and "200 ms p99" decide a lot.
- Stateful app servers. Sessions or files on local disk break load balancing and autoscaling. Keep state in a database, cache, or object store.
- Putting slow work on the request path. Email, image processing, and fan-out belong on a queue.
- One consistency choice for everything. Split by feature: money strong, counters and feeds eventual.
- Technology names as answers. "Use Kafka" is not a design; say what problem it solves here and what it costs.
1.8Self-check
If you miss one, reread the lesson it points to.
- What is the difference between availability and durability?
Reveal
Availability is whether the system answers right now. Durability is whether confirmed data survives failures. A system can be down (unavailable) without losing data (durable), or up while having lost data.
- Name three things that make an app server stateful, and where each should live instead.
Reveal
Sessions (Redis or a signed cookie/token), uploaded files (object storage), in-memory counters or caches that must be shared (Redis or the database).
- Why is "the system should be scalable" a weak requirement?
Reveal
It has no number. Say what grows (users, writes per second, data size) and to what level, so it can drive choices like sharding or caching.
- Give one example of the latency vs throughput trade-off.
Reveal
Batching: a Kafka producer waits a few milliseconds (linger) to send messages together, raising throughput but adding latency to each message.
- A design doc proposes sharding the database on day one for 5,000 users. What do you say?
Reveal
One well-indexed PostgreSQL primary with replicas handles this easily. Sharding adds cross-shard queries, rebalancing, and operational cost; defer it until estimates show writes or data size exceed one node.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 2: Networking essentials
Every arrow in a diagram is a network call with a cost and a failure mode. This module covers the parts of networking that change design decisions: TCP vs UDP, DNS, HTTP versions, TLS, and how to push data to clients in real time.
One line each, then reveal.
- A video call drops a packet. Should the app wait for it to be resent?
Reveal
No. A late video frame is useless, so video calls use UDP (or WebRTC over UDP) and skip lost packets. File transfers and APIs need every byte in order, so they use TCP.
- You change a DNS record to fail over to a new IP. Why do some users keep hitting the old IP for a while?
Reveal
Resolvers and clients cache the old answer until its TTL expires, and some ignore short TTLs. DNS failover is never instant.
- A chat app needs the server to push messages instantly to the phone. Which protocol?
Reveal
A persistent connection: WebSocket (two-way), or server-sent events if only the server pushes. Polling wastes requests and adds delay.
- Why do services keep connection pools instead of opening a connection per request?
Reveal
Each new TCP + TLS connection costs 2 round trips of handshakes plus CPU for crypto. Reusing connections removes that cost from every request after the first.
- Your gRPC service has 10 backends but one gets all the traffic. Why?
Reveal
gRPC uses long-lived HTTP/2 connections. An L4 load balancer balances connections, not requests, so one connection carries everything to one backend. Use an L7 (request-aware) load balancer or client-side balancing.
2.1Addresses, ports, and layers
A server runs PostgreSQL and a web app. How does a packet arriving at the machine know which program it is for?
Reveal the answer
By the destination port. The IP address identifies the machine; the port identifies the program on it: 443 for HTTPS, 5432 for PostgreSQL, 6379 for Redis, 9092 for Kafka.
Shipping a parcel. The IP address is the building, the port is the apartment, TCP is a tracked courier that guarantees delivery in order, UDP is a postcard: cheap and fast, no tracking.
You do not need all 7 OSI layers in an interview. You need three: L3 network (IP: routing between machines), L4 transport (TCP or UDP: delivery between programs), and L7 application (HTTP, gRPC, WebSocket, DNS: the message format).
Load balancers are described by layer. An L4 balancer routes by IP and port without reading the request. An L7 balancer reads HTTP and can route by path, header, or cookie (Module 5).
| Layer | Protocols | Unit | Design impact |
|---|---|---|---|
| L7 Application | HTTP, gRPC, WebSocket, DNS | Request / message | Routing by URL, auth, caching, retries |
| L4 Transport | TCP, UDP, QUIC (over UDP) | Segment / datagram | Reliability, ordering, connection cost |
| L3 Network | IP | Packet | Addressing, routing, cross-region latency |
2.2TCP and UDP
Name two applications that should use UDP and two that must use TCP. What property decides it?
Reveal the answer
UDP: live video or voice calls, online games, DNS lookups, QUIC/HTTP/3 (which builds its own reliability on top). TCP: web APIs, database connections, file transfer, email. The deciding property: whether late data is still useful. If a retransmitted packet would arrive too late to matter, skip it (UDP); if every byte must arrive in order, use TCP.
| TCP | UDP | |
|---|---|---|
| Connection | Yes: 3-way handshake first | No: just send |
| Delivery | Guaranteed, retransmits lost segments | Best effort, may lose packets |
| Order | In order | No ordering |
| Flow and congestion control | Yes | No (application decides) |
| Overhead | Higher (handshake, acks, 20+ byte header) | Lower (8 byte header) |
| Used for | HTTP/1.1, HTTP/2, databases, gRPC | DNS, video calls, games, QUIC (HTTP/3) |
Go deeperHead-of-line blocking and why HTTP/3 moved to UDP
TCP delivers bytes in order. If one packet is lost, every later packet waits in the receive buffer until the lost one is retransmitted, even if those later packets belong to unrelated requests. This is head-of-line (HOL) blocking.
HTTP/2 multiplexes many requests on one TCP connection, so one lost packet stalls all of them. HTTP/3 runs on QUIC, which is built on UDP and tracks each stream separately: a lost packet only stalls its own stream. QUIC also combines the transport and TLS handshakes into one round trip and keeps a connection alive when a phone switches from Wi-Fi to mobile data (connection IDs instead of IP and port).
2.3DNS
You type api.shop.com. Nothing is cached anywhere. Who answers, in what order?
Reveal the answer
Your OS asks a recursive resolver (your ISP's, or 8.8.8.8). The resolver asks a root server (which points to the .com servers), then a .com TLD server (which points to shop.com's name servers), then the authoritative server for shop.com, which returns the IP. Every answer is cached for its TTL, so the next lookup is usually instant.
| Record | Maps | Example use |
|---|---|---|
| A / AAAA | Name to IPv4 / IPv6 address | api.shop.com -> 203.0.113.7 |
| CNAME | Name to another name | www.shop.com -> shop.cdn-provider.net |
| MX | Domain to mail servers | Email routing |
| NS | Domain to its authoritative name servers | Delegation |
| TXT | Free text | Domain ownership checks, SPF for email |
DNS is also a coarse load balancer:
- Round-robin DNS: return several IPs; clients spread across them. No health awareness.
- GeoDNS / latency-based routing: return the IP of the region closest to the user (Route 53 latency routing, Cloud DNS geolocation).
- Failover routing: health-check a region and stop returning its IP when it fails.
DNS changes propagate only as fast as caches expire. A TTL of 60 s helps, but some resolvers and clients cache longer. For fast failover, put a load balancer or anycast IP in front and fail over behind it; use DNS for slower, coarse moves.
2.4HTTP versions and TLS
A web page loads 80 small files from one domain. Why is it faster over HTTP/2 than HTTP/1.1?
Reveal the answer
HTTP/1.1 handles one request at a time per connection, so browsers open about 6 connections per domain and queue the rest. HTTP/2 multiplexes all 80 requests over one connection at once and compresses repeated headers.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Requests per connection | One at a time (keep-alive reuses the connection) | Many, multiplexed as streams | Many, independent streams |
| Headers | Plain text, repeated | Binary, compressed (HPACK) | Binary, compressed (QPACK) |
| HOL blocking | Per connection | At the TCP level | Only per stream |
| Handshake for new HTTPS | TCP + TLS: 2 round trips | Same | 1 round trip (0 on resumption) |
TLS (the S in HTTPS) gives three things: encryption (nobody can read the traffic), integrity (nobody can modify it), and authentication (the certificate proves the server is really shop.com). TLS 1.3 needs one round trip; TLS 1.2 needed two.
Most systems terminate TLS at the load balancer or gateway: the LB holds the certificate, decrypts, and forwards to services over the private network. PCI DSS requires strong encryption for card data over public networks, and many teams also encrypt inside the private network to reduce risk: traffic is re-encrypted to the service, or services talk with mutual TLS (mTLS), where both sides present certificates. A service mesh (Module 14) automates mTLS between services.
2.5Real-time: polling, long polling, SSE, WebSockets
Pick a mechanism for each: (a) live sports score shown on a web page, (b) multiplayer chat, (c) checking whether a report export has finished, roughly every 30 s.
Reveal the answer
(a) Server-sent events: only the server pushes, works over plain HTTP, auto-reconnects. (b) WebSocket: both sides send frequently. (c) Plain polling is fine: rare checks, simple, cacheable, no long-lived connections to manage.
| Mechanism | Direction | How it works | Use when |
|---|---|---|---|
| Short polling | Client pulls | Request every N seconds | Updates are rare and delay is fine |
| Long polling | Client pulls, server holds | Server keeps the request open until data or timeout, client re-requests | Need push but cannot use WebSocket or SSE |
| Server-sent events (SSE) | Server to client | One HTTP response that stays open and streams text/event-stream events; browser auto-reconnects with the last event ID |
Feeds, notifications, live scores, LLM token streaming |
| WebSocket | Both directions | HTTP request with Upgrade: websocket, server answers 101, then frames flow both ways over the same TCP connection |
Chat, collaborative editing, games |
Scaling persistent connections
Each WebSocket is a long-lived connection pinned to one server. Two problems follow: the load balancer must keep each client on its server for the life of the connection, and a message for Bob must reach the specific server holding Bob's connection.
Go deeperOperating WebSocket fleets
- Capacity: one tuned server can hold tens of thousands to hundreds of thousands of mostly idle connections; memory per connection and file descriptor limits decide the number.
- Heartbeats: send ping/pong every 20-60 s so you detect dead connections and keep NAT and load balancer idle timeouts from cutting them.
- Deploys: draining a gateway disconnects its clients. Clients must reconnect with backoff and jitter, or every client reconnects at once (a thundering herd).
- Missed messages: a reconnecting client asks for messages after the last ID it saw; the server reads them from storage. The connection is a delivery optimization, never the source of truth.
2.6RPC and gRPC
Two internal services exchange 50,000 small requests per second. Why might they use gRPC instead of JSON over HTTP?
Reveal the answer
gRPC sends compact binary Protocol Buffers instead of JSON text (smaller, faster to parse), multiplexes calls over HTTP/2, generates typed client and server code from one .proto contract, and supports streaming and deadlines natively.
- gRPC = Protocol Buffers (schema and binary encoding) + HTTP/2 (transport). Four call types: unary, server streaming, client streaming, bidirectional streaming.
- Strengths: performance, strict contracts, code generation in many languages, deadlines that propagate across calls.
- Costs: not human-readable, browsers need a gRPC-Web proxy, and long-lived HTTP/2 connections need L7 or client-side load balancing.
- Typical split: REST/JSON for public and browser-facing APIs, gRPC between internal services. API styles are compared in Module 3.
A reverse proxy sits in front of servers and acts for them (TLS termination, compression, caching, routing): NGINX, Envoy. A forward proxy sits in front of clients and acts for them (egress control, corporate filtering). Load balancers and API gateways are specialized reverse proxies.
Interviewers rarely ask about TCP flags, but they do expect you to pick the right client-server channel and say why: "SSE for the live score because only the server pushes", "WebSockets for chat with a registry so any server can route to the user's connection", "gRPC between internal services, behind an L7 balancer".
2.7Common mistakes
Common mistakes
- Ignoring connection setup cost. New TCP + TLS per request adds 2 round trips. Pool and reuse connections.
- Expecting instant DNS failover. Cached answers outlive TTL changes; fail over behind a stable IP when speed matters.
- WebSockets for everything. If only the server pushes, SSE is simpler. If updates are rare, polling is simpler.
- Treating the socket as storage. Messages sent while a client was disconnected must come from a database on reconnect.
- L4 load balancing for gRPC. Connections, not requests, get balanced, so load piles onto a few backends.
- Saying "UDP is unreliable, so never use it". It is the right choice when late data is worthless.
2.8Self-check
- What does TLS provide besides encryption?
Reveal
Integrity (tampering is detected) and authentication (the certificate proves the server's identity; with mTLS, the client's too).
- Why does HTTP/2 still suffer from head-of-line blocking, and what fixes it?
Reveal
All streams share one TCP connection, and TCP delivers in order, so one lost packet stalls every stream. HTTP/3 over QUIC recovers loss per stream.
- A client reconnects its WebSocket after 2 minutes offline. How does it get the messages it missed?
Reveal
It sends the last message ID (or timestamp) it received; the server queries durable storage for newer messages and sends them, then resumes live delivery.
- What is the difference between GeoDNS and an anycast IP?
Reveal
GeoDNS returns different IPs based on where the resolver is. Anycast announces the same IP from many locations and lets internet routing deliver packets to the nearest one. Anycast reacts faster because it does not depend on DNS caches.
- When is plain polling the best choice?
Reveal
When updates are infrequent and a delay of one interval is acceptable, for example checking a long export job every 30 s. It is simple, stateless, and cache-friendly.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 3: API design
The API is the contract between your system and everything that calls it. A good one makes retries safe, pages stable, and changes backward compatible. In an interview it is the bridge from requirements to architecture.
One line each.
- Which HTTP status for a successful POST that created an order?
Reveal
201 Created, with aLocationheader pointing at the new order (for example/orders/123). - A client retries
POST /paymentsafter a timeout. What prevents a double charge?Reveal
An idempotency key: the client sends a unique key per logical payment; the server stores the first result under that key and returns it for any retry.
- Page 2 of a feed shows a post that was already on page 1. What pagination bug is this?
Reveal
Offset pagination. A new item was inserted at the top, shifting everything down one position. Cursor (keyset) pagination fixes it.
- Generating a report takes 2 minutes. What should
POST /reportsreturn?Reveal
202 Acceptedwith a job ID or status URL. The client polls the status resource or gets a webhook when it is done. - You need to rename a JSON field used by mobile apps you cannot force-update. What do you do?
Reveal
Add the new field, keep sending the old one, and remove it only after usage drops to zero, or ship the change in a new API version.
3.1Resources, methods, and status codes
Which of these can a client safely retry without side effects: GET /users/7, PUT /users/7 {name: "A"}, POST /orders, DELETE /orders/9?
Reveal the answer
GET, PUT, and DELETE are idempotent: repeating them leaves the server in the same state as doing them once (PUT sets the same value, a second DELETE finds nothing to delete). POST is not: each call may create a new order. That is why POST needs an idempotency key for safe retries.
REST models the system as resources (nouns, each with a URL) and uses the standard HTTP methods (verbs) on them. GET /users/7/orders lists Alice's orders; POST /users/7/orders creates one.
A library catalog. Every book has a shelf address (URL). You look a book up (GET), replace its record (PUT), fix one field (PATCH), add a new book (POST), or remove one (DELETE). The verbs are the same for every shelf.
| Method | Purpose | Safe (no change) | Idempotent |
|---|---|---|---|
| GET | Read a resource or list | Yes | Yes |
| POST | Create, or run an action | No | No |
| PUT | Replace a resource fully | No | Yes |
| PATCH | Change some fields | No | Not guaranteed |
| DELETE | Remove a resource | No | Yes |
| Code | Meaning | Typical use |
|---|---|---|
| 200 / 201 / 204 | OK / Created / No content | Read, create, delete |
| 202 | Accepted for later processing | Async jobs |
| 301 / 302 | Moved permanently / temporarily | Redirects (URL shortener!) |
| 304 | Not modified | Conditional GET with ETag |
| 400 / 422 | Malformed / semantically invalid request | Validation errors |
| 401 / 403 | Not authenticated / not allowed | Missing token / wrong permissions |
| 404 / 409 | Not found / conflict with current state | Duplicate unique key, version conflict |
| 412 | Precondition failed | If-Match ETag no longer current |
| 429 | Too many requests | Rate limited; send Retry-After |
| 500 / 502 / 503 / 504 | Server error / bad upstream / overloaded / upstream timeout | Retry 502, 503, 504 with backoff |
3.2Designing the API for a feature
Write the endpoints for a photo app: upload a photo, get a user's photos, like a photo, get the home feed.
Reveal the answer
POST /v1/photos -> 201 {photo_id, upload_url}
GET /v1/users/{user_id}/photos?cursor=...&limit=20
PUT /v1/photos/{photo_id}/likes/me -> 204 (idempotent like)
DELETE /v1/photos/{photo_id}/likes/me -> 204 (unlike)
GET /v1/feed?cursor=...&limit=20
Making "like" a PUT on a sub-resource makes it naturally idempotent: liking twice is still one like. The upload returns a presigned URL so the bytes go straight to object storage (Module 7).
Rules that keep an API clean:
- Nouns in paths, verbs in methods:
POST /orders, notPOST /createOrder. For actions that are not CRUD, use a sub-resource:POST /orders/9/cancel. - The user comes from the auth token, not from the body:
GET /feed, notGET /feed?user_id=7. Otherwise anyone can read anyone's feed. - Every list is paginated and has a maximum page size.
- Return what the client needs next: IDs, a cursor, a status URL.
- Use IDs that do not leak information: random or time-ordered IDs, not sequential ones that reveal your order count (Module 17).
In an interview, write 3-5 endpoints with method, path, key parameters, and response, then move on. Ten minutes on API details is too long; zero minutes means the interviewer cannot see how clients use your system.
3.3Pagination
Why does LIMIT 20 OFFSET 100000 get slower as users scroll, even with an index?
Reveal the answer
The database must still walk past the first 100,000 rows before returning 20. Cost grows with the offset. Keyset pagination (WHERE id < last_seen_id ORDER BY id DESC LIMIT 20) jumps straight to the right place with the index, so every page costs the same.
Offset (?page=3) |
Cursor / keyset (?cursor=abc) |
|
|---|---|---|
| Jump to page N | Yes | No, only next or previous |
| Cost of deep pages | Grows with offset | Constant with an index |
| Stable under inserts and deletes | No: duplicates and skips | Yes |
| Use for | Admin tables, small data | Feeds, timelines, any large or live list |
The cursor is usually an opaque, encoded value of the sort key, for example base64 of (created_at, id). Include the ID as a tie-breaker so rows with equal timestamps are not skipped.
3.4Idempotency keys and safe retries
A payment request times out. The client does not know whether the charge happened. Design the server behavior so the client can always retry safely.
Reveal the answer
The client generates a unique key per logical operation (a UUID) and sends it in an Idempotency-Key header. The server records the key before doing the work (with a unique constraint), stores the final response under it, and on any request with a key it has already finished, returns the stored response without redoing the work. If the first request is still in progress, the retry gets 409 Conflict (or waits) instead of starting a second charge.
Go deeperImplementing idempotency correctly
- Key scope: unique per client (or account) plus key, so two merchants can use the same string.
- Atomic claim:
INSERT ... (key, status='in_progress')with a unique constraint. The loser of a race sees the conflict and does not start the work. - Same key, different body: compare a hash of the request; if it differs, return
422. Otherwise a buggy client could reuse a key for a different payment. - Store the outcome, including failures that are final (a declined card), so the retry gets the same answer.
- Expiry: keep keys long enough to cover realistic retries. Stripe, for example, documents keeping them for at least 24 hours.
- Downstream calls: pass an idempotency key on to the card network or PSP too, so your own retries cannot double-charge.
The same pattern protects any "do it exactly once" effect: sending a notification, creating a loan account, posting a ledger entry. A unique constraint on a business key ((merchant_id, idempotency_key) or (event_id)) in the database is the most reliable guard, more reliable than a Redis TTL key, because it is atomic with the write and survives restarts.
3.5REST, gRPC, and GraphQL
A mobile home screen needs data from 6 services. With REST it makes 6 calls over a slow network. Name two ways to fix this.
Reveal the answer
(1) A backend-for-frontend or aggregation endpoint: one call to a server that fans out to the 6 services over the fast internal network. (2) GraphQL: the client sends one query describing exactly the fields it needs and the GraphQL server resolves them.
| REST (JSON over HTTP) | gRPC | GraphQL | |
|---|---|---|---|
| Shape | Resources and verbs | Typed procedures from a .proto contract |
One endpoint; client sends a query for exact fields |
| Payload | JSON text | Protocol Buffers, binary | JSON |
| Strengths | Simple, universal, HTTP caching works | Fast, typed, streaming, code generation | No over-fetching, one round trip for nested data |
| Weaknesses | Over- and under-fetching, many calls | Needs HTTP/2, browsers need a proxy | HTTP caching is harder, N+1 resolvers, query cost control |
| Best for | Public APIs, web and mobile | Internal service-to-service | Rich clients with varied data needs |
N+1 in GraphQL: a query for 50 posts and each post's author can trigger 1 query for posts plus 50 for authors. Batch with a DataLoader (collect IDs, one WHERE id IN (...) query). Protect the server with query depth and cost limits.
3.6Versioning, errors, and evolution
Which of these changes break existing clients: adding a new optional response field, removing a field, making an optional request field required, adding a new endpoint?
Reveal the answer
Breaking: removing a field and making an optional request field required. Safe (additive): a new optional response field and a new endpoint. Clients should ignore fields they do not know (the tolerant reader rule), which is what makes additive changes safe.
- Version in the path (
/v1/orders) is the most common and easiest to route; header-based versioning keeps URLs stable but is less visible. - Prefer additive change inside a version; create
v2only for breaking changes, and run both until clients migrate. - One error format everywhere: an HTTP status plus a body like
{"error": {"code": "card_declined", "message": "...", "request_id": "..."}}. The machine-readable code lets clients branch; the request ID lets support find logs. - Tell clients about limits:
429withRetry-After, and headers for the current quota. - Optimistic concurrency: return an
ETag(a version); clients sendIf-Matchon updates; the server returns412if someone else changed it first.
3.7Async APIs and webhooks
Your API calls a partner whose response takes up to 5 minutes. How do you avoid holding the HTTP request open?
Reveal the answer
Return 202 Accepted immediately with a status URL; a worker does the slow call. The client either polls the status URL or registers a webhook URL that you call when the job finishes.
Webhooks are the server calling the client. To make them reliable:
- Sign every payload: an HMAC-SHA256 of the body plus a timestamp, using a shared secret. The receiver recomputes it and rejects stale timestamps to stop replays.
- Retry with backoff on non-2xx responses, for hours or days, and give the receiver a way to fetch missed events.
- Receivers must be idempotent: the same event can arrive twice. Deduplicate by event ID.
- Receivers should answer fast: store the event, return 200, process asynchronously.
3.8Common mistakes
Common mistakes
- Verbs in URLs (
/getUser) and actions that hide side effects behind GET. - Taking the user ID from the request body for "my" resources instead of from the auth token.
- Unbounded lists with no pagination or page-size cap.
- Offset pagination for feeds, which duplicates and skips items and slows down on deep pages.
- Retrying POST without an idempotency key, which turns network timeouts into duplicate orders or charges.
- Breaking changes without a new version, and clients that crash on unknown fields.
3.9Self-check
- Why is
PUT /photos/9/likes/mea better "like" endpoint thanPOST /photos/9/like?Reveal
PUT on a sub-resource is idempotent: liking twice leaves one like. The POST version would need extra dedup logic for retries.
- What should a cursor contain for a feed sorted by time?
Reveal
The sort key of the last item seen plus a unique tie-breaker, for example
(created_at, post_id), encoded so clients treat it as opaque. - Two requests with the same idempotency key arrive at the same time. What stops both from charging?
Reveal
An atomic claim: inserting the key with a unique constraint (or a conditional write) before doing the work. Only one insert succeeds; the other gets a conflict and waits or returns 409.
- When would you choose gRPC over REST?
Reveal
For internal service-to-service calls with high volume, strict contracts, or streaming, where binary encoding, code generation, and HTTP/2 multiplexing pay off.
- How does a webhook receiver know a request really came from the sender?
Reveal
It verifies the signature: HMAC of the payload (and timestamp) with the shared secret must match the signature header, and the timestamp must be recent.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 4: Back-of-the-envelope estimation
A two-minute estimate decides whether you need one database or fifty, whether the cache fits in memory, and whether you need a CDN. The skill is not precision; it is getting the order of magnitude right and saying what it implies.
Round hard. Within 2x is good enough.
- 1 million requests per day is about how many per second?
Reveal
About 12 per second (1,000,000 / 86,400). Remember it: 1M/day is roughly 10/s.
- 100M users each upload one 200 KB photo per day. Storage per day?
Reveal
100M x 200 KB = 20 TB per day, about 7 PB per year before replication.
- A service peaks at 30,000 requests/s and one server handles 2,000. How many servers?
Reveal
15 at peak, so run about 20 or more to survive losing a zone and to absorb spikes.
- Can 50M user profiles of 2 KB each fit in one Redis node's memory?
Reveal
50M x 2 KB = 100 GB. That fits on one large machine, but it is near the limit; shard it across a few nodes for headroom and failover.
- Which is faster: a network round trip inside a datacenter or a random read from an SSD?
Reveal
The SSD random read (tens of microseconds) is faster than a datacenter round trip (about half a millisecond). Both are about 1,000 times slower than reading memory.
4.1Why estimate at all
An interviewer says "1 billion URLs shortened per month, each read 100 times". Without computing exact numbers, what two design decisions might these numbers change?
Reveal the answer
(1) Storage and sharding: 1B new rows a month is 12B a year, too many to keep comfortably in one relational node for years, so plan for a horizontally scalable store or sharding. (2) Caching: about 40,000 reads per second on average is read-heavy enough that a cache in front of the database is clearly worth it.
How many servers, whether data fits in one database or needs sharding, whether hot data fits in a cache, whether bandwidth needs a CDN, and which component is the bottleneck.
Planning a wedding. You do not count grains of rice; you estimate guests x plates x cost per plate to decide between a hall for 100 and one for 1,000. The precision only has to be good enough to pick the hall.
Write the formula, round every number to one significant figure, keep units visible, and state the implication out loud: "40K reads/s, so we need a cache."
4.2Numbers to remember
How many bytes is: a 64-bit timestamp, a UUID stored in binary, a 140-character ASCII message, a typical row for a user profile?
Reveal the answer
8 bytes; 16 bytes (36 as a text string); 140 bytes; a few hundred bytes to about 1 KB. These small sizes add up when multiplied by billions of rows.
| Power | Approx value | Unit |
|---|---|---|
| 2^10 | 1 thousand (10^3) | KB |
| 2^20 | 1 million (10^6) | MB |
| 2^30 | 1 billion (10^9) | GB |
| 2^40 | 1 trillion (10^12) | TB |
| 2^50 | 1 quadrillion (10^15) | PB |
| Time and rates | Value |
|---|---|
| Seconds in a day | 86,400, round to 10^5 |
| Seconds in a month / year | about 2.5 million / about 30 million |
| 1M requests per day | about 12 per second |
| 1B requests per day | about 12,000 per second |
| Typical sizes | Value |
|---|---|
| int / long / timestamp | 4 / 8 / 8 bytes |
| UUID | 16 bytes binary, 36 as text |
| URL | about 100 bytes |
| Short post or tweet with metadata | about 300 bytes to 1 KB |
| Compressed photo | about 200 KB (thumbnail 20 KB, full 1-3 MB) |
| 1 minute of HD video at 5 Mbps | about 40 MB |
Rough capacity per node
These are planning numbers for simple operations on modern hardware. Real numbers depend on payload size, query shape, and tuning, so say "roughly" and move on.
| Component | Rough capacity per node |
|---|---|
| Stateless API server | 1,000-10,000 requests/s for light requests |
| PostgreSQL / MySQL primary | Thousands to low tens of thousands of simple indexed queries/s; fewer writes |
| Redis | Around 100,000 simple ops/s, more with pipelining |
| Kafka broker | Hundreds of MB/s of messages |
| Cassandra node | Thousands to tens of thousands of writes/s |
| Network card | 10-25 Gbps, about 1-3 GB/s |
| Server memory / SSD | 64-512 GB RAM, several TB of SSD |
4.3Estimating traffic
50M daily active users each open the app 10 times a day and each open loads a feed. Average and peak feed QPS?
Reveal the answer
50M x 10 = 500M feed loads a day. 500M / 10^5 is about 5,000/s on average (exactly 5,787). Peak is commonly 2-3x the average, so plan for about 15,000/s, more if traffic spikes around events.
The formula: QPS = daily active users x actions per user per day / 86,400. Then:
- Split reads and writes. They usually scale differently and hit different components. A 100:1 read-to-write ratio points at caching and read replicas; write-heavy points at partitioning, queues, and append-friendly storage.
- Peak vs average. Use 2-3x for normal daily peaks; 10x or more for flash sales, ticket launches, or live events.
- Fan-out multiplies. One post to 1,000 followers is 1 write request but 1,000 feed updates if you precompute feeds (Case study 4).
4.4Estimating storage
A chat app stores 20B messages a day at about 100 bytes each, for 5 years, with 3 replicas. Total storage?
Reveal the answer
20B x 100 B = 2 TB per day. x 365 x 5 = about 3.65 PB. x 3 replicas = about 11 PB. That number tells you this needs a horizontally scalable store (Cassandra, HBase, or sharded SQL) and probably cheaper storage tiers for old messages.
Storage = new items per day x size x retention days x replication. Then say what it implies:
- Single-digit TB and a few thousand writes/s: one well-provisioned relational primary with replicas is usually fine.
- Tens of TB, or writes beyond what one primary handles: partition the data (Module 10).
- Large binary objects (photos, video): never in the database. Object storage holds the bytes; the database holds metadata and the object key (Module 7).
- Indexes add space: often 20-100% on top of the raw data.
4.5Bandwidth, cache, and servers
Readers fetch 10,000 images per second at 200 KB each. Egress bandwidth? What does that imply?
Reveal the answer
10,000 x 200 KB = 2 GB/s, about 16 Gbps. That would saturate the network of several servers and cost a lot in egress, so put a CDN in front: most bytes then come from edge caches near users.
- Bandwidth = QPS x response size. Gbps = GB/s x 8.
- Cache size: many systems follow an 80/20 shape, where about 20% of items get about 80% of reads. Caching 20% of a day's read volume is a common first estimate and usually an overestimate, since repeated reads hit the same items.
- Servers = peak QPS / QPS per server, then add headroom (run at 50-70% of capacity) and redundancy (survive losing one zone).
Guided designEstimate a photo-sharing app200M daily users, 0.1 uploads and 50 views per user per day
- 1Upload and view QPS
Compute the average and peak for both.
Write your answer, then reveal
Uploads: 200M x 0.1 = 20M per day, about 230/s on average and about 700/s at 3x peak. Views: 200M x 50 = 10B per day, about 115,000/s on average and about 350,000/s at peak. Reads outnumber writes 500 to 1.
- 2Photo storage
Assume about 2 MB stored per photo across all sizes. Per day and per year?
Write your answer, then reveal
20M x 2 MB = 40 TB per day, about 15 PB per year. Object storage handles durability internally (replication or erasure coding), so do not multiply by 3 here; do say it is the dominant cost and old photos can move to colder, cheaper tiers.
- 3Metadata storage
About 1 KB per photo row (owner, caption, time, object key, counts).
Write your answer, then reveal
20M x 1 KB = 20 GB per day, about 7 TB per year. Too big for one node over several years, but small compared with the images, so a sharded relational store or a wide-column store works.
- 4Egress
Views fetch a 200 KB display size. What bandwidth, and what does it mean?
Write your answer, then reveal
115,000/s x 200 KB = 23 GB/s on average, about 180 Gbps. Only a CDN can serve this economically; origin servers should see only cache misses.
- 5What the numbers decide
Summarize the design consequences in three bullets.
Write your answer, then reveal
- Bytes go to object storage plus a CDN; uploads go straight to storage with presigned URLs.
- Reads dominate: cache metadata and precompute or cache feeds.
- Metadata needs partitioning; pick a shard key such as
user_id(Module 10).
Capacity planning at work uses the same arithmetic with measured inputs: take today's peak QPS from dashboards, apply the growth forecast and a safety factor, divide by per-instance throughput from a load test, and add enough redundancy to lose a zone. Re-run it before every big launch or sale.
Spend about 5 minutes on estimates and never leave a number without its consequence. Interviewers care far less about the exact figure than about whether it changes your design: sharding or not, cache or not, CDN or not.
4.6Little's Law and utilization
A service handles 2,000 requests/s and each request holds a database connection for 50 ms. How many connections are busy at any moment? What happens if the database slows to 200 ms per query?
Reveal the answer
Busy connections = throughput x time held = 2,000 x 0.05 s = 100. At 200 ms it becomes 400: the same traffic now needs 4x the connections, which is how a slow dependency exhausts pools and threads in services that call it.
- Little's Law: items in a system = arrival rate x time each item spends in it (
L = lambda x W). It holds for any stable system, so it sizes thread pools, connection pools, and consumer counts. Consumers needed = message rate x processing time per message (5,000 msg/s x 20 ms = 100 concurrent consumers). - Utilization drives queueing: as a server nears 100% busy, waiting time explodes. In a simple single-server queue, average wait is about
service time x rho / (1 - rho), whererhois utilization:
| Utilization | Wait relative to service time |
|---|---|
| 50% | 1x |
| 80% | 4x |
| 90% | 9x |
| 95% | 19x |
That curve is why capacity plans target 50-70% utilization at peak, and why latency spikes long before CPU reaches 100%.
4.7Common mistakes
Common mistakes
- False precision. "115,740.74 QPS" wastes time. Say "about 120K".
- Dropping units. Mixing bits and bytes (Gbps vs GB/s) or KB and MB produces errors of 8x or 1,000x.
- Forgetting peaks. Capacity is sized for the peak, not the average.
- Estimating without a conclusion. Every number should end in "so we need..." or "so one node is enough".
- Storing blobs in the database because the storage math was skipped.
- Ignoring replication and indexes when sizing databases.
4.8Self-check
- 300M daily users each post 2 messages of 1 KB. Daily storage and write QPS?
Reveal
600M messages x 1 KB = 600 GB per day; 600M / 86,400 is about 7,000 writes/s on average, about 20,000 at peak.
- Convert 2 GB/s to Gbps.
Reveal
16 Gbps (multiply bytes by 8).
- Your estimate says 4 TB of hot data and 2,000 writes/s. Do you shard on day one?
Reveal
Not necessarily. One strong primary with replicas can handle this; plan the shard key now so you can split later, and shard when growth projections cross one node's limits.
- Why add headroom on top of peak QPS / per-server QPS?
Reveal
Servers degrade near full utilization (queueing makes latency spike), and you must survive losing a zone or a deploy taking servers out.
- Read QPS is 200K/s, read size 10 KB. What is the bandwidth, and where should the reads go?
Reveal
2 GB/s (16 Gbps). Serve from cache and, if the content is public, a CDN; the database should see only misses.
Mastery checklist
Tick an item only when it is true with your notes closed.
Decode a design prompt
Every design prompt hides one or two dominant pressures: read-heavy, write-heavy, fan-out, real time, big files, money, location, spikes. Name the pressure and the building blocks follow. This page starts with six drills, then gives the method and the signal table they use.
Six drills
Do each drill in about 5 minutes on paper. Answer each step before revealing it. The goal is speed at spotting what matters, not a full design.
Guided designDrill 1: a pastebinUsers paste text up to 1 MB and share a link; links expire
- 1Users and core actions
Who uses it, and what are the two or three actions?
Write your answer, then reveal
Anyone creates a paste (write) and anyone with the link reads it (read). Optional: expiry, private pastes, syntax highlighting.
- 2Dominant pressure
Read-heavy or write-heavy? What is unusual?
Write your answer, then reveal
Read-heavy (each paste read many times), and the payload is a blob up to 1 MB. Expiry adds cleanup work.
- 3Building blocks
Which blocks follow from that?
Write your answer, then reveal
Object storage for paste bodies, a small metadata store (key, owner, expiry, object key), unique short keys (Module 17), cache plus CDN for hot pastes, a background job or TTL to delete expired pastes.
- 4The one hard problem
What will the interviewer push on?
Write your answer, then reveal
Key generation without collisions, and serving popular pastes cheaply (CDN with correct cache headers). It is the URL shortener plus blobs.
Guided designDrill 2: live comments on a sports stream2M viewers watch one match; comments should appear within 2 seconds
- 1Users and core actions
What happens, and how often?
Write your answer, then reveal
Viewers post comments (maybe 5,000/s at peaks) and every viewer sees new comments in near real time.
- 2Dominant pressure
What is the multiplier?
Write your answer, then reveal
Fan-out in real time: one comment must reach up to 2M open connections. Writes are modest; delivery is huge.
- 3Building blocks
Which blocks?
Write your answer, then reveal
WebSocket or SSE gateways holding connections, pub/sub to fan messages to all gateways, sampling or batching (send the latest 20 comments per second, not all), a comment store for history, rate limits and moderation.
- 4The one hard problem
Where does it break?
Write your answer, then reveal
Pushing every comment to every viewer is impossible at peak; the answer is to batch and sample per viewer and to fan out through a tree of gateways.
Guided designDrill 3: a payment API for merchantsMerchants charge cards through your API; no double charges
- 1Users and core actions
Who calls it and what must happen?
Write your answer, then reveal
Merchant servers create payments; you call card networks or processors; merchants get results and webhooks.
- 2Dominant pressure
Speed or correctness?
Write your answer, then reveal
Correctness: exactly-once effects across unreliable networks, an auditable ledger, and partners that time out. Volume is moderate.
- 3Building blocks
Which blocks?
Write your answer, then reveal
Idempotency keys, a relational ledger (double entry), a payment state machine, outbox to publish events, retries with backoff to the processor, webhooks with signatures, daily reconciliation.
- 4The one hard problem
What will be probed?
Write your answer, then reveal
A timeout from the processor: did the charge happen? Answer with idempotent retries using the processor's idempotency key, status queries, and reconciliation.
Guided designDrill 4: nearby gymsUsers see gyms within 5 km, sorted by distance; 1M gyms, data changes rarely
- 1Dominant pressure
What kind of query is this?
Write your answer, then reveal
A read-heavy geospatial query over mostly static data.
- 2Building blocks
Which blocks?
Write your answer, then reveal
A geospatial index (geohash or quadtree, or PostGIS or Elasticsearch geo queries), read replicas or an in-memory index per region, caching of popular areas, a separate service for gym details.
- 3The one hard problem
What detail matters?
Write your answer, then reveal
Cell edges: query the user's cell plus neighbors, then filter by exact distance; choose cell precision from the radius.
Guided designDrill 5: a flash sale100K users try to buy 1,000 discounted phones at 12:00
- 1Dominant pressure
Name two.
Write your answer, then reveal
A huge spike at a known moment, and strict inventory correctness (no overselling).
- 2Building blocks
Which blocks?
Write your answer, then reveal
A waiting room or queue to admit users at a controlled rate, pre-scaled stateless tiers, inventory decremented atomically (a conditional update in the DB, or an atomic Redis counter reconciled with the DB), reservation with a payment timeout, CDN for static pages.
- 3The one hard problem
What will be probed?
Write your answer, then reveal
Preventing oversell under contention while keeping the database alive: atomic decrements, reservations that expire, and admission control in front.
Guided designDrill 6: a metrics platform100K servers send 200 metrics every 10 seconds; dashboards and alerts
- 1Dominant pressure
Read-heavy or write-heavy?
Write your answer, then reveal
Write-heavy: 100K x 200 / 10 = 2M data points per second, append-only, time-ordered.
- 2Building blocks
Which blocks?
Write your answer, then reveal
A buffer (Kafka), a time-series store (or wide-column with time buckets), downsampling (raw for days, 1-minute rollups for months), an alert evaluator, a query layer with caching for dashboards.
- 3The one hard problem
What will be probed?
Write your answer, then reveal
Cardinality (unique label combinations), storage cost and retention tiers, and keeping ingestion working when the query side is overloaded.
The method
Use this sequence on every prompt. It is the same order as the interview framework, compressed to what drives decisions.
| Step | Ask yourself | Output |
|---|---|---|
| 1. Users and actions | Who uses it? What are the 3-5 core actions? What is out of scope? | Functional requirements |
| 2. Numbers | How many users, actions per day, read/write ratio, data size, peak? | QPS, storage, bandwidth (Module 4) |
| 3. Qualities | Latency target, availability, consistency per action, durability, compliance | Non-functional requirements |
| 4. Dominant pressure | Which one or two pressures from the table below dominate? | One sentence: "This is a read-heavy fan-out problem" |
| 5. Blocks | Which building blocks answer those pressures? | High-level diagram |
| 6. Hard problem | Where will it break at 10x? What fails first? | 2-3 deep dives |
| 7. Trade-offs | What did you give up, and why is it acceptable? | Spoken trade-offs |
Signals and the blocks they point to
| Signal in the prompt or numbers | Pressure | Reach for |
|---|---|---|
| Same data read far more than written | Read-heavy | Cache-aside, read replicas, CDN, denormalized read models |
| Writes in the tens of thousands per second, append-only | Write-heavy | Kafka buffer, LSM / wide-column store, partitioning, batching |
| One action must reach many users (followers, viewers) | Fan-out | Fan-out on write or read (hybrid), pub/sub, feed cache |
| "Instantly", "live", chat, presence | Real time | WebSocket or SSE gateways, pub/sub, connection registry |
| Photos, video, files, documents | Large objects | Object storage, presigned uploads, CDN, async processing |
| Money, inventory, seats, "never twice" | Strong correctness | Relational DB transactions, conditional updates, idempotency keys, outbox, reconciliation |
| Users worldwide, low latency everywhere | Geo-distribution | CDN, geo-routing, regional replicas, data residency |
| Known spike moment (sale, launch, match) | Spiky load | Queue, waiting room, pre-scaling, load shedding |
| Text search, filters, relevance | Query flexibility | Inverted index (Elasticsearch) fed by CDC |
| "Near me", maps, drivers | Location | Geohash, quadtree, S2 or H3, in-memory grid for moving objects |
| Counts, top-K, trending, unique visitors | Aggregation | Stream processing, sharded counters, count-min sketch, HyperLogLog |
| Steps across services that must all happen or be undone | Workflow | Saga with orchestrator, state machine, compensations |
| Run at a time in the future, repeatedly | Scheduling | Time-bucketed job store, leases, idempotent jobs |
| Must be ordered (per account, per chat) | Ordering | Partition by that key; single writer per key |
| Reports over everything | Analytics | Columnar warehouse fed by CDC or batch export |
| IDs across many servers | Coordination-free IDs | Snowflake or UUIDv7 |
Say the pressure out loud before drawing: "This is read-heavy with a hot-key problem for viral links, so the core is a cache in front of a key-value store." Interviewers score the reasoning, and the rest of your design becomes a consequence of it.
Quick reflex quiz
One line each. Then check which blocks follow.
- "Design Google Docs."
Reveal
Real-time collaboration with concurrent edits to the same object: ordering and merging (OT or CRDT), per-document session servers, WebSockets (Case study 16).
- "Design YouTube."
Reveal
Large objects and read-heavy delivery: upload and transcoding pipeline, object storage, CDN with adaptive bitrate (Case study 9).
- "Design a ride-hailing service."
Reveal
Location plus real time: frequent location updates of moving drivers, geo index in memory, matching, and a trip state machine (Case study 11).
- "Design Ticketmaster."
Reveal
Spiky load plus strong correctness: waiting room, seat holds with expiry, atomic reservation (Case study 12).
- "Design a web crawler."
Reveal
Write-heavy distributed work with politeness constraints: URL frontier, dedup with Bloom filters and content hashes, per-host rate limits (Case study 8).
- "Design a notification system."
Reveal
Fan-out and reliability across channels: queues per channel, retries, dedup, user preferences, rate limits (Case study 6).
Module 5: Scaling and load balancing
Most systems scale by running many identical, stateless copies of a service behind a load balancer. This module covers why statelessness matters, how load balancers pick a server, how to remove single points of failure, and how autoscaling keeps up with demand.
- Your app stores user sessions in server memory. What breaks when you add a second server behind a load balancer?
Reveal
A user logged in on server 1 may be sent to server 2, which has no session, so they appear logged out. Move sessions to a shared store (Redis) or use signed tokens.
- Requests vary from 5 ms to 5 s. Why can round robin overload one server?
Reveal
Round robin counts requests, not work. A server stuck with several 5 s requests keeps receiving its turn. Least connections or least outstanding requests adapts to actual load.
- Your single load balancer dies. What happens, and how do you prevent it?
Reveal
Everything behind it is unreachable. Run load balancers redundantly (managed LBs span zones; self-run ones use an active-passive pair with a floating IP).
- Autoscaling adds servers when CPU passes 70%. Traffic doubles in 30 seconds. Why do users still see errors?
Reveal
New instances take minutes to boot and warm up. Scaling reacts after demand rises, so keep headroom, scale on a leading metric, or pre-scale for known events.
- When is buying a bigger machine the right answer?
Reveal
When it is cheaper than the complexity of distributing: databases that are hard to shard, small systems, or a quick fix while you redesign. It has a ceiling and keeps a single point of failure.
5.1Vertical and horizontal scaling
A database server at 90% CPU. Option A: move to a machine with 4x the cores. Option B: add 3 more database servers. What does each cost you?
Reveal the answer
A (vertical) is simple: no code change, no distributed data. But it has a hardware ceiling, gets expensive at the top end, and is still one machine that can fail. B (horizontal) scales further and survives failures, but the data must be replicated or partitioned, which brings consistency and routing problems (Modules 9 and 10).
| Vertical (scale up) | Horizontal (scale out) | |
|---|---|---|
| How | Bigger CPU, more RAM, faster disk | More machines |
| Code changes | None | Service must be stateless, or data must be partitioned |
| Ceiling | The biggest machine you can buy | Practically none |
| Failure | Still a single machine | Survives losing nodes |
| Cost curve | Rises steeply at the high end | Roughly linear |
| Good for | Databases early on, quick fixes | Stateless services, large data, high availability |
Stateless services scale horizontally almost for free. Stateful systems (databases, caches, queues) are where scaling gets hard, which is why most of this handbook is about data.
5.2Stateless services
List every place a typical web server might keep state between requests.
Reveal the answer
In-memory sessions, local file uploads, in-process caches, scheduled jobs that run on "the" server, WebSocket connections, and local counters (for example a rate limit counted in memory). Each one breaks when requests for the same user land on different servers.
A stateless service keeps nothing between requests that another copy would need. Any instance can serve any request, so the load balancer can send traffic anywhere and you can add, remove, or replace instances freely.
A bank with many tellers. Any teller can serve you because your account lives in the bank's central ledger, not in one teller's notebook.
Sessions in Redis or in a signed token (JWT) carried by the client; files in object storage; shared caches in Redis; durable data in the database; scheduled jobs in a distributed scheduler with a lock (Case study 14).
Sticky sessions (the LB keeps sending a user to the same server, by cookie or IP hash) is a shortcut that keeps state on servers. It works, but load becomes uneven, and when that server dies its users lose their sessions. Prefer external state; use stickiness only as an optimization (for example to keep a warm local cache), never for correctness.
5.3Load balancers: L4 and L7
You want /api/* to go to API servers and /images/* to go to image servers behind one domain. Which kind of load balancer can do that, and why?
Reveal the answer
An L7 (application) load balancer. Routing by URL path requires reading the HTTP request, which means terminating TLS and parsing HTTP. An L4 load balancer only sees IP addresses and ports.
| L4 (transport) | L7 (application) | |
|---|---|---|
| Sees | IP, port, TCP/UDP | Full HTTP: path, headers, cookies |
| Balances | Connections | Individual requests |
| Features | Very fast, protocol-agnostic, preserves client connection | Path and header routing, TLS termination, retries, compression, auth hooks, WebSocket upgrade |
| Examples | AWS NLB, Google TCP/UDP LB, LVS, HAProxy in TCP mode | AWS ALB, NGINX, Envoy, HAProxy in HTTP mode |
| Pick when | Non-HTTP traffic, extreme throughput, pass-through TLS | Web and API traffic, microservice routing, gRPC |
5.4Choosing a server: algorithms and health checks
Three servers have 4, 9, and 2 requests in flight. Where does round robin send the next request if it is server B's turn? Where should it go?
Reveal the answer
Round robin sends it to B because it is B's turn, even though B is the busiest. It should go to C, which has the fewest requests in flight. That is what least connections (or least outstanding requests) does.
| Algorithm | How it picks | Good for | Watch out |
|---|---|---|---|
| Round robin | Next server in order | Uniform, short requests | Ignores load and server size |
| Weighted round robin | In order, more turns for bigger servers | Mixed hardware, canary at 5% | Weights are static |
| Least connections / least requests | Fewest in flight | Uneven request durations | Needs accurate counts |
| Power of two choices | Pick 2 at random, use the less loaded | Many LB nodes with partial information | Slightly less exact, avoids herding |
| IP hash / consistent hash | Hash of client IP or a key | Session affinity, cache locality | Uneven if keys are skewed |
| Least response time | Lowest recent latency | Latency-sensitive traffic | Can oscillate |
Health checks keep traffic away from broken servers:
- Active: the LB calls
/healthevery few seconds and removes a server after N failures, adding it back after M successes. - Passive (outlier detection): the LB watches real traffic and ejects servers that return too many errors or time out.
- Readiness vs liveness (Kubernetes): failing readiness takes a pod out of the load balancer (it is warming up or overloaded); failing liveness restarts it (it is stuck). A health endpoint that checks the database can take every pod out at once when the database blips, so keep readiness checks about the instance itself.
- Connection draining: on deploy or scale-in, stop sending new requests to an instance and let in-flight ones finish before killing it.
5.5Removing single points of failure
You have 10 app servers behind one load balancer and one database. List the single points of failure and a fix for each.
Reveal the answer
The load balancer (run redundant LB nodes across zones or use a managed LB), the database (a standby replica with automatic failover), the zone or datacenter they all sit in (spread across availability zones), and DNS or the certificate if managed by hand (use managed DNS, automate renewals).
- Redundancy at every tier: at least N+1 instances, spread across availability zones (separate power and network within a region).
- Load balancers themselves: cloud LBs are already distributed across zones. Self-run ones use an active-passive pair sharing a virtual IP (keepalived/VRRP): if the active one stops sending heartbeats, the passive one takes the IP.
- Across regions: GeoDNS or anycast sends users to the nearest healthy region (global server load balancing). Region failover also needs the data to be there (Module 15).
- Availability multiplies: two components in series, each 99.9%, give about 99.8%. Two redundant copies in parallel, each 99.9%, give about 99.9999% if their failures are independent. Module 15 has a calculator.
5.6API gateways and reverse proxies
Ten microservices each implement their own authentication, rate limiting, and request logging. What is the problem, and where should that logic live?
Reveal the answer
Duplicated, inconsistent cross-cutting logic: one service forgets a check, versions drift, and fixing a bug means 10 deploys. Put it in an API gateway (the single front door) and keep services focused on business logic.
An API gateway is an L7 reverse proxy with API features: authentication and token validation, rate limiting and quotas, routing to services, request and response transformation, TLS termination, request IDs, and sometimes response caching or aggregation. Examples: Kong, Apigee, AWS API Gateway, Envoy-based gateways.
- A backend for frontend (BFF) is a gateway per client type (web, iOS, partner API) that shapes responses for that client.
- Keep the gateway thin. Business logic in the gateway turns it into a bottleneck every team must change.
- The gateway is on every request, so it must be redundant and fast, and it must fail predictably (Module 15).
5.7Autoscaling
Why is "scale out when CPU is above 70% for 5 minutes" too slow for a flash sale that starts at 12:00 sharp?
Reveal the answer
Demand jumps in seconds; the metric needs 5 minutes to trigger, and new instances need minutes to start and warm caches. Pre-scale before the known event (scheduled scaling), keep headroom, and put a queue or waiting room in front of the hottest path (Case study 12).
- Scale on a metric that tracks load: CPU, requests per instance, queue length (best for workers), or p99 latency.
- Target tracking: "keep average CPU near 60%" adds and removes instances to hold the target.
- Cooldowns and limits: avoid flapping (adding and removing repeatedly); set a maximum so a bug or attack cannot scale you into a huge bill.
- Scale-in carefully: drain connections and finish in-flight jobs before terminating.
- Scheduled or predictive scaling for daily peaks and known events.
- Remember the dependencies: doubling app servers doubles the connections to the database. Scale the bottleneck, not just the easy tier.
On Kubernetes, a Service plus an Ingress or Gateway gives you L4 and L7 balancing, readiness probes decide which pods receive traffic, and the Horizontal Pod Autoscaler scales on CPU or custom metrics such as queue lag. Size database connection pools so that max pods x pool size stays under the database's connection limit, or put PgBouncer in between.
Say "stateless services behind a load balancer, spread across zones" early in every design, then spend your time on the stateful parts. Interviewers check that you remove single points of failure without being asked.
5.8Common mistakes
Common mistakes
- Sessions, files, or counters in server memory, then adding a load balancer and wondering why users get logged out.
- Health checks that call the database: one database blip removes every instance from the load balancer.
- A single load balancer, NAT gateway, or database in a design that claims high availability.
- Round robin for very uneven request costs.
- Autoscaling the app tier into a database connection storm. Use connection pooling (PgBouncer) and size pools to the database's limit.
- Business logic in the API gateway.
5.9Self-check
- Why is an L7 load balancer needed for gRPC services?
Reveal
gRPC multiplexes many calls over one long-lived HTTP/2 connection. An L4 LB pins the whole connection to one backend; an L7 LB balances individual calls.
- What is the difference between readiness and liveness checks?
Reveal
Readiness decides whether the instance gets traffic (out of the LB while starting or overloaded). Liveness decides whether it is restarted (stuck or deadlocked).
- Two components in series each have 99.9% availability. What is the combined availability?
Reveal
0.999 x 0.999 = about 99.8%. Series chains lose availability with every component.
- What problem does "power of two choices" solve?
Reveal
With many load balancer nodes each picking the globally least-loaded server, they all pick the same one and overload it (herding). Picking the better of two random servers spreads load well with little coordination.
- When are sticky sessions acceptable?
Reveal
As a performance optimization (cache locality, a WebSocket that must stay on one server) when losing the stickiness does not break correctness, because the real state lives elsewhere.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 6: Caching
A cache keeps a copy of hot data somewhere faster than its source. It is the cheapest way to cut latency and database load, and the easiest way to serve wrong data. This module covers where to cache, how to read and write through a cache, how to keep it fresh, and how caches fail.
- Reads are 100K/s. Cache hit ratio goes from 90% to 99%. What happens to database read load?
Reveal
It drops 10x, from 10K/s to 1K/s. The miss rate, not the hit rate, decides database load.
- After updating a user's name in the database, should you update the cached value or delete it?
Reveal
Usually delete it. The next read reloads the current value. Updating the cache from the writer races with other writers and can leave an older value in the cache.
- A popular key expires and 5,000 requests miss at once. What is this called and what is one fix?
Reveal
A cache stampede (thundering herd). Fix: let one request reload the key while the others wait or get the stale value (single-flight), or refresh hot keys before they expire.
- Attackers request random IDs that do not exist, so every request misses the cache and hits the database. Fix?
Reveal
Cache negative results (an empty marker with a short TTL) and reject impossible IDs early, for example with a Bloom filter of valid IDs.
- Why put a small in-process cache in front of Redis for a few extremely hot keys?
Reveal
One Redis node holds each key, so a very hot key overloads that one node. A local copy on every app server spreads the load, at the cost of a few seconds of staleness.
6.1Why caching works
A page needs a database query that takes 20 ms; a Redis read takes 1 ms. With a 95% hit ratio, what is the average latency? What is it at 80%?
Reveal the answer
Average = hit x 1 ms + miss x (1 ms + 20 ms). At 95%: 0.95 x 1 + 0.05 x 21 = about 2 ms. At 80%: 0.8 x 1 + 0.2 x 21 = about 5 ms. Small changes in hit ratio move both latency and database load a lot.
Access patterns are skewed: a small fraction of items gets most of the reads (popular products, celebrity profiles, today's posts). Keep those in memory and most requests never reach the slow store.
A chef keeps salt, oil, and onions on the counter and everything else in the storeroom. The counter is small but serves most reaches. Deciding what earns counter space and when to throw out stale items is the whole art.
Every cache is a second copy of the data, so it can be stale. Caching is a trade of freshness for speed; decide per piece of data how stale is acceptable.
- Hit ratio = hits / (hits + misses). Database load = miss ratio x read QPS.
- Cache data that is read often, changes rarely, and is expensive to compute or fetch.
- Do not cache data that must be exactly current for correctness (an account balance used to approve a payment) unless the cache is part of a consistent design.
6.2Where to cache
For each, pick a cache layer: (a) product images, (b) a user's session, (c) a currency-rate table used in every request, (d) the rendered public home page.
Reveal the answer
(a) CDN. (b) Distributed cache (Redis). (c) In-process cache on each server, refreshed every minute: tiny, read constantly, tolerates a little staleness. (d) CDN or reverse proxy cache, with a short TTL.
| Layer | Holds | Freshness control |
|---|---|---|
| Browser / app | Static files, API responses with cache headers | Cache-Control, ETag; you cannot purge it |
| CDN | Static assets, public pages, video segments | TTL plus purge API (Module 7) |
| Reverse proxy | Whole responses | TTL, purge |
| In-process | Tiny, very hot, mostly static data | Short TTL; each server has its own copy |
| Distributed (Redis, Memcached) | Objects, sessions, computed results, counters | TTL plus explicit delete; shared by all servers |
| Database buffer pool | Pages of tables and indexes | Automatic; always consistent |
6.3Reading and writing through a cache
Writes must be fast, and losing the last second of writes on a crash is acceptable. Reads are mostly of data written recently. Which write pattern fits?
Reveal the answer
Write-behind (write-back): write to the cache, return immediately, and flush to the database asynchronously in batches. Fast writes and recent data is already cached, at the cost of possible loss if the cache dies before flushing. Counters, view counts, and analytics events fit; payments do not.
| Pattern | Read | Write | Strengths | Weaknesses |
|---|---|---|---|---|
| Cache-aside (lazy loading) | App reads cache; on miss reads DB and fills cache | App writes DB, then deletes cache key | Simple; cache holds only what is read; cache failure is survivable | First read after a write misses; stale-set races (below) |
| Read-through | App reads only the cache; the cache loads from DB on miss | Usually combined with one of the write patterns | Loading logic in one place | Cache must know how to load |
| Write-through | Cache is always populated for written keys | Write goes to cache and DB synchronously | Reads after writes are fresh | Slower writes; caches data that may never be read |
| Write-behind (write-back) | As above | Write to cache; flush to DB asynchronously | Very fast writes, batching | Data loss if the cache fails before the flush; harder consistency |
| Write-around | Filled on read miss | Write straight to DB, skip the cache | Avoids filling the cache with write-once data | Reads right after writes miss |
Default answer for most read-heavy services: cache-aside with a TTL, delete on write. Mention write-through when reads right after writes must be fast, and write-behind only when you can tolerate loss.
6.4Invalidation and consistency
With cache-aside, a writer updates the database and deletes the key. How can the cache still end up holding the old value?
Reveal the answer
A reader missed the cache and read the old value from the database just before the write. It then paused (GC, slow network). Meanwhile the writer updated the database and deleted the key. The reader wakes up and sets the old value into the cache, where it stays until the TTL expires.
Ways to bound or remove staleness, from simplest:
- Always set a TTL. It is the safety net that limits any bug or race to N seconds.
- Delete, do not update, on write. Two concurrent writers updating the cache can apply in the wrong order; a delete cannot.
- Delayed double delete: delete, then delete again a short time later, to catch a racing reader. Cheap, not a guarantee.
- Versioned values: store a version with the value and only overwrite with a newer version (a conditional set or Lua script).
- Leases: on a miss the cache hands the reader a token; a delete invalidates outstanding tokens, so the late set is rejected. Facebook's memcache uses this.
- Invalidate from the database's change log (CDC, Module 13): a consumer reads committed changes and deletes affected keys, so no write path can forget.
"How stale is acceptable?" is a product question. A profile picture can be 5 minutes old; a card's available credit limit shown before a purchase should come from the source of truth or from a cache updated in the same flow. Write the staleness budget next to each cached item in the design doc.
6.5Eviction
A cache has room for 1 million items and receives a nightly batch job that scans 50 million items once. Why can LRU behave badly here, and what helps?
Reveal the answer
The scan touches 50M items once each, and LRU evicts the genuinely hot items to make room for items that will never be read again (cache pollution). LFU-style policies keep frequently used items, and many systems skip the cache for bulk scans entirely (write-around, or a separate connection that bypasses caching).
| Policy | Evicts | Good for |
|---|---|---|
| LRU (least recently used) | Item not used for the longest time | Most workloads with recency |
| LFU (least frequently used) | Item used least often | Stable popularity; resists one-off scans |
| FIFO | Oldest inserted | Simple, rarely best |
| TTL-based | Items past their expiry first | Time-bound data |
| Random | Any item | Cheap; surprisingly decent |
In Redis, maxmemory caps memory and maxmemory-policy picks the policy: allkeys-lru or allkeys-lfu for a pure cache, volatile-* variants to evict only keys that have a TTL. The default is noeviction, which returns errors on writes when memory is full, the right choice when Redis holds data you cannot lose. Redis approximates LRU and LFU by sampling keys, which is cheap and close enough.
6.6When caches fail
At midnight, a deploy flushes the whole cache. Within seconds the database is at 100% CPU and timing out. Name the problem and two fixes.
Reveal the answer
A cold cache (and a cache avalanche): every request misses at once. Fixes: warm the cache before taking traffic (preload hot keys), roll out gradually so only part of the fleet is cold, and rate-limit or queue requests to the database so it degrades rather than collapses.
| Problem | What happens | Fixes |
|---|---|---|
| Stampede (thundering herd) | A hot key expires; thousands of requests miss together and all query the DB | Single-flight per key; a short lock so one request rebuilds; serve stale while one refreshes; refresh hot keys early (probabilistic early expiry) |
| Avalanche | Many keys expire at the same moment, or the cache restarts | Add random jitter to TTLs; warm up; replicate the cache; circuit-break DB calls |
| Penetration | Requests for keys that do not exist always miss | Cache "not found" with a short TTL; Bloom filter of valid keys; validate input |
| Hot key | One key gets a huge share of traffic and saturates its node | In-process cache for that key; replicate it under several keys (k#1..k#N) and read a random copy |
| Big key | One value is megabytes (a huge list); slow to read and move | Split it; paginate; store large blobs elsewhere |
6.7Redis essentials
Which Redis structure fits each: (a) a game leaderboard, (b) a set of users online now, (c) unique visitor count of about 100M IDs using little memory, (d) a per-user object with 10 fields.
Reveal the answer
(a) Sorted set (ZADD, ZRANGE ... REV, ZREVRANK). (b) Set, or keys with TTL refreshed by heartbeats. (c) HyperLogLog (about 12 KB per counter, under 1% standard error). (d) Hash.
| Structure | Use it for |
|---|---|
| String | Cached objects, counters (INCR), locks (SET key val NX PX 30000) |
| Hash | Objects with fields, updated field by field |
| List | Queues, recent items (LPUSH + LTRIM) |
| Set | Membership, tags, unique items |
| Sorted set | Leaderboards, rankings, time-ordered feeds, sliding-window rate limits |
| Stream | Append-only log with consumer groups (light Kafka-like use) |
| HyperLogLog | Approximate unique counts |
| Bitmap | Daily active flags per user ID |
| Geo | Nearby search by radius |
How Redis behaves, in the points interviews probe:
- Commands execute on one thread, so each command is atomic. Multi-step logic made atomic with
MULTI/EXECor a Lua script (used for rate limiters, Case study 2). Newer versions use extra threads for network I/O, but command execution stays single-threaded. - Persistence is optional: RDB snapshots (periodic, may lose minutes) and AOF (append-only log, typically fsync every second, so up to about a second of loss).
- Replication is asynchronous: a failover can lose recently acknowledged writes. Redis Sentinel automates failover for a primary and its replicas.
- Redis Cluster shards keys over 16,384 hash slots (
CRC16(key) mod 16384). Multi-key commands work only within one slot; a hash tag like{user:7}:cartforces related keys into the same slot. - Valkey is the open-source, Redis-compatible fork that many managed cache services now offer; everything here applies to it.
- Memcached is the simpler alternative: multithreaded, strings only, no persistence or replication, sharded by the client. Fine for pure caching; Redis wins when you need data structures.
Do not treat Redis as a database of record unless you have designed for its durability model (AOF with fsync, replicas, and an accepted loss window). As a cache, design so a full Redis loss is survivable: slower, not wrong.
For every cache you add, state four things: what is cached and its key, the pattern (usually cache-aside), the TTL and staleness budget, and what happens on a miss storm or cache outage. That checklist covers most caching follow-ups.
6.8Common mistakes
Common mistakes
- No TTL. Every bug and race then lives forever.
- Updating the cache on write instead of deleting, so concurrent writes leave old values.
- Caching without a staleness budget for each data type.
- Forgetting the cold start: a flushed cache or new region sends full load to the database.
- One huge hot key on one Redis node with no local cache or key replication.
- Assuming the cache is always up. If Redis is down, the service should degrade (read from the DB with a rate limit), not crash.
- Caching personalized responses in a shared CDN without varying the cache key by user: one user's data served to another.
6.9Self-check
- Why does cache-aside delete the key on write instead of setting the new value?
Reveal
Setting from the write path races with other writers and with readers repopulating old values; deleting forces the next read to load the current committed value.
- Write-through vs write-behind: which can lose data, and when?
Reveal
Write-behind. Writes are acknowledged after reaching the cache; if the cache fails before flushing to the database, those writes are lost.
- How do you stop a stampede on a hot key?
Reveal
Let one caller rebuild (single-flight or a short lock) while others wait or serve the stale value; refresh before expiry; add jitter to TTLs.
- In Redis Cluster, why might
MGET user:1 user:2fail, and how do you fix it?Reveal
The keys may hash to different slots on different nodes. Use a hash tag so related keys share a slot, or fetch them separately.
- Your service must survive a total Redis outage. What does the design need?
Reveal
Timeouts and a circuit breaker on cache calls, a fallback to the database protected by rate limits or load shedding, and no data that exists only in Redis.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 7: CDN and object storage
Files, images, video, backups, and exports do not belong in your database or on your app servers. They live in object storage and reach users through a CDN. This module covers the storage types, direct uploads with presigned URLs, how CDNs cache, and how to control cost and freshness.
- Where do you store 5 PB of user photos, and where do you store the photo's caption and owner?
Reveal
Bytes in object storage (S3, GCS); metadata (owner, caption, object key, status) in a database. Never put large blobs in the database.
- A 2 GB video upload through your API servers ties up threads and bandwidth. Better design?
Reveal
The API returns a presigned URL and the client uploads straight to object storage, in parts, so it can resume. Your servers only handle small metadata requests.
- You deploy new CSS but users still get the old file from the CDN for a day. How do you avoid this next time?
Reveal
Put a content hash or version in the file name (
app.3f9a1c.css) and cache it for a year. A new deploy references a new file name, so nothing needs purging. - Private documents must be downloadable through the CDN only by their owner. How?
Reveal
Signed URLs (or signed cookies): the app issues a short-lived URL with a signature that the CDN verifies before serving.
- Why does a CDN help even for content that cannot be cached?
Reveal
Users terminate TCP and TLS at a nearby edge (fewer long round trips), and the edge keeps warm, optimized connections to your origin.
7.1Block, file, and object storage
Match each to a storage type: (a) a PostgreSQL data directory, (b) a shared folder that several servers mount, (c) 100 million user-uploaded images.
Reveal the answer
(a) Block storage (a disk volume, like EBS or a local NVMe SSD): low latency, random writes. (b) File storage (NFS, EFS, Filestore): a shared file system with directories. (c) Object storage (S3, GCS): cheap, durable, effectively unlimited, accessed by key over HTTP.
| Block | File | Object | |
|---|---|---|---|
| Looks like | A raw disk | A shared folder tree | A huge key-value store of files |
| Access | Mounted by one server, random reads and writes | Mounted by many servers via NFS/SMB | HTTP API: PUT, GET, DELETE by key |
| Latency | Lowest (sub-millisecond) | Low | Higher (tens of milliseconds to first byte) |
| Scale and cost | Limited per volume, priciest per GB | Medium | Effectively unlimited, cheapest per GB |
| Typical use | Databases, OS disks | Legacy apps, shared configs | Images, video, backups, logs, data lakes |
What to know about object storage (S3 as the example):
- Flat namespace: a bucket holds objects under keys like
photos/2026/10/abc.jpg; the slashes are just characters, not real directories. - Objects are replaced whole: no in-place edits. To change a file, write a new version. S3 also supports conditional writes (
If-None-Match: *to create only if absent,If-Matchfor compare-and-swap), useful for simple locks and manifests. - Very durable: S3 is designed for 99.999999999% (11 nines) durability by storing data redundantly across multiple facilities.
- Strong read-after-write consistency for S3 since 2020: after a successful PUT, reads see the new object.
- Storage classes and lifecycle rules: move objects to cheaper infrequent-access or archive tiers after N days, and delete them after the retention period.
- Versioning and object lock protect against accidental deletes and support write-once retention for compliance.
7.2Uploads and downloads with presigned URLs
Design photo upload so that app servers never handle the image bytes, the photo is resized after upload, and the app knows when it is ready.
Reveal the answer
- Client asks the API to start an upload; API creates a metadata row with
status = pendingand returns a presigned PUT URL for a specific key. - Client PUTs the bytes directly to object storage.
- Storage emits an "object created" event to a queue; a worker validates, scans, and generates sizes.
- Worker updates metadata to
readywith the keys of each size; the client polls or receives a push.
- A presigned URL is a normal storage URL plus a signature and expiry, created with the server's credentials. Whoever holds it can do exactly one thing (PUT this key, or GET this key) until it expires.
- Large files go in parts (S3 multipart upload, GCS resumable upload): each part is retried on its own, and parts can upload in parallel.
- Validate after upload, not before: check type, size, and malware in the worker, since the client controls the bytes.
- Downloads of private files use presigned GET URLs or CDN signed URLs, so authorization stays in your API while bytes flow from storage or the CDN.
7.3How a CDN works
A user in Mumbai fetches an image stored in a US region. Without a CDN each request costs about 250 ms of round trips. What does a CDN change for the first request and for the next one?
Reveal the answer
The first request goes to the nearby Mumbai edge, which misses and fetches from the origin (still slow, but the TLS handshake ended nearby). The edge caches the image; the next request from anyone near Mumbai is served in about 10-20 ms without touching the origin.
| Pull CDN | Push CDN | |
|---|---|---|
| How content arrives | Edge fetches from origin on the first miss | You upload content to the CDN ahead of time |
| Good for | Most websites and APIs; long-tail content | Large, predictable files (game patches, software releases) |
| Downside | First request per edge is slow | You manage what is where; storage costs |
Controlling what the CDN caches:
Cache-Control: public, max-age=31536000, immutablefor versioned static files (hash in the file name). They never need purging.s-maxagesets the TTL for shared caches (the CDN) separately from browsers.no-cachemeans "store it, but revalidate before each use" (withETag/If-None-Match, a304is cheap).no-storemeans "never store it", for sensitive responses.- Cache key: by default the URL. Adding headers or cookies to the key (for language or device) splits the cache; never cache per-user responses under a shared key.
- Purge / invalidation removes content early, but takes time to reach all edges. Versioned URLs avoid needing it.
- Stale-while-revalidate: serve the cached copy while fetching a fresh one in the background.
CDNs do more than cache: TLS termination near users, DDoS absorption, a web application firewall (WAF), bot filtering, image resizing at the edge, and edge functions. For an API, a CDN in front often cuts latency even at a 0% cache hit rate because the long-haul connection to origin is kept warm.
7.4Serving images and video
An app shows each photo at 4 sizes. Should you resize at upload time or on request?
Reveal the answer
Both are valid. Pre-generating at upload (in the worker) makes reads simple and fast and costs storage for every size. Resizing on request at the edge (an image CDN or a resize service behind the CDN, cached after the first hit) saves storage and supports new sizes without reprocessing, but the first request per size is slow. Common choice: pre-generate the few standard sizes, resize on demand for rare ones.
- Images: store the original; serve resized, compressed variants (WebP or AVIF where supported) from the CDN.
- Video: transcode each upload into several resolutions and bitrates and cut each into short segments (2-10 s). A manifest lists the variants; the player switches quality per segment based on bandwidth. This is adaptive bitrate streaming (HLS or MPEG-DASH), and every segment is a cacheable CDN object. Case study 9 designs the full pipeline.
7.5Cost, retention, and security
Regulation requires keeping statement files for 7 years, but users only open files from the last 3 months. How do you keep the cost low?
Reveal the answer
A lifecycle policy: keep recent files in the standard tier, move them to an infrequent-access tier after about 90 days and to an archive tier after a year, and delete after 7 years. Add object lock (write-once) if regulation requires that files cannot be altered or deleted early.
- Storage tiers trade retrieval speed and fees for a lower monthly price. Archive tiers can take minutes to hours to restore.
- Egress is often the biggest bill. Serving through a CDN reduces origin egress; keep compute in the same region as the bucket.
- Encrypt at rest with keys from a key management service (server-side encryption with KMS); restrict buckets to private and grant access through presigned URLs and IAM roles, never public listing.
- Checksums (MD5 or CRC) on upload detect corruption; store them in metadata for later verification.
Whenever a design involves files, say three things without being asked: bytes go to object storage and never through your API servers (presigned URLs), metadata goes to a database, and reads go through a CDN with versioned URLs. Then spend your time on processing and cost.
7.6Common mistakes
Common mistakes
- Blobs in the database or on app server disks.
- Uploading through your API servers instead of directly to storage.
- Trusting the client's file type and size without validating after upload.
- Overwriting files at the same URL and relying on CDN purges; use versioned names.
- Caching personalized or private content at a shared CDN key.
- Public buckets for convenience.
- Ignoring egress and tiering, which dominate storage cost at scale.
7.7Self-check
- What exactly does a presigned URL grant?
Reveal
One operation (for example PUT or GET) on one object key until an expiry time, authorized by a signature made with the server's credentials.
- Pull vs push CDN: which suits a news site with millions of articles?
Reveal
Pull. The CDN fetches articles on demand and caches the popular ones; pushing millions of rarely read pages ahead of time is wasteful.
- What does an origin shield protect against?
Reveal
Many edge locations missing on the same object at the same time and each fetching it from the origin. The shield collapses them into one origin fetch.
- Difference between
no-cacheandno-store?Reveal
no-cacheallows storing but requires revalidation before use.no-storeforbids storing at all. - How does adaptive bitrate streaming fit a CDN well?
Reveal
Video is split into small segment files per quality level, each a normal cacheable object, so the CDN serves them like any static file and the player picks quality per segment.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 8: Databases and data models
The database is usually the hardest part of a design to change and the first to become a bottleneck. This module covers transactions and isolation, locking, indexes, how storage engines trade read speed for write speed, the NoSQL families, and how to choose.
- Two users buy the last item at the same moment and both succeed. Name the bug and one fix.
Reveal
A race on read-modify-write (lost update / oversell). Fix with an atomic conditional update:
UPDATE items SET stock = stock - 1 WHERE id = ? AND stock > 0, and treat 0 rows updated as sold out. - Why does adding an index make writes slower?
Reveal
Every insert, update, or delete must also update each index on the table. Reads get faster; writes do more work.
- Which storage engine design is better for very write-heavy workloads such as event logs?
Reveal
An LSM tree (Cassandra, RocksDB): writes are sequential appends to a log and memory, flushed in batches. B-trees (PostgreSQL, MySQL) update pages in place, which costs more random I/O per write.
- You need "all orders by customer, newest first" and "monthly revenue by city across 2 billion orders". Same database?
Reveal
Usually not. The first is OLTP (row store with an index on customer and time). The second is OLAP (a columnar warehouse such as BigQuery, Redshift, or ClickHouse), fed from the OLTP database.
- When is a document database a better fit than SQL?
Reveal
When data is naturally a self-contained aggregate read and written as a whole (a product with variable attributes, a user's settings) and you rarely join across aggregates.
8.1Relational databases and ACID
A transfer moves 500 from account A to B: debit A, credit B. The server crashes between the two statements. What should the database guarantee, and which ACID letter is that?
Reveal the answer
Atomicity: either both changes commit or neither does. After the crash, recovery rolls back the partial transaction, so money is neither lost nor created. Durability then guarantees that once the commit is acknowledged, it survives the crash.
Data in tables of rows and columns with a fixed schema, related by keys and combined with joins. SQL lets you ask new questions without redesigning storage.
A well-run accounts office with ledgers that cross-reference each other. Every entry follows the format, and a transaction either makes all its ledger entries or none.
For most new systems, PostgreSQL or MySQL is the right default until numbers or access patterns say otherwise. They scale further than most people expect, especially with replicas, caching, and good indexes.
| Letter | Guarantee | How databases provide it |
|---|---|---|
| Atomicity | All statements in a transaction commit, or none do | Undo/redo logs; rollback on failure |
| Consistency | Constraints and invariants hold before and after (unique keys, foreign keys, checks) | Constraints plus correct application logic |
| Isolation | Concurrent transactions do not see each other's partial work, to the chosen level | Locks or multi-version concurrency control (MVCC) |
| Durability | A committed transaction survives crashes | Write-ahead log flushed to disk before commit is acknowledged; replicas |
"Consistency" in ACID means "invariants hold". It is a different idea from "consistency" in CAP (every reader sees the latest write, Module 11). Interviewers like to check that you keep them apart.
8.2Isolation levels and anomalies
Two on-call doctors both open the app. Each checks "at least 2 doctors on call?" (yes), then marks themselves off call. Both commits succeed. Now nobody is on call. No row was written by both. Which anomaly is this, and which isolation level prevents it?
Reveal the answer
Write skew: each transaction read the same condition, then wrote a different row, and together they broke the rule. Snapshot isolation (PostgreSQL "repeatable read") allows it. Serializable prevents it, or you can lock the rows you read (SELECT ... FOR UPDATE) or move the rule into a single row you both update.
| Anomaly | What goes wrong |
|---|---|
| Dirty read | Reading another transaction's uncommitted change, which may roll back |
| Non-repeatable read (read skew) | Reading the same row twice in one transaction and getting different values |
| Phantom | Re-running a query and getting new rows that another transaction inserted |
| Lost update | Two read-modify-write cycles overwrite each other |
| Write skew | Two transactions read overlapping data, then write different rows, breaking an invariant together |
| Level | Prevents | Still allows | Notes |
|---|---|---|---|
| Read committed | Dirty reads and writes | Non-repeatable reads, phantoms, lost updates, write skew | Default in PostgreSQL, Oracle, SQL Server |
| Repeatable read / snapshot isolation | Above plus non-repeatable reads | Write skew (and, depending on engine, some phantoms or lost updates) | Default in MySQL InnoDB. PostgreSQL's version is snapshot isolation and aborts one of two concurrent updates to the same row |
| Serializable | All of the above | Nothing; the result equals some serial order | PostgreSQL uses serializable snapshot isolation (aborts conflicting transactions, so clients must retry) |
In design interviews, say which operations need protection and how: "Booking a seat uses a conditional update on the seat row, so two buyers cannot both win." That is more convincing than naming an isolation level for the whole database.
8.3Locking: pessimistic and optimistic
Ten workers poll a jobs table for pending jobs. How do you make each job go to exactly one worker without workers blocking each other?
Reveal the answer
SELECT ... FROM jobs WHERE status = 'pending' ORDER BY created_at LIMIT 10 FOR UPDATE SKIP LOCKED, then mark them running in the same transaction. Each worker locks a different set of rows and skips rows already locked by others.
| Pessimistic locking | Optimistic concurrency control | |
|---|---|---|
| Idea | Lock the row before changing it; others wait | Read a version; write only if the version is unchanged |
| SQL | SELECT ... FOR UPDATE |
UPDATE ... SET v = v + 1 WHERE id = ? AND v = ? (0 rows = conflict, retry) |
| Best when | Conflicts are frequent; the critical section is short | Conflicts are rare; many readers |
| Risks | Waiting, deadlocks, locks held across slow calls | Retry storms under heavy contention |
- Never hold a database lock across a network call to another service; a slow call then blocks everyone.
- Deadlocks happen when two transactions lock rows in opposite orders. Lock in a consistent order (for example by ID) and keep transactions short. Databases detect deadlocks and abort one transaction.
SKIP LOCKEDturns a table into a simple work queue;NOWAITfails immediately instead of waiting.
8.4Indexes
A table has an index on (customer_id, created_at). Which queries can use it efficiently: (a) WHERE customer_id = 5 ORDER BY created_at DESC, (b) WHERE created_at > yesterday, (c) WHERE customer_id = 5 AND created_at > yesterday?
Reveal the answer
(a) and (c) can: the index is sorted by customer, then time, so it seeks to customer 5 and reads in time order. (b) cannot seek efficiently, because created_at is the second column; rows for one time range are spread across every customer. This is the leftmost prefix rule.
- B+tree index: sorted; supports equality, ranges,
ORDER BY, and prefix matches. The default in relational databases. - Composite index
(a, b, c): usable fora,a, b, anda, b, c. Put equality columns first, then the range or sort column. - Covering index: contains every column the query needs, so the table itself is not read (
INCLUDEcolumns in PostgreSQL). - Partial index: indexes only rows matching a condition (
WHERE status = 'pending'), small and fast for hot subsets. - Hash index: equality only. Inverted index: word to documents, for full-text search (Module 17). Geospatial index: R-tree, geohash (Module 17).
- Cost: every index slows writes and uses space. Index for your actual queries; check them with
EXPLAIN.
8.5Storage engines: B-trees and LSM trees
An IoT platform writes 500,000 sensor readings per second and reads them rarely. Why might a B-tree database struggle, and what design handles it better?
Reveal the answer
A B-tree updates pages in place, so each insert can cause random page writes, plus WAL writes and page splits. At very high write rates that random I/O becomes the limit. An LSM tree buffers writes in memory and a sequential log, then writes large sorted files sequentially, giving much higher write throughput.
| B-tree (PostgreSQL, MySQL InnoDB) | LSM tree (Cassandra, RocksDB, HBase, ScyllaDB) | |
|---|---|---|
| Writes | Update pages in place, plus WAL | Append to WAL and memtable; flush sorted files |
| Reads | One path down the tree | Check memtable, then files newest to oldest; Bloom filters skip files |
| Strength | Predictable reads, range queries, transactions | Very high write throughput, good compression |
| Cost | Random writes, page splits, fragmentation | Read amplification, compaction uses I/O and CPU in the background |
8.6The NoSQL families
Pick a family for each: (a) chat messages per conversation, 10B per day; (b) a product catalog with different attributes per category; (c) "friends of friends who like jazz"; (d) CPU metrics every 10 s from 100K servers; (e) session lookup by token.
Reveal the answer
(a) Wide-column (Cassandra, HBase, Bigtable), partitioned by conversation and sorted by time. (b) Document (MongoDB) or JSON columns in PostgreSQL. (c) Graph (Neo4j). (d) Time-series (Prometheus, InfluxDB, TimescaleDB). (e) Key-value (Redis, DynamoDB).
| Family | Data model | Strengths | Limits | Examples |
|---|---|---|---|---|
| Key-value | Key to opaque value | Simplest, fastest, scales by key | No queries by value | Redis, DynamoDB, Riak |
| Document | Key to JSON-like document with indexes | Flexible schema, whole aggregates in one read | Joins across documents are weak | MongoDB, Couchbase, Firestore |
| Wide-column | Partition key to sorted rows of columns | Huge write volume, time-ordered data per key, multi-datacenter | Must design tables per query; limited secondary indexes | Cassandra, HBase, Bigtable, ScyllaDB |
| Graph | Nodes and edges | Multi-hop relationship queries | Hard to shard; niche | Neo4j, Neptune |
| Time-series | Timestamped measurements | Fast append, downsampling, retention | Narrow use | Prometheus, InfluxDB, TimescaleDB |
| Search | Inverted index over documents | Full-text, relevance, facets | Not a source of truth; near real-time | Elasticsearch, OpenSearch |
| Columnar (OLAP) | Columns stored separately | Scans and aggregates over billions of rows | Slow single-row writes and lookups | BigQuery, Snowflake, Redshift, ClickHouse |
| Distributed SQL | SQL tables sharded and replicated with consensus | SQL and transactions at horizontal scale | Higher write latency; cost | Spanner, CockroachDB, YugabyteDB, TiDB |
8.7Choosing a database
Payments ledger, 5,000 writes/s, must never lose or double-apply an entry, needs joins for reporting. Which database, and what would push you to something else later?
Reveal the answer
A relational database (PostgreSQL or MySQL) with strong constraints and transactions; it handles 5,000 writes/s on good hardware, especially with batching. Push factors: sustained write growth beyond one primary (shard by account, or move to distributed SQL such as Spanner or CockroachDB) or global multi-region writes. Reporting goes to a warehouse fed by change data capture, not to the primary.
Questions that decide the choice:
- What are the access patterns? Lookups by key, range scans by time, ad hoc queries, joins, full-text, graph traversals.
- Do you need multi-row transactions? Money, inventory, bookings: yes. Likes, views, logs: rarely.
- What is the read/write ratio and volume? Write-heavy at huge scale favors LSM-based wide-column stores.
- How big will it get? Single-digit TB and a few thousand writes/s: one relational primary plus replicas. Tens of TB or more writes: partitioning or a natively distributed store.
- What consistency does each feature need? (Module 11)
- What does the team already run well? Operational experience is a real requirement.
| Myth | Reality |
|---|---|
| "SQL does not scale" | Sharded MySQL runs some of the largest sites; distributed SQL scales writes horizontally |
| "NoSQL has no schema" | The schema moves into the application and the queries; you still must design it |
| "NoSQL has no transactions" | DynamoDB and MongoDB offer multi-item transactions, with limits |
| "Pick one database" | Most systems use several: OLTP, cache, search, warehouse, each fed from the source of truth |
8.8Data modeling for scale
A feed page shows each post with the author's name and avatar. Joining posts to users on every read is slow at scale. What are your options?
Reveal the answer
Denormalize: copy the author's name and avatar URL into the post (or the feed entry), accepting that a name change must update or tolerate old copies. Or cache user profiles and join in the application from the cache. Both trade write complexity or staleness for fast reads.
- Normalize for correctness (one place to update) when writes and consistency matter. Denormalize for read speed when reads dominate and slight staleness is acceptable.
- Model by access pattern for NoSQL: list the queries first, then design one table (or key layout) per query.
- Keep transactions inside one partition where possible: put data that changes together under the same partition key (Module 10).
- Separate OLTP and OLAP: the transactional database serves the product; a warehouse fed by change data capture or batch export serves analytics.
- Soft deletes and audit trails: append-only history tables (who changed what, when) are often required in regulated domains.
In financial systems, the common pattern is a relational database as the system of record (ledgers, accounts, state machines enforced with constraints), Kafka for events, Redis for hot reads and rate limits, a search engine for support tooling, and a warehouse for regulatory reports. Each copy is derived from the system of record and can be rebuilt from it.
Name the store per data type with a reason, write the primary key and main index, and say how the hottest query is served. "PostgreSQL because bookings need transactions; index on (event_id, seat_id)" scores better than a list of databases.
8.9Common mistakes
Common mistakes
- Read-modify-write without protection for counters, stock, balances, and seats.
- Choosing NoSQL "for scale" for a workload that needs joins and transactions and fits in one PostgreSQL.
- No index for the main query, or an index whose column order does not match it.
- Indexing everything, slowing every write.
- Holding row locks across remote calls.
- Running analytics on the primary, slowing the product.
- Mixing up ACID consistency and CAP consistency.
8.10Self-check
- Which isolation level stops write skew, and what must clients do when using it in PostgreSQL?
Reveal
Serializable. PostgreSQL may abort a transaction with a serialization failure, so clients must retry it.
- How does optimistic concurrency detect a conflict?
Reveal
The update includes the version it read (
WHERE id = ? AND version = ?). If someone else updated first, the version changed, 0 rows are updated, and the client retries or reports a conflict. - Why do LSM trees need compaction?
Reveal
Flushes create many overlapping sorted files; reads would check more and more files, and deleted or overwritten data would never be reclaimed. Compaction merges files, drops obsolete versions, and keeps reads fast.
- Why can't a Cassandra table efficiently answer a query that does not include the partition key?
Reveal
The partition key decides which nodes hold the data. Without it, the query must ask every node and scan, so you design another table keyed for that query.
- What is a covering index?
Reveal
An index that contains all columns a query needs, so the database answers from the index alone without reading the table rows.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 9: Replication
Replication keeps copies of the same data on several machines, so the system survives failures, serves more reads, and sits closer to users. The price is that copies can disagree. This module covers leader-based, multi-leader, and leaderless replication, lag, failover, and quorums.
- Why replicate a database at all? Give three reasons.
Reveal
Availability (survive a machine or zone failure), read scaling (spread reads over followers), and lower latency (copies near users). Also durability: data survives the loss of one disk.
- A user edits their profile, refreshes, and sees the old version. What happened?
Reveal
The write went to the leader, the read went to a follower that had not received it yet (replication lag). The read-your-writes guarantee was broken.
- With asynchronous replication, the leader crashes right after acknowledging a write. What can happen to that write?
Reveal
It can be lost: the new leader (a follower) may never have received it.
- Two network halves each believe they have the leader and both accept writes. What is this called?
Reveal
Split brain. Prevent it with majority-based election (only the side with a majority can elect a leader) and fencing tokens that reject writes from an old leader.
- N = 3 replicas, writes wait for 2, reads ask 2. Will a read see the latest successful write?
Reveal
Usually: R + W = 4 > N = 3, so with a strict quorum and no concurrent writes, every read set overlaps the write set in at least one replica. It is not a strict guarantee (sloppy quorums, concurrent or partially failed writes, and clock-based conflict resolution can still return stale data), as the end of this module explains.
9.1Why replicate
Your database is the only copy, on one machine in one zone. List what replication fixes and what new problem it creates.
Reveal the answer
Fixes: machine or disk loss (durability), downtime while the machine is replaced (availability), read capacity limits (followers serve reads), and distance to far users (copies in other regions). New problem: the copies can disagree for a while, so readers may see stale data, and failover can lose or duplicate writes.
Keep the same data on several nodes. Every change is shipped from one copy to the others, usually as a log of changes.
A head office and branch offices with copies of the same customer register. Changes are mailed to the branches; a branch that has not received today's mail answers with yesterday's data.
Single-leader (one node takes writes), multi-leader (several nodes take writes and sync with each other), leaderless (clients write to several nodes directly, using quorums).
9.2Single-leader replication
Reads are 95% of traffic and the leader's CPU is at 90%. What does adding two followers change, and what does it not change?
Reveal the answer
Reads can move to the followers, so leader load drops sharply. Writes still all go through the single leader, so write capacity does not change; for that you need partitioning (Module 10).
- How changes ship: the leader sends its write-ahead log or a logical change stream (PostgreSQL streaming replication, MySQL binlog). Followers apply changes in the same order.
- Read scaling: add followers for read-heavy workloads. Each extra follower adds load on the leader to ship the log, and all of them lag a little.
- Used by: PostgreSQL, MySQL, SQL Server, MongoDB replica sets, Redis, Kafka partitions (each partition has a leader).
9.3Synchronous and asynchronous replication
For a payments ledger in one region, would you wait for all followers to confirm each commit? Why or why not?
Reveal the answer
Not all: the slowest or a failed follower would block every commit. A common choice is semi-synchronous: wait for one follower (ideally in another zone) to confirm, then acknowledge. A confirmed write then exists on two machines, while the other followers catch up asynchronously.
| Mode | Leader acknowledges after | Durability on leader crash | Latency and availability |
|---|---|---|---|
| Synchronous (all) | Every follower stored the write | No loss | Slowest; one dead follower blocks writes |
| Semi-synchronous | At least one follower stored it | No loss if that follower takes over | Small added latency; tolerates other followers failing |
| Asynchronous | Only its own log | Recent writes can be lost | Fastest; followers can lag far behind |
Consensus-based systems (Module 11) generalize this: a write commits once a majority stores it, which tolerates a minority of slow or failed nodes without losing acknowledged writes.
9.4Replication lag and what users see
A user posts a comment and is redirected to the post page, served by a follower 3 seconds behind. Their comment is missing. Give two fixes.
Reveal the answer
(1) Read-your-writes: route this user's reads of the post to the leader for a short time after they write (for example 10 s), or until the follower has replayed past the write's log position. (2) Show the comment from the client's own state (optimistic UI) while the server catches up. Also possible: pin the user to a follower that is known to be caught up.
| Guarantee | Broken when | Typical fix |
|---|---|---|
| Read-your-writes | You do not see your own recent write | Read your own data from the leader for a while; or wait until the replica passes your write's log position |
| Monotonic reads | A later read shows older data than an earlier read (time goes backwards) | Pin each user to one replica (hash of user ID) |
| Consistent prefix | You see an answer before the question it replies to | Keep causally related writes in the same partition; or track causal dependencies |
Lag is normally milliseconds, but it spikes during bulk jobs, large migrations, or long-running queries on followers. Monitor replication lag as a first-class metric and stop routing reads to followers beyond a threshold.
9.5Failover
The leader stops responding. List the steps of an automatic failover and one way each step can go wrong.
Reveal the answer
- Detect failure, usually by missed heartbeats or timeouts. Too short a timeout causes needless failovers during GC pauses or load spikes; too long means a long outage.
- Choose a new leader, ideally the most up-to-date follower, by election or by a coordinator. A lagging follower loses writes.
- Reconfigure: clients and the remaining followers switch to the new leader. If the old leader comes back still believing it leads, there are two leaders (split brain).
- Lost writes: with async replication, acknowledged writes on the old leader that never reached the new one are gone. Their IDs may even be reused, which corrupts other systems that saw them.
- Split brain: prevent it by requiring a majority to elect a leader (a 3-node or 5-node cluster) and by fencing: each leader term has a higher epoch number, and storage or downstream systems reject requests carrying an older epoch.
- Use battle-tested tooling: Patroni (PostgreSQL, using etcd for consensus), managed databases with automatic failover (RDS Multi-AZ, Cloud SQL HA), Redis Sentinel. Do not hand-roll failover.
- RPO and RTO (Module 15) describe the result: how much data you can lose (RPO) and how long recovery takes (RTO).
9.6Multi-leader replication
Users in India and the US both edit the same shared document title at the same moment, each writing to their local region's leader. What must the system decide, and what are the options?
Reveal the answer
It must resolve a write conflict. Options: last-write-wins by timestamp (simple, silently drops one edit), merge both values (show both, or app-specific merge), keep both as siblings for the user to resolve, or use a CRDT that merges automatically. Best of all is avoiding conflicts by routing each document to one home leader.
- Use when: multiple regions each need low-latency writes, clients work offline (each device is a "leader" that syncs later), or collaborative editing.
- Conflict resolution:
- Last write wins (LWW): keep the value with the highest timestamp. Simple, but loses data, and clock skew can pick the "wrong" winner.
- Application merge: union of shopping cart items, max of counters, flag for manual resolution.
- CRDTs (conflict-free replicated data types): counters, sets, and text types designed so concurrent updates always merge to the same result (Case study 16).
- Avoid conflicts: give each record a home region and send all its writes there.
- Cost: much harder to reason about; avoid unless the requirement truly needs it.
9.7Leaderless replication and quorums
A key is stored on 3 replicas. You want writes to survive one replica being down, and reads to always see the latest successful write. Choose W and R.
Reveal the answer
W = 2 and R = 2. A write succeeds with one replica down (2 of 3 acknowledge), a read succeeds with one replica down, and R + W = 4 > 3 guarantees the read set overlaps the write set. W = 3 would block writes when any replica is down; R = 1 with W = 2 could read the one replica that missed the write.
Dynamo-style stores (Cassandra, Riak, the original Dynamo; ScyllaDB) let the client write to and read from several replicas directly:
- N replicas per key, W acknowledgments required for a write, R replies required for a read. R + W > N means reads and writes overlap. Common choice: N = 3, W = 2, R = 2 (Cassandra's
QUORUM). - Tunable:
W = 1, R = 1for speed with possible staleness;W = Nfor reads that only need one replica but writes that fail when any node is down. - Read repair: when a read sees a stale replica, it writes the newer value back to it.
- Anti-entropy: a background process compares replicas using Merkle trees (hash trees over key ranges) and copies only the ranges that differ.
- Hinted handoff: if a target replica is down, another node stores the write with a "hint" and delivers it when the replica returns. With a sloppy quorum, those stand-in nodes count toward W, which keeps writes available but weakens the overlap guarantee.
- Conflicts still happen with concurrent writes; stores use LWW timestamps (Cassandra) or version vectors and siblings (Riak).
R + W > N does not make a leaderless store linearizable. Concurrent writes, sloppy quorums, and partial failures can still produce surprising reads. Say "quorums give overlap, which makes stale reads much less likely," and use a consensus-based system when you need strict guarantees.
When you add replicas, interviewers often probe the read-after-write case. Have the answer ready: route a user's own reads to the leader for a short window or until the replica passes the write's log position.
9.8Common mistakes
Common mistakes
- Assuming followers are up to date, then reading your own write from one.
- Ignoring lost writes in async failover for data that must never be lost.
- Two-node clusters with automatic failover: no majority is possible, so split brain is likely. Use 3 or 5 voters.
- Multi-leader "for availability" without a conflict strategy.
- Quoting R + W > N as strong consistency.
- Reporting and backups on the leader instead of a dedicated follower.
9.9Self-check
- How does a follower know it has caught up to a client's write?
Reveal
The leader returns the log position (LSN) of the write; the client or router sends it with later reads, and a follower serves the read only once it has replayed past that position.
- Why do consensus clusters use an odd number of nodes?
Reveal
A majority of 3 is 2 and of 4 is 3, so a 4th node adds cost without tolerating more failures (both tolerate 1). 5 nodes tolerate 2.
- What is a fencing token and who checks it?
Reveal
A number that increases with every new leader (or lock holder). The resource being written (storage, a downstream service) remembers the highest token seen and rejects requests with lower ones.
- When is multi-leader replication worth its complexity?
Reveal
When several regions need low-latency local writes, or clients must work offline and sync later, and the data model has a clear conflict strategy.
- What do Merkle trees do in anti-entropy?
Reveal
They summarize key ranges as hashes, so two replicas compare a few hashes from the root down and transfer only the ranges whose hashes differ.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 10: Partitioning and consistent hashing
When data or writes outgrow one machine, you split the data into partitions (shards) on different nodes. The hard parts are choosing the key, keeping load even, moving data when nodes change, and answering queries that span partitions. Consistent hashing is the tool that makes adding and removing nodes cheap.
- Replication vs partitioning: which one increases write capacity?
Reveal
Partitioning. Replication copies all data to every replica, so every replica still handles every write; partitioning gives each node a different subset of writes.
- You shard orders by
order_date. Why is that a bad shard key for writes?Reveal
All new orders have today's date, so every write lands on the newest shard (a hot partition) while the others sit idle.
- You shard with
hash(key) mod 4and add a fifth server. Roughly what fraction of keys must move?Reveal
About 80% (N/(N+1)). That is why plain mod N is replaced by consistent hashing or a fixed number of slots.
- A celebrity's account gets 1,000x the traffic of others and overloads one shard. One fix?
Reveal
Split that hot key: append a suffix (
celeb#0..#9) to spread writes across shards and combine on read, and cache the hot reads. - How do you find all orders for a product when orders are sharded by customer?
Reveal
Either ask every shard and merge the results (scatter-gather), or maintain a global secondary index (or a separate table) partitioned by product ID.
10.1Why partition
A table grows by 200 GB a month and takes 30,000 writes/s. Replicas are already in place. Why do replicas not help, and what does?
Reveal the answer
Replicas copy every write, so each still takes 30,000 writes/s and stores all the data. Splitting the table across, say, 10 shards gives each one about 3,000 writes/s and a tenth of the data. In practice you combine both: partition for scale, replicate each partition for availability.
Split the dataset by a partition key so each node owns a subset. Requests for a key go only to the node that owns it.
A library too big for one building is split across branches by the first letter of the author's name. Each branch handles its own books and visitors. Picking the split rule well decides whether one branch has a queue out the door.
Each partition is itself replicated, and each node holds leaders for some partitions and followers for others, so load and failures spread evenly.
10.2Choosing a partition key
For each, pick a partition key and name one risk: (a) chat messages, (b) a multi-tenant SaaS where one customer is 30% of traffic, (c) IoT readings queried by device and time.
Reveal the answer
(a) conversation_id: messages of one chat stay together and in order; risk: a huge group chat becomes a hot partition. (b) tenant_id: keeps a tenant's data together for queries and isolation; risk: the giant tenant overloads its shard, so give it dedicated shards or sub-partition by (tenant_id, user_id). (c) device_id (possibly with a time bucket like (device_id, day)): spreads load across devices, keeps one device's readings ordered; risk: unbounded partitions without the time bucket.
A good partition key:
- Has many distinct values (high cardinality), so data can spread across many nodes.
- Spreads load evenly, both data size and request rate. Watch for skew: a few very hot keys.
- Matches the main access pattern, so common queries hit one partition. The key you always know at query time (user ID, conversation ID) is usually the right one.
- Keeps things that must change together in one partition, so transactions stay local (an account and its ledger entries).
The partition key is the hardest decision to change later. In an interview, say the key, why it fits the main query, and what happens with the hottest key.
10.3Range and hash partitioning
You need "all events between 10:00 and 10:05" quickly. Would you partition by range of timestamp or by hash of event ID? What does each cost?
Reveal the answer
Range by time makes that query hit one or two partitions, but all current writes go to the newest partition (hot). Hash by event ID spreads writes evenly, but the time-range query must ask every partition. A common compromise: partition by hash of a source ID (spread writes), and inside each partition sort by time (fast per-source ranges); or use time buckets plus a hash prefix.
| Range partitioning | Hash partitioning | |
|---|---|---|
| Rule | Key ranges (A-F, G-M; or by date) | hash(key) decides the partition |
| Range queries | Efficient | Must query all partitions |
| Load balance | Risk of hot ranges (sequential keys, popular letters, "today") | Even, unless single keys are hot |
| Used by | HBase, Bigtable, Spanner, MongoDB range sharding | Cassandra, DynamoDB, Redis Cluster, MongoDB hashed sharding |
Compound keys get both: Cassandra hashes the partition key to pick the node, then sorts rows inside the partition by clustering columns. PRIMARY KEY ((user_id), created_at) spreads users evenly and keeps each user's rows in time order (Module 8).
10.4Consistent hashing
With hash(key) mod N, why does adding one node move almost every key? What property do we want instead?
Reveal the answer
The partition depends on N itself: change N and almost every key's remainder changes, so about N/(N+1) of keys move. We want adding or removing one node to move only about 1/N of the keys: just the share the new node takes over or the departing node held.
How it works:
- Hash both nodes and keys onto the same circle of hash values (for example 0 to 2^32 - 1).
- A key belongs to the first node clockwise from its position.
- Adding a node takes keys only from its clockwise neighbor; removing a node gives its keys to its neighbor. About K/N keys move (K keys, N nodes).
- Virtual nodes: each physical node is placed at many points (for example 100-256). This evens out the arcs, spreads a failed node's load across all others instead of one neighbor, and lets bigger machines take more points.
- Replication on the ring: store each key on the next N distinct physical nodes clockwise (Dynamo, Cassandra).
Not every system uses a ring. Redis Cluster uses 16,384 fixed hash slots assigned to nodes; Kafka uses a fixed number of partitions per topic; Elasticsearch fixes the number of primary shards per index. Fixed slots achieve the same goal: adding a node moves whole slots, not individual keys, and the key-to-slot mapping never changes. Related algorithms: rendezvous (highest random weight) hashing and Google's jump consistent hash.
10.5Hot spots and skew
A "like" counter for one viral post gets 200,000 increments per second, all to one key on one shard. How do you spread it?
Reveal the answer
Shard the counter itself: write increments to post:9:likes#0 to #15 chosen at random, so 16 keys (on different shards) share the load, and sum them on read (or periodically aggregate into the main counter). Reads of the total can be cached for a second. Batching increments in memory and flushing every second also cuts writes.
- Hot keys: celebrities, viral items, a dominant tenant. Fixes: key salting (add a suffix to spread writes, merge on read), caching hot reads, dedicated partitions for known giants.
- Hot ranges: sequential IDs or timestamps in range partitioning. Fixes: hash prefix, random or time-ordered-but-spread IDs, time buckets combined with another key.
- Skewed sizes: one partition grows far larger (a huge group chat). Fixes: split by
(key, bucket)such as month, cap partition size.
Hash a time-ordered ID and you lose locality; use random IDs as a primary key in a B-tree and you scatter inserts across the index. Many teams use time-ordered IDs (UUIDv7, Snowflake) as the primary key inside a shard and hash a different field, such as the user ID, to pick the shard.
10.6Rebalancing and request routing
You started with 4 shards on 4 machines and now need 6 machines. If you had created 64 logical partitions on day one, how would growing to 6 machines work?
Reveal the answer
Move whole logical partitions: each of the 6 machines takes about 10-11 of the 64. The key-to-partition mapping never changes; only the partition-to-machine assignment does. That is far easier than re-splitting data, which is why many systems create many more partitions than nodes up front.
| Strategy | How | Used by |
|---|---|---|
| Fixed number of partitions | Create many partitions up front; move whole partitions between nodes | Kafka, Redis Cluster slots, Elasticsearch, Riak |
| Dynamic splitting | Split a partition when it grows too big; merge small ones | HBase, Bigtable, DynamoDB, MongoDB |
| Consistent hashing with virtual nodes | Nodes join the ring and take arcs | Cassandra, Dynamo, many caches |
How a request finds the right node:
- Any node forwards: the client contacts any node, which forwards to the owner (Cassandra coordinator).
- Routing tier: a proxy that knows the map (Vitess VTGate for MySQL, MongoDB
mongos, a Redis proxy). - Partition-aware clients: the client library caches the map and goes straight to the owner (Redis Cluster clients follow
MOVEDredirects, Kafka clients fetch metadata).
The map itself lives in a coordination service (ZooKeeper, etcd) or is spread by gossip, and moving data happens in the background, throttled so it does not overload the cluster.
10.7Queries across partitions
Users are sharded by user_id. Support staff need "find user by email". What are your options?
Reveal the answer
(1) Scatter-gather: ask every shard and merge; fine if rare and shards are few. (2) A global lookup table email -> user_id, itself partitioned by email, kept in sync on every user change. (3) Send user records to a search index (Elasticsearch) and query that. For a frequent, latency-sensitive query, use (2).
| Local index (per partition) | Global index (partitioned by term) | |
|---|---|---|
| Write cost | Cheap: same partition as the row | A write may touch another partition's index; often updated asynchronously |
| Read cost | Scatter-gather to all partitions | Read one index partition |
| Freshness | Immediate | Can lag |
| Examples | Cassandra secondary indexes, Elasticsearch (per shard), MongoDB | DynamoDB global secondary indexes |
Cross-partition joins and transactions are expensive: avoid them through key choice and denormalization, run them in batch or analytics systems, or use a store that supports distributed transactions (Spanner, CockroachDB) and accept the latency. Patterns such as sagas (Module 13) coordinate multi-partition business operations.
Always name the partition key, why it matches the main query, and what the hottest key looks like. Then say how you would rebalance. This is one of the most common deep dives at L4.
10.8Common mistakes
Common mistakes
- Sharding too early when one node plus replicas would do.
- A low-cardinality or monotonic shard key (country, status, date).
- Plain
mod Nfor anything that will grow. - Ignoring the hottest key: every key choice has a worst case; name it.
- Frequent scatter-gather queries on the hot path.
- Cross-shard transactions by default, instead of keeping related data on one shard.
10.9Self-check
- Why do virtual nodes improve consistent hashing?
Reveal
With one point per node, arcs are uneven and a failed node's load lands on one neighbor. Many points per node even out arc sizes and spread a failed node's keys across all remaining nodes.
- When adding the 11th node to a 10-node consistent-hashing cluster, about what fraction of keys moves?
Reveal
About 1/11 of the keys, the share the new node takes over.
- How does Redis Cluster map a key to a node?
Reveal
CRC16(key) mod 16384gives a hash slot; each node owns a set of slots. Moving slots between nodes rebalances without changing the key-to-slot mapping. - What is a partition key vs a clustering key in Cassandra?
Reveal
The partition key is hashed to choose the nodes that store the row; clustering keys sort rows within that partition, enabling ordered range reads inside it.
- Global secondary index reads are fast. What is the trade-off?
Reveal
Writes become more expensive or the index lags, because updating a row may require updating an index partition on another node, often asynchronously.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 11: Consistency, CAP, and consensus
Once data has copies, you must decide what readers are allowed to see and what the system does when nodes cannot talk. This module gives precise meanings for consistency models, CAP and PACELC, explains how consensus (Raft) makes many machines agree, and covers locks, fencing, and clocks.
- "CAP means you pick any two of consistency, availability, and partition tolerance." What is wrong with this?
Reveal
Partitions are not optional in a distributed system; they happen. CAP says that during a partition you must choose between consistency (refuse some requests) and availability (answer, possibly with stale data).
- Why do etcd and ZooKeeper clusters usually have 3 or 5 nodes?
Reveal
They need a majority to agree. 3 nodes tolerate 1 failure, 5 tolerate 2. Even sizes add cost without adding tolerance.
- A service holds a Redis lock with a 30 s expiry, pauses for 40 s in garbage collection, then writes. What can go wrong?
Reveal
The lock expired; another client acquired it and is writing too. Both write. A fencing token checked by the storage prevents the stale holder's write.
- Two servers order events by their wall-clock timestamps. Why is that unsafe?
Reveal
Clocks drift and jump (NTP corrections), so a later event can carry an earlier timestamp. Use a single sequencer, logical clocks, or a consensus log for ordering.
- Which needs strong consistency: a "like" count, a username uniqueness check, a news feed, a bank transfer?
Reveal
Username uniqueness and the bank transfer. Like counts and feeds can be eventually consistent.
11.1Consistency models
You update your profile photo in India. A friend in the US refreshes 1 second later and sees the old photo; 5 seconds later they see the new one. Which consistency model allows this, and which would forbid it?
Reveal the answer
Eventual consistency allows it: replicas converge after a delay. Linearizability (strong consistency) forbids it: once your write completed, every later read anywhere must return the new photo. Paying for linearizability across continents would add tens to hundreds of milliseconds to every write or read.
| Model | Guarantee | Example need |
|---|---|---|
| Linearizable (strong) | The system behaves like a single copy: after a write completes, every later read sees it (or something newer) | Unique usernames, leader election, account balance checks, seat booking |
| Causal | If B depends on A (a reply to a post), everyone sees A before B; unrelated writes can appear in any order | Comment threads, chat |
| Session guarantees | Read your own writes; never go back in time within a session (Module 9) | Profile edits, settings |
| Eventual | If writes stop, all replicas converge to the same value | Like counts, view counts, feeds, DNS, caches |
Choose per operation, not per system. "Placing an order is strongly consistent; the order history page reads from a replica and can lag a second" is a strong design statement.
11.2The CAP theorem
A network partition splits two replicas of an inventory database. A customer on side B wants to buy the last unit, which may already have been sold on side A. What are the system's two choices?
Reveal the answer
Consistency (CP): side B refuses or delays the purchase because it cannot confirm the latest stock; no oversell, but some customers get errors. Availability (AP): side B accepts the order using its possibly stale stock; customers are served, but the shop may oversell and must reconcile later (refund, backorder).
- C (in CAP) = linearizability. A = every request to a non-failed node gets a non-error response. P = the system keeps working despite lost messages between nodes.
- Since partitions happen, the real choice is CP or AP during a partition:
- CP: refuse or time out some requests to avoid stale or conflicting data (etcd, ZooKeeper, HBase, Spanner, a single-leader SQL database whose minority side cannot accept writes).
- AP: keep answering and reconcile later (Cassandra and DynamoDB with eventual reads, DNS, multi-leader setups, most caches).
- Many systems are tunable: Cassandra with
QUORUMreads and writes behaves much more like CP than withONE.
11.3PACELC: the trade-off when nothing is broken
No partition is happening. Why might a system still choose to return slightly stale data?
Reveal the answer
Latency. Guaranteeing the latest value means coordinating with other replicas (a quorum or the leader), possibly in other zones or regions, on every request. Reading the nearest replica is faster. That trade-off exists all the time, not only during partitions.
PACELC: if there is a Partition, choose A or C; Else, choose Latency or Consistency.
| System (default settings) | During partition | Normal operation |
|---|---|---|
| Cassandra, DynamoDB (eventually consistent reads) | Available | Low latency |
| Spanner, etcd, HBase | Consistent | Consistent (pays latency) |
| ZooKeeper | Consistent for writes | Writes linearizable; reads served locally and may be stale unless the client calls sync() first |
| MongoDB, Cassandra with quorums | Depends on read and write concern settings | Depends |
11.4Consensus and Raft
Five machines must agree on which one is the leader, even if two crash and the network delays messages. Why is "the node with the lowest ID that is alive is the leader" not enough?
Reveal the answer
Nodes cannot reliably tell "dead" from "slow" or "cut off". Two groups that cannot see each other would each pick their own lowest ID and both act as leader (split brain). Consensus protocols fix this with majorities: a leader needs votes from more than half the nodes, and two majorities of the same cluster always overlap, so two leaders cannot both win the same term.
What consensus gives you: a group of nodes agrees on one ordered log of decisions, even when a minority crash or messages are delayed. Everything built on it (leader election, locks, configuration, metadata) inherits that agreement.
Raft in five points:
- Roles: one leader, the rest followers. Followers that hear no heartbeat within a randomized timeout become candidates.
- Election: a candidate increments the term, votes for itself, and asks others. A node grants at most one vote per term, and only to a candidate whose log is at least as up to date as its own. A majority wins.
- Log replication: clients send writes to the leader, which appends them and sends them to followers.
- Commit: an entry is committed once stored on a majority; then it is applied and acknowledged. Committed entries are never lost, because any future leader must have them to win a majority vote.
- Fault tolerance:
2f + 1nodes tolerateffailures. Writes stop if a majority is unavailable (CP).
Paxos solves the same problem; Raft was designed to be easier to understand. Where you meet them: etcd (Raft, the store behind Kubernetes), ZooKeeper (ZAB), Consul (Raft), Kafka KRaft metadata (Raft), CockroachDB and TiKV (Raft per data range), Spanner (Paxos per split).
Do not implement consensus in an interview or at work. Say "store the leader lease and configuration in etcd or ZooKeeper" and explain what you get: linearizable writes, leases, watches, and majority-based fault tolerance.
11.5Coordination services, leases, and locks
Ten instances of a scheduler run, but each job must be triggered by only one. How do you choose which instance runs jobs, and what happens if it dies?
Reveal the answer
Leader election through a coordination service: each instance tries to create a lease-backed key (an etcd lease or a ZooKeeper ephemeral node). The one that succeeds is leader and keeps renewing the lease. If it dies, the lease expires and another instance takes over. Jobs should still be idempotent, because a paused old leader might run a job once more before it notices.
Coordination services (etcd, ZooKeeper, Consul) give small key-value storage with linearizable writes (etcd reads are linearizable by default; ZooKeeper reads can be slightly stale unless preceded by sync()), plus:
- Leases / ephemeral nodes: keys that disappear when the client stops renewing, for leader election and membership.
- Watches: notifications when a key changes, for configuration and service discovery.
- Monotonic revision numbers: usable as fencing tokens.
Distributed locks come in two kinds:
| Efficiency lock | Correctness lock | |
|---|---|---|
| Purpose | Avoid duplicate work (two workers sending the same report) | Prevent corruption (two writers to one account) |
| If it fails occasionally | Some wasted work | Data corruption |
| Good enough | Redis SET key value NX PX 30000, released with a check-and-delete script |
Consensus-backed lock (etcd, ZooKeeper) plus fencing tokens, or a database constraint |
The simplest correct lock is often the database you already have: a unique constraint, a conditional update (UPDATE ... WHERE status = 'pending'), SELECT ... FOR UPDATE SKIP LOCKED, or PostgreSQL advisory locks. They are atomic with the data change. A Redis lock in front of a database write still leaves a window where both can write; the database check closes it.
11.6Time, clocks, and ordering
Two replicas accept writes to the same shopping cart while disconnected. Later, last-write-wins by timestamp keeps one cart and drops the other. Why is this a problem even if the clocks are perfect?
Reveal the answer
Both writes were made without knowing about each other (they are concurrent). Picking the later one silently discards a real user action (an added item). The system needs to detect concurrency and merge (union the carts), which timestamps cannot tell you but version vectors can.
- Wall clocks drift and jump: NTP keeps servers within milliseconds usually, but not reliably, and corrections can move time backwards. Never use wall-clock time alone to order events across machines or to decide which write wins when it matters.
- Monotonic clocks measure durations on one machine (timeouts, latency); use them for "how long", never for "when".
- Lamport clocks: a counter incremented on every event and set to
max(local, received) + 1on every message. They give a total order consistent with causality, but cannot tell you whether two events were concurrent. - Vector clocks / version vectors: one counter per node. Compare two vectors: if one is greater or equal in every slot, it happened after; otherwise the events were concurrent and need merging (Riak, Dynamo).
- Hybrid logical clocks: physical time plus a logical counter; close to wall time and causally safe (CockroachDB, YugabyteDB).
- TrueTime (Google Spanner): GPS and atomic clocks give a time interval with a known error bound of a few milliseconds; Spanner waits out that uncertainty before committing, which gives globally consistent ordering of transactions.
Avoid reciting CAP. Instead, state per feature what happens during a partition: "bookings refuse writes on the minority side; the event page serves cached data." That shows you understand the trade-off, not just the acronym.
11.7Common mistakes
Common mistakes
- "Pick two of three" for CAP, or calling a single-node database "CA" as if it never meets a partition.
- One consistency level for the whole system.
- Hand-rolled leader election with heartbeats and lowest-ID rules.
- Redis locks for correctness without fencing or a database-level guard.
- Ordering events by wall-clock time across machines, or last-write-wins where losing writes matters.
- Even-sized consensus clusters, or spreading a 3-node cluster so one zone holds 2 nodes and its failure loses the majority.
11.8Self-check
- Why can two Raft leaders never both commit entries in the same term?
Reveal
Winning a term requires votes from a majority, and each node votes at most once per term. Two majorities of one cluster always share a node, so only one candidate can win a given term.
- What happens to writes in a 5-node Raft cluster when 3 nodes are down?
Reveal
They stop. No majority (3 of 5) is reachable, so nothing can commit. The system chooses consistency over availability.
- Linearizable vs eventual consistency for a ticket booking system's seat inventory?
Reveal
Linearizable (or an equivalent atomic conditional update on one leader), because two buyers must never both get the same seat.
- What does a vector clock tell you that a Lamport clock does not?
Reveal
Whether two events are concurrent. Lamport clocks order everything, even events that did not influence each other.
- Why does PACELC matter more day to day than CAP?
Reveal
Partitions are rare, but the latency cost of consistency is paid on every request. Most design choices are about latency versus consistency in normal operation.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 12: Queues and streams
Messaging lets one part of a system hand work or events to another without waiting. It absorbs bursts, isolates failures, and lets many consumers react to one event. This module covers queues, pub/sub, and logs, Kafka's model, delivery guarantees, retries and dead letters, and stream processing.
- An order service calls email, inventory, and analytics services synchronously. Analytics is slow, so checkout is slow. Fix?
Reveal
Publish an
OrderPlacedevent to a topic. Email, inventory, and analytics each consume it at their own pace; checkout returns after the publish. - Kafka: a topic has 6 partitions and a consumer group has 10 consumers. How many consumers are busy?
Reveal
- In a classic consumer group each partition is read by at most one consumer, so 4 consumers sit idle. (Kafka's newer share groups, below, are the exception.)
- You need all events for one order processed in order. What decides that in Kafka?
Reveal
The message key. Use
order_idas the key so all events for an order go to the same partition, which is ordered. - A consumer processes a payment, then crashes before committing its offset. What happens on restart?
Reveal
It reprocesses the same message (at-least-once delivery). The handler must be idempotent, for example by recording processed message IDs.
- One malformed message makes the consumer throw every time and blocks the partition. What do you do?
Reveal
Retry a few times with backoff, then move it to a dead-letter queue and continue. Alert so a human can inspect and replay it.
12.1Why asynchronous messaging
A flash sale sends 50,000 orders per second for 2 minutes; the payment processor handles 5,000/s. What does a queue in between change?
Reveal the answer
The queue absorbs the burst: orders are accepted and stored durably, and payment workers drain them at 5,000/s for about 20 minutes. Without the queue, 90% of requests would fail or time out. The cost: payments complete later, so the user sees "order received, confirming payment" rather than an instant result.
The producer writes a message to a durable broker and moves on. Consumers read and process it when they can. Producer and consumer no longer need to be up, fast, or even known to each other at the same time.
A restaurant ticket rail. Waiters clip orders to the rail and go back to customers; cooks take tickets as they free up. A rush makes the rail longer, not the waiters slower, and a new station (desserts) can read the same tickets.
Decoupling, burst absorption (load leveling), retries without the caller waiting, fan-out to many consumers, and a replayable history of events.
Eventual consistency (the effect happens later), duplicate and out-of-order handling, an extra system to operate, and harder debugging across asynchronous hops.
Commands vs events: a command asks for something to happen ("SendEmail", one intended handler). An event states that something happened ("OrderPlaced", any number of listeners). Events keep producers unaware of who reacts, which is what makes adding a new consumer free.
12.2Queue, pub/sub, and log
You want 20 workers to share resize jobs, and separately you want email, search, and audit to each receive every new-user event. Which messaging shape fits each?
Reveal the answer
Resize jobs: a work queue with competing consumers; each job goes to exactly one worker. New-user events: publish-subscribe; every subscriber gets its own copy. A Kafka topic can do both: one consumer group of 20 workers shares partitions, and each of email, search, and audit is its own consumer group.
| Work queue | Pub/sub | Log (stream) | |
|---|---|---|---|
| Delivery | One consumer per message | Every subscriber gets a copy | Every consumer group reads all; each tracks an offset |
| After consumption | Message deleted | Deleted per subscriber | Kept for the retention period |
| Replay | No | Usually no (some managed services can seek back, such as Google Pub/Sub) | Yes: reset the offset |
| Ordering | Mostly FIFO, weakened by parallel consumers and retries | Per subscriber, often unordered | Strict within a partition |
| Examples | RabbitMQ queues, SQS | SNS, Google Pub/Sub, RabbitMQ fanout exchange | Kafka, Kinesis, Pulsar, Redis Streams |
12.3How Kafka works
A topic has 3 partitions. Group A has 2 consumers, group B has 1. Draw who reads which partition. Then group A adds a third consumer: what changes?
Reveal the answer
Group A: consumer 1 reads two partitions, consumer 2 reads one. Group B: its one consumer reads all three. Adding a third consumer to group A triggers a rebalance and each of its consumers gets one partition. Group B is unaffected; groups are independent.
- Topic = named stream, split into partitions. Each partition is an ordered, append-only log; each message has an offset.
- Key decides partition:
hash(key) mod partitions. Same key, same partition, so per-key order is preserved. No key: spread across partitions. - Consumer groups: within a classic consumer group, each partition goes to one consumer, so parallelism is capped by the partition count. Different groups are independent readers.
- Share groups ("queues for Kafka", production-ready since Kafka 4.2 in 2026): many consumers read the same partition and acknowledge records one by one, like a work queue. More consumers than partitions becomes useful, but per-key ordering is lost.
- Offsets: consumers commit the offset they have processed; on restart they resume from it. Retention is time- or size-based (days, often), not "until read".
- Replication: each partition has a leader and followers on other brokers. The in-sync replica (ISR) set holds followers that are caught up. With
acks=allandmin.insync.replicas=2(replication factor 3), an acknowledged write exists on at least 2 brokers. - Log compaction: optionally keep only the latest message per key, turning a topic into a durable changelog (current state per key).
- Metadata: modern Kafka (4.0 onward) manages cluster metadata with its own Raft quorum (KRaft) instead of ZooKeeper.
Interviewers often ask "how many partitions?" Answer from throughput: if one consumer handles 2,000 msg/s and the peak is 40,000 msg/s, you need at least 20 partitions; choose more (say 32-64) for growth, since adding partitions later changes key-to-partition mapping and breaks per-key ordering during the change.
Go deeperEvent schemas and evolution
Producers and consumers deploy independently and events live for days or forever in the log, so event formats must change without breaking readers.
- Use a schema (Avro, Protobuf, or JSON Schema) and a schema registry that checks every new version for compatibility before producers can publish it.
- Compatibility modes: backward (consumers on the new schema can read old events; the usual default), forward (consumers on the old schema can read new events), full (both).
- Safe changes: add optional fields with defaults; stop writing a field before removing it. Unsafe: renaming or retyping fields, reusing Protobuf field numbers, making optional fields required.
- Breaking change: publish a new event version (or topic), run both while consumers migrate, then retire the old one.
12.4Delivery guarantees
Which do you choose for (a) click analytics, (b) sending a payment instruction to a bank: at-most-once or at-least-once? What extra work does each need?
Reveal the answer
(a) Either works; at-least-once with approximate dedup is common, and losing a few clicks is acceptable. (b) At-least-once plus idempotency: never lose the instruction, and make duplicates harmless with an idempotency key the bank honors and a processed-messages table on your side.
| Guarantee | How | Risk | Use for |
|---|---|---|---|
| At-most-once | Commit (ack) before processing; no retries | Lost messages | Metrics where loss is fine |
| At-least-once | Process, then commit; retry on failure | Duplicates | The default for almost everything |
| Exactly-once (effectively-once) | At-least-once delivery plus idempotent or transactional processing | Complexity | Money, inventory, counts that must be exact |
How exactly-once is really achieved:
- Idempotent consumers: store the message ID (or a business key) with a unique constraint in the same database transaction as the effect. A duplicate hits the constraint and is skipped.
- Kafka idempotent producer: the broker drops duplicate sends caused by producer retries (enabled by default in recent versions).
- Kafka transactions: a read-process-write job (consume from one topic, produce to another) commits its output messages and its input offsets atomically; readers use
read_committed. This is exactly-once within Kafka; side effects in external systems still need idempotency. - Stream processors (Flink) checkpoint state and offsets together and use transactional sinks for end-to-end exactly-once.
Say it plainly in interviews: "Delivery is at-least-once; processing is idempotent, so the effect happens exactly once."
12.5Ordering and parallelism
Account events Deposit 100 then Withdraw 80 must apply in order. Yet you need 50 consumers in parallel. How?
Reveal the answer
Partition by account ID. All events of one account land in one partition and are processed in order by one consumer, while 50+ partitions spread different accounts across 50 consumers. Order is guaranteed per account, not globally, and that is all the business needs.
- Global order and parallelism conflict. Pick the narrowest unit that must be ordered (per account, per order, per chat) and use it as the key.
- Retries break order: if message 5 goes to a retry topic while message 6 succeeds, they apply out of order. For strict per-key order, retry in place (blocking that partition) or park all later messages of the same key.
- Hot keys cap throughput: one key's messages are processed by one consumer. Split keys if needed (Module 10).
- Rebalances move partitions between consumers and can cause reprocessing of uncommitted messages; the newer consumer rebalance protocol (Kafka 4.0 onward) rebalances incrementally instead of pausing the whole group. Keep processing idempotent and commit regularly.
12.6Failures: retries, dead letters, and backpressure
A downstream API is down for 10 minutes. Your consumer retries each message immediately, forever. What happens to the consumer and to the API?
Reveal the answer
The consumer spins on one message, so the whole partition stalls behind it (head-of-line blocking), and the constant retries hammer the API, possibly slowing its recovery. Use exponential backoff, a bounded number of attempts, delay or retry topics, a circuit breaker, and a dead-letter queue.
- Transient vs permanent errors: retry timeouts and 503s; send validation errors and malformed payloads straight to the dead-letter queue (DLQ).
- Backoff with jitter, and a cap on attempts.
- DLQ is not a trash can: alert on it, show the error, and build a replay tool.
- Backpressure: when consumers fall behind, consumer lag (latest offset minus committed offset) grows. Monitor it, autoscale consumers on it (up to the partition count), and slow or shed producers if the backlog would exceed retention.
- Visibility timeout (SQS): a received message is hidden for N seconds; if not deleted in time, it reappears for another consumer. Set it longer than processing time.
12.7Choosing a broker
Pick: (a) background jobs for a small team on AWS, (b) an event backbone where 10 teams consume the same order events and analytics replays last week, (c) complex routing of tasks by type and priority.
Reveal the answer
(a) SQS: fully managed, simple, scales automatically. (b) Kafka (or a managed equivalent such as Confluent, MSK, Kinesis, or Pub/Sub): durable log, many independent consumer groups, replay. (c) RabbitMQ: exchanges and routing keys, priorities, per-message acknowledgments.
| Kafka | RabbitMQ | SQS | Google Pub/Sub | |
|---|---|---|---|---|
| Model | Partitioned log | Broker with exchanges and queues | Managed queue | Managed pub/sub |
| Retention and replay | Days or forever; replay by offset | Until acknowledged (streams add replay) | Up to 14 days, no replay | Configurable retention, seek to time |
| Ordering | Per partition | Per queue (single consumer) | FIFO queues: per message group | Per ordering key |
| Throughput | Very high | High | Very high (standard queues) | Very high |
| Strengths | Event backbone, many consumers, stream processing | Flexible routing, priorities, low latency tasks | Zero operations | Global, zero operations |
12.8Stream processing
You count payments per minute. A phone was offline and sends a payment that happened at 10:01 at 10:07. Which minute should it count in, and how long should you wait for late events?
Reveal the answer
It belongs to 10:01 by event time (when it happened), not 10:07 (processing time). A watermark says "events up to time T have probably arrived"; the 10:01 window closes when the watermark passes 10:01 plus an allowed lateness. Events later than that are dropped, sent to a side output, or trigger a correction.
- Stream processors (Apache Flink, Kafka Streams, Spark Structured Streaming, Google Dataflow) run continuous jobs: filter, enrich, join, aggregate over windows, with state that is checkpointed so a crash resumes without losing or double-counting.
- Event time vs processing time: use event time for correctness, with watermarks to bound waiting.
- Windows: tumbling (fixed, non-overlapping: per-minute counts), sliding (overlapping: last 5 minutes, updated every minute), session (activity bursts separated by gaps).
- Batch vs stream: batch (Spark, MapReduce, SQL on a warehouse) processes a bounded dataset, simpler and cheaper for daily reports; streaming gives results in seconds. The Lambda architecture runs both and merges them; Kappa uses one streaming pipeline and replays the log to recompute.
In payments and fintech, Kafka plus Flink is a common backbone: transaction events flow through Kafka; Flink jobs compute running aggregates, detect fraud patterns within windows, route records by status, and write results to databases and alerting systems. Side outputs catch records that fail parsing so one bad record does not stop the pipeline.
Go deeperBatch processing and data lakes
- Batch model: MapReduce splits work into map (transform each record), shuffle (group by key across machines), and reduce (aggregate each group). Spark generalizes this into a graph of transformations kept in memory where possible.
- Data lake: raw and processed data in object storage in columnar files (Parquet or ORC), laid out by partition such as
events/dt=2026-10-17/, so queries skip irrelevant days. - Table formats (Apache Iceberg, Delta Lake, Apache Hudi) add atomic commits, schema evolution, and time travel on top of those files, so many engines (Spark, Trino, Flink, BigQuery) can read and write the same tables safely.
- Backfills: when a bug is fixed, rerun the job over the affected partitions. Make outputs idempotent by overwriting whole partitions (write to a new location, then swap), never appending.
- Batch or stream? Batch for daily reports, model training data, and exact recomputation; streaming when results are needed within seconds. Many systems run both (Case study 15).
12.9Common mistakes
Common mistakes
- Assuming exactly-once delivery and writing non-idempotent consumers.
- More consumers than partitions in a classic consumer group, expecting more throughput (use more partitions, or share groups if you do not need per-key order).
- No message key when per-entity order matters, or a low-cardinality key that creates hot partitions.
- Infinite immediate retries that block a partition and hammer a sick dependency.
- Dead-letter queues nobody watches.
- Publishing an event outside the database transaction that made the change, so a crash leaves them inconsistent (use the outbox pattern, Module 13).
- Using a queue where the caller needs the answer now, then building a slow request-reply over it.
12.10Self-check
- What limits consumer parallelism in a Kafka consumer group?
Reveal
The number of partitions, for a classic consumer group: each partition is assigned to at most one consumer in the group. Share groups (Kafka 4.2+) lift this limit at the cost of per-key ordering.
- What do
acks=allandmin.insync.replicas=2together guarantee?Reveal
A write is acknowledged only after it is stored on at least 2 in-sync replicas, so losing one broker cannot lose acknowledged data.
- How does an idempotent consumer make at-least-once delivery safe?
Reveal
It records each processed message ID (or business key) with a unique constraint, atomically with the side effect. A redelivered message is detected and skipped.
- What is consumer lag and why alert on it?
Reveal
The gap between the newest offset and the consumer's committed offset. Growing lag means consumers cannot keep up; if it exceeds retention, data is lost.
- Event time vs processing time: which do you use for "transactions per minute" and why?
Reveal
Event time, so late or delayed events count in the minute they actually happened; watermarks decide how long to wait.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 13: Distributed transactions and data integrity
When one business action spans several services or databases, there is no single transaction to roll back. This module covers why two-phase commit is rarely used between services, how sagas and compensations replace it, how the outbox pattern keeps data and events in sync, and how idempotency, event sourcing, CQRS, and reconciliation keep data correct.
- The order service saves an order, then publishes
OrderPlacedto Kafka. It crashes between the two. What is broken?Reveal
The order exists but the event was never published, so nothing downstream happens. Writing to two systems without a shared transaction is the dual-write problem.
- Why not use a distributed transaction (two-phase commit) across the order, payment, and inventory services?
Reveal
2PC locks resources until every participant votes and the coordinator decides; if the coordinator fails, participants block. It couples service availability and is often unsupported by brokers and third-party APIs.
- A saga charged the card but shipping failed permanently. What does the saga do?
Reveal
Run compensating actions in reverse order: refund the payment, release the inventory, mark the order cancelled.
- The same "OrderPlaced" event arrives twice at the email service. How do you avoid two emails?
Reveal
Make the consumer idempotent: record the event ID in a processed-events table with a unique constraint in the same transaction as the work, and skip duplicates.
- What is reconciliation and why do payment systems need it even when the code is correct?
Reveal
Comparing your records with an external source of truth (bank or processor settlement files) to find mismatches. Networks, partners, and timeouts create discrepancies no amount of local correctness can rule out.
13.1Why cross-service transactions are hard
Placing an order must: create the order, reserve stock (inventory service, its own DB), and charge the card (payment service and an external processor). Why can't you wrap this in one BEGIN ... COMMIT?
Reveal the answer
The three pieces live in different databases and an external system. A database transaction covers only one database. You need either a protocol that coordinates several (2PC), or a sequence of local transactions with a way to undo completed steps when a later one fails (a saga).
Each service owns its data and can only commit locally. A business transaction becomes several local transactions coordinated by messages, with explicit handling for partial failure.
Booking a trip through separate airline, hotel, and car companies. No single agent can make all three commit atomically. You book them one by one, and if the car is unavailable you cancel the hotel and flight (often with a fee). That is a saga, and the cancellations are compensations.
13.2Two-phase commit
In 2PC, both participants vote YES and then the coordinator crashes before sending COMMIT. What can the participants do?
Reveal the answer
Nothing safely. They promised to commit if told to, so they cannot abort on their own, and they cannot commit without knowing the decision. They wait, holding locks, until the coordinator recovers. This blocking is 2PC's core weakness.
- Where 2PC works well: inside one distributed database, where participants are themselves replicated and highly available (Spanner runs 2PC over Paxos groups; CockroachDB uses a similar approach). The database hides it from you.
- Where it fits badly: across microservices, message brokers, and third-party APIs. Locks across services, coupled availability, and limited support (XA) make it rare in modern service architectures.
13.3Sagas
Write the saga for a ride booking: reserve driver, authorize payment, confirm ride. Give the compensation for each step. Which step needs no compensation, and why?
Reveal the answer
- Reserve driver; compensation: release driver.
- Authorize payment; compensation: void the authorization.
- Confirm ride; this is the last step, so if it succeeds the saga is done and nothing after it can fail. Put steps that cannot be undone (sending a confirmation, capturing money) last, after everything that can fail.
| Orchestration | Choreography | |
|---|---|---|
| How | A central orchestrator tells each service what to do and tracks state | Each service reacts to events and emits its own |
| Visibility | One place shows the whole flow and its state | Flow is spread across services; harder to see |
| Coupling | Orchestrator knows all steps | Services know only events |
| Best for | Complex flows, many steps, timeouts, human steps | Short flows with 2-3 steps |
| Tools | Temporal, Camunda, AWS Step Functions, a hand-built state machine | Kafka topics and event handlers |
Rules that make sagas work:
- Every step is idempotent, because messages and retries repeat.
- Compensations are semantic, not rollbacks: a refund, a cancellation notice, a release. They must also be idempotent and retried until they succeed.
- No isolation: other users can see intermediate states (stock reserved, not yet paid). Use status fields (
PENDING) as semantic locks and design the UI for them. - Order steps by risk: steps that are easy to undo first, irreversible steps (capture, ship, notify) last.
- Timeouts are failures too: a step that never answers must eventually trigger compensation or a query of the remote state.
Card payments follow this shape naturally: authorize (hold funds, reversible by void) during checkout, capture only when the order ships. If anything fails before capture, the compensation is a void instead of a refund: it releases the hold without moving money, which is cheaper and faster than a refund (the customer may still see a pending hold until it is voided). Authorizations expire after a network-dependent number of days, so capture before expiry or re-authorize.
13.4Transactional outbox and change data capture
How can a service guarantee that "order saved" and "OrderPlaced published" either both happen or neither, without 2PC?
Reveal the answer
Write the event into an outbox table in the same database transaction as the order. Commit makes both durable together. A separate relay then reads the outbox (by polling or by tailing the database log) and publishes to Kafka, retrying until it succeeds. Publishing may repeat, so consumers deduplicate.
- Polling relay:
SELECT ... FROM outbox WHERE published = false ORDER BY id LIMIT 100 FOR UPDATE SKIP LOCKED, publish, mark published. Simple; adds polling load and some latency. - Change data capture (CDC): a connector such as Debezium reads the database's replication log (PostgreSQL WAL, MySQL binlog) and publishes each committed change. Low latency, no polling, preserves commit order.
- Inbox / dedup on the consumer side: store processed event IDs with a unique constraint in the consumer's own transaction.
- CDC beyond the outbox: stream every table change to caches (invalidation), search indexes, and warehouses, so derived stores stay in sync without dual writes in application code.
13.5Idempotency everywhere
List four ways to make "credit 100 to account 9 for transfer T1" safe to repeat.
Reveal the answer
(1) A unique constraint on transfer_id in the ledger table, so a second insert fails. (2) An idempotency key table storing the result of T1. (3) A conditional update: UPDATE transfers SET status = 'done' WHERE id = 'T1' AND status = 'pending', applying the credit only when 1 row changed. (4) Model the operation as setting state, not adding: record the entry for T1 with an upsert keyed by T1.
| Technique | How it works | Example |
|---|---|---|
| Idempotency key | Store request key and result; replay the result | Idempotency-Key on payment APIs (Module 3) |
| Natural unique key | Unique constraint on a business ID | One ledger entry per (transfer_id, leg) |
| Processed-message table | Record message IDs consumed | Kafka consumer dedup |
| Conditional write | Change only if in the expected state or version | WHERE status = 'pending', WHERE version = 7 |
| Set, do not increment | Overwrite with an absolute value | "balance as of event 42 = 900" instead of "+100" |
A Redis key with a TTL is a weak dedup guard: it can expire, be evicted, or be lost in a failover, and it is not atomic with the database write. Use it to reduce duplicates cheaply; use a database constraint where duplicates cause real harm.
13.6Event sourcing
A bank account table stores only the current balance. An auditor asks how the balance on 3 March was reached. What design answers that by construction?
Reveal the answer
Event sourcing: store every change as an immutable event (Deposited 500, Withdrew 120) in order, and derive the balance by replaying them. Any past state can be rebuilt by replaying up to that date. Double-entry ledgers in accounting work exactly this way.
- Events are the source of truth; current state is a cached projection. Snapshots every N events keep rebuilds fast.
- Strengths: full audit trail, time travel, rebuild new read models from history, natural fit for event-driven systems.
- Costs: schema evolution of old events, eventual consistency of projections, more moving parts, and "delete this user's data" requests need special handling (for example, encrypting personal data per user and deleting the key).
- Optimistic concurrency: append with an expected version per aggregate (
UNIQUE (account_id, version)), so two concurrent commands cannot both apply. - Use it where history matters intrinsically (ledgers, order lifecycles, compliance). Do not use it for simple CRUD.
13.7CQRS
An order system has a normalized write schema with strict rules, but the "my orders" page needs a joined, filtered, paginated view at 50x the write rate. What can you separate?
Reveal the answer
Separate the write model (commands validated against the normalized schema) from read models built for each query (a denormalized "orders by user" table, a search index). Changes flow from the write side to the read models by events or CDC. Reads scale independently and become simple lookups, at the cost of a short lag.
- CQRS (command query responsibility segregation) = different models for writing and reading. Often paired with event sourcing, but it does not require it: CDC from a normal database works.
- Read-your-writes needs care: after a command, the read model may lag. Return the new state in the command response, or wait for the projection to catch up to the write's version.
13.8Reconciliation
Your ledger says 10,000 payments settled yesterday; the processor's settlement file lists 9,998. Name three likely causes.
Reveal the answer
Timeouts where you recorded failure but the processor actually succeeded (or the reverse), payments that settle on a different day (cutoff times, time zones), duplicates on one side, refunds or chargebacks recorded differently, and plain bugs. Reconciliation finds and classifies each mismatch so it can be fixed.
- Compare independent records of the same facts: your ledger vs the processor's or bank's settlement files, your inventory vs the warehouse count.
- Run it on a schedule (daily for payments), match by stable IDs, classify mismatches (missing on our side, missing on theirs, amount differs), and route them to automated fixes or a human queue.
- Keep append-only audit records so every correction is itself recorded, never an in-place edit.
- In interviews for payment-like systems, mentioning reconciliation is a strong signal that you know distributed systems never agree perfectly on their own.
Payment, order, and booking designs almost always get an "and what if this step fails halfway?" question. Answer with: idempotency keys, a saga or state machine with compensations, the outbox for events, and reconciliation as the safety net.
13.9Common mistakes
Common mistakes
- Dual writes to a database and a broker in application code.
- 2PC across microservices or third-party APIs.
- Sagas without idempotent steps and compensations, or with irreversible steps early.
- Assuming compensation always succeeds: it needs retries and an alert path.
- Exactly-once claims without idempotent consumers.
- Event sourcing for plain CRUD, paying its complexity for no benefit.
- No reconciliation for money movement.
13.10Self-check
- Orchestration vs choreography: which would you use for a 7-step loan disbursement with a manual approval step?
Reveal
Orchestration: one state machine tracks progress, timeouts, retries, and the human step, and shows exactly where each loan is.
- What does Debezium read, and what does that give you over a polling outbox?
Reveal
The database's replication log (WAL or binlog). It publishes committed changes in commit order with low latency and no polling queries.
- Why must saga compensations be idempotent?
Reveal
They are retried after failures and timeouts, and the same compensation message can be delivered more than once.
- How does event sourcing prevent two concurrent withdrawals from both applying?
Reveal
Each append states the expected aggregate version; a unique constraint on
(aggregate_id, version)lets only one succeed, and the other retries against the new state. - Where does 2PC still make sense today?
Reveal
Inside a distributed database whose participants are replicated consensus groups (Spanner, CockroachDB), where blocking is rare and handled internally.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 14: Microservices and architecture patterns
Splitting a system into services lets teams deploy and scale independently, and adds network calls, distributed data, and operations work. This module covers when to split, how to draw boundaries, how services find and talk to each other, gateways and service meshes, and how to migrate safely.
- A 6-person startup asks whether to begin with 15 microservices. What do you say?
Reveal
Start with a modular monolith: clear internal modules, one deployable. Split out a service when a module needs independent scaling, deployment, or ownership. Microservices solve team and scale problems the startup does not have yet.
- Three services read and write the same database tables. Why is this a problem?
Reveal
They are coupled through the schema: no service can change its tables without breaking the others, and no one owns the data. It is a distributed monolith.
- Service A calls B, which calls C, which calls D, all synchronously, each 99.9% available. Combined availability?
Reveal
About 99.6% (0.999^4), and latency is the sum of all hops. Long synchronous chains are fragile.
- How does a service in Kubernetes find the payments service?
Reveal
Through DNS:
payments.default.svc.cluster.localresolves to a Service virtual IP, and Kubernetes routes to healthy pods behind it. - What does a service mesh sidecar do?
Reveal
It proxies all traffic in and out of the service, adding mTLS, retries, timeouts, load balancing, traffic shifting, and metrics without changes to application code.
14.1Monolith, modular monolith, microservices
Name two problems that microservices solve and two they create.
Reveal the answer
Solve: independent deployment by separate teams (no release train), independent scaling of hot parts, failure isolation, technology choice per service. Create: network latency and partial failures between services, distributed data consistency (no cross-service transactions), more infrastructure (discovery, tracing, CI/CD per service), and harder debugging.
| Monolith | Modular monolith | Microservices | |
|---|---|---|---|
| Deploy | One unit | One unit, strict internal modules | Each service separately |
| Data | One database | One database, schema per module | Database per service |
| Calls between parts | In-process | In-process through module interfaces | Network (REST, gRPC, events) |
| Scaling | Whole app | Whole app | Per service |
| Good for | Small teams, early products | Most teams until scale or org size demands more | Many teams, independent release and scaling needs |
A food court versus one restaurant kitchen. Separate stalls can open, close, and change menus independently and one fire does not close all of them, but they need a shared payment system, trays that move between stalls, and coordination for a combo meal.
Systems mirror the communication structure of the organization. Service boundaries work best when they match team boundaries: one team owns a service end to end.
14.2Finding service boundaries
An e-commerce company plans services named UserService, DatabaseService, and ValidationService. What is wrong with these boundaries, and what would be better?
Reveal the answer
They split by technical layer, so every feature change touches all of them. Split by business capability instead: Catalog, Cart, Orders, Payments, Shipping, Customer accounts. Each owns its data and its business rules and changes mostly on its own.
- Split by business capability (a bounded context in domain-driven design): each service owns a coherent set of business rules and the data behind them.
- Database per service: other services get data through the owner's API or events, never by querying its tables.
- High cohesion, low coupling: things that change together stay together. If two services must always deploy together, they are one service.
- Signs of a distributed monolith: shared database, synchronous call chains for every request, lock-step deployments, a "common" library every service must upgrade together.
14.3How services communicate
Checkout needs the price (must be correct now) and should trigger loyalty points (can happen later). Sync or async for each?
Reveal the answer
Price: synchronous request (gRPC or REST) because the answer is needed to continue. Loyalty points: asynchronous event (OrderPlaced) because checkout should not wait for or fail because of the loyalty service.
- Synchronous (REST, gRPC) when the caller needs the answer to proceed. Always set timeouts, limit retries, and add circuit breakers (Module 15).
- Asynchronous (events, queues) for side effects, fan-out, and long work. Accept eventual consistency.
- Keep chains short: fan out in parallel instead of in series, and cache data owned by other services where staleness is acceptable.
- Event-carried state transfer: events include the data consumers need (
OrderPlacedwith items and address), so consumers do not call back for it.
14.4Service discovery
Payment service instances come and go with autoscaling. How does the orders service always reach a live instance?
Reveal the answer
Instances register (or are registered by the platform) in a registry with health checks. Either the caller asks the registry and load-balances itself (client-side discovery), or it calls a stable name or virtual IP and a load balancer routes to a healthy instance (server-side discovery, as in Kubernetes Services).
| Client-side | Server-side | |
|---|---|---|
| Who balances | Caller's library | Load balancer or platform proxy |
| Pros | No extra hop; smart balancing per call | Callers stay simple; any language |
| Cons | Library per language; registry coupling | Extra hop; LB must be highly available |
| Examples | Eureka with a client library, Consul with gRPC client balancing | Kubernetes Services, AWS ALB, service mesh |
14.5Gateways, BFFs, and service meshes
Forty services each implement mTLS, retries, and request metrics differently. Where can this logic live instead, without rewriting every service?
Reveal the answer
In a service mesh: a sidecar proxy beside each service handles mTLS, retries, timeouts, and telemetry, configured centrally by a control plane. North-south traffic (from outside) is handled by the API gateway; east-west traffic (between services) by the mesh.
- API gateway: the front door for external clients (auth, rate limits, routing; Module 5).
- Backend for frontend (BFF): a gateway per client type that aggregates and shapes data for that client.
- Service mesh: sidecars (or node-level proxies) for service-to-service traffic. Strong for mTLS everywhere, canary traffic splits, and uniform metrics. Costs extra latency per hop, resource overhead, and operational complexity; adopt it when the service count justifies it.
14.6Data across services
The "order details" page needs order data (Orders), customer name (Customers), and shipment status (Shipping). How do you build it without a cross-service join?
Reveal the answer
Options: (1) API composition: a BFF calls all three in parallel and merges. (2) A read model: a service consumes events from all three and maintains a denormalized "order details" view (CQRS, Module 13). (3) Copy needed fields into the order at creation (customer name), accepting staleness. Use (1) for low traffic, (2) for heavy or complex reads.
- Every piece of data has one owner; others hold copies derived from the owner's events.
- Reports and analytics read from a warehouse fed by all services, not from service databases.
- Cross-service invariants ("an order references a real customer") are enforced by the owning flows and sagas, not by foreign keys.
14.7Migrating safely: the strangler fig
You must move a 10-year-old monolith to services without a big-bang rewrite. What is the first step?
Reveal the answer
Put a routing layer (gateway or proxy) in front of the monolith so you can redirect individual routes. Then extract one well-bounded capability at a time into a new service, route its traffic there, and repeat. The monolith shrinks until it can be retired. At every step the system is working and changes can be rolled back by routing.
- Extract the data too: a service that still reads the monolith's tables is not independent. Use CDC to sync data during the move, then switch ownership.
- Run in parallel (shadow traffic, compare outputs) before switching critical paths.
- Serverless functions (AWS Lambda, Cloud Functions) fit event-driven glue and spiky, short tasks: scale to zero and no servers to manage, with cold starts, execution time limits, and per-invocation cost at high volume.
Most real migrations to services are slow strangler projects: a gateway routes paths, CDC keeps old and new data stores in sync during the move, and teams run old and new code paths in parallel (shadow traffic) before switching. Plan for months, not a weekend.
Do not open an interview with "I will use microservices". Draw the services the requirements need, show how they communicate (sync for answers, events for side effects), and mention data ownership. Boundaries with reasons beat a large box count.
14.8Common mistakes
Common mistakes
- Microservices by default for a small team or an unproven product.
- Shared databases between services.
- Splitting by technical layer instead of by business capability.
- Long synchronous call chains without timeouts and circuit breakers.
- No distributed tracing, so a slow request across 8 services cannot be explained (Module 16).
- Big-bang rewrites instead of incremental strangler migration.
14.9Self-check
- What is a bounded context?
Reveal
A part of the business with its own consistent model and language (Orders, Payments). It is a natural boundary for a service and its data.
- When should a call between services be asynchronous?
Reveal
When the caller does not need the result to continue: side effects, notifications, fan-out to many consumers, and slow work.
- What is the difference between an API gateway and a service mesh?
Reveal
The gateway handles north-south traffic (external clients to services): auth, rate limits, routing. The mesh handles east-west traffic (service to service): mTLS, retries, traffic shifting, telemetry.
- How does event-carried state transfer reduce coupling?
Reveal
Events carry the data consumers need, so consumers do not make synchronous callbacks to the producer and keep working when it is down.
- Why does the strangler fig pattern start with a routing layer?
Reveal
It lets you move one route at a time to new code, roll back by changing routing, and keep the system working throughout.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 15: Reliability, resilience, and rate limiting
Everything fails: machines, networks, dependencies, deploys. Reliable systems expect it and contain the damage. This module covers availability math and SLOs, timeouts and retries done right, circuit breakers and bulkheads, rate limiting algorithms, load shedding, safe deployments, and disaster recovery.
- 99.99% availability allows how much downtime per year?
Reveal
About 52.6 minutes a year (about 4.4 minutes a month).
- A dependency is slow, not down. Your service has no timeout on calls to it. What happens?
Reveal
Threads and connections wait indefinitely, pools run dry, and your service stops serving even requests that do not need the slow dependency. Slow is often worse than down.
- Why add random jitter to retry backoff?
Reveal
Without jitter, all clients that failed together retry together, creating synchronized waves of load. Jitter spreads the retries out.
- A fixed-window limiter allows 100 requests per minute. How can a client send 200 in 2 seconds?
Reveal
100 at the end of one window and 100 at the start of the next. Sliding windows or token buckets fix this.
- What are RPO and RTO?
Reveal
RPO (recovery point objective): how much data you can afford to lose, as time. RTO (recovery time objective): how long you can afford to be down.
15.1Availability, SLIs, SLOs, and error budgets
A request passes through a load balancer (99.99%), an app tier (99.9%), and a database (99.95%), in series. What is the best availability you can promise? How would you raise it?
Reveal the answer
0.9999 x 0.999 x 0.9995 is about 99.84%. To raise it, add redundancy to the weakest tiers (more app instances across zones, a standby database with automatic failover) and remove components from the critical path.
| Availability | Downtime per year | Per month |
|---|---|---|
| 99% | 3.65 days | 7.3 hours |
| 99.9% | 8.77 hours | 43.8 minutes |
| 99.95% | 4.38 hours | 21.9 minutes |
| 99.99% | 52.6 minutes | 4.4 minutes |
| 99.999% | 5.3 minutes | 26 seconds |
- Series (all must work): multiply availabilities. Every extra dependency lowers the total.
- Parallel (any one copy is enough):
1 - (1 - a)^n. Two independent 99% copies give 99.99%. - SLI (indicator): a measured number, for example the fraction of requests that succeed in under 300 ms.
- SLO (objective): the target for the SLI, for example 99.9% over 30 days. Internal.
- SLA (agreement): a contractual promise with penalties, set looser than the SLO.
- Error budget:
1 - SLO. With a 99.9% SLO you may "spend" 0.1% of requests on failures. When the budget is used up, slow down releases and fix reliability; when there is budget left, ship faster.
15.2How systems fail
Name four ways a dependency can fail other than "it is down".
Reveal the answer
It is slow (latency spikes), it returns errors for some requests, it returns wrong or partial data, it accepts requests but never answers (hangs), or it works for some callers but not others (a gray failure). Also: it is fine but your network path to it is not.
- Crash failures are the easy case: health checks catch them.
- Slow and gray failures are harder: the node looks alive but serves badly. Timeouts and outlier detection catch them.
- Overload turns into cascading failure: one slow service makes callers hold resources, so they slow down, and their callers too.
- Changes cause most incidents: deploys, configuration pushes, certificate expiry, schema migrations. Safe rollout matters as much as redundancy.
- Correlated failures break redundancy assumptions: the same bug on every replica, one zone's power, a shared dependency.
15.3Timeouts, retries, and backoff
The gateway, the orders service, and the database client each retry failed calls 3 times. The database starts failing. How many database calls does one user click cause?
Reveal the answer
Up to 3 x 3 x 3 = 27. Retries at every layer multiply load exactly when the system is weakest, turning a brief problem into an outage. Retry at one layer (usually closest to the failure or the edge), with backoff, jitter, and a retry budget.
Rules:
- Every remote call has a timeout, set from the dependency's latency (for example a bit above its p99), not a default of 30 s or infinity.
- Propagate deadlines: if the user's request has 800 ms left, downstream calls should not wait 2 s. gRPC deadlines do this.
- Retry only what is safe and likely to succeed: idempotent operations (or ones with idempotency keys) and transient errors (timeouts, 503, connection reset). Do not retry 400s.
- Exponential backoff with jitter: wait
random(0, min(cap, base x 2^attempt))between attempts. - Limit retries: a small number of attempts and a retry budget (for example, retries may add at most 10% extra load).
// TransientException: your own unchecked exception for retryable failures (timeouts, 503s)
static <T> T callWithRetry(Supplier<T> call, int maxAttempts) throws InterruptedException {
long baseMs = 100, capMs = 5_000;
for (int attempt = 0; ; attempt++) {
try {
return call.get(); // must be idempotent
} catch (TransientException e) { // timeouts, 503s: not 4xx
if (attempt + 1 >= maxAttempts) throw e;
long ceiling = Math.min(capMs, baseMs << attempt); // 100, 200, 400, ...
Thread.sleep(ThreadLocalRandom.current().nextLong(ceiling + 1));
}
}
}
15.4Circuit breakers, bulkheads, and fallbacks
The recommendations service is down. The product page calls it on every view and waits for the timeout (1 s) each time. What two things should change?
Reveal the answer
A circuit breaker should stop calling it after a failure threshold, failing fast instead of waiting 1 s per request. And the page should degrade gracefully: render without recommendations (or with a cached or default list) instead of failing.
- Circuit breaker: protects callers from waiting on a failing dependency and gives the dependency room to recover. Libraries: Resilience4j (Java), or the mesh (Envoy outlier detection).
- Bulkhead: isolate resources per dependency (separate thread pools, connection pools, or instances), so a slow dependency can only exhaust its own pool, not the whole service. Named after the watertight compartments of a ship.
- Fallbacks and graceful degradation: cached data, defaults, reduced features ("recommendations unavailable"), queued writes. Decide per feature which are critical and which are optional.
- Fail fast on the critical path: return a clear error quickly rather than hang.
15.5Rate limiting
You want to allow each API key 10 requests per second on average, but also allow short bursts of up to 50. Which algorithm, and what are its two parameters?
Reveal the answer
A token bucket with refill rate 10 tokens per second and bucket size 50. Each request spends a token; an idle client accumulates up to 50 tokens for a burst; the long-run average is capped at 10 per second.
| Algorithm | How it works | Strengths | Weaknesses |
|---|---|---|---|
| Token bucket | Tokens refill at rate r up to size b; each request takes one | Allows bursts up to b, average r; simple state (tokens, last refill time) | Bursts may be unwanted downstream |
| Leaky bucket | Requests enter a queue that drains at a fixed rate | Smooth, constant output rate | Adds queueing delay; bursts wait or drop |
| Fixed window counter | Count per calendar window (per minute) | Very cheap | Up to 2x the limit across a window boundary |
| Sliding window log | Keep timestamps of recent requests; count those in the last window | Exact | Memory per request |
| Sliding window counter | Weighted mix: prev_count x overlap + curr_count |
Close to exact, cheap | Approximate |
Where and how to enforce limits:
- At the edge (API gateway, CDN, WAF) for abuse and per-client quotas; inside services to protect themselves and their dependencies.
- Key by API key, user, IP, or tenant, often several at once (per user and global).
- Distributed counters live in Redis; do the read-check-update atomically with a Lua script, or
INCRandEXPIRE key 60 NXinside oneMULTItransaction; as two separate commands, a crash between them leaves a counter that never expires (Case study 2 designs this end to end). - Respond with
429 Too Many RequestsandRetry-After; tell clients their remaining quota. - Decide failure behavior: if the limiter's Redis is down, fail open (allow traffic, risk overload) or fail closed (reject, risk an outage). Most public APIs fail open with a local fallback limit.
15.6Load shedding and backpressure
At 3x normal load, your service accepts every request, queues them, and every response takes 20 s, so all users time out. What would serve more users?
Reveal the answer
Reject excess requests early and fast (503 or 429) so the requests you accept finish within their deadline. Serving 60% of users well beats serving 100% of users badly. Shed the least important traffic first (prefetches, analytics, background sync) and keep checkout and login.
- Load shedding: drop work when over capacity, ideally lowest priority first, before it consumes resources.
- Bounded queues: an unbounded queue only hides overload until memory or latency explodes.
- Adaptive concurrency limits: limit in-flight requests based on observed latency, rather than a fixed number.
- Backpressure: slow producers down when consumers fall behind (blocking, credits, or lag-based throttling), instead of buffering forever.
- Waiting rooms: for known spikes (ticket sales), admit users at a controlled rate (Case study 12).
15.7Safe deployments
A new release has a bug that fails 2% of checkouts. Which deployment strategy would have limited the blast radius, and how would you have detected it?
Reveal the answer
A canary release: send 1-5% of traffic to the new version, compare its error rate and latency with the old version, and roll back automatically when the canary is worse. Only a small share of users would have seen the bug, for minutes.
| Strategy | How | Rollback | Cost |
|---|---|---|---|
| Rolling | Replace instances in batches | Roll forward or back in batches | Low; mixed versions run together |
| Blue-green | Bring up a full new environment, switch traffic at once | Switch back instantly | Double capacity during the switch |
| Canary | Small share of traffic first, then grow | Route traffic back | Needs good metrics and automation |
| Feature flags | Ship code dark, turn features on per user or percentage | Turn the flag off | Flag debt; config must be managed carefully |
- Database migrations use expand and contract: add the new column or table (expand), deploy code that writes both and reads the new, backfill, switch reads, then remove the old (contract). Every step works with both code versions.
- Configuration is code: review and canary config changes too; a bad config push has caused many large outages.
15.8Disaster recovery and multi-region
A payment system's region goes down. The business says: lose at most 1 second of confirmed payments, be back within 15 minutes. Which DR strategy fits, and what does the data layer need?
Reveal the answer
At least warm standby, likely active-passive with automatic failover: a second region running a scaled-down copy, with data replicated nearly synchronously (RPO about 1 s needs synchronous or very low-lag replication of the ledger). Failover must be automated and rehearsed to meet a 15-minute RTO. Active-active would need careful handling of concurrent writes to the same accounts.
- Backups: automated, encrypted, stored in another region or account, and regularly restored in tests. An untested backup is a hope, not a backup.
- Point-in-time recovery: base backups plus write-ahead log archives let you restore to just before a bad migration or deletion.
- Active-passive: one region serves; the other receives replicated data and takes over on failure. Simple data model; failover takes time and may lose async-replicated writes.
- Active-active: both regions serve. Lowest RTO; needs conflict-free data design (partition users by home region, or a globally consistent database such as Spanner, at a latency cost).
- Practice: run failover drills (game days) and chaos experiments; recovery that is never exercised fails when needed.
A practical checklist for every new dependency call in a service: timeout set from the dependency's p99, retries only for idempotent calls with backoff and jitter, a circuit breaker, a bulkhead (separate pool), a fallback, and a metric plus alert on its error rate. Most production incidents trace back to one of these missing.
When asked "what if X fails?", answer in three parts: how you detect it (health checks, timeouts, metrics), how the system keeps working (redundancy, failover, degradation), and how it recovers (retries, replay, reconciliation).
15.9Common mistakes
Common mistakes
- No timeouts, or library defaults of minutes.
- Retries at every layer, without backoff, jitter, or budgets.
- Retrying non-idempotent calls without idempotency keys.
- Health checks that hide gray failures (always 200) or that take down the fleet when a shared dependency blips.
- Fixed-window rate limits where bursts at boundaries matter.
- Unbounded queues instead of load shedding.
- Untested backups and never-rehearsed failovers.
- Claiming 99.99% for a design with a single database and no failover.
15.10Self-check
- Two independent replicas, each 99.5% available, behind a perfect load balancer. Combined availability?
Reveal
1 - 0.005^2 = 99.9975%, assuming independent failures and instant failover.
- What is an error budget used for?
Reveal
To balance speed and reliability: while budget remains, teams can ship and take risks; when it is spent, they prioritize reliability work over features.
- What does the half-open state of a circuit breaker do?
Reveal
After the cooldown, it lets a limited number of test calls through. Success closes the breaker; failure opens it again.
- Token bucket vs leaky bucket: which smooths output to a constant rate?
Reveal
Leaky bucket. Token bucket allows bursts up to the bucket size.
- Why is "expand and contract" needed for schema changes during rolling deploys?
Reveal
Old and new code run at the same time, so every intermediate schema must work with both versions; expand first, contract only after all code uses the new shape.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 16: Observability and security
You cannot run what you cannot see, and you cannot trust what you have not secured. This module covers metrics, logs, and traces, why percentiles matter more than averages, alerting on symptoms, and the security building blocks a design must name: authentication, authorization, tokens, OAuth, mTLS, encryption, and secrets.
- Average latency is 80 ms, but users complain. What metric should you look at?
Reveal
High percentiles (p95, p99). The average hides the slow tail that a real share of users hit.
- A request touches 8 services and is slow. Which tool tells you where the time went?
Reveal
Distributed tracing: one trace ID across all services, with a timed span per operation.
- 401 vs 403?
Reveal
401 Unauthorized means not authenticated (no or invalid credentials). 403 Forbidden means authenticated but not allowed to do this.
- A JWT has been stolen. Why is that worse than a stolen session ID, and what limits the damage?
Reveal
A JWT is valid until it expires and services verify it without a lookup, so you cannot simply delete it. Keep access tokens short-lived (minutes), use refresh tokens that can be revoked, and keep a denylist for emergencies.
- Where should a database password live?
Reveal
In a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager), injected at runtime with access control and rotation. Never in code, images, or plain config files.
16.1Metrics, logs, and traces
For each question, pick metrics, logs, or traces: (a) Is the error rate up right now? (b) Why did order 4471 fail? (c) Which service added 300 ms to checkout?
Reveal the answer
(a) Metrics: cheap aggregated numbers over time, ideal for dashboards and alerts. (b) Logs: detailed events for a specific request, found by order ID or request ID. (c) Traces: the timing of each hop for one request.
| Signal | What it is | Answers | Watch out |
|---|---|---|---|
| Metrics | Numbers aggregated over time (counters, gauges, histograms) | Is something wrong? How much? Trends | High-cardinality labels (user ID) explode cost |
| Logs | Timestamped records of events | What exactly happened to this request? | Volume and cost; unstructured text is hard to query |
| Traces | A tree of timed spans for one request across services | Where did the time go? Which dependency failed? | Usually sampled; needs context propagation |
- RED for services: Rate (requests/s), Errors, Duration (latency distribution).
- USE for resources: Utilization, Saturation (queueing), Errors.
- Structured logs (JSON) with a request ID / trace ID on every line, propagated through every hop, including message headers.
- OpenTelemetry is the standard way to emit all three signals; backends include Prometheus and Grafana, Datadog, Jaeger, and Cloud Monitoring.
16.2Percentiles and tail latency
A page calls 100 backend shards in parallel and waits for all. Each shard is slow (above its p99) 1% of the time. What fraction of page loads is slow?
Reveal the answer
1 - 0.99^100, about 63%. With fan-out, a backend's rare slow case becomes the page's common case. That is why large systems track p99 and p99.9 and use techniques like hedged requests.
- Report percentiles, not averages: p50 (typical), p95, p99 (tail), p99.9 for high-volume paths. Compute them from histograms; averaging percentiles across servers is mathematically wrong.
- Tail causes: garbage collection pauses, queueing under load, cache misses, noisy neighbors, retries, cold starts.
- Tail fixes: reduce fan-out, set tight timeouts, hedged requests (send a second copy to another replica if the first has not answered by the p95 time, use whichever returns first), and keep utilization below the point where queues grow.
16.3Alerting and on-call
An alert fires whenever any server's CPU exceeds 80%. It wakes the on-call engineer 5 times a week, and users are usually fine. What should you alert on instead?
Reveal the answer
Symptoms users feel: error rate and latency against the SLO (for example, "error budget burning 10x faster than allowed over the last hour"). CPU belongs on a dashboard for diagnosis. Every page should be urgent, actionable, and tied to user impact.
- Page on symptoms (SLO burn rate, error rate, latency, queue lag past a threshold); ticket on causes (disk filling slowly, certificate expiring in 14 days).
- Every alert has a runbook: what it means, how to check, how to mitigate.
- Dashboards per service: RED metrics, dependencies, saturation, recent deploys.
- Blameless postmortems after incidents: timeline, root causes, and action items that remove the failure class.
On-call for a data pipeline adds signals beyond request metrics: consumer lag, records per minute against the expected baseline, age of the newest processed record (freshness), dead-letter queue size, and reconciliation mismatches. "Zero errors" can still mean "zero records processed".
16.4Authentication and authorization
User 7 calls GET /orders/9912. Order 9912 belongs to user 8. Authentication succeeded. What check is missing, and what is this bug called?
Reveal the answer
Authorization: checking that the authenticated user is allowed to read this specific resource (owner or permitted role). Skipping it is an insecure direct object reference (IDOR), one of the most common API vulnerabilities. Check ownership on every object access, server-side.
- Authentication (AuthN): who are you? Passwords plus MFA, SSO, OAuth/OIDC, API keys, client certificates.
- Authorization (AuthZ): what may you do? RBAC (roles such as admin, support), ABAC (attributes: department, region, time), relationship-based (owner, shared-with; Google Zanzibar style). Enforce on the server for every request and every object.
- Least privilege for users, services, and database accounts.
| Server-side session | Self-contained token (JWT) | |
|---|---|---|
| Client holds | Random session ID in a cookie | Signed token with claims (user, roles, expiry) |
| Per-request cost | Lookup in a session store (Redis) | Verify signature locally |
| Revocation | Delete the session: immediate | Wait for expiry, or keep a denylist |
| Fits | Web apps, one domain | APIs, mobile, many services verifying independently |
JWT rules: verify signature, exp, iss, and aud; prefer asymmetric signing (RS256 or ES256) so services verify with a public key; keep access tokens short-lived; never put secrets in the payload (it is only encoded, not encrypted); verify against an allow-list of algorithms (reject alg: none and HS/RS confusion); store browser tokens in HttpOnly, Secure, SameSite cookies rather than local storage, and since browsers send cookies automatically, protect state-changing requests against CSRF.
16.5OAuth 2.0 and OpenID Connect
A budgeting app wants to read your bank transactions. Why should you never give it your bank password, and what does OAuth give instead?
Reveal the answer
A password grants everything, forever, and you cannot revoke it from one app without changing it everywhere. OAuth gives the app a limited, revocable access token for specific scopes (read transactions only), issued by the bank's authorization server after you consent. The app never sees your password.
- OAuth 2.0 is delegated authorization: an app gets an access token with scopes to call an API on a user's behalf.
- OpenID Connect (OIDC) adds authentication on top: an ID token (a JWT) that says who the user is. "Sign in with Google" is OIDC.
- Flows: authorization code with PKCE for web, mobile, and single-page apps; client credentials for service-to-service calls with no user. The old implicit flow is deprecated.
16.6Service-to-service security
An attacker gets onto your internal network. In a "trusted internal network" design, what can they do, and what design prevents it?
Reveal the answer
They can call any internal service, since services accept requests from anywhere inside. Zero-trust design prevents it: every service authenticates every caller (mTLS with service identities, or signed service tokens) and authorizes it (orders may call payments; analytics may not), regardless of network location.
- mTLS: both sides present certificates, so each service knows the caller's identity and traffic is encrypted. A service mesh issues and rotates certificates automatically.
- Service tokens: client-credentials OAuth tokens or signed JWTs with the calling service's identity and scopes.
- Request signing (HMAC of method, path, body, and timestamp with a shared secret): common for partner and bank integrations and for webhooks; the timestamp and a nonce stop replays.
- Network policies still help as a second layer: deny by default, allow known paths.
16.7Protecting data
You must store card numbers to charge customers monthly. How do you minimize the risk and the compliance scope?
Reveal the answer
Do not store them yourself if possible: use the payment processor's vault and keep only its token. If you must, isolate a small tokenization service that stores encrypted card numbers (keys in an HSM or KMS), returns tokens to every other system, and is the only component in PCI DSS scope.
- In transit: TLS everywhere, including internal traffic for sensitive data.
- At rest: disk and database encryption, plus field-level encryption for the most sensitive fields.
- Envelope encryption: data is encrypted with a data key; the data key is encrypted with a master key held in KMS or an HSM. Rotating the master key does not require re-encrypting all data.
- Secrets management: Vault or a cloud secrets manager, short-lived credentials, automatic rotation, audit of every access.
- PII minimization: collect less, keep it shorter, mask it in logs, restrict who can see it, and support deletion requests.
- Tokenization: replace sensitive values (card numbers) with tokens; only the vault can map them back.
- Audit logs: append-only records of who accessed or changed sensitive data.
Design-level defenses interviewers like to hear: CDN and WAF in front for DDoS and common attacks; rate limits per user and IP; parameterized queries against injection; per-object authorization against IDOR; idempotency keys and timestamps against replays; least-privilege IAM roles instead of long-lived keys.
Close every design with one or two sentences on observability (the key metric and alert) and security (how callers authenticate and what data is protected). It takes 30 seconds and is a common gap in otherwise good answers.
Go deeperWeb security basics: CORS, CSRF, XSS, SSRF
- CORS: browsers block a page from reading responses from another origin unless the server allows it with
Access-Control-Allow-Origin. It protects users, not your API; it is not authentication. Never combine a wildcard origin with credentials. - CSRF: another site makes the browser send a request to your site with the user's cookies. Defend with
SameSitecookies, CSRF tokens on state-changing requests, and Origin checks. APIs that take tokens in anAuthorizationheader are not exposed this way. - XSS: attacker-supplied script runs inside your page. Escape all output by context, set a Content-Security-Policy, and keep session cookies
HttpOnlyso scripts cannot read them. - SSRF: your server fetches a URL the attacker controls (webhooks, link previews, image fetchers, crawlers) and is pointed at internal services or the cloud metadata endpoint (
169.254.169.254). Allow-list destinations, resolve DNS and block private and link-local ranges before connecting, route fetches through an egress proxy, and require the token-based metadata service (IMDSv2 on AWS).
16.8Common mistakes
Common mistakes
- Averages instead of percentiles in dashboards and SLOs.
- No trace or request ID propagated across services and queues.
- Alerting on causes (CPU, memory) instead of user-facing symptoms.
- Authenticating but not authorizing each object (IDOR).
- Long-lived JWTs with no revocation path; secrets in JWT payloads.
- Trusting the internal network instead of authenticating service calls.
- Secrets in code or config files, and sensitive data in logs.
16.9Self-check
- What are RED and USE?
Reveal
RED (rate, errors, duration) for request-driven services; USE (utilization, saturation, errors) for resources like CPU, disks, and pools.
- What is a hedged request?
Reveal
Sending a duplicate request to another replica if the first has not answered within a short delay (for example the p95), and using whichever answers first. It trims tail latency for a small amount of extra load.
- OAuth vs OIDC in one sentence each.
Reveal
OAuth 2.0 delegates authorization: an app gets a scoped token to call an API on a user's behalf. OIDC adds authentication: an ID token that states who the user is.
- Why is PKCE used even by apps that cannot keep a client secret?
Reveal
It binds the authorization code to a secret verifier created by the app for that login, so a stolen code cannot be exchanged for tokens by anyone else.
- What does envelope encryption make cheap?
Reveal
Key rotation and access control: the master key stays in KMS, only small data keys are re-wrapped, and every decrypt of a data key is authorized and audited by KMS.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 17: Building blocks toolkit
Many case studies hinge on one specialized component: unique IDs, a Bloom filter, a geospatial index, a search or vector index, a leaderboard, a delayed-job store. This module is the toolkit: what each block does, how it works inside, and when to reach for it.
- Why not use a database auto-increment ID as the public order ID in a sharded system?
Reveal
Each shard would generate its own sequence (collisions), a single global sequence is a bottleneck, and sequential IDs leak business volume. Use Snowflake-style or UUIDv7 IDs.
- A web crawler must check whether it has seen a URL among 10 billion. Exact set too big for memory. What structure?
Reveal
A Bloom filter: a few bits per URL, no false negatives, a small false positive rate (skipping a few unseen URLs is acceptable).
- Count unique daily visitors across 500M events with a few KB of memory?
Reveal
HyperLogLog: about 12 KB per counter in Redis with under 1% standard error.
- "Find restaurants within 2 km" on 50M places. What index?
Reveal
A geospatial index: geohash prefixes in a B-tree, a quadtree, S2 or H3 cells, or a database with spatial indexes (PostGIS, Elasticsearch geo queries).
- Real-time leaderboard of 10M players: top 100 and any player's rank. What do you use?
Reveal
A Redis sorted set:
ZADDto update scores,ZRANGE key 0 99 REVfor the top 100,ZREVRANKfor a player's rank, each O(log n).
17.1Unique ID generation
You need IDs that are unique across 200 servers, roughly sortable by time, fit in 64 bits, and need no network call per ID. Design the bits.
Reveal the answer
Snowflake: 1 unused sign bit, 41 bits of milliseconds since a custom epoch (about 69 years), 10 bits of machine ID (1,024 machines), 12 bits of per-millisecond sequence (4,096 IDs per ms per machine). Each machine generates IDs locally; uniqueness comes from the machine ID; sorting by ID approximately sorts by time.
| Option | Size | Sortable | Coordination | Notes |
|---|---|---|---|---|
| DB auto-increment | 64 bit | Yes | Single DB | Bottleneck and single point of failure; leaks counts |
| UUIDv4 | 128 bit | No | None | Random; scatters inserts across a B-tree index |
| UUIDv7 | 128 bit | Yes (ms) | None | Time-ordered prefix plus random bits (RFC 9562, 2024); index-friendly; PostgreSQL 18 has a built-in uuidv7() |
| Snowflake | 64 bit | Yes (ms) | Machine ID assignment | Compact; watch clock moving backwards |
| Ticket server / ranges | 64 bit | Roughly | A central counter hands out blocks (1,000 IDs at a time) | Simple; small gaps when a server restarts |
Snowflake trusts the clock. If NTP moves time backwards, a generator can repeat IDs; implementations wait until the clock passes the last timestamp, or refuse to generate.
17.2Probabilistic data structures
A Bloom filter says "maybe present" for a key. Can you trust it? What if it says "not present"?
Reveal the answer
"Not present" is always correct (no false negatives). "Maybe present" can be wrong (a false positive), so check the real store when it matters. That makes Bloom filters ideal as a cheap guard in front of expensive lookups.
| Structure | Answers | Memory | Error | Typical use |
|---|---|---|---|---|
| Bloom filter | Is x in the set? | About 10 bits per item for 1% false positives | False positives only | Skip disk lookups (LSM SSTables), crawler seen-URLs, block known-bad items, cache penetration guard |
| HyperLogLog | How many distinct items? | About 12 KB per counter (Redis) | About 0.8% standard error | Unique visitors, distinct queries |
| Count-min sketch | About how often did x occur? | Small fixed grid of counters | Overestimates only | Heavy hitters, top-K trending, rate of an IP |
- A standard Bloom filter cannot delete items (clearing a bit could erase others); counting Bloom filters or cuckoo filters support deletes.
- Count-min sketch: d hash functions each increment one counter in its own row; the estimate is the minimum across rows. Pair it with a min-heap to keep the current top K.
17.3Geospatial indexing
Why can't you answer "places within 2 km" with a normal B-tree index on (lat, lng)?
Reveal the answer
A B-tree sorts by one dimension first. An index on (lat, lng) can narrow latitude, but every place in that latitude band, around the whole globe, still matches. You need an index that keeps points close in both dimensions close in the index: geohash strings, quadtrees, space-filling curves (S2), or R-trees.
| Approach | How | Strengths | Weaknesses |
|---|---|---|---|
| Geohash | Base32 string of interleaved lat/lng bits; prefix = cell | Works with any key-value or B-tree store; easy sharding by prefix | Fixed cell sizes; edge effects need neighbor queries |
| Quadtree | Recursive split of dense cells | Adapts to density | Usually in memory; must be rebuilt or updated |
| S2 / H3 | Hierarchical cells on a sphere (squares / hexagons) | Even cells, good coverings, used at Google and Uber | More complex library |
| R-tree (PostGIS) | Bounding-box tree | Arbitrary shapes, exact queries in SQL | Single-database scale |
For moving objects (drivers updating every few seconds), keep current positions in memory, keyed by cell (for example Redis geo sets or an in-memory grid per city), not in a disk index that is rewritten constantly. Case study 11 builds this.
17.4Search and inverted indexes
Why is WHERE description LIKE '%refund%' slow on 100M rows, and what does a search engine do instead?
Reveal the answer
A leading wildcard cannot use a B-tree index, so the database scans every row. A search engine builds an inverted index at write time: each term maps to the list of documents containing it, so a query looks up a few posting lists and intersects them.
- Elasticsearch / OpenSearch: an index is split into primary shards (fixed at creation) with replicas; queries scatter to shards and gather the top results; relevance uses BM25 by default.
- Near real-time: new documents become searchable after a refresh (1 s by default), not instantly.
- Not the source of truth: write to the database first, then feed the search index through CDC or events; rebuild the index from the database when mappings change.
- Analyzers decide tokenization, lowercasing, stemming, and synonyms; they matter more for quality than hardware.
17.5Counters, leaderboards, and top-K
How do you show "views: 1.2M" on a viral video receiving 50,000 views per second without overloading one database row?
Reveal the answer
Do not update one row per view. Count in memory or in sharded Redis counters (video:42:views#0..#15), flush aggregated increments to the database every few seconds, and read the displayed number from a cache. Exact real-time precision is not needed for a view count.
- Counters: Redis
INCR(atomic), sharded counters for hot keys, periodic flush to durable storage. Approximate is usually fine for display. - Leaderboards: Redis sorted sets (
ZADD,ZINCRBY,ZRANGE ... REV,ZREVRANK). For hundreds of millions of players, shard by score range or keep exact ranks only for the top tier and approximate the rest. - Top-K / trending: exact counting per key in a stream processor for moderate cardinality; count-min sketch plus a heap for huge cardinality; windowed (last hour) using stream windows (Module 12, Case study 15).
17.6Delayed jobs and scheduling
Send a reminder exactly 24 hours after sign-up for 10 million users a day. How do you store and trigger these without scanning a huge table every second?
Reveal the answer
Store each job keyed by its due time: a Redis sorted set scored by due timestamp (workers pop entries with score <= now), or a database table indexed by (due_at, status) and partitioned into time buckets that workers claim with SKIP LOCKED. Workers only look at the current bucket. Jobs must be idempotent, because a worker can crash after sending.
- Delay queues: some brokers support short delays (SQS delay up to 15 minutes; RabbitMQ per-message TTL plus a dead-letter exchange); longer or exact delays need a scheduler store.
- Time-bucketed tables scale to billions of scheduled tasks; Case study 14 designs a distributed job scheduler with leases and retries.
17.7Push, pull, and fan-out
A user with 50M followers posts. Writing the post into 50M follower feeds takes minutes. What alternative serves their followers?
Reveal the answer
Do not fan out on write for that user. Store the post once; when a follower opens their feed, merge in posts from the few celebrities they follow at read time (fan-out on read). Normal users still use fan-out on write. This hybrid is the standard answer (Case study 4).
| Fan-out on write (push) | Fan-out on read (pull) | |
|---|---|---|
| Work happens | When content is created | When content is read |
| Read latency | Fast: precomputed | Slower: gather and merge |
| Write cost | Proportional to followers | Constant |
| Problem case | Celebrities | Users who follow many active accounts |
Presence (online status) is a related pattern: clients send heartbeats every N seconds; a key with a TTL (online:user7, expiring after 2N) marks them online; changes are pushed only to friends who are currently connected.
Many of these blocks are one command away in existing infrastructure: Redis has sorted sets, HyperLogLog, Bloom filters (module), and geo commands; PostgreSQL has PostGIS and partial indexes; Elasticsearch handles text and geo queries. Reach for the existing tool before building a custom index.
Use these blocks by name with their trade-off: "a Bloom filter to skip known URLs, accepting a 1% false positive rate", "Snowflake IDs so posts sort by time without a central counter". Naming the error or cost is what shows depth.
17.8Vector search and LLM-era building blocks
A support portal must match "my card was charged twice" to the help article "Duplicate transactions", which shares no words with it. Why does keyword search fail, and what replaces it?
Reveal the answer
Keyword search (BM25 over an inverted index) needs shared terms. Semantic search turns queries and documents into vectors with an embedding model, so similar meanings land close together, and finds the nearest vectors. Production systems usually combine both (hybrid search).
- Embeddings: a model maps text or images to vectors of hundreds to a few thousand numbers; similarity is cosine or dot product. Changing the model means re-embedding everything, so version indexes.
- Approximate nearest neighbor (ANN) indexes: exact search compares against every vector. HNSW (a layered proximity graph) is fast with high recall but memory-hungry; IVF clusters vectors and searches only nearby clusters, often with compression (product quantization). Both trade a little recall for large speedups.
- Where vectors live: pgvector inside PostgreSQL for small and medium corpora; OpenSearch or Elasticsearch k-NN alongside text search; dedicated vector databases (Milvus, Qdrant, Pinecone) or managed services at large scale.
- Hybrid ranking: run BM25 and vector search, merge the lists (for example reciprocal rank fusion), optionally rerank the top results with a stronger model. Apply access-control filters during retrieval so users never see chunks they may not read.
- Serving LLM features: rate limits and quotas counted in tokens, not requests; streaming responses over SSE; tight timeouts with fallbacks; caching repeated prompts; and cost tracking per tenant. Keep the index fresh from the source of truth through CDC, like any search index.
17.9Common mistakes
Common mistakes
- Auto-increment IDs across shards, or random UUIDv4 as a clustered primary key on a hot table.
- Treating a Bloom filter "maybe" as "yes".
- Exact distinct counts or exact view counts where approximate answers are fine and far cheaper.
- Lat/lng B-tree indexes for radius search.
- Search index as the source of truth.
- Scanning a whole jobs table to find due work.
- Fan-out on write for celebrity accounts.
17.10Self-check
- How many IDs per millisecond can one Snowflake worker generate, and what happens beyond that?
Reveal
4,096 (12 sequence bits). After that it waits for the next millisecond.
- Why does a geohash search query neighboring cells?
Reveal
Points close to the user can lie across a cell boundary and have a different prefix; checking the 8 neighbors covers them.
- Which probabilistic structure would you use for "top 10 hashtags in the last hour" at Twitter scale?
Reveal
A count-min sketch per window to estimate counts, with a min-heap of the current top K, or exact counting in a stream processor if the key space is manageable.
- How does a Redis sorted set give a player's rank quickly?
Reveal
It is ordered by score (a skip list plus a hash table), so
ZREVRANKfinds the position in O(log n). - Why must search indexes be fed from the database rather than written directly by the app alongside the DB?
Reveal
Two separate writes can diverge on failure (dual write). Feeding from the database's change stream keeps the index eventually consistent with the source of truth and rebuildable.
Mastery checklist
Tick an item only when it is true with your notes closed.
Case study 1: URL shortener
Turn long URLs into short codes and redirect fast. A classic first design: tiny writes, huge read volume, and one real puzzle, generating short unique codes at scale.
Design it yourself first: requirements with numbers, estimates, API, data model, a diagram of the write and read paths, and two deep dives. Then compare section by section.
Reveal the answer
A strong answer covers: a 7-character base62 code and why 7 is enough; a code generation scheme that cannot collide (counter ranges or a key service) or handles collisions (hash plus retry); a cache in front of a key-value store for redirects; 301 vs 302 and its effect on analytics; click analytics sent asynchronously; expiry, custom aliases, and abuse protection.
C1.1Requirements
Functional
- Create a short URL for a long URL; optional custom alias and expiry.
- Redirect a short URL to its long URL.
- Optional: click counts per URL.
- Out of scope: user accounts UI, link editing, QR codes.
Non-functional
- 100M new URLs per day; reads outnumber writes about 100 to 1.
- Redirect latency under 50 ms at p99 (excluding the user's network).
- 99.99% availability for redirects; creation can be slightly less available.
- Codes never collide and are not guessable in sequence; links last 5 years by default.
C1.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Write QPS | 100M / 86,400 | about 1,200/s, about 3,500/s at peak |
| Read QPS | 1,200 x 100 | about 120,000/s, about 350,000/s at peak |
| URLs over 5 years | 100M x 365 x 5 | about 180 billion |
| Storage | 180B x 500 bytes | about 90 TB before replication |
| Code length | 62^7 is about 3.5 trillion | 7 base62 characters are enough |
What it means: reads dominate, so the redirect path must be served mostly from cache. 90 TB and simple key lookups point to a horizontally scalable key-value store (DynamoDB, Cassandra) or sharded SQL keyed by code.
C1.3API
POST /v1/urls
{ "long_url": "https://...", "custom_alias": "sale24", "expires_at": "2027-01-01" }
-> 201 { "code": "aZ3k9Qx", "short_url": "https://sho.rt/aZ3k9Qx" }
-> 409 if the alias is taken, 400 for an invalid URL, 429 if rate limited
GET /{code}
-> 302 Location: https://... (or 301, see deep dives)
-> 404 unknown code, 410 expired
C1.4Data model
| Field | Type | Notes |
|---|---|---|
code |
string, primary key | 7 characters; the partition key |
long_url |
string | up to about 2 KB |
owner_id |
ID, nullable | for rate limits and management |
created_at, expires_at |
timestamps | expires_at can drive a store-level TTL |
One access pattern dominates: get by code. A key-value store partitioned by code spreads load evenly with no hot shards except viral links, which the cache absorbs.
C1.5High-level design
- Shorten service takes the next ID from a block it leased, encodes it in base62, and writes
code -> long_urlwith a conditional insert. - Redirect service reads the cache, falls back to the store on a miss, fills the cache, and returns the redirect. It emits a click event asynchronously.
- Separate read and write services so the 100x larger read path scales and deploys on its own.
C1.6Deep dives
Guided designDeep divesAnswer each before revealing
- 1How do you generate the code?
Compare hashing the URL, a counter in base62, and a pre-generated key service.
Write your answer, then reveal
Approach How Pros Cons Hash + truncate First 7 chars of base62(SHA-256(long URL)) Same URL gets same code; stateless Collisions: must check and retry with a salt; extra read per write Counter + base62 Unique number from a counter, encoded in base62 Never collides; no read before write Needs distributed counter; sequential codes are guessable Key generation service Pre-generate random unused codes; servers take batches Random, no collisions, fast Extra service and storage; keys in a batch lost on a crash (acceptable) A solid choice: each shorten server leases a block of IDs (for example 1M) from a small strongly consistent store, so there is no per-request coordination. To avoid guessable sequential codes, pass the number through a reversible scramble (a fixed bijective permutation or format-preserving encryption) that maps the range [0, 62^7) onto itself, so codes stay 7 characters (a full 64-bit number can need 11 base62 characters).
- 2301 or 302?
What changes for users, caches, and analytics?
Write your answer, then reveal
301 Moved Permanently lets browsers and proxies cache the redirect by default, so repeat clicks may never reach you: less load, but you lose click counts and cannot reliably change or expire the target. 302 Found (or 307) is not cached by default, so every click hits your service: accurate analytics and control, more load. In practice the
Cache-Controlheader decides caching as much as the code does: either code can work withCache-Control: private, max-age=...(Bitly, for example, uses 301). 302 or 307 is the safe default when the target can change. - 3How do you serve 350K redirects per second at p99 under 50 ms?
Which layers absorb the reads?
Write your answer, then reveal
A Redis cluster in front of the store with LRU eviction; hot links have hit ratios near 100%. Size the cache from links clicked per day, not from all 180B links: even 100M active links x 500 bytes is about 50 GB, which a small Redis cluster holds. Redirect servers can also keep a small in-process cache for the very hottest codes. The store handles only misses, as single-key lookups.
- 4How do you count clicks without slowing redirects?
Where do click events go?
Write your answer, then reveal
The redirect service publishes a small event (code, time, referrer, country) to Kafka without waiting on it, and a stream job aggregates counts per code per minute into an analytics store. Losing a tiny fraction of events in a crash is acceptable for analytics; the redirect itself must never fail because analytics is down.
- 5Custom aliases and duplicate long URLs
How do you prevent two users from claiming the same alias? Should the same long URL return the same code?
Write your answer, then reveal
Write with a conditional put (insert only if absent); the loser gets 409. Use a separate namespace or minimum length for aliases so they never collide with generated codes. For duplicates, most services create a new code per request (different owners, expiries, and analytics); deduplicating needs an index on a hash of the long URL and leaks whether someone else shortened it.
- 6Expiry and abuse
How do you remove expired links and stop abuse?
Write your answer, then reveal
Store
expires_at; the store's TTL feature deletes rows, and the redirect service checks expiry on read (return 410) in case deletion lags. Rate-limit creation per user and IP, check long URLs against malware and phishing lists asynchronously (disable the code if flagged), and never use sequential codes that allow enumerating everyone's links.
C1.7Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| A link goes viral | One key gets most traffic | Cache hit near 100%; in-process cache; CDN caching with a short TTL |
| Cache cluster down | All reads hit the store | Store sized for it with headroom, or rate-limited fallback; replicas for the cache |
| ID range store down | New blocks cannot be leased | Servers hold large blocks, so creation continues for hours |
| Analytics pipeline down | Lost or delayed counts | Redirects unaffected; Kafka retains events for replay |
| Region outage | Redirects fail | Multi-region read replicas of the store and caches; GeoDNS |
C1.8What a strong answer shows
L4 (SDE2): correct estimates and code length, a collision-free generation scheme with its trade-offs, cache in front of a partitioned key-value store, 301 vs 302 explained, async analytics. L5 adds: multi-region active-active reads, hot-key handling, abuse and security (scanning, enumeration), data retention and cost, and a clear story for what happens when each component fails.
C1.9Follow-up questions
- How many characters do you need for 1 trillion URLs?
Reveal
62^7 is about 3.5 trillion, so 7 base62 characters. 6 characters give about 57 billion, not enough.
- Why not use the database's auto-increment ID?
Reveal
A single sequence is a write bottleneck and single point of failure, and sequential codes expose volume and allow enumeration. Leased ranges plus scrambling keep uniqueness without those problems.
- A viral link's cache entry expires and 50,000 requests miss at once. What protects the URL store?
Reveal
Request coalescing (single-flight) so one request reloads the key while the others wait, refreshing hot keys before they expire, and jittered TTLs so many keys do not expire together (Module 6).
- How would you support editing the destination of a link?
Reveal
Use 302 so clients do not cache the old target, update the row, and delete the cache entry (cache-aside delete on write).
- How do you make redirects fast for users in India and the US?
Reveal
Deploy redirect services and caches in both regions, replicate the URL store to both (reads are local; writes can go to a home region or a multi-region store), and use GeoDNS to route users to the nearest region.
Case study 2: Distributed rate limiter
Limit how many requests each client can make, across dozens of gateway instances, adding only a millisecond or two. The interesting parts are the algorithm, atomic shared state, placement, and what happens when the limiter itself fails.
Design a rate limiter for a public API: requirements, algorithm choice, where it runs, how instances share state, the response to clients, and failure behavior.
Reveal the answer
A strong answer covers: rules per key (API key, user, IP) and per window; token bucket or sliding window with reasons; enforcement in the gateway; shared state in Redis updated atomically with a Lua script; 429 with Retry-After and quota headers; fail-open with a local fallback; rule configuration pushed to gateways; and a note on multi-region accuracy.
C2.1Requirements
Functional
- Limit requests per client key per rule, for example 100 per minute per API key, 10 per second per IP for login.
- Several rules can apply to one request (per second and per day; per user and global).
- Rejected requests get
429 Too Many Requestswith retry information. - Rules change without redeploying.
Non-functional
- 1M requests/s across all gateways; 10M active keys.
- Adds under 2 ms at p99.
- The limiter must not take the API down: if it fails, traffic flows (fail open) with a rough local limit.
- Approximate accuracy is acceptable (a few percent over the limit briefly).
C2.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Limiter checks | One per rule per request | 1-3M Redis operations/s |
| State per key | Tokens + timestamp in a hash, about 50-100 bytes | 10M keys x 2 rules x 100 B = about 2 GB |
| Redis cluster | About 100K-plus ops/s per shard | 10-30 shards with replicas |
C2.3Where it runs
| Placement | Pros | Cons |
|---|---|---|
| Client side | No server cost | Untrusted; clients can ignore it |
| API gateway / middleware | One place for all services; rejects before backends spend work | Gateway needs shared state |
| Sidecar / service mesh | Per-service limits without code | Configuration spread out |
| Inside each service | Protects that service's own resources | Duplicated logic |
Put user-facing quotas in the gateway; let individual services also protect themselves (concurrency limits, load shedding).
C2.4Algorithm and atomic state
Token bucket per key: allows short bursts up to the bucket size and an average rate equal to the refill rate (Module 15 compares the alternatives). The read-refill-take-write sequence must be atomic, or two gateways can both see the last token and both allow. A Lua script runs atomically in Redis:
-- KEYS[1] = bucket key, ARGV = capacity, refill per second, cost
local cap, rate, cost = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
local t = redis.call('TIME') -- server clock: no client skew
local now = tonumber(t[1]) + tonumber(t[2]) / 1e6
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(b[1]) or cap
local ts = tonumber(b[2]) or now
tokens = math.min(cap, tokens + math.max(0, now - ts) * rate) -- refill; clamp if clock moved back
local allowed = 0
if tokens >= cost then tokens = tokens - cost; allowed = 1 end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', KEYS[1], math.ceil(cap / rate) * 2) -- idle keys disappear
return { allowed, math.floor(tokens) }
For a simple fixed window, INCR key plus EXPIRE key 60 works only if both run atomically (one Lua script, or INCR plus EXPIRE key 60 NX inside MULTI on Redis 7+); otherwise a crash between them leaves a counter that never expires. A sliding window counter keeps two window counters and weights the previous one.
C2.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Why not keep counters in each gateway's memory?
What goes wrong with 40 gateway instances?
Write your answer, then reveal
Each instance sees only its share of traffic, so a client spread across 40 instances gets up to 40x the limit. Options: shared counters in Redis (accurate, one network hop), routing each key to a fixed gateway by consistent hashing (local counters work, but uneven load), or local counters with a per-instance share of the limit (limit / N, inaccurate when traffic is uneven). Shared Redis is the usual answer; local counters work as a fallback.
- 2Redis is down or slow. What does the gateway do?
Fail open or fail closed?
Write your answer, then reveal
Set a tight timeout (a few ms) on the limiter call. On failure, most public APIs fail open so a limiter outage does not become an API outage, but switch to a local in-memory limiter with a conservative per-instance limit to keep some protection. Security-critical limits (login attempts, OTP sends) may fail closed instead. Alert in both cases.
- 3How do you stop one huge customer's key from overloading one Redis shard?
Think hot keys.
Write your answer, then reveal
The key's bucket lives on one shard, but each request is one cheap script call, so even 50K/s for one key is usually fine. Beyond that: check a local short-lived "this key is over limit" flag before calling Redis (rejected clients cost nothing), or split the key's limit across N sub-buckets on different shards.
- 4Global limits across regions
A customer's limit is 1,000/s worldwide and they call from 3 regions.
Write your answer, then reveal
Exact global counting needs cross-region round trips on every request, which is too slow. Common answers: give each region a share of the limit (static or adjusted every few seconds from usage), or count locally and sync totals asynchronously, accepting brief overshoot. Say that strict global limits trade latency for accuracy.
- 5What does the client see?
Design the response.
Write your answer, then reveal
429 Too Many RequestswithRetry-After(seconds), plus quota headers such asX-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset(an IETF draft standardizesRateLimitheaders). Document limits; clients should back off with jitter. - 6How are rules managed?
Where do they live and how do gateways learn about changes?
Write your answer, then reveal
Rules (key type, limit, window, burst, scope) live in a config store or database, owned by a small admin service. Gateways cache them in memory and refresh on a push (watch on etcd, a pub/sub message) or poll every few seconds. Rule changes are versioned and can be rolled out gradually.
C2.6Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Redis shard failure | Limiter calls for its keys fail | Replicas with automatic failover; fail open to local limits meanwhile |
| Redis latency spike | Every request slows | Tight timeout, fail open, local over-limit cache |
| Bad rule pushed | Legitimate traffic rejected | Versioned rules, gradual rollout, quick rollback, dry-run mode that logs instead of rejecting |
| Clock differences between gateways | Inconsistent refill | Use Redis server time inside the script |
| Many users behind one IP (carrier NAT, offices) | Legitimate users blocked together | Limit by API key or user first; keep IP limits generous; for IPv6, limit by /64 prefix since one user can rotate addresses |
C2.7What a strong answer shows
L4 (SDE2): the algorithm and its burst behavior, atomic updates with Lua, gateway placement, correct 429 response, fail-open decision with reasons. L5 adds: multi-region limits, hot-key protection, rule management and safe rollout, a dry-run mode, and limits per tenant tier as a product feature.
C2.8Follow-up questions
- Why is
GET count; if count < limit then INCRwrong across gateways?Reveal
Two gateways can read the same count and both increment past the limit. The check and update must be one atomic operation (
INCRthen compare, or a Lua script). - Token bucket or sliding window log for a login endpoint allowing 5 attempts per 15 minutes per account?
Reveal
A sliding window (log or counter): the limit is about attempts in any 15-minute span, and bursts are exactly what you want to block. Volume is low, so the log's memory cost is fine.
- How do you limit concurrent requests rather than requests per second?
Reveal
A counter of in-flight requests per key: increment on start, decrement on finish (with a TTL or lease so crashed requests do not leak), reject when at the maximum.
- How would you test the limiter before enforcing a new rule?
Reveal
Shadow or dry-run mode: evaluate the rule and log or emit metrics on what would have been rejected, then enforce once the numbers look right.
Case study 3: Distributed key-value store
Design a key-value store like Dynamo or Cassandra: always writable, horizontally scalable, tunable consistency. This case study ties together partitioning, replication, quorums, failure detection, repair, and storage engines.
Design put(key, value) and get(key) for a store holding 100 TB with 500K ops/s that must stay writable when nodes fail. Cover partitioning, replication, consistency, failure handling, and the storage engine on each node.
Reveal the answer
A strong answer covers: consistent hashing with virtual nodes; N replicas on distinct nodes and zones; tunable N, R, W quorums; versioning and conflict resolution (vector clocks or last-write-wins); gossip-based membership and failure detection; hinted handoff for short outages and Merkle-tree anti-entropy for long ones; read repair; and an LSM-tree storage engine with commit log, memtable, SSTables, and Bloom filters.
C3.1Requirements
Functional: put(key, value), get(key), delete(key). Values up to about 1 MB, typically a few KB.
Non-functional
- 100 TB of data, 500K operations/s, growing.
- Always writable: accept writes during node failures and partitions (AP), with tunable consistency per request.
- Single-digit millisecond latency at p99 within a region.
- Add and remove nodes without downtime; no single point of failure.
C3.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Raw data with 3 replicas | 100 TB x 3 | 300 TB |
| Nodes for storage | 300 TB / about 4 TB usable per node | about 75-100 nodes |
| Load per node | 500K x 3 replicas / 100 nodes | about 15K ops/s per node, comfortable for LSM storage |
C3.3API and data model
get(key) -> list of (value, version) # several values if concurrent writes conflicted
put(key, value, ctx) -> ok # ctx = version the client last read
delete(key, ctx) -> ok # writes a tombstone
consistency per call: ONE | QUORUM | ALL
The store treats values as opaque bytes. A version (vector clock, or a timestamp for last-write-wins) travels with each value.
C3.4High-level design
- Partitioning: consistent hashing with virtual nodes (Module 10). Each node announces its tokens; any node can compute where a key lives.
- Replication: the preference list for a key is the next N distinct physical nodes clockwise, placed in different racks or zones.
- Coordinator: the node a client contacts (or a partition-aware client) forwards to the replicas and waits for W acks or R replies.
- No leader per key: any replica accepts writes, which keeps writes available during failures.
C3.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Consistency choices
Pick N, R, W for (a) a shopping cart that must always accept writes and (b) a user profile that should rarely be stale.
Write your answer, then reveal
N = 3 for both. (a) W = 1, R = 1, or W = 2 with sloppy quorums: writes almost never fail; conflicts are merged later. (b) W = 2, R = 2 (QUORUM): R + W > N means reads overlap the latest write unless concurrent writes or sloppy quorums intervene. The API lets each request choose.
- 2Conflicting writes
Two clients write the same key on different replicas during a partition. What does the store return later?
Write your answer, then reveal
With last-write-wins (Cassandra), the higher timestamp wins and the other write is silently lost; simple but depends on clocks. With vector clocks (Dynamo, Riak), the store detects that neither version descends from the other and returns both ("siblings"); the application merges them (union of carts) and writes back the merged value with a new clock.
- 3Detecting failures
How do nodes learn that a node is down, without a central master?
Write your answer, then reveal
Gossip: every second each node exchanges membership and heartbeat counters with a few random peers, so state spreads through the cluster in a few rounds. A node whose heartbeat stops advancing is suspected down (Cassandra uses an adaptive accrual failure detector). Suspicion only affects routing; data stays assigned until an operator or policy removes the node permanently.
- 4Temporary failures
Replica D is down for 2 minutes. How do writes for its keys still reach W = 2?
Write your answer, then reveal
Sloppy quorum and hinted handoff: the coordinator writes to the next healthy node on the ring (F) instead, with a hint "this belongs to D". When D returns, F sends the hinted writes to D and deletes them. Writes stay available; the overlap guarantee is weaker during the outage.
- 5Permanent divergence
A replica was down for a day or lost writes. How do replicas converge without copying everything?
Write your answer, then reveal
Anti-entropy with Merkle trees: each replica keeps a hash tree over its key ranges; two replicas compare roots and descend only into subtrees whose hashes differ, then exchange just those keys. Read repair fixes stale replicas noticed during reads.
- 6Storage engine
What does each node do with a write?
Write your answer, then reveal
Append to a commit log (durability), insert into an in-memory memtable, acknowledge. When the memtable fills, flush it as an immutable sorted SSTable; background compaction merges SSTables and drops overwritten values and expired tombstones. Reads check the memtable, then SSTables newest first, skipping files with per-file Bloom filters.
- 7Deletes
Why can't a delete just remove the key from one replica?
Write your answer, then reveal
A replica that missed the delete would later "resurrect" the value through repair. Deletes write a tombstone that replicates like a value and is removed only after a grace period longer than any expected outage.
- 8Adding a node
How does a new node join without downtime?
Write your answer, then reveal
It takes tokens on the ring, and existing replicas stream the affected ranges to it in the background (throttled). While streaming runs, writes go to the old owners and the joining node, but reads stay on the old owners, since the new node does not have all the data yet; when streaming completes, ownership switches.
C3.6Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Node down briefly | Its replicas miss writes | Sloppy quorum, hinted handoff, read repair |
| Node lost | Replication factor drops for its ranges | Rebuild from other replicas; anti-entropy |
| Zone outage | Up to one replica per key lost if placed well | Place replicas across zones |
| Hot key | One replica set overloaded | Client-side caching, key splitting, more replicas for reads |
| Compaction backlog | Read latency rises, disk fills | Throttle writes, tune compaction strategy, add nodes |
C3.7What a strong answer shows
L4 (SDE2): consistent hashing with virtual nodes, replication on distinct nodes, R + W > N and what it means, conflict handling, hinted handoff, and an LSM write path. L5 adds: precise trade-offs of sloppy quorums, tombstones and resurrection, gossip and failure detection details, compaction strategies, multi-datacenter replication, and operational concerns like streaming and rebalancing.
C3.8Follow-up questions
- Why use virtual nodes?
Reveal
They balance data across nodes, spread a failed node's load across many peers, and let bigger machines take more tokens.
- How is this different from a single-leader database like PostgreSQL with replicas?
Reveal
Any replica accepts writes, there is no failover step, and writes stay available in partitions. The cost: conflicts, weaker consistency, and no multi-key transactions.
- How would you support range queries?
Reveal
Hash partitioning destroys key order. Use compound keys (partition key hashed, clustering keys sorted inside the partition) for ranges within a partition, or range partitioning with dynamic splitting (Bigtable-style) for global ranges.
- Why might
QUORUMreads still return stale data?Reveal
A concurrent write may be in progress, a sloppy quorum may have stored the write on stand-in nodes, or a failed write may have reached fewer than W replicas but not been rolled back.
Case study 4: News feed
Show each user a feed of recent posts from people they follow, in under 200 ms, for hundreds of millions of users. The core decision is when to do the work of assembling feeds: when a post is written, or when a feed is read.
Design the home feed for a social app: posting, following, and reading a paginated feed. Include estimates, the fan-out strategy, storage for posts and feeds, and how celebrities are handled.
Reveal the answer
A strong answer covers: fan-out on write into per-user feed lists in a cache, fan-out on read for celebrity accounts (hybrid), a post store and follow graph store, hydration of post IDs into full posts from caches, cursor pagination, async fan-out workers via Kafka, feeds only for active users, and how deletes, privacy, and new follows behave.
C4.1Requirements
Functional
- Publish a post (text, images).
- Follow and unfollow users.
- Read a home feed of posts from followed users, newest first (ranking is an extension), paginated.
- Out of scope: comments, ads, the ranking model itself.
Non-functional
- 500M daily active users; feed load under 200 ms at p99.
- A new post appears in followers' feeds within about 5 seconds (eventual consistency is fine).
- Highly available reads; the author sees their own post immediately.
C4.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Posts | 500M x 0.2 per day = 100M/day | about 1,200/s |
| Feed reads | 500M x 10 per day = 5B/day | about 58K/s, about 150K/s at peak |
| Fan-out writes | 100M posts x 200 average followers | 20B feed inserts/day, about 230K/s |
| Feed cache | 500M users x 500 post IDs x 8 bytes | about 2 TB; keep only active users |
Reads are 50x posts, so precomputing feeds pays off. But the fan-out volume is large and skewed: a post by an account with 50M followers would be 50M inserts.
C4.3API
POST /v1/posts { "text": "...", "media_ids": [...] } -> 201 { "post_id": ... }
PUT /v1/users/{id}/followers/me -> 204 (follow, idempotent)
GET /v1/feed?cursor=...&limit=20 -> 200 { "posts": [...], "next_cursor": "..." }
C4.4Data model
| Store | Key | Contents |
|---|---|---|
Posts (sharded by post_id) |
post_id (Snowflake, time-ordered) |
author, text, media keys, created_at |
| Follow graph | (follower_id, followee_id), stored twice: by follower and by followee |
follow time; counts cached separately |
| Feed cache (Redis) | feed:{user_id} |
list of the latest ~500 post IDs |
| Post / user caches | post:{id}, user:{id} |
hydrated objects for rendering |
Time-ordered post IDs mean sorting by ID sorts by time, which makes merging and cursors simple.
C4.5High-level design
C4.6Deep dives
Guided designDeep divesAnswer each before revealing
- 1Push, pull, or hybrid?
Compare fan-out on write and fan-out on read, then choose.
Write your answer, then reveal
Fan-out on write (push): on each post, insert its ID into every follower's feed list. Reads are fast (one list read). Writes cost one insert per follower: fine for typical users, terrible for celebrities. Fan-out on read (pull): on each feed read, fetch recent posts of everyone the user follows and merge. Writes are cheap; reads are slow and expensive. Hybrid: push for normal authors; for authors above a follower threshold (say 100K), skip fan-out and merge their recent posts at read time. Each reader follows only a few celebrities, so the read-time merge stays small.
- 2Feed storage
Where do feed lists live, and for whom?
Write your answer, then reveal
Redis lists or sorted sets keyed by user, capped at a few hundred IDs (
LPUSH+LTRIM). Only for users active in the last N days: inactive users get no fan-out, and their feed is rebuilt by pull when they return. The feed cache is a derived structure; losing it means rebuilding, not data loss. - 3Hydration
The feed has 20 post IDs. How do you return full posts fast?
Write your answer, then reveal
Batch-fetch posts and authors from caches (
MGET), fall back to stores for misses, and fill the caches. Counts (likes, comments) come from counter caches. Hydration happens at read time so edits, deletes, and profile changes show up without rewriting feeds. - 4Pagination
How does "load more" work while new posts keep arriving?
Write your answer, then reveal
Cursor pagination by post ID: the next page asks for IDs lower than the last one seen. New posts at the top do not shift pages. "New posts available" at the top is a separate request for IDs greater than the newest shown.
- 5The author sees their own post
Fan-out is asynchronous. How does the author see their post at once?
Write your answer, then reveal
Insert the post into the author's own feed synchronously during publish, or have the client show it optimistically and merge on read.
- 6Deletes, blocks, and privacy
A post is deleted after fan-out to 10M feeds. Do you remove it everywhere?
Write your answer, then reveal
No. Mark it deleted in the post store; hydration drops deleted or no-longer-visible posts (blocked authors, changed privacy) at read time. Lazy cleanup avoids millions of writes. Deleting a whole account is different: privacy laws (GDPR, India's DPDP Act) require removing the user's data from the post store, caches, search indexes, analytics archives, and eventually backups. Track every derived store, run a deletion workflow across them, and for archives and backups encrypt each user's data with its own key and delete the key (crypto-shredding).
- 7New follows and unfollows
What happens to the feed right after following someone?
Write your answer, then reveal
Backfill: insert the followee's few most recent post IDs into the follower's feed (async). Unfollow: filter at read time and let old entries age out.
- 8Ranking
How would ranking change the design?
Write your answer, then reveal
Feed assembly gathers a larger candidate set (from the feed list plus celebrity pulls), scores candidates with a ranking model using features from a feature store, and caches the ranked result briefly. The storage design stays; a ranking stage is added to the read path.
C4.7Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Celebrity post | Massive fan-out | Hybrid: pull celebrities at read time |
| Fan-out backlog (Kafka lag) | Posts appear late | Scale workers on lag; prioritize active followers |
| Feed cache node loss | Some feeds missing | Rebuild on read by pull; replicas |
| Hot post (viral) | Post cache key overloaded or a stampede when it expires | In-process caching, key replication, request coalescing, early refresh |
| Follow graph hot shard | A celebrity's follower list is huge | Shard follower lists by (followee, bucket); stream it in pages |
C4.8What a strong answer shows
L4 (SDE2): estimates showing read-heaviness and fan-out cost, the hybrid strategy with a threshold, IDs in feeds plus hydration, cursor pagination, async fan-out. L5 adds: active-user optimization, celebrity edge cases (a celebrity following many celebrities), ranking pipeline, cost of the feed cache, and multi-region feed generation.
C4.9Follow-up questions
- Why store post IDs in feeds instead of full posts?
Reveal
IDs are small (8 bytes), so feeds fit in memory; edits and deletes show up through hydration without rewriting millions of feeds.
- A user follows 5,000 accounts. Which strategy hurts them?
Reveal
Fan-out on read: merging 5,000 accounts per read is slow. Push handles them well, since their feed is precomputed.
- How would you build a "Trending now" section?
Reveal
A stream job counts engagement per post over sliding windows and keeps a top-K, cached and refreshed every minute (Case study 15).
- How do you keep fan-out from delaying important posts?
Reveal
Separate queues or priorities (close friends, recent interactions first), and fan out to recently active followers before inactive ones.
Case study 5: Chat system
Deliver messages between users in real time, in order, without losing any, across phones and laptops that go offline. The hard parts are persistent connections at scale, per-conversation ordering, delivery acknowledgments, and sync after reconnect.
Design one-to-one and group chat with online presence, sent/delivered/read receipts, offline delivery, and multi-device sync.
Reveal the answer
A strong answer covers: WebSocket gateways plus a session registry; messages persisted before acknowledging; per-conversation sequence numbers for ordering; client-generated IDs for dedup on retry; a wide-column message store partitioned by conversation; routing to the recipient's gateway through pub/sub or Kafka; push notifications for offline users; sync from the last seen sequence on reconnect; presence through heartbeats with TTLs; group fan-out strategy.
C5.1Requirements
Functional
- One-to-one and group chats (groups up to a few hundred members).
- Text messages; media via object storage (Module 7).
- Sent, delivered, and read receipts; online and last-seen presence.
- Offline users get push notifications and receive messages on reconnect; history syncs across devices.
Non-functional
- 500M daily active users, up to 100M connected at once.
- Delivery under about 200 ms when both users are online in the same region.
- Never lose an acknowledged message; preserve order within a conversation.
C5.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Messages | 500M x 50 per day = 25B/day | about 300K/s, about 1M/s at peak |
| Storage | 25B x 200 bytes | about 5 TB/day, about 1.8 PB/year before replication |
| Connections | 100M concurrent / about 100K per gateway | about 1,000 gateway servers |
Write-heavy, append-only, read mostly as "latest messages of a conversation": a wide-column store partitioned by conversation fits (Cassandra, HBase, ScyllaDB).
C5.3API
client -> server send { client_msg_id, conversation_id, body }
server -> client ack { client_msg_id, message_id, seq } # "sent"
server -> client message { conversation_id, seq, sender, body, sent_at }
client -> server delivered { conversation_id, up_to_seq }
client -> server read { conversation_id, up_to_seq }
HTTP: GET /v1/conversations/{id}/messages?after_seq=...&limit=50 (sync and history)
C5.4Data model
CREATE TABLE messages (
conversation_id bigint,
bucket int, -- seq / 10000: caps partition size, computable from a cursor
seq bigint, -- per-conversation sequence, increasing
message_id bigint, -- global ID (Snowflake)
sender_id bigint,
body blob, -- ciphertext if end-to-end encrypted
sent_at timestamp,
PRIMARY KEY ((conversation_id, bucket), seq)
) WITH CLUSTERING ORDER BY (seq DESC);
Other stores: conversation membership, per-user per-device read cursors ((user, device, conversation) -> last_seq), and the session registry in Redis (user -> [gateway, device]).
C5.5High-level design
C5.6Deep dives
Guided designDeep divesAnswer each before revealing
- 1Ordering
Two messages sent close together arrive at different chat servers. How do all members see the same order?
Write your answer, then reveal
Give each message a sequence number per conversation, assigned by the single owner of that conversation (route all messages of a conversation to one partition or consistent-hash owner), which assigns
seqand persists messages inseqorder. Clients sort byseqand advance their cursor only over contiguous numbers: if 5 is missing while 6 has arrived, they refetch the gap instead of skipping it, and give up on a gap only after a timeout. Wall-clock timestamps are only for display. - 2Never lose, never duplicate
The network drops Alice's ack and her app resends. What prevents a duplicate?
Write your answer, then reveal
The client generates
client_msg_idonce per message and reuses it on retry. The server keeps a dedup record (per conversation) and returns the originalmessage_idandseqinstead of storing again. Acknowledge "sent" only after the message is durably stored. - 3Delivery and receipts
How do the three ticks work?
Write your answer, then reveal
Sent: the server stored it. Delivered: the recipient's device acknowledged receiving
up_to_seq. Read: the recipient opened the conversation up toseq. Receipts are messages too, routed back to the sender's devices and stored as cursors. - 4Offline and multi-device
Bob's laptop was offline for 3 hours; his phone was online. What happens when the laptop reconnects?
Write your answer, then reveal
Each device keeps its own cursor (last seq per conversation). On reconnect it asks for messages after its cursor for each active conversation and receives them from the store, then switches to live delivery. While Bob had no online device, a push notification (APNs/FCM) was sent.
- 5Group chats
How do you deliver a message to a 300-member group?
Write your answer, then reveal
Store the message once in the conversation's partition, then fan out delivery: look up members and route to each online member's gateway, push to offline ones. For very large channels (thousands of members), do not push every message: members fetch on open, and only mentions trigger notifications.
- 6Presence
How do you show "online" for 500M users without huge write load?
Write your answer, then reveal
Clients send heartbeats (every 30 s) over their connection; the gateway refreshes
online:{user}with a TTL of about 60 s. Status changes are published only to friends who are currently viewing the user (subscribe on open), not to everyone. Last-seen is written lazily. - 7Gateway failure
A gateway with 100K connections crashes. What happens?
Write your answer, then reveal
Clients reconnect (with jittered backoff) through the load balancer to other gateways and re-register in the session registry. Messages sent meanwhile are stored; reconnecting clients sync from their cursors. No message lives only in a gateway.
- 8End-to-end encryption
How does it change the design?
Write your answer, then reveal
Clients encrypt with keys the server never sees (Signal protocol); the server stores and routes ciphertext. Server-side search and moderation of content become impossible, and multi-device needs per-device keys.
C5.7Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Gateway crash | 100K clients disconnect | Reconnect with jitter; sync from cursors |
| Hot group | One partition receives a burst | Rate limits per conversation; batch fan-out |
| Recipient's gateway unknown or stale | Message not delivered live | Stored anyway; push notification; sync on reconnect |
| Store partition grows huge (old busy group) | Slow reads | Already bounded: the partition key includes bucket = seq / 10000, so sync from a cursor knows which buckets to read |
| Push provider throttling | Delayed notifications | Queue with retries, collapse multiple notifications |
C5.8What a strong answer shows
L4 (SDE2): WebSocket gateways with a session registry, persist-then-ack, per-conversation sequence numbers, client message IDs for dedup, wide-column storage keyed by conversation, offline push and cursor-based sync. L5 adds: group fan-out strategy by size, presence at scale, gateway draining and reconnect storms, multi-region routing, end-to-end encryption implications, and storage tiering.
C5.9Follow-up questions
- Why not use timestamps for message order?
Reveal
Clocks differ between devices and servers, and two messages can share a timestamp. A per-conversation sequence assigned by one owner gives a single agreed order.
- Why acknowledge only after storing the message?
Reveal
If the server acked first and crashed, the sender would believe the message was sent while it is lost. Persist, then ack.
- How would you design message search?
Reveal
Without end-to-end encryption: index messages in Elasticsearch partitioned by user or conversation, fed from the store's change stream. With end-to-end encryption: search on the device over locally stored history.
- Users in India chat with users in the US. Where is the conversation stored?
Reveal
In one home region per conversation (often the creator's), so ordering has one owner; gateways in each region forward to it. Reads can be served from replicas; cross-region latency affects only delivery time.
Case study 6: Notification system
Send push, SMS, email, and in-app notifications on behalf of every service in the company, reliably, without spamming users, and without letting a marketing blast delay a one-time password.
Design a notification platform that other services call. Cover channels, priorities, user preferences, templates, retries, dedup, provider failures, and delivery tracking.
Reveal the answer
A strong answer covers: one API with idempotency keys; preference and template lookup; separate queues per channel and priority; workers that call third-party providers with retries, backoff, and failover; dedup so retries never double-send; per-user and per-provider rate limits; quiet hours and time zones; device token management; delivery status from provider callbacks; bulk campaigns fanned out in throttled batches.
C6.1Requirements
Functional
- Services send notifications to users through push (iOS, Android), SMS, email, and in-app.
- Transactional (OTP, payment alerts) and bulk (marketing campaigns, digests); scheduled sends.
- Respect user preferences: channel opt-outs, categories, quiet hours.
- Templates with localization; delivery tracking (sent, delivered, opened).
Non-functional
- 1B notifications/day, bursts of 10M+ from campaigns.
- OTPs and security alerts delivered within seconds even during a campaign.
- No duplicate sends from retries; no lost transactional messages.
- Survive a provider outage (an SMS vendor going down).
C6.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Average rate | 1B / 86,400 | about 12K/s |
| Campaign burst | 10M in 10 minutes | about 17K/s on top of normal traffic |
| Status records | 1B x 200 bytes | 200 GB/day; keep 30-90 days hot |
C6.3API
POST /v1/notifications
Idempotency-Key: 6f1c... # retries never double-send
{
"user_id": 7, "category": "payment_alert", "priority": "high",
"channels": ["push", "sms"], "template": "txn_debit",
"params": {"amount": "1,250.00", "merchant": "Cafe"}, "send_at": null
}
-> 202 { "notification_id": "..." }
C6.4High-level design
Flow: validate and dedup by idempotency key, look up preferences and contact info (device tokens, phone, email), render the template per locale, then enqueue one message per channel into the right priority queue. Channel workers call providers, retry failures, and record status; providers report delivery through callbacks.
C6.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Priorities
A 50M-user campaign is queued when a user requests an OTP. How does the OTP go out in seconds?
Write your answer, then reveal
Separate queues (topics) and worker pools per priority and channel:
sms-highis never behindsms-bulk. Bulk sends are throttled to a budget so they cannot exhaust shared provider quotas. Transactional traffic gets reserved capacity. - 2Dedup and exactly-once feeling
Workers retry after a timeout from the email provider. How do you avoid two identical emails?
Write your answer, then reveal
Delivery is at-least-once, so dedup at each step: the API stores the idempotency key; each channel message has a stable ID (
notification_id + channel); the worker checks and records "sent" in a dedup store before and after the provider call; providers that accept idempotency keys get one. A rare duplicate after an ambiguous timeout is accepted for low-risk channels; for OTPs the code itself makes duplicates harmless. - 3Provider failures
The primary SMS vendor returns errors for 20 minutes. What happens?
Write your answer, then reveal
Workers retry with backoff a few times, a circuit breaker opens on the vendor, and traffic fails over to a secondary vendor. Messages that still fail go to a dead-letter queue. Track per-provider success rate and latency to route automatically.
- 4Preferences, quiet hours, and fatigue
How do you avoid spamming users?
Write your answer, then reveal
Check preferences per category and channel at send time (they may change after scheduling). Defer non-urgent messages during quiet hours in the user's time zone. Rate-limit per user per category (for example at most 3 marketing pushes a day) and collapse repeats ("5 new messages" instead of 5 pushes).
- 5Device tokens
Push needs device tokens. How do you keep them valid?
Write your answer, then reveal
Apps register tokens on login and refresh; store
user -> [device tokens]. When APNs or FCM reports a token as invalid or unregistered, delete it. Send to all active devices, or only the most recent one, by category. - 6Bulk campaigns
How do you send 50M notifications without a spike that breaks everything?
Write your answer, then reveal
A campaign service splits the audience into batches (by user ID ranges), enqueues them gradually under a rate limit, and tracks progress so it can pause, resume, or cancel. Rendering happens per batch.
- 7Tracking
How do you know a notification was delivered or opened?
Write your answer, then reveal
Providers send delivery callbacks (webhooks); emails carry tracking pixels and links; apps report opens. All update a status log keyed by notification ID, feeding analytics and retries.
C6.6Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Provider outage | Channel stops | Retries, circuit breaker, failover vendor, DLQ |
| Campaign burst | Queues fill, quotas used up | Separate priorities, throttled bulk sends |
| Preference service down | Cannot decide whether to send | Cached preferences; fail closed for marketing, open for security alerts |
| Duplicate events from producers | Repeated sends | Idempotency keys at the API |
C6.7What a strong answer shows
L4 (SDE2): queue per channel and priority, idempotency and dedup, retries with backoff and DLQ, preferences and rate limits per user, provider callbacks. L5 adds: multi-provider routing on live metrics, campaign orchestration and throttling, time-zone-aware scheduling, compliance (unsubscribe, consent records), and cost controls per channel.
C6.8Follow-up questions
- Why not call providers directly from each service?
Reveal
Every service would reimplement retries, preferences, templates, rate limits, and provider failover, inconsistently. A platform centralizes them and protects users from spam.
- Can you guarantee exactly-once delivery of an SMS?
Reveal
No. Provider calls can time out after sending. You get at-least-once with dedup, which makes duplicates rare, and design messages so a duplicate is harmless.
- How would you schedule a notification for 9 AM in each user's time zone?
Reveal
Store the send time per user in UTC computed from their time zone, in a time-bucketed scheduler (Case study 14), which enqueues when due.
- In-app notifications: how are they stored and delivered?
Reveal
Write each to a per-user inbox table (newest first, with read flags); push a real-time event over the app's WebSocket if connected; the app fetches the inbox on open.
Case study 7: Search autocomplete
Return the top suggestions for every keystroke in tens of milliseconds. The trick is to do the expensive work (counting and ranking queries) offline and serve precomputed answers from memory and caches.
Design typeahead for a search box: suggestions for each prefix ranked by popularity, refreshed from recent searches. Cover the data structure, serving path, build path, sharding, caching, and freshness.
Reveal the answer
A strong answer covers: a trie (or prefix table) with the top-k suggestions precomputed at each node; serving from memory behind browser and CDN caches; debouncing in the client; an offline (or streaming) pipeline that aggregates query logs into counts, builds the trie, and ships snapshots; sharding by prefix with attention to skew; atomic swaps of new versions; filtering offensive suggestions; and an optional trending blend.
C7.1Requirements
Functional: as the user types, return the top 10 completions for the current prefix, ranked by search frequency; suggestions refresh at least daily (hourly for trending); filter unsafe terms.
Non-functional: 100M daily users; responses within 100 ms end to end, so the server side should take 10-20 ms; highly available; slightly stale results are fine.
C7.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Prefix requests | 100M users x 10 searches x 6 requests each (debounced) | 6B/day, about 70K/s, about 200K/s at peak |
| Distinct queries kept | Long tail cut at a minimum count | about 100M queries |
| Trie size | Nodes for 100M queries (shared prefixes) plus top-10 references per node | tens of GB: fits in memory across a few shards with replicas |
C7.3High-level design
C7.4Deep dives
Guided designDeep divesAnswer each before revealing
- 1Data structure
A plain trie finds all completions of "hol" by walking its subtree. Why is that too slow, and what fixes it?
Write your answer, then reveal
Popular prefixes ("a", "ho") have millions of descendants; scanning and sorting them per keystroke is far too slow. Store the top-k completions at every node during the build; a query walks at most the prefix length and returns the cached list. The cost is memory (k references per node), bounded by capping prefix length (for example 20 characters). An equivalent alternative is a prefix table in a key-value store:
prefix -> top 10, generated offline. - 2Building the data
How do raw searches become rankings?
Write your answer, then reveal
Search events stream into Kafka and land in storage. A batch job (hourly or daily) aggregates counts per query over a window, with decay so old popularity fades, drops rare and blocked queries, then builds the trie with top-k per node and writes a versioned snapshot to object storage. A streaming job can maintain short-window counts for trending queries and blend them in.
- 3Updating servers
How do servers switch to the new trie without errors?
Write your answer, then reveal
Each server loads the new snapshot into memory alongside the old one, then swaps a pointer atomically; or bring up new instances with the new version and shift traffic (blue-green). Never mutate the serving trie in place under load.
- 4Sharding
The trie is too big or too hot for one server. How do you split it?
Write your answer, then reveal
By prefix range (a-f, g-m, ...) with replicas per shard. Prefix popularity is skewed ("s" and "c" are far more common than "x"), so split by measured load, not by letters evenly, and keep a small routing map. Every shard has several replicas for read throughput and availability.
- 5Caching
Where else can you save work?
Write your answer, then reveal
Clients debounce keystrokes (wait about 100-150 ms) and cache results per prefix for the session. Responses for short, common prefixes are identical for everyone, so the CDN or an edge cache can serve them with a TTL of minutes. Most traffic never reaches the service.
- 6Personalization and safety
How do you add a user's own recent searches and keep offensive suggestions out?
Write your answer, then reveal
Fetch the user's recent queries matching the prefix (client side or a small per-user store) and merge them with the global list. Apply a blocklist when building (so blocked terms never enter the trie) and as a fast filter at serve time for emergencies.
C7.5Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Build pipeline fails | Suggestions grow stale | Keep serving the last good snapshot; alert |
| Bad snapshot (corrupt, offensive) | Wrong suggestions everywhere | Validate before publish; roll back to previous version |
| Hot prefix shard | Latency rises | Split the range, add replicas, rely on CDN caching |
| Breaking news query | Not in the daily build | Streaming trending layer with short windows |
C7.6What a strong answer shows
L4 (SDE2): top-k at each trie node, offline aggregation and build, in-memory serving with replicas, client debounce and caching, versioned snapshots. L5 adds: freshness blending with a streaming layer, load-based sharding for skew, multi-language handling, typo tolerance, and abuse resistance (people trying to push queries into suggestions).
C7.7Follow-up questions
- Why not query the search engine (Elasticsearch prefix queries) directly per keystroke?
Reveal
It works at small scale, but ranking by global query popularity per keystroke at 200K/s is expensive; precomputed top-k per prefix is far cheaper and faster.
- How do you keep someone from manipulating suggestions by searching a phrase repeatedly?
Reveal
Count unique users rather than raw searches, rate-limit per user and IP, require a minimum spread of users, and review fast-rising terms.
- How would you support "did you mean" for typos?
Reveal
Fuzzy matching on the client query (edit distance over the trie or n-gram indexes) with a separate, smaller index; return corrections when the exact prefix has few results.
- How big is k, and why not store all completions?
Reveal
Usually 5-10, what the UI shows. Storing everything at every node would multiply memory by the subtree sizes.
Case study 8: Web crawler
Download a billion pages a month, politely, without fetching the same page twice or getting stuck in traps. A crawler is a feedback loop: every page fetched produces more URLs to fetch.
Design a crawler for a search engine: from seed URLs, fetch pages, extract links, and keep going. Cover the URL frontier, politeness, dedup of URLs and content, storage, distribution across machines, and recrawling.
Reveal the answer
A strong answer covers: a frontier with priority queues and per-host queues; politeness (one connection per host, crawl delay, robots.txt); URL normalization and a URL-seen check (Bloom filter plus store); content dedup (checksums and SimHash for near duplicates); DNS caching; storing raw pages in object storage; partitioning work by host so politeness stays local; trap protection; and recrawl scheduling by change rate.
C8.1Requirements
Functional: start from seeds; fetch HTML pages; extract and enqueue new links; store page content for indexing; recrawl pages over time; obey robots.txt.
Non-functional: 1B pages per month; polite to every site; robust to malformed pages and traps; scalable by adding machines; extensible to new content types.
C8.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Fetch rate | 1B / (30 x 86,400) | about 400 pages/s, plan for 800-1,000 |
| Storage | 1B x 500 KB average | about 500 TB per month of raw pages (compress, tier) |
| URLs seen | Tens of billions over time | Bloom filter of about 10 bits each: tens of GB, sharded |
C8.3High-level design
C8.4Deep dives
Guided designDeep divesAnswer each before revealing
- 1The URL frontier
How do you decide what to fetch next while staying polite?
Write your answer, then reveal
Two layers. Front queues by priority (PageRank-like importance, freshness need); a selector takes from high priority more often. Back queues: one queue per host, each mapped to one worker thread, plus a min-heap of "next time this host may be fetched". A worker takes the host whose time has come, fetches one URL, and sets the next allowed time (crawl delay). This guarantees at most one in-flight request per host.
- 2Politeness and robots.txt
What exactly do you respect?
Write your answer, then reveal
Fetch and cache each host's
robots.txt(refresh daily) and skip disallowed paths; honor crawl delays; limit concurrency per host and per IP (many hosts share one server); identify the crawler in the User-Agent; back off on 429 and 503. - 3URL dedup
How do you avoid fetching the same URL twice among tens of billions?
Write your answer, then reveal
Normalize first (lowercase host, remove default ports, fragments, tracking parameters, sort query parameters). Then check a Bloom filter sharded by URL hash: "definitely new" goes on; "maybe seen" is either skipped (accepting a tiny loss) or confirmed against a persistent URL store.
- 4Content dedup
Different URLs often serve the same page. How do you detect it?
Write your answer, then reveal
Exact duplicates: a checksum (SHA-256 of normalized content) in a seen-content store. Near duplicates (same article with different ads or dates): SimHash fingerprints, where similar documents have fingerprints differing in few bits. Skip storing and extracting links from duplicates.
- 5Distribution
How do you spread work over 100 machines without breaking politeness?
Write your answer, then reveal
Partition by host (hash of hostname): one machine owns all of a host's URLs, so per-host queues and delays stay local. Extracted links for other hosts are sent to their owners. Consistent hashing lets machines join and leave.
- 6Traps and junk
How do you avoid infinite calendars and session-ID URLs?
Write your answer, then reveal
Limit URL length and path depth, cap pages per host per crawl cycle, detect repeating path segments, drop known session parameters, and lower the priority of hosts that produce many near-duplicates.
- 7DNS and fetching
Why is DNS a bottleneck, and what else slows fetching?
Write your answer, then reveal
Each new host needs a lookup, and synchronous resolvers block threads. Run a local caching resolver with asynchronous lookups. Use non-blocking I/O for fetches, timeouts, size limits, and redirect limits.
- 8Recrawling
How often do you revisit a page?
Write your answer, then reveal
Estimate each page's change rate from history (did the checksum change since last time?) and its importance; schedule high-change, important pages (news front pages) often and static pages rarely. Use conditional requests (
If-Modified-Since, ETags) to save bandwidth.
C8.5Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Worker crash | Its in-flight URLs lost | Frontier stored durably; leases with timeouts re-queue URLs |
| Slow or hostile site | Threads stuck | Timeouts, size limits, per-host budgets |
| Frontier grows unbounded | Memory exhaustion | Keep queues on disk, hold only heads in memory; drop low priority |
| JavaScript-only pages | Empty content | Separate rendering pool with headless browsers, used selectively |
C8.6What a strong answer shows
L4 (SDE2): frontier with per-host queues and politeness, URL normalization and Bloom-filter dedup, content checksums, host-based partitioning, raw storage in object store. L5 adds: prioritization and recrawl policy, SimHash near-dup detection, trap handling, rendering JavaScript selectively, and freshness versus coverage trade-offs.
C8.7Follow-up questions
- BFS or DFS over the link graph?
Reveal
Roughly BFS with priorities: DFS dives into one site and violates politeness; prioritized BFS spreads across hosts and reaches important pages earlier.
- Why partition by host and not by URL hash?
Reveal
Politeness is per host. If a host's URLs were spread over many machines, they would need to coordinate their request rate to that host.
- What happens to a Bloom filter false positive?
Reveal
A new URL is wrongly treated as seen and skipped. At a 1% rate this is an accepted loss; important URLs can be double-checked against the exact store.
- How would you crawl only pages that changed?
Reveal
Conditional GETs with ETag or Last-Modified, sitemaps with lastmod dates, and adaptive recrawl intervals from observed change history.
Case study 9: Video streaming platform
Let creators upload videos and let millions watch them smoothly on any connection. Most of the design is about bytes: getting big uploads in reliably, transcoding them in parallel, and serving segments from CDN caches near viewers.
Design a YouTube-like service: upload, processing, playback with adaptive quality, and view counts. Estimate storage and bandwidth and explain where each byte lives.
Reveal the answer
A strong answer covers: resumable direct uploads to object storage; a processing pipeline triggered by events that splits video into chunks and transcodes them in parallel into multiple resolutions and bitrates; packaging into HLS or DASH segments with manifests; thumbnails; metadata and status in a database; playback via signed manifests and CDN-served segments; adaptive bitrate in the player; and asynchronous view counting.
C9.1Requirements
Functional: upload videos (up to a few GB); process them into streamable formats; watch with automatic quality; see title, description, view counts. Out of scope: comments, recommendations, live streaming (follow-up).
Non-functional: 100M daily viewers; playback starts within about 2 seconds and rarely stalls; uploads survive network drops; durable storage; cost-efficient bandwidth.
C9.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Uploads | 500K videos/day x 300 MB average | 150 TB/day raw |
| Processed copies | About 4-6 renditions, together similar to or larger than the raw size | Another 150-300 TB/day |
| Views | 100M users x 5 videos | 500M views/day, about 6K starts/s |
| Bandwidth | If 10M people watch at once at about 3 Mbps | about 30 Tbps: only a CDN can deliver this |
C9.3High-level design
C9.4Deep dives
Guided designDeep divesAnswer each before revealing
- 1Upload
A 2 GB upload over mobile data fails at 70%. How does the design handle it?
Write your answer, then reveal
Resumable, chunked uploads straight to object storage (S3 multipart or GCS resumable): the client uploads parts with presigned URLs and retries only failed parts. The API creates a video record (
status = uploading) first and finalizes it when all parts are in. App servers never stream the bytes. - 2Transcoding pipeline
How do you turn one upload into streamable renditions quickly?
Write your answer, then reveal
An upload event creates a job. A coordinator splits the video into chunks (by GOP or scene boundaries), fans the chunks out to many transcoder workers that encode each chunk into each target resolution and bitrate (codecs such as H.264, VP9, AV1), then merges and packages them into HLS or DASH segments of a few seconds with manifests. Thumbnails and audio tracks are separate tasks. Model it as a DAG of tasks with retries per task; workers are stateless and scale on queue depth (often spot instances).
- 3Playback
What happens when a viewer presses play?
Write your answer, then reveal
The watch API checks permissions and returns metadata plus a manifest URL (signed if needed). The player downloads the manifest (list of renditions and segment URLs), starts with a low or medium quality segment to start fast, measures throughput, and switches quality per segment. All segments come from the CDN.
- 4CDN strategy
Popular videos are watched millions of times; most videos are watched rarely. How do you serve both cheaply?
Write your answer, then reveal
Popular content stays hot in edge caches (pre-position new releases from big creators). The long tail is served from regional caches or the origin with an origin shield so misses are collapsed. Very large platforms place cache appliances inside ISP networks (Netflix Open Connect) to cut transit costs.
- 5View counts
How do you count 500M views a day without hammering the database?
Write your answer, then reveal
Players send view events to an ingestion endpoint into Kafka; a stream job deduplicates (same user and video within a window), filters bots, and aggregates counts per video per minute; counts are stored and cached. Displayed counts lag by seconds or minutes, which is fine.
- 6Storage cost
Video storage grows by hundreds of TB per day. How do you control cost?
Write your answer, then reveal
Keep the original plus a set of renditions; generate rare renditions on demand; move old, rarely watched videos (and their originals) to colder storage tiers; delete failed or abandoned uploads with lifecycle rules.
- 7Live streaming
What changes for live?
Write your answer, then reveal
Ingest a live stream (RTMP or SRT) at a nearby ingest server, transcode in real time, and publish segments continuously with a rolling manifest. Latency is set by segment length and buffering; low-latency HLS or DASH uses partial segments. There is no time to transcode in parallel chunks after the fact.
C9.5Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Transcoder crash | Chunk not processed | Task retries; idempotent outputs keyed by video, chunk, rendition |
| Upload spike | Processing backlog | Queue absorbs it; autoscale workers; prioritize short or popular videos |
| CDN miss storm on a viral video | Origin overloaded | Origin shield, request collapsing, pre-warming |
| Viewer bandwidth drops | Stalls | Adaptive bitrate, buffer targets, short segments |
C9.6What a strong answer shows
L4 (SDE2): storage and bandwidth estimates that force a CDN, direct resumable uploads, an event-driven parallel transcoding pipeline, segments and manifests with adaptive bitrate, and async view counting. L5 adds: CDN economics and tiering, codec choices versus device support, DRM and signed URLs, live streaming latency trade-offs, and cost controls.
C9.7Follow-up questions
- Why split video into segments of a few seconds?
Reveal
Each segment is a cacheable file, the player can switch quality at every segment boundary, and seeking jumps to the right segment without downloading the rest.
- How do you prevent people from hotlinking or downloading private videos?
Reveal
Signed, expiring URLs or cookies for manifests and segments, checked by the CDN; DRM for premium content.
- Where are video metadata and video bytes stored?
Reveal
Metadata (title, owner, status, rendition list) in a database; bytes (originals and segments) in object storage, delivered via the CDN.
- How do you make playback start faster?
Reveal
Start with a lower bitrate, keep the first segment small, preconnect to the CDN, and prefetch the beginning of likely next videos.
Case study 10: File storage and sync
Keep a folder in sync across laptops and phones, like Dropbox or Google Drive. Splitting files into content-addressed blocks makes uploads incremental and deduplicated; a change journal with cursors makes sync reliable; conflicts are handled explicitly.
Design file upload, download, automatic sync across devices, sharing, and version history. Large files must not be re-uploaded fully after a small change.
Reveal the answer
A strong answer covers: chunking files into blocks named by their hash; uploading only missing blocks directly to object storage; a metadata service (strongly consistent, sharded by user or namespace) that stores file versions as lists of block hashes; a per-namespace change journal; devices syncing from a cursor and getting notified by long polling or WebSocket; conflict handling with conflicted copies; sharing via ACLs; versions and deletion retention.
C10.1Requirements
Functional: upload and download files; automatic sync of a folder across a user's devices; offline edits that sync later; share files and folders; version history and restore.
Non-functional: 50M daily users; files up to tens of GB; never lose data; sync within seconds when online; use little bandwidth for small edits.
C10.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Stored data | 500M users x 10 GB average used | about 5 EB before dedup and compression |
| File changes | 50M users x 20 changes/day | 1B/day, about 12K metadata commits/s |
| Block size | 4 MB blocks | a 1 GB file = 256 blocks |
The metadata (who has which files and versions) is small but must be strongly consistent. The bytes are huge but immutable once written, which makes them easy to store and cache.
C10.3High-level design
C10.4Deep dives
Guided designDeep divesAnswer each before revealing
- 1Upload protocol
How does the client upload a modified 1 GB file efficiently and safely?
Write your answer, then reveal
The client splits the file into blocks and hashes each (SHA-256). It asks the metadata service which hashes are missing, uploads only those blocks (directly to the block store via presigned URLs, in parallel, resumable), then commits a new file version: path, size, and the ordered list of block hashes. The commit is the atomic moment the new version exists. The commit checks and pins every block it references in the same transaction, and garbage collection deletes only blocks that have been unreferenced for longer than a grace period, so a block the server just reported as present cannot vanish before the commit.
- 2Metadata model
What tables does the metadata service need?
Write your answer, then reveal
Namespaces (a user's root or a shared folder), file entries (namespace, path, latest version), versions (file, version number, block list, author, time), block references (hash to location and reference count), and a journal per namespace: an ordered log of changes with a sequence number. It needs transactions and is sharded by namespace, so a relational database (sharded MySQL or PostgreSQL) fits.
- 3Sync
How does the phone learn about changes and catch up after being offline?
Write your answer, then reveal
Each device stores a cursor: the last journal sequence it applied for each namespace. It keeps a long-poll or WebSocket open to the notification service, which signals "namespace changed". The device then asks for changes after its cursor, downloads missing blocks, applies them, and advances the cursor. After days offline the same call returns everything missed.
- 4Conflicts
Two devices edit the same file offline and both sync. What happens?
Write your answer, then reveal
Each commit says which version it was based on. The first commit wins and becomes the latest version; the second commit's base is now stale, so the server rejects it as a conflict, and the client saves its version as "file (conflicted copy from Laptop)". Automatic merging is not attempted for arbitrary files (documents with real-time co-editing use a different design, Case study 16).
- 5Dedup and its risks
Should identical blocks be shared across all users?
Write your answer, then reveal
Global dedup saves the most space but lets a user test whether someone else has a specific file (upload a block and see if it was "already there"), a privacy leak. Many services dedup per user or per organization only. Encrypted storage per user also limits cross-user dedup.
- 6Sharing and permissions
How do shared folders work?
Write your answer, then reveal
A shared folder is its own namespace with an ACL (owner, editors, viewers); each member mounts it in their tree. Permission checks happen in the metadata service on every operation; downloads use short-lived signed URLs so the block store does not need to know ACLs.
- 7Deletes and history
How do version history and deletion work with shared blocks?
Write your answer, then reveal
Deleting a file creates a tombstone version; older versions remain restorable for a retention period. Blocks are reference-counted (or found by mark-and-sweep over live versions) and deleted only when no version anywhere references them.
C10.5Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Upload interrupted | Partial blocks | Blocks are independent and resumable; commit only after all are present |
| Metadata shard hot (a huge shared folder) | Slow commits | Split namespaces, rate-limit, cache listings |
| Notification server loss | Devices miss "changed" signals | Clients poll on reconnect and periodically; the journal is the source of truth |
| Corrupted block | Bad file | Verify hash on upload and download; store redundantly |
C10.6What a strong answer shows
L4 (SDE2): separating metadata from bytes, hash-named blocks with incremental upload, a commit step, a change journal with device cursors, long-poll or WebSocket notifications, and conflicted copies. L5 adds: content-defined chunking, dedup privacy trade-offs, garbage collection of blocks, sharing semantics, cold storage tiering, and client-side encryption.
C10.7Follow-up questions
- Why name blocks by their content hash?
Reveal
The name proves the content (integrity check), identical content gets the same name (dedup), and the client can ask "which of these do you already have?" before uploading.
- Why must metadata be strongly consistent when blocks need not be?
Reveal
Two devices must agree on which version is latest and detect conflicts. Blocks are immutable: once written under a hash they never change, so there is nothing to disagree about.
- How do you list a folder with 100,000 files quickly?
Reveal
Store entries indexed by
(namespace, parent path), paginate with cursors, and let clients use their synced local index instead of listing from the server. - How does the client know which local files changed?
Reveal
A file system watcher (OS events) plus periodic scans comparing size, modification time, and hashes against its local index.
Case study 11: Nearby search and ride matching
Two location problems in one: finding places near a user (mostly static data, read-heavy), and matching riders with moving drivers whose positions change every few seconds (write-heavy, real time, and a driver must never get two trips).
Design (a) "restaurants near me" and (b) ride matching: drivers report location every 4 seconds, riders request a ride, the system offers it to a nearby driver and assigns exactly one.
Reveal the answer
A strong answer covers: for static places, a geohash or quadtree index (or PostGIS) with caching and a neighbor-cell search; for drivers, an in-memory geo index keyed by cells and sharded by city, updated from a location stream without touching disk per update; matching by estimated time of arrival (ETA), not straight-line distance; offers with timeouts; an atomic state change so a driver is assigned once; a trip state machine; and recovery when an index node fails.
C11.1Requirements
Functional
- Places: find businesses within a radius of a point, with filters; business data changes rarely.
- Rides: drivers send location every 4 s while online; a rider requests a ride; the system finds nearby available drivers, offers the trip, assigns one; both see live positions during the trip.
Non-functional
- 1M drivers online at peak; matching within a few seconds; nearby search under 100 ms.
- A driver is never assigned two trips at once; a trip is never assigned to two drivers.
- High availability: a location index failure must not stop rides for long.
C11.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Location updates | 1M drivers / 4 s | 250K writes/s |
| Location record | driver ID, lat, lng, time, status | about 50-100 bytes; 1M drivers = 100 MB in memory |
| Ride requests | 20M rides/day | about 230/s, peaks of a few thousand |
| Places | 200M businesses | static index fits on a few machines; reads cached |
250K writes/s of tiny, constantly overwritten data says: keep current positions in memory, not in a disk-based database row per update.
C11.3Static places: nearby search
Store businesses in a database (source of truth) and build a geospatial index: a geohash column with a B-tree index (query the user's cell plus 8 neighbors, then filter by exact distance), a quadtree in memory, or PostGIS / Elasticsearch geo queries. Choose geohash precision so the cell plus its 8 neighbors covers the radius: 5 characters (cells about 4.9 km) for a 1 km radius, 6 characters (about 1.2 x 0.6 km) for about 0.5 km. Popular areas are cached; business detail pages are cached separately. Updates are rare, so rebuilding the index nightly or updating it asynchronously is fine.
C11.4High-level design for rides
C11.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Geo index for moving drivers
How do you store 1M positions updated every 4 s and query "available drivers near this point"?
Write your answer, then reveal
In memory, partitioned by city or region (each shard owns a set of cells). Map each driver to a cell (geohash, S2, or H3) and keep
cell -> set of available driversplusdriver -> (position, cell, time). An update overwrites the position and moves the driver between cells only when the cell changes. A query reads the rider's cell and neighbors. Redis geo sets or a custom in-memory service both work. Redis expires whole keys, not members of a set, so store each driver's last-seen time and prune stale members on a timer (or use per-time-bucket keys with a TTL); a driver who stops reporting disappears within seconds. - 2Matching
Is the closest driver by straight-line distance the best choice?
Write your answer, then reveal
No. A driver 500 m away across a river can be 15 minutes away. Get candidates from the geo index, compute ETAs with a routing service (road graph, traffic), and rank by ETA plus factors like driver rating or acceptance rate. Large systems batch requests every second or two and solve assignment jointly to reduce total wait.
- 3Exactly one assignment
Two matchers offer different riders to the same driver at the same moment. How do you prevent double assignment?
Write your answer, then reveal
The driver's state lives in a store with an atomic conditional update:
available -> offered(trip)succeeds for only one trip (compare-and-set on a version, or a lease in Redis with fencing). The trip movessearching -> driver_assignedthe same way. Offers time out (about 10-15 s) and return the driver to available. - 4Offer flow
The chosen driver ignores the offer. What happens?
Write your answer, then reveal
The offer expires, the driver goes back to available (perhaps with a cooldown), and matching moves to the next candidate. The rider sees "finding a driver" throughout. All steps are recorded on the trip state machine so a crash resumes correctly.
- 5Index node failure
The shard holding Mumbai's index crashes. How fast do you recover?
Write your answer, then reveal
Positions are soft state: a replacement node (or replica) rebuilds within one update interval because every driver reports again in 4 s. Replicas or a standby can take over immediately. Trip and driver assignment state lives in a durable store, not in the index.
- 6Live tracking during a trip
How do rider and driver see each other's location?
Write your answer, then reveal
Driver updates already flow in; the trip service publishes the driver's position to the rider's connection (WebSocket via a gateway) every few seconds. Positions are also written to the location stream for ETA updates, history, and fraud checks.
- 7Hot spots
Thousands of riders request at a stadium after a concert. What breaks?
Write your answer, then reveal
One cell's shard becomes hot and supply runs out. Split hot cells finer, batch matching in the area, and use surge pricing or queueing to balance demand and supply.
C11.6Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Index shard crash | No matches in that area for seconds | Replica takeover; rebuild from next updates |
| Driver app disconnects | Stale position | Expire positions after N missed updates; mark unavailable |
| Routing service slow | Matching slows | Cached ETA estimates per cell pair; fall back to distance |
| City-level spike | Hot shard | Finer cells, more shards per city, batching |
C11.7What a strong answer shows
L4 (SDE2): separate static-place search from moving-driver tracking, in-memory cell-based index sharded by region, atomic driver assignment with timeouts, trip state machine, estimates that justify memory over disk. L5 adds: ETA-based and batched matching, hot-spot handling, multi-region design, supply and demand balancing, and how location data feeds pricing and fraud systems.
C11.8Follow-up questions
- Why not write every location update to PostgreSQL?
Reveal
250K writes/s of rows that are overwritten every 4 seconds would waste disk and I/O for data that is only useful for a few seconds. Keep current positions in memory; stream history asynchronously to cheaper storage.
- Geohash or quadtree for drivers?
Reveal
Fixed cells (geohash, S2, H3) are simpler for constantly moving points because updating a position is a cell lookup. Quadtrees adapt to density but must rebalance as points move.
- How do you find drivers near a cell boundary?
Reveal
Query the rider's cell and its neighbors (or a ring of H3 cells), then filter by real distance or ETA.
- What consistency does the driver state need?
Reveal
Strong, for the assignment step: a conditional update so only one trip wins. Positions can be eventually consistent.
Case study 12: Ticket booking
Millions of fans arrive at 10:00 for 50,000 seats. Two problems collide: a huge spike that must not take the site down, and seats that must never be sold twice. The design answers both with admission control in front and atomic seat holds behind.
Design booking for concerts or movie seats: browse events, see the seat map, hold seats while paying, and confirm. Handle a launch spike of millions of users.
Reveal the answer
A strong answer covers: static event pages on a CDN; a virtual waiting room that admits users at a controlled rate; a seat map served from a cache (may be slightly stale); seat holds via an atomic conditional update in the database (or Redis holds confirmed in the database) with an expiry; payment with idempotency inside the hold window; confirmation that checks the hold is still valid; a sweeper for expired holds; and bot and fairness controls.
C12.1Requirements
Functional: browse events; view the seat map with availability; select and hold seats for about 10 minutes; pay; receive tickets; release unpaid holds.
Non-functional: launch spikes of 10M users for 50K seats; no seat sold twice; the site stays up under the spike; fair access; holds expire reliably.
C12.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Launch traffic | 10M users refreshing | millions of requests/s at the edge for a few minutes |
| Bookable rate | 50K seats in, say, 15 minutes | about 55 bookings/s: tiny for the database |
| Seat rows | 50K seats per event | small: one database partition per event |
The database is not the bottleneck; the crowd is. Admit people at the rate the booking flow can serve, and make each booking atomic.
C12.3API and data model
GET /v1/events/{id}/seats -> seat map (cached, refreshed every few seconds)
POST /v1/events/{id}/holds {"seats": ["A12","A13"]} -> 201 {"hold_id", "expires_at"} or 409 if taken
POST /v1/holds/{hold_id}/purchase Idempotency-Key: ... -> 201 {"order_id"} or 410 if the hold expired
CREATE TABLE seats (
event_id bigint, seat_id text,
status text NOT NULL, -- AVAILABLE | HELD | BOOKED
hold_id uuid, hold_expires_at timestamptz, order_id bigint,
PRIMARY KEY (event_id, seat_id)
);
-- Hold two seats atomically: succeed only if every seat is free (or its hold expired)
UPDATE seats SET status = 'HELD', hold_id = :hold, hold_expires_at = now() + interval '10 minutes'
WHERE event_id = :event AND seat_id IN ('A12', 'A13')
AND (status = 'AVAILABLE' OR (status = 'HELD' AND hold_expires_at < now()));
-- If rows updated != 2, roll back the transaction and return 409.
C12.4High-level design
C12.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1No double booking
Two users click the same seat at the same millisecond. What guarantees only one hold succeeds?
Write your answer, then reveal
The hold is a single conditional
UPDATE ... WHERE status = 'AVAILABLE'in a transaction: the database row lock serializes the two updates, the second sees the seat already held and updates 0 rows. Alternatives:SELECT ... FOR UPDATE NOWAITon the seat rows, or a RedisSET seat NX PX 600000hold followed by the database update as the final authority. Never read availability and then write in separate steps without a guard. - 2Hold expiry
The user closes the tab during payment. How do the seats come back?
Write your answer, then reveal
Holds carry
hold_expires_at. The hold query already treats expired holds as available (lazy expiry), so no seat stays stuck even if the sweeper is late. A background sweeper also resets expired holds toAVAILABLEto keep the seat map accurate. - 3Payment inside the hold
Payment succeeds 1 second after the hold expired and someone else took the seat. What now?
Write your answer, then reveal
Confirmation is a conditional update
HELD -> BOOKED WHERE hold_id = :hold(and not expired). If it fails, the order fails and the payment is voided or refunded automatically (a compensation, Module 13). Better: authorize payment first, confirm the seat, then capture; a failed confirmation just voids the authorization. - 4The spike
10M users arrive at once. How does the system survive?
Write your answer, then reveal
Serve all static content (event page, seat map shell) from the CDN. Put a virtual waiting room in front of the booking flow: users get a position in a queue (a signed token with an admission time) and are admitted at a rate the booking service can handle. Pre-scale stateless services before launch. Rate-limit per user and IP, and shed excess traffic with a clear "you are in line" page rather than errors.
- 5Seat map freshness
The seat map is read by everyone constantly. Do you query the database each time?
Write your answer, then reveal
No. Cache the map per event (Redis or the CDN) and refresh every second or two from the database, or update it from seat change events. A stale map only means a user may pick a seat that just went; the hold step returns 409 and they pick again.
- 6Fairness and bots
How do you stop bots from grabbing all tickets?
Write your answer, then reveal
Waiting-room randomization at start time, CAPTCHA or device checks at admission, per-account and per-payment-card ticket limits, verified fan programs, and anomaly detection on purchase patterns.
- 7General admission
Some events have no seat numbers, just 20,000 standing tickets. What changes?
Write your answer, then reveal
Use a counter per ticket category:
UPDATE inventory SET held = held + :n WHERE event = :e AND sold + held + :n <= capacity, plus one row per hold(hold_id, qty, expires_at). A counter cannot expire holds lazily, so a sweeper releases expired holds and decrementsheld; otherwise abandoned holds would shrink sellable stock forever. The same conditional pattern prevents overselling. For extreme rates, pre-split inventory into buckets (shards) and decrement them in parallel.
C12.6Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Thundering herd at launch | Everything overloads | CDN, waiting room, pre-scaling, load shedding |
| Hot event partition | Lock contention on popular seats | Short transactions; holds keep locks brief; queue admission |
| Payment provider slow | Holds expire during payment | Extend hold once while payment is in progress; idempotent retries |
| Sweeper down | Map shows stale holds | Lazy expiry in queries keeps correctness |
C12.7What a strong answer shows
L4 (SDE2): atomic conditional seat holds with expiry, payment within the hold and failure compensation, a cached seat map, a waiting room and CDN for the spike. L5 adds: fairness and anti-bot strategy, inventory sharding for general admission, hold extension rules, end-to-end failure handling with the payment provider, and capacity planning for the launch.
C12.8Follow-up questions
- Why not lock seats with a long database transaction while the user pays?
Reveal
Payment takes minutes; holding row locks that long blocks other transactions and connections. Use a short transaction to set a HELD status with an expiry instead.
- Optimistic or pessimistic locking for seats?
Reveal
The conditional update is an optimistic, atomic check-and-set; it fits because each attempt is a single short statement.
FOR UPDATE NOWAITis the pessimistic version that fails fast instead of waiting. - How would you design this for a cinema chain with thousands of small shows?
Reveal
The same model, partitioned by show; contention is low, so the waiting room is unnecessary except for blockbuster openings.
- How do you handle a user double-clicking "Buy"?
Reveal
An idempotency key on the purchase request, plus the conditional HELD -> BOOKED update, so the second request returns the same order.
Case study 13: Payment system
Accept payments for merchants, move money correctly, and prove it. Scale is modest compared with feeds or chat; correctness is everything. The design centers on idempotency, a payment state machine, a double-entry ledger, safe handling of ambiguous processor responses, and reconciliation.
Design a Stripe-like payments platform: merchants create payments through an API; you charge cards through processors; merchants get webhooks; refunds and payouts happen later. No double charges, no lost money, full audit trail.
Reveal the answer
A strong answer covers: idempotency keys end to end (merchant to you, you to processor); a payment state machine stored in a relational database with conditional transitions; authorize and capture; the outbox pattern to publish events; signed, retried webhooks; a double-entry ledger with immutable entries; handling processor timeouts by querying status and reconciling; daily reconciliation with settlement files; tokenization to limit PCI scope; integer minor units for money; and a risk check before authorization.
C13.1Requirements
Functional
- Merchants create payments (card, UPI, wallets) and refunds through an API.
- Authorize and capture (immediately or later); void uncaptured authorizations.
- Notify merchants of outcomes (webhooks); pay out merchant balances on a schedule.
- Full history: every money movement recorded and explainable.
Non-functional
- Never charge twice for one intended payment; never lose a confirmed payment.
- 99.99% API availability; payment latency is dominated by processors (hundreds of ms to seconds).
- Volume: 10M payments/day (about 120/s average, peaks of a few thousand per second during sales).
- Compliance: PCI DSS for card data, audit trails, data residency where required.
C13.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Payments | 10M/day | about 120/s, plan for 3,000/s peaks |
| Ledger entries | About 3-6 per payment (customer, merchant, fees, refunds) | 30-60M rows/day |
| Storage | 60M x 300 bytes | about 18 GB/day; years of history fit in a sharded relational store |
The numbers fit relational databases comfortably; choose them for transactions and constraints, and shard by merchant or account when needed.
C13.3API
POST /v1/payments
Idempotency-Key: 8b4f... # required; same key = same payment
{ "amount": 125000, "currency": "INR", "payment_method": "pm_tok_...", "capture": "manual" }
-> 201 { "id": "pay_77", "status": "authorized" } (or "requires_action" for 3-D Secure)
POST /v1/payments/pay_77/capture Idempotency-Key: ... -> 200 { "status": "captured" }
POST /v1/refunds { "payment": "pay_77", "amount": 125000 } Idempotency-Key: ...
Webhook to merchant: payment.succeeded, payment.failed, refund.succeeded (signed)
Amounts are integers in minor units (paise, cents), never floating point.
C13.4High-level design
C13.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Idempotency end to end
The merchant's request times out and they retry. The processor call from your side times out too. How do you guarantee one charge?
Write your answer, then reveal
The merchant's
Idempotency-Keyis claimed atomically in your database before any work (Module 3), together with a fingerprint of the request; a retry with the same key and parameters returns the stored outcome or waits for the in-flight attempt, while the same key with different parameters is rejected. Keys are kept for a defined window (Stripe documents at least 24 hours); after that, a reused key counts as a new request. Your call to the processor carries your own idempotency key derived from the payment attempt ID (a retry of the same attempt reuses it; a new attempt with a different card gets a new one), so your retries cannot create a second charge there either. - 2State machine
What states does a payment go through, and how are transitions protected?
Write your answer, then reveal
created -> requires_action (3-D Secure) -> authorized -> captured -> settled, withpending_confirmation(outcome unknown),failed,voided,refunded(and partially refunded) branches. Each transition is a conditional update (UPDATE payments SET status = 'captured' WHERE id = ? AND status = 'authorized'), and the allowed transitions are enforced in code and ideally by a database check. An event is written to the outbox in the same transaction. - 3Ambiguous outcomes
The processor call times out. Did the authorization happen?
Write your answer, then reveal
Treat it as unknown, not failed. Mark the payment
pending_confirmation, then query the processor's status API with your idempotency key (with backoff), or retry the same request (safe because it is idempotent). Late webhooks from the processor also resolve it. Anything still unknown is caught by reconciliation. Never report failure to the merchant while the outcome is unknown. - 4The ledger
Why double-entry, and what are its rules?
Write your answer, then reveal
Every money movement is recorded as entries on at least two accounts so that total debits equal total credits (for example: processor receivable, merchant payable, fee revenue). Entries are append-only and immutable; corrections are new reversing entries. Balances are derived by summing (with cached running balances). This makes errors visible (unbalanced transactions are rejected) and gives a complete audit trail.
- 5Events and webhooks
How do merchants and internal systems reliably learn about outcomes?
Write your answer, then reveal
The state change and an outbox row commit together; a relay publishes to Kafka (Module 13). A webhook service delivers to merchant endpoints with an HMAC signature and timestamp, retries with backoff for days, and exposes an events API so merchants can fetch anything they missed. Merchants must handle duplicates.
- 6Refunds
Two partial refunds for the same payment arrive at the same moment with different idempotency keys. What stops refunding more than was captured?
Write your answer, then reveal
Idempotency keys only stop duplicates of the same request. Guard the amount itself with a conditional update in the refund transaction:
UPDATE payments SET refunded = refunded + :amt WHERE id = :id AND refunded + :amt <= captured; if 0 rows change, reject the refund. Then post the refund's ledger entries in the same transaction. - 7Reconciliation
Your records and the processor's settlement file disagree on 3 payments. What process finds and fixes this?
Write your answer, then reveal
A daily job loads processor settlement files and matches them to your ledger by processor reference and amount. Mismatches are classified (missing on our side, missing on theirs, amount differs, status differs) and either auto-corrected (for example, marking a timed-out payment as succeeded and posting entries) or sent to an operations queue. Payouts to merchants are computed only from reconciled balances.
- 8Card data and PCI
How do you keep most of the system out of PCI scope?
Write your answer, then reveal
Card numbers go from the customer's browser straight to a tokenization service (hosted fields or an SDK), which stores them encrypted (keys in an HSM or KMS) and returns a token. Everything else, including merchant servers and your payment service, handles only tokens. The vault is small, isolated, and audited.
- 9Scaling and availability
How does this grow to 10x and survive a region failure?
Write your answer, then reveal
Shard payments and ledger by merchant or account (keeping a payment's entries on one shard so transactions stay local). Run synchronous replication within the region and a warm standby or active-passive setup across regions with tight RPO. Data residency rules can constrain this: India's RBI requires payment data to be stored in India, so cross-region disaster recovery needs two Indian regions. Processor connectors are stateless and fail over between processors where the business allows (routing by success rate and cost).
C13.6Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Processor timeout | Unknown outcome | Pending state, status queries, idempotent retries, reconciliation |
| Processor outage | Payments fail | Circuit breaker, route to a secondary processor, clear errors to merchants |
| Duplicate merchant requests | Double charge risk | Idempotency keys with atomic claim |
| Webhook endpoint down | Merchant misses events | Retries for days, events API, dashboard |
| Bug posts wrong entries | Balances wrong | Ledger invariants checked per transaction; reversing entries; reconciliation alerts |
| Authorization expires before capture | Capture fails | Track the network's authorization validity window; capture or re-authorize before it lapses |
| Customer disputes a charge (chargeback) | Funds pulled back weeks later | Dispute state machine, evidence submission, ledger entries for the reversal and fees |
C13.7What a strong answer shows
L4 (SDE2): idempotency keys with atomic claims, a state machine with conditional transitions, authorize vs capture, outbox plus webhooks, careful handling of timeouts as unknown, integer money. L5 adds: double-entry ledger design and invariants, reconciliation as a first-class component, PCI scope reduction through tokenization, multi-processor routing, and regional resilience with explicit RPO.
C13.8Follow-up questions
- Why not use floats for amounts?
Reveal
Binary floating point cannot represent most decimal fractions exactly, so sums drift. Use integers in minor units (or exact decimal types) and store the currency with every amount.
- Why authorize first and capture later?
Reveal
Authorization holds funds without moving them; if the order cannot be fulfilled, a void releases the hold at no cost. Capture happens when goods ship or the service is delivered.
- Could you use an eventually consistent NoSQL store for the ledger?
Reveal
It is possible with careful design, but you lose multi-row transactions and constraints that make balanced entries and state transitions simple. A relational database (or distributed SQL) is the natural fit.
- How do you test that the ledger is always balanced?
Reveal
Enforce per-transaction balance in the write path, run continuous invariant checks (sum of all entries is zero; account balances match entry sums), and alert on any violation.
Case study 14: Distributed job scheduler
Run millions of jobs at the right time, once or on a cron schedule, across a fleet that can lose machines at any moment. The core problems: finding due jobs efficiently, making sure each run is triggered once, and executing at least once with idempotent jobs.
Design a scheduler service: clients register one-time and recurring (cron) jobs that call an HTTP endpoint or run a task. Jobs must fire within about a second of their time, survive node failures, retry on failure, and keep run history.
Reveal the answer
A strong answer covers: a jobs table keyed by next run time and partitioned (time buckets and shards); scheduler nodes that each own shards through leases from a coordination service; polling only "due soon" jobs of owned shards; claiming each run with a conditional update or a unique (job_id, scheduled_time) key; enqueueing runs to a queue for workers; at-least-once execution with idempotency keys passed to the job; retries with backoff; heartbeats for long jobs; computing the next cron time in the job's time zone; and spreading load at popular times like midnight.
C14.1Requirements
Functional: create, update, cancel jobs; one-time (run_at) and recurring (cron expression with time zone); payload or callback URL; retries with a policy; run history and status.
Non-functional: 100M scheduled jobs, 10M runs per day with peaks at round hours; trigger accuracy about 1 s; no lost runs; duplicate triggers extremely rare and harmless; horizontally scalable; no single point of failure.
C14.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Average runs | 10M / 86,400 | about 120/s |
| Peak at 00:00 | Many cron jobs at midnight, say 2M in the first minute | about 33K/s for a minute: needs smoothing |
| Jobs table | 100M x 1 KB | about 100 GB, sharded |
C14.3Data model
| Table | Key | Fields |
|---|---|---|
jobs |
job_id |
owner, schedule (cron, tz) or run_at, payload, retry policy, next_run_at, status |
due_index |
(shard, time_bucket, next_run_at, job_id) |
lets a scheduler read "my shard, next minute" cheaply |
runs |
(job_id, scheduled_time) unique |
attempt count, status, worker, started/finished, error |
C14.4High-level design
C14.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Finding due jobs
Why not
SELECT * FROM jobs WHERE next_run_at <= now()every second?Write your answer, then reveal
On 100M rows, from many nodes, every second, that query fights over the same index and duplicates work. Instead, split jobs into shards (hash of
job_id) and time buckets (minute ofnext_run_at). Each scheduler node reads only its shards' next bucket a little ahead of time, loads due jobs into an in-memory timer (a priority queue or timing wheel), and fires them on time. Jobs created or rescheduled for the near future (sayrun_at = now + 5 s), after their bucket was already loaded, must not be missed: the API also notifies the owning node to add them to its timer, and nodes rescan the current bucket periodically as a safety net. - 2One trigger per run
Two scheduler nodes both think they own shard 7 after a network blip. How do you avoid double triggers?
Write your answer, then reveal
Shard ownership is a lease in a coordination service (etcd or ZooKeeper) with an epoch number. The node then claims the run and records it in one database transaction: insert into
runswith unique(job_id, scheduled_time)and statusPENDING, and advance the job'snext_run_atwith a conditional update from the expected old value (the epoch acts as a fencing token). Only one claim succeeds. Enqueueing happens after the commit through an outbox relay or a sweeper that picks upPENDINGruns older than a few seconds, so a crash between claim and enqueue cannot lose the run; workers dedup on(job_id, scheduled_time). - 3Execution guarantees
A worker crashes halfway through a job. What happens, and what must job authors do?
Write your answer, then reveal
Delivery is at-least-once: the run message's visibility timeout or lease expires and another worker retries it. So jobs receive an idempotency key (
job_id + scheduled_time) and must make their effects idempotent. Long jobs send heartbeats to extend their lease; missing heartbeats mark them failed and eligible for retry. - 4Recurring jobs
When do you compute the next run time of a cron job, and what about time zones?
Write your answer, then reveal
Right after claiming a run, compute the next fire time from the cron expression in the job's time zone (handling daylight saving gaps and repeats by documented rules), convert to UTC, and update
next_run_atin the same transaction as the claim. That way a crash cannot skip or duplicate the next occurrence. - 5Midnight spikes
Millions of jobs are scheduled at 00:00. How do you protect the system and the endpoints they call?
Write your answer, then reveal
Add small jitter where the job allows it (spread over the first minute), rate-limit per tenant and per target endpoint, prefetch due jobs a little early, and let the queue absorb the burst while workers scale on queue depth.
- 6Retries and failures
A job's endpoint returns 500. What is the retry policy?
Write your answer, then reveal
Exponential backoff with jitter up to a maximum number of attempts or time window, recorded per attempt in
runs; then mark it failed, alert the owner, and optionally move it to a dead-letter list for manual replay. Most 4xx errors are permanent and fail immediately, except 408 (timeout) and 429 (rate limited), which are retried, honoringRetry-After.
C14.6Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Scheduler node dies | Its shards stop firing | Lease expires in seconds; another node takes the shards, re-enqueues PENDING runs, and catches up on missed due jobs |
| Coordination service unavailable | No new leases | Existing leases keep running until expiry; nodes stop firing when unsure (safety over liveness) |
| Clock skew between nodes | Early or late firing | NTP; fire based on database or coordinator time; small lookahead |
| Worker backlog | Jobs run late | Autoscale workers on queue lag; priority queues per tier |
C14.7What a strong answer shows
L4 (SDE2): time-bucketed, sharded storage of due jobs; leases for shard ownership; atomic claim per run; at-least-once execution with idempotency keys; retries with backoff; next-run computation done atomically. L5 adds: fencing tokens, timing wheels, multi-tenant fairness and rate limits, daylight-saving semantics, catch-up policy after outages (run missed jobs or skip), and operational visibility.
C14.8Follow-up questions
- Why not just use one leader that schedules everything?
Reveal
It works up to a point and is simpler, but one node becomes the throughput limit and its failover pauses all scheduling. Sharding with leases spreads load and limits the blast radius of a failure.
- What is a timing wheel?
Reveal
A circular array of buckets, one per time slot (for example per second); jobs are placed in the slot for their fire time, and a pointer advances each tick and fires that slot. Insert and fire are O(1).
- Exactly-once execution of an external HTTP call?
Reveal
Not achievable in general: a call can succeed and the acknowledgment be lost. Use at-least-once triggering with an idempotency key the endpoint honors.
- What should happen to runs missed during a 2-hour outage?
Reveal
It depends on the job: some must run once to catch up (billing), some should run every missed occurrence, and some should skip to the next scheduled time (a cache refresh). Make it a per-job policy.
Case study 15: Ad click aggregation and top-K trending
Count a billion ad clicks a day per ad per minute, fast enough for dashboards and exact enough for billing, and keep a live top-K of the most clicked ads. This is the canonical stream processing design: windows, event time, dedup, hot keys, and exactly-once counts.
Design a system that ingests ad click events, shows advertisers clicks per ad per minute with a few minutes of delay, answers "top 100 ads in the last hour", and produces billing numbers that must be correct.
Reveal the answer
A strong answer covers: durable ingestion into Kafka keyed by ad ID; a stream processor with event-time tumbling windows and watermarks; dedup by click ID; exactly-once state and output (checkpoints plus idempotent upserts keyed by ad and window); handling hot ads with two-stage aggregation; top-K per window from per-partition heaps; an OLAP store for queries; raw events archived; and a batch recompute that reconciles billing.
C15.1Requirements
Functional: record clicks; aggregate clicks per ad per minute (with dimensions such as country); query time ranges; top-K most clicked ads over the last N minutes; filter invalid clicks.
Non-functional: 1B clicks/day, peaks of 50K/s; aggregates available within a few minutes; billing numbers exact; tolerate late events (mobile devices offline) and duplicates; no data loss.
C15.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Average ingest | 1B / 86,400 | about 12K/s; peaks 50K/s |
| Raw storage | 1B x 200 bytes | about 200 GB/day |
| Aggregates | A row exists only for an (ad, minute) with at least one click | at most 1B rows/day, and many hold a single click; roll up to hours early |
C15.3High-level design
C15.4Deep dives
Guided designDeep divesAnswer each before revealing
- 1Windows and late events
A click happened at 10:01:30 but arrives at 10:04. Which minute does it count in, and when is the 10:01 window final?
Write your answer, then reveal
Count it in 10:01 by event time. A watermark tracks how far event time has progressed (for example, max event time seen minus 2 minutes). The 10:01 window fires when the watermark passes 10:02. Events that arrive later, but before the watermark passes 10:02 plus the allowed lateness, re-fire the window with a corrected count (the upsert overwrites it). Events after that go to a late-events side output for the batch recompute.
- 2Duplicates and fraud
The same click can be logged twice (client retries) and bots click ads. How do you filter?
Write your answer, then reveal
Each click carries a unique click ID (generated server-side at the redirect); the stream job keeps recently seen IDs in keyed state with a TTL and drops repeats. Invalid traffic rules (same user and ad within seconds, known bot signatures, data center IPs) run in the stream job or a separate fraud pipeline that marks clicks invalid before billing.
- 3Exactly-once counts
A stream worker crashes after counting some events. How do you avoid double or missing counts?
Write your answer, then reveal
The stream processor (Flink) checkpoints its operator state together with Kafka offsets; after a crash it rewinds to the last checkpoint and reprocesses, producing the same state. Output is made idempotent by upserting keyed rows
(ad_id, window_start) -> count, so re-emitted results overwrite instead of adding. Alternatively, use transactional sinks committed with checkpoints. - 4Hot ads
One ad gets 30% of all clicks during a big campaign, so one partition is overloaded. What do you do?
Write your answer, then reveal
Two-stage aggregation: first aggregate by
(ad_id, random salt 0-9)across many workers, then combine the 10 partial counts per ad in a second, much smaller stage. The same trick works for any skewed key. - 5Top-K
How do you compute the top 100 ads per hour from per-minute counts?
Write your answer, then reveal
Because the stream is partitioned by ad ID, each ad's count for a window is complete in one partition. Each partition keeps a min-heap of its top 100 for the window; a final step merges the partition heaps into a global top 100, which is exact. For extremely high-cardinality keys (all search terms), use a count-min sketch plus heap to approximate.
- 6Billing accuracy
Streaming numbers may be slightly off. How is billing made exact?
Write your answer, then reveal
Archive every raw event to object storage. A daily batch job recomputes counts from the raw data with the final fraud labels and late events included, compares with the streaming results, and the batch output is what is billed. Differences above a threshold raise alerts. This is the Lambda pattern: fast approximate path plus exact batch path.
- 7Serving queries
Advertisers filter by ad, campaign, country, and time range. Where do the aggregates live?
Write your answer, then reveal
A columnar OLAP store (ClickHouse, Apache Druid, Apache Pinot, or BigQuery) with rows keyed by ad and minute plus dimension columns, pre-rolled up to hours and days for long ranges. A query service adds authorization and caching.
C15.5Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Stream job failure | Aggregates pause | Restart from checkpoint; Kafka retains raw events |
| Kafka partition hot | Lag on a hot ad | Adding partitions does not split one key: salt the producer key for known hot ads, or partition randomly and key by ad inside the stream job |
| Late events beyond lateness | Streaming undercounts | Side output to batch path; billing from batch |
| OLAP store overloaded | Slow dashboards | Rollups, caching, query limits per advertiser |
C15.6What a strong answer shows
L4 (SDE2): Kafka plus stream processor with tumbling windows, event time and watermarks, dedup by click ID, idempotent upserts, OLAP store for queries, and a top-K that is correct because of the partitioning. L5 adds: exactly-once mechanics (checkpoints, transactional sinks), skew handling, a reconciliation path for billing, fraud integration, and cost and retention of raw and aggregated data.
C15.7Follow-up questions
- Why partition the raw click topic by ad ID?
Reveal
All clicks for an ad land in one partition, so its window counts are computed in one place without cross-partition merging (except for salted hot ads).
- Tumbling or sliding windows for "clicks in the last hour, updated every minute"?
Reveal
Sliding (hopping) windows of one hour advancing every minute, or tumbling one-minute counts summed over the last 60 at query time, which is cheaper to store.
- Why not write each click directly to the database and use
COUNT(*)?Reveal
At 50K writes/s plus heavy aggregate queries, the database would be overwhelmed; pre-aggregation reduces 1B events to far fewer rows and makes queries cheap.
- How would you show a near real-time "top trending" list to users?
Reveal
Sliding-window counts with a heap per partition, merged and cached every few seconds, served from the cache.
Case study 16: Collaborative document editor
Several people type into the same document at the same time and all of them end up with the same text, instantly. The design rests on giving each document one place that orders edits, and on an algorithm (OT or CRDT) that merges concurrent edits correctly.
Design Google Docs-style real-time co-editing: concurrent edits, live cursors, history, offline edits, and many documents.
Reveal the answer
A strong answer covers: clients send small operations (insert, delete) over WebSockets; one session server owns each active document (found through a directory with leases) and assigns each operation a revision number; operational transformation (or CRDTs) to merge concurrent operations; an append-only operation log plus periodic snapshots; broadcasting transformed operations to collaborators; ephemeral cursors and presence; reconnect and offline sync from the last acknowledged revision; failover of the session owner; and permissions.
C16.1Requirements
Functional: open and edit a shared document; see others' edits within a fraction of a second; see their cursors; version history; work offline briefly and sync; share with permissions.
Non-functional: edits appear to collaborators in under 200 ms; no edit is ever lost; all replicas converge to the same text; millions of documents, typically fewer than 10 simultaneous editors per document (up to about 100).
C16.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Active documents | 5M documents open at peak | spread across hundreds of session servers |
| Operations | 5M docs x 1 op/s average while typing | about 5M ops/s overall, small per document |
| Op size | about 50-100 bytes | logs grow; snapshots bound replay time |
Load per document is tiny; the challenge is correctness of concurrent edits and routing every editor of a document to the same place.
C16.3High-level design
C16.4Deep dives
Guided designDeep divesAnswer each before revealing
- 1Why not send the whole document or lock paragraphs?
What goes wrong with simpler approaches?
Write your answer, then reveal
Sending full documents loses concurrent edits (last write wins). Locking paragraphs blocks collaborators and still needs merging for edits near boundaries. Sending small operations and merging them keeps everyone editing freely.
- 2Operational transformation
Alice and Bob both edit revision 5. How does the server produce one consistent order?
Write your answer, then reveal
The session server is the single sequencer: it applies operations in arrival order and gives each a revision. An incoming operation based on an older revision is transformed against all operations applied since (shifting positions for inserts and deletes that happened before it), then applied and broadcast. Clients do the same for their own unacknowledged local operations, so every replica converges. Google Docs uses OT with a central server.
- 3CRDTs
How do CRDTs differ, and when are they better?
Write your answer, then reveal
CRDTs give every character a unique, ordered identifier so concurrent inserts and deletes commute: any order of applying them gives the same result, with no central transform step. They suit offline-first and peer-to-peer editing (Yjs, Automerge). Costs: per-character metadata and tombstones that grow memory, and more complex garbage collection. OT needs a central sequencer but keeps documents compact.
- 4Routing and ownership
How do all editors of one document reach the same session server, and what if it dies?
Write your answer, then reveal
A directory maps document IDs to session servers using consistent hashing or explicit leases in a coordination service. On failure, the lease expires, another server takes ownership, loads the latest snapshot plus subsequent operations from the log, and clients reconnect and resend unacknowledged operations (idempotent by client operation IDs).
- 5Persistence
How do you store a document that receives thousands of small edits?
Write your answer, then reveal
Append every operation to an operation log keyed by
(doc_id, revision)before acknowledging, and write a snapshot every N operations or minutes. Loading a document is "latest snapshot + operations after it". History and "restore version" come from snapshots and the log. - 6Cursors and presence
Should cursor positions be stored?
Write your answer, then reveal
No. Cursors and selections are ephemeral: broadcast them through the session server at a limited rate, transform them against operations like any position, and drop them when a user disconnects.
- 7Offline edits
A user edits on a plane for an hour. How do their edits merge?
Write your answer, then reveal
The client keeps its local operations based on the last revision it saw. On reconnect it sends them; the server transforms them against everything that happened meanwhile (OT), or the CRDT state merges automatically. Large divergences can produce surprising but consistent results, so some products show a review of merged changes.
C16.5Failures and bottlenecks
| Failure or bottleneck | Effect | Mitigation |
|---|---|---|
| Session server crash | Editing pauses briefly | Lease takeover, reload snapshot + log, clients resend pending ops |
| Very popular document (1,000 viewers) | Broadcast fan-out | Separate read-only viewers onto a fan-out tier; limit editors |
| Log grows without bound | Slow loads | Snapshots and compaction |
| Network flaps | Duplicate operations | Client operation IDs make resends idempotent |
C16.6What a strong answer shows
L4 (SDE2): operations over WebSockets, one owner per document that sequences operations, a correct explanation of why concurrent edits need transformation, operation log plus snapshots, reconnect with resend. L5 adds: OT vs CRDT trade-offs in depth, ownership failover with leases and fencing, offline merge behavior, presence at scale, and permission changes during a live session.
C16.7Follow-up questions
- Why does a single session server per document simplify correctness?
Reveal
It provides one total order of operations (revision numbers), so transformation only has to handle operations against a known history, and every client converges to the server's sequence.
- Does a single owner per document limit scale?
Reveal
Not in practice: each document has few editors and little traffic, and millions of documents are spread across many servers.
- How would you implement "suggestion mode" or comments anchored to text?
Reveal
As additional operations or annotations with anchors expressed in positions that are transformed like cursors, so they move with the text.
- What changes for a spreadsheet instead of a text document?
Reveal
Operations target cells and ranges (set value, insert row); conflicts are per cell and simpler, but formulas and row and column insertions need transformation of references.
Cheat sheets
One-page summaries for revision the night before. Every row links back to the module that explains it.
Numbers
| Rule of thumb | Value |
|---|---|
| Seconds per day | 86,400, about 10^5 |
| 1M requests/day | about 12/s; 1B/day is about 12K/s |
| Peak / average | 2-3x normally; 10x+ for launches |
| Memory reference / SSD random read / DC round trip / cross-continent round trip | ~100 ns / ~20-100 us / ~0.5 ms / ~150 ms |
| Redis / SQL primary / stateless server | ~100K ops/s / thousands to low tens of thousands queries/s / 1K-10K req/s |
| Base62 codes | 62^6 is about 57 billion; 62^7 is about 3.5 trillion |
| Snowflake | 41-bit ms time, 10-bit machine, 12-bit sequence (4,096 per ms) |
Details: Module 4.
Availability
| Nines | Per year | Per month |
|---|---|---|
| 99.9% | 8.77 h | 43.8 min |
| 99.95% | 4.38 h | 21.9 min |
| 99.99% | 52.6 min | 4.4 min |
| 99.999% | 5.3 min | 26 s |
Series: multiply. Parallel copies: 1 - (1 - a)^n. RPO = data you can lose; RTO = time to recover. Module 15.
Which database?
| Need | Choose | Example |
|---|---|---|
| Transactions, joins, constraints, moderate scale | Relational | PostgreSQL, MySQL |
| SQL with horizontal write scale, global | Distributed SQL | Spanner, CockroachDB |
| Lookup by key at huge scale | Key-value | DynamoDB, Redis |
| Flexible documents read as a whole | Document | MongoDB |
| Huge append-heavy writes, time-ordered per key | Wide-column (LSM) | Cassandra, Bigtable, HBase |
| Full-text search, facets | Search index | Elasticsearch, OpenSearch |
| Metrics over time | Time-series | Prometheus, InfluxDB, TimescaleDB |
| Analytics over billions of rows | Columnar warehouse | BigQuery, Snowflake, ClickHouse |
| Multi-hop relationships | Graph | Neo4j |
Caching patterns
| Pattern | Write path | Risk |
|---|---|---|
| Cache-aside (default) | Write DB, delete cache key | Stale-set race; bound with TTL |
| Write-through | Write cache and DB together | Slower writes |
| Write-behind | Write cache, flush to DB later | Data loss on cache failure |
| Write-around | Write DB only | First read misses |
Failure modes: stampede (single-flight, early refresh), avalanche (TTL jitter), penetration (negative cache, Bloom filter), hot key (local cache, key replication). Module 6.
Replication, partitioning, consistency
| Topic | Remember |
|---|---|
| Sync vs async replication | Sync: no loss, slower, blocks on slow replica. Async: fast, can lose recent writes on failover |
| Lag anomalies | Read-your-writes, monotonic reads, consistent prefix |
| Quorum | R + W > N gives overlap; N = 3, W = 2, R = 2 is common |
| Partition key | High cardinality, even load, matches the main query |
| Consistent hashing | Adding a node moves about 1/N of keys; virtual nodes balance load |
| CAP | During a partition choose consistency (refuse) or availability (answer, maybe stale) |
| PACELC | Else: latency vs consistency, paid on every request |
| Consensus | 2f + 1 nodes tolerate f failures; majority commits (Raft) |
| Locks | Lease + fencing token, or a database constraint |
Messaging
| Kafka | RabbitMQ | SQS | |
|---|---|---|---|
| Model | Partitioned log, replay | Broker queues, routing | Managed queue |
| Ordering | Per partition (by key) | Per queue | FIFO per message group |
| Best for | Event backbone, streams, many consumers | Task routing, priorities | Simple background jobs |
Delivery is at-least-once; make consumers idempotent. Retries with backoff, then a DLQ. Classic consumer groups cap parallelism at the partition count; Kafka share groups (4.2+) do not, but lose per-key order. Module 12.
Cross-service correctness
| Problem | Pattern |
|---|---|
| Write DB and publish event atomically | Transactional outbox, or CDC |
| Multi-service business transaction | Saga (orchestration or choreography) with compensations |
| Retries must not repeat effects | Idempotency keys, unique constraints, conditional updates |
| Separate read and write needs | CQRS with projections |
| Full history required | Event sourcing |
| Records must match a partner | Reconciliation |
Real-time and APIs
| Need | Use |
|---|---|
| Rare updates, delay fine | Polling |
| Server pushes only | Server-sent events |
| Two-way, frequent | WebSocket (with connection registry and pub/sub) |
| Internal high-volume calls | gRPC |
| Public API | REST with cursor pagination, idempotency keys, 429 + Retry-After |
| Rich client with varied data | GraphQL or a BFF |
Rate limiting
| Algorithm | One line |
|---|---|
| Token bucket | Bursts up to b, average r; the usual default |
| Leaky bucket | Constant output rate; smooths |
| Fixed window | Cheap; allows 2x at boundaries |
| Sliding log | Exact; memory per request |
| Sliding counter | Weighted approximation; cheap and close |
Atomic in Redis with Lua; fail open with a local fallback. Module 15, Case study 2.
Trade-off phrases worth saying
- "Reads outnumber writes 100 to 1, so I'll put a cache in front and accept up to 60 seconds of staleness for this data."
- "This must never double-charge, so the write path uses an idempotency key with a unique constraint, and events go out through an outbox."
- "I'll partition by
user_idbecause every main query includes it; the risk is very large users, which I'll handle by splitting their data into buckets." - "During a network partition, booking chooses consistency: we reject rather than risk selling a seat twice. Browsing chooses availability and serves cached data."
- "I'm keeping this synchronous because the caller needs the answer; everything else goes on a queue."
- "One PostgreSQL primary handles this load today; I'd plan the shard key now and shard when we pass about X writes per second."
Interview framework
A system design round rewards a clear process as much as a good design. This page gives a time plan for 45 minutes, what to say in each phase, how to choose deep dives, what L4 and L5 interviewers listen for, and the red flags to avoid.
The 45-minute plan
| Minutes | Phase | Output | Watch out |
|---|---|---|---|
| 0-5 | Requirements | 3-5 functional requirements, numeric non-functional ones, out of scope | Do not draw yet |
| 5-10 | Estimates | QPS (average, peak), storage, bandwidth, read/write ratio, and what they imply | Round hard; state the implication |
| 10-15 | API and data model | 3-5 endpoints; main entities, keys, and the store for each | Keep it short |
| 15-30 | High-level design | Diagram of the main flows; walk one request end to end | Start simple, then evolve |
| 30-42 | Deep dives | 2-3 hardest problems: bottlenecks, consistency, failures | Let the interviewer steer; propose if they do not |
| 42-45 | Wrap-up | Trade-offs, what you would do next, monitoring | Summarize; do not start new topics |
What to say in each phase
Requirements
- "Let me confirm scope: users can do A, B, C. I'll leave out D and E unless you want them."
- "Roughly how many users and requests? Read-heavy or write-heavy? Global or one region?"
- "What matters most: latency, availability, or strict correctness? Is it fine if X is a few seconds stale?"
Estimates
- Write the formula, round, and conclude: "About 120K reads per second, so the read path needs a cache; 90 TB over five years, so one database node is not enough."
High-level design
- Draw clients, entry point (LB or gateway), services, data stores, caches, queues. Number the flows.
- Walk through one write and one read: "The client calls..., the service writes..., then publishes..."
- Name each technology with its reason: "Redis here because reads are key lookups and we accept 60 s staleness."
Deep dives
Pick from what the numbers and requirements make hard:
- The bottleneck your estimates exposed (hot keys, write volume, fan-out, bandwidth).
- The correctness risk (double booking, double charge, lost messages, ordering).
- A failure (a node, a dependency, a region, a spike) and how the design degrades.
For each: state the problem, give two options, choose one with its trade-off, and say how you would know it works (a metric).
Wrap-up
- "The main trade-offs are... With more time I would add... I'd monitor..."
What interviewers look for
SDE-2 / L4 (Google SWE III, Meta E4, Amazon SDE2): a working, reasonably scaled design; correct use of standard building blocks; estimates that drive decisions; clear trade-offs; sensible data model and partition key; handles 1-2 deep dives well with guidance. You may need prompting for the hardest failure modes, but you respond to it well.
L5 adds: drives the interview without prompting, finds the hard problems unasked, goes deep on several (consistency, failure, scaling limits), discusses operations (monitoring, rollout, migration, cost), and compares alternatives quantitatively.
| Dimension | Weak | Solid (L4) | Strong (L5) |
|---|---|---|---|
| Requirements | Starts drawing | Clarifies scope and numbers | Uncovers hidden requirements (consistency per feature, compliance) |
| Design | Buzzwords, missing flows | Coherent components, clear data flow | Elegant, minimal, evolves with scale |
| Data | No model or wrong store | Fitting store, keys, partitioning | Access-pattern-driven model, hot spots handled |
| Trade-offs | None stated | Names main trade-offs | Quantifies them, picks per requirement |
| Failures | Ignored | Handles obvious failures | Systematic: what fails, detection, recovery, degradation |
| Communication | Silent or rambling | Structured, checks in | Leads, adapts to interviewer cues |
Drawing on a whiteboard
- One shape per kind of component (see the legend); label every box with its role, not only a product name.
- Solid arrows for synchronous calls, dashed for async messages; number the steps of a flow.
- Write key decisions next to the component: "partition key: user_id", "TTL 60 s", "W = 2".
- Leave space to the right for the evolution; do not erase, extend.
Red flags
Common mistakes
- Designing before clarifying scope and scale.
- Numbers that change nothing: estimates without conclusions.
- Technology lists ("Kafka, Redis, Cassandra") without reasons.
- Single points of failure in a design that claims high availability.
- Ignoring the data model and partitioning.
- Strong consistency everywhere or nowhere, instead of per feature.
- Going silent for long stretches; think out loud.
- Arguing with the interviewer's constraint instead of adapting the design.
How to practice
- Do each case study cold with a 45-minute timer, speaking out loud (record yourself).
- Compare with the handbook; write down what you missed in one line each.
- Redo it from memory when the tracker brings it back.
- From week 6 of the roadmap, do a mock with a peer every two weeks, alternating interviewer and candidate.
- Before each real interview, skim the cheat sheets and redo two case studies quickly (20 minutes each, diagram only).
Glossary
Every term used in the handbook, in one line each, with a link to where it is explained. Search finds these too.
- ACID
- Atomicity, consistency, isolation, durability: the guarantees of database transactions. Read more
- Active-active
- Several regions serve traffic at the same time; needs a conflict strategy for writes. Read more
- Adaptive bitrate
- The video player switches quality per segment based on measured bandwidth. Read more
- Anti-entropy
- Background process that finds and repairs differences between replicas, often with Merkle trees. Read more
- API gateway
- Single entry point that handles auth, rate limits, and routing to services. Read more
- At-least-once
- Delivery that may repeat messages but never loses them; consumers must be idempotent. Read more
- At-most-once
- Delivery that never repeats but may lose messages. Read more
- Availability
- Fraction of time a system answers requests successfully. Read more
- B-tree
- Sorted, balanced tree index used by relational databases; good for lookups and ranges. Read more
- Backpressure
- Slowing producers when consumers cannot keep up, instead of buffering forever. Read more
- BFF (backend for frontend)
- A gateway tailored to one client type that aggregates and shapes data. Read more
- Bloom filter
- Compact set that answers "definitely not" or "probably yes". Read more
- Blue-green deploy
- Run a full new environment and switch all traffic at once; switch back to roll back. Read more
- Bulkhead
- Isolating resources per dependency so one failure cannot exhaust everything. Read more
- Cache-aside
- App reads cache, loads from DB on miss, and deletes the cache key on write. Read more
- Canary release
- Send a small share of traffic to a new version and compare before rolling out. Read more
- CAP theorem
- During a network partition a system must choose consistency or availability. Read more
- CDC (change data capture)
- Streaming a database's committed changes from its log to other systems. Read more
- CDN
- Network of edge caches that serve content close to users. Read more
- Circuit breaker
- Stops calling a failing dependency for a while, then tests it again. Read more
- Consistent hashing
- Mapping keys and nodes onto a ring so adding a node moves only about 1/N of keys. Read more
- Consumer group
- Set of consumers sharing a Kafka topic's partitions; in a classic group each partition goes to one member. Read more
- Consumer lag
- How far a consumer is behind the newest message. Read more
- Count-min sketch
- Small structure estimating item frequencies; only overestimates. Read more
- CQRS
- Separate models for writes (commands) and reads (queries). Read more
- CRDT
- Data type whose concurrent updates always merge to the same result. Read more
- Dead-letter queue (DLQ)
- Where messages go after repeated processing failures, for inspection and replay. Read more
- Denormalization
- Storing copies of data to make reads faster, at the cost of harder updates. Read more
- DNS
- System that maps names to IP addresses, with caching by TTL. Read more
- Double-entry ledger
- Recording every money movement as balanced debits and credits. Read more
- Dual write
- Writing to two systems without a shared transaction; can leave them inconsistent. Read more
- Durability
- Confirmed data survives crashes and failures. Read more
- Envelope encryption
- Data keys encrypt data; a master key in KMS encrypts the data keys. Read more
- Error budget
- The failure allowance implied by an SLO, used to balance speed and reliability. Read more
- Event sourcing
- Storing every change as an event and deriving current state by replay. Read more
- Eventual consistency
- Replicas converge if writes stop; reads may be stale meanwhile. Read more
- Exactly-once (effectively)
- At-least-once delivery plus idempotent or transactional processing. Read more
- Failover
- Promoting a replica to replace a failed leader. Read more
- Fan-out on write / read
- Precompute feeds when posting, or assemble them when reading. Read more
- Fencing token
- Increasing number checked by storage to reject a stale lock holder or leader. Read more
- Geohash
- Base32 string encoding of a location; shared prefixes mean nearby cells. Read more
- gRPC
- RPC framework using Protocol Buffers over HTTP/2. Read more
- Hedged request
- Sending a duplicate request after a delay and using the faster answer. Read more
- Hinted handoff
- A stand-in node holds writes for a down replica and delivers them later. Read more
- Horizontal scaling
- Adding more machines rather than bigger ones. Read more
- Hot key
- A key receiving a disproportionate share of traffic. Read more
- HTTP/2 and HTTP/3
- Multiplexed HTTP over TCP, and over QUIC (UDP) without TCP head-of-line blocking. Read more
- HyperLogLog
- Structure that estimates distinct counts with a few KB of memory. Read more
- Idempotency key
- Client-chosen ID that lets the server return the original result for retries. Read more
- Idempotent
- An operation that has the same effect whether done once or many times. Read more
- Inverted index
- Map from each term to the documents containing it; powers search. Read more
- Isolation level
- How much concurrent transactions can see of each other; read committed to serializable. Read more
- JWT
- Signed token carrying claims, verified without a lookup; hard to revoke early. Read more
- Kafka
- Distributed, partitioned, replicated log used as an event backbone. Read more
- L4 / L7 load balancer
- Balances TCP connections by address, or HTTP requests by content. Read more
- Last write wins (LWW)
- Conflict rule that keeps the value with the latest timestamp, dropping others. Read more
- Latency vs throughput
- Time per request versus requests per second; batching trades one for the other. Read more
- Leader election
- Choosing one node to coordinate, typically through consensus and leases. Read more
- Lease
- Time-limited ownership that must be renewed; expires if the holder dies. Read more
- Linearizability
- Strongest consistency: the system behaves like a single up-to-date copy. Read more
- Little's Law
- Items in a system = arrival rate x time in system; sizes pools and consumer counts. Read more
- Load shedding
- Rejecting excess work early so accepted work still succeeds. Read more
- Long polling
- Server holds a request open until data arrives or a timeout. Read more
- LSM tree
- Write-optimized storage: memtable, sorted files, background compaction. Read more
- Merkle tree
- Tree of hashes used to find differing ranges between replicas cheaply. Read more
- Microservices
- Independently deployable services, each owning its data. Read more
- mTLS
- TLS where both sides present certificates, authenticating service to service. Read more
- Multi-leader replication
- Several nodes accept writes and replicate to each other; conflicts must be resolved. Read more
- OAuth 2.0
- Delegated authorization: an app gets a scoped token to act for a user. Read more
- Object storage
- Durable, cheap storage of files by key over HTTP (S3, GCS). Read more
- OIDC
- Identity layer on OAuth 2.0 that adds an ID token for login. Read more
- Operational transformation (OT)
- Adjusting concurrent edit operations so all replicas converge. Read more
- Optimistic locking
- Detect conflicts with a version check at write time and retry. Read more
- Outbox pattern
- Writing an event row in the same transaction as the data, then relaying it. Read more
- PACELC
- If partitioned, availability or consistency; else, latency or consistency. Read more
- Partition key
- The field that decides which shard or partition holds a record. Read more
- Percentile (p99)
- Value below which 99% of measurements fall; used for latency. Read more
- Pessimistic locking
- Lock rows before changing them (
SELECT ... FOR UPDATE). Read more - Presigned URL
- Time-limited URL granting one operation on one object. Read more
- Quadtree
- Tree that splits space into four cells wherever points are dense. Read more
- Quorum
- Minimum replicas that must respond; R + W > N gives overlap. Read more
- Raft
- Consensus protocol based on leader election and majority log replication. Read more
- RAG
- Retrieval-augmented generation: retrieve relevant chunks, then let an LLM answer from them. Read more
- Rate limiting
- Restricting how many requests a client may make per time window. Read more
- Read repair
- Fixing a stale replica noticed during a read. Read more
- Read-your-writes
- A user always sees their own recent writes. Read more
- Reconciliation
- Comparing your records with an external source to find and fix mismatches. Read more
- Replication lag
- Delay before a follower reflects the leader's writes. Read more
- Reverse proxy
- Server in front of servers: TLS termination, routing, caching. Read more
- RPO / RTO
- How much data you can lose / how long recovery may take. Read more
- Saga
- Sequence of local transactions with compensations, replacing a distributed transaction. Read more
- Schema registry
- Service that stores event schemas and rejects incompatible changes. Read more
- Server-sent events (SSE)
- One-way server-to-client event stream over HTTP. Read more
- Service discovery
- How a caller finds healthy instances of a service. Read more
- Service mesh
- Sidecar proxies plus a control plane for service-to-service traffic. Read more
- Sharding
- Splitting data across nodes by a partition key. Read more
- Kafka (4.2+) group with queue semantics: many consumers per partition, per-record acks, no per-key order. Read more
- SLI / SLO / SLA
- Measured indicator / internal target / contractual promise. Read more
- Sloppy quorum
- Counting stand-in nodes toward W during failures; keeps writes available. Read more
- Snowflake ID
- 64-bit time-ordered ID: timestamp, machine ID, sequence. Read more
- Split brain
- Two nodes both acting as leader after a partition. Read more
- SSRF
- Tricking a server into fetching internal URLs; block private ranges and allow-list destinations. Read more
- Stampede (thundering herd)
- Many requests rebuilding the same expired cache key at once. Read more
- Stateless service
- Service that keeps no per-client state between requests. Read more
- Sticky session
- Load balancer keeps a client on one server. Read more
- Strangler fig
- Migrating a monolith piece by piece behind a routing layer. Read more
- TCP / UDP
- Reliable ordered byte stream / connectionless datagrams. Read more
- Token bucket
- Rate limiter allowing bursts up to a bucket size and an average refill rate. Read more
- Two-phase commit (2PC)
- Atomic commit across participants via prepare and commit; blocks if the coordinator fails. Read more
- UUIDv7
- 128-bit ID with a millisecond timestamp prefix; sortable and index-friendly. Read more
- Vector clock
- Per-node counters that detect whether events are ordered or concurrent. Read more
- Vector search
- Finding items whose embedding vectors are nearest to a query vector, using ANN indexes such as HNSW. Read more
- Virtual nodes
- Many ring positions per physical node to balance consistent hashing. Read more
- Watermark
- Stream processing marker saying events up to time T have likely arrived. Read more
- WebSocket
- Full-duplex connection upgraded from HTTP. Read more
- Write skew
- Two transactions read the same data and write different rows, breaking a rule together. Read more
- Write-ahead log (WAL)
- Log written before data pages so changes survive crashes. Read more
Roadmap and revision
A 13-week plan at two HLD sessions a week (about 5 hours), with case studies starting in week 4 and mocks from week 8. Stretch it to 18-20 weeks if you alternate HLD with LLD sessions.
Weekly plan
Each session is about 2-2.5 hours. Start every session by clearing the revision queue on the home page (15-20 minutes).
| Week | Session 1 | Session 2 |
|---|---|---|
| 1 | M1 Systems and requirements | M2 Networking |
| 2 | M3 API design | M4 Estimation + decode drills |
| 3 | M5 Scaling and LB | M6 Caching |
| 4 | M7 CDN and object storage | C1 URL shortener + C2 Rate limiter |
| 5 | M8 Databases (part 1: ACID, isolation, locking) | M8 (part 2: indexes, engines, NoSQL, choosing) |
| 6 | M9 Replication | M10 Sharding |
| 7 | M11 Consistency, CAP, consensus | C3 Key-value store |
| 8 | M12 Queues and streams | M13 Transactions; first mock |
| 9 | M14 Microservices + C4 News feed | C5 Chat + C6 Notifications |
| 10 | M15 Reliability | M16 Observability and security; mock |
| 11 | C12 Tickets + C13 Payments | M17 Toolkit + C7 Autocomplete |
| 12 | C8 Crawler + C11 Nearby and rides | C9 Video + C10 File sync |
| 13 | C14 Scheduler + C15 Ad clicks | C16 Co-editing; 60-minute mock |
After week 13: one or two practice prompts from the tracker per week, plus the revision queue, until your interviews.
Revision rules
- Spaced review: set a status after every case study and practice prompt. The queue brings designs back after 1, 2, 4, 10, or 30 days.
- Redesign, do not reread: on review, close the page, set a 20-30 minute timer, and draw the design from scratch. Then compare.
- One-line misses: keep a running list of what you forgot ("forgot hot keys", "no idempotency on retry"). Read it before every mock.
- Mastery checklists: revisit them at the end of each phase; untick anything that is no longer true.
Mock interviews
| When | Format | Focus |
|---|---|---|
| Week 8 | 45 min with a peer, a Starter or Core prompt | Process: requirements, estimates, a clean diagram |
| Week 10 | 45 min, a Core prompt you have not done | Deep dives and failure handling |
| Week 13 | 60 min, a Stretch prompt | Driving the interview, trade-offs, L5-style depth |
| Then | Every 1-2 weeks | Rotate prompts from the tracker |
Score each mock with the rubric on the interview framework page and note the weakest dimension; that decides the next week's revision.
Using the handbook at work
- Before writing a design doc, run the decode method on the problem and use the cheat sheets as a checklist.
- During design reviews, use each module's "Common mistakes" box as review questions.
- During incidents and postmortems, revisit Module 15 and Module 16: most incidents map to a missing timeout, retry policy, idempotency guard, or alert.
Design practice tracker
All 16 case studies and 20 more practice prompts in one list. Set a status after each attempt; due designs come back in the revision queue. Practice prompts reuse the case studies and modules listed under each.
Your designs
How to use practice prompts
Practice prompts have no write-up of their own. Each one combines ideas from the case studies and modules listed under it. Attempt it with a 45-minute timer, then reread the linked material to check your design, and set your status honestly.
| Status | When to choose it |
|---|---|
| Could not design it | You could not get a coherent architecture without help |
| Designed with hints | You needed to look something up for the core design |
| Designed independently | Full design, estimates, and two deep dives on your own |
| Needs revision | You did it before, but a redo went badly |
| Can teach it | Done twice, weeks apart, and you can answer every follow-up |
Backup
Back up or move your progress
Progress saves to your account automatically. Use this only to move progress by hand, for example from a private window.
SDE-2 readiness check
Tick an item only when you can do it with notes closed. When every box on this page is ticked and your recent mocks score "solid" or better on every rubric row, you are ready for SDE-2 system design rounds.