System Design — Concept Notes
Easy words, full depth. How to run a system design interview or written exam, every building block you will be asked about, ten worked case studies, and a revision kit for the night before. Print this page for a PDF.
1. What Interviewers Actually Test
A system design question is an open problem like "Design Twitter" or "Design a URL shortener". There is no single correct answer. The interviewer wants to watch how you think: do you ask good questions, make sensible choices, and explain why?
A system here means all the parts that make a product work together: servers, databases, caches, queues, and the connections between them. Design means choosing those parts and explaining the trade-offs.
It is like an architect meeting a client. Nobody expects the final blueprint in 45 minutes. They expect good questions, a sensible sketch, and clear reasons for every wall.
What they score you on
| Signal | What it looks like when you do it well |
|---|---|
| Problem scoping | You ask questions before drawing. You write down what is in and out of scope. |
| Structured thinking | You follow a clear order (the framework in section 2) instead of jumping around. |
| Technical depth | You can go deep on one or two parts: how the cache is invalidated, how the data is sharded. |
| Trade-off reasoning | Every choice comes with "I chose X over Y because…, the cost is…". |
| Scale awareness | You use rough numbers to justify choices. You know when one database is enough and when it is not. |
| Communication | You think out loud, check in with the interviewer, and accept hints gracefully. |
Level expectations
- Junior / new grad: a working high-level design, basic building blocks (load balancer, cache, database), sensible API. Depth is a bonus.
- Mid-level: all of the above, plus you drive the conversation, make estimates, and go deep on one area.
- Senior: you find the hard part yourself, discuss failure modes, consistency, cost and operations without being asked.
Written exam vs live interview
In a written exam (university paper, online assessment, take-home) nobody can answer your questions. So you state your assumptions instead of asking them. The marker is looking for the same signals, but on paper.
| Live interview | Written exam |
|---|---|
| Ask clarifying questions | Write "Assumptions:" as a list at the top |
| Draw and talk at the same time | Draw a labelled diagram, then explain each box in numbered points |
| Interviewer steers you to the deep dive | You pick 1–2 deep dives yourself and label them clearly |
| Trade-offs mentioned as you go | A separate "Trade-offs" heading, ideally a small table |
| Time: 35–45 minutes | Time: split marks by headings; do not spend it all on one part |
How to structure a written answer
- Assumptions and requirements — functional (what it does) and non-functional (how well), as bullet points.
- Estimates — 3–5 lines of numbers: users, requests per second, storage.
- API — 3–5 endpoints.
- Data model — tables or collections with key fields.
- Architecture diagram — boxes and arrows, every box labelled, numbered arrows for the main flow.
- Component explanation — one short paragraph per box, following the arrow numbers.
- Scaling and deep dives — the hard part of this problem, explained properly.
- Trade-offs and alternatives — a table: choice, why, cost.
- Conclusion — two sentences summarising the design.
The full template is in the revision section: Written-exam template.
2. The Step-by-Step Framework
Use the same eight steps for every question. The steps keep you calm, make sure nothing important is forgotten, and show the interviewer you have a process.
Functional requirements are the features: "users can post a photo". Non-functional requirements are the qualities: "the feed loads in under 200 ms", "the system never loses a photo".
Like a pilot's checklist. Experienced pilots still use it, because under pressure memory fails and checklists do not.
Step 1 — Clarify requirements (about 5 min)
Split them into two lists and write both on the board.
- Functional: "Who uses it? What are the 3–5 core actions?" Pick the core ones and say the rest are out of scope.
- Non-functional: scale, latency (speed), availability (uptime), consistency (does everyone see the same data at once?), durability (never lose data).
Good questions to ask:
- "How many daily active users (DAU) should I design for?"
- "Is it read-heavy or write-heavy? Roughly what ratio?"
- "Is it global, or one region?"
- "What matters more if we must choose: always available, or always correct?"
- "Do we need real-time updates, or is a few seconds of delay fine?"
- "Mobile, web, or both? Any existing systems I must integrate with?"
- "How long do we keep data? Is there anything we must never lose?"
Step 2 — Back-of-the-envelope estimates (about 3 min)
Rough maths for requests per second, storage and bandwidth (section 3). The point is not precision; the point is to decide things like "one database is fine" or "we must shard".
Step 3 — API (about 3 min)
List the main endpoints: method, path, inputs, output. This makes the functional requirements concrete.
POST /api/v1/urls { longUrl } -> { shortUrl }
GET /{shortCode} -> 302 redirect
Step 4 — Data model (about 4 min)
Main entities (things you store) and their key fields. Say which database type you will use and why (section 10).
Step 5 — High-level design (about 10 min)
Draw the boxes: client, load balancer, services, cache, database, queue, storage. Walk through one request end to end. Start simple; do not add Kafka before you need it.
Step 6 — Deep dive (10–12 min)
Pick the hardest part of this problem, or let the interviewer pick. Examples: generating unique short codes, fan-out for a feed, matching riders to drivers. Go properly deep here.
Step 7 — Bottlenecks and scaling (about 5 min)
Ask "what breaks first at 10x the traffic?" Then fix it: caching, replicas, sharding, queues. Mention single points of failure (one box whose failure takes everything down).
Step 8 — Wrap-up (about 2 min)
Summarise the design in 3–4 sentences, list what you would add with more time (monitoring, security, analytics), and name one trade-off you consciously made.
3. Back-of-the-Envelope Estimation
"Back of the envelope" means quick, rough maths you could do on the back of an envelope. You round aggressively and aim for the right order of magnitude (is it 100, 10,000 or 1,000,000?).
The estimates answer three questions: how many requests per second hit the system, how much storage it needs, and how much bandwidth (data moved per second) it uses.
Like planning a party: you do not count grains of rice, you just check whether you need one pot or ten.
Powers of two (data sizes)
| Power | Approx value | Name | Feel for it |
|---|---|---|---|
| 210 | 1 thousand | 1 KB (kilobyte) | A short text message with metadata |
| 220 | 1 million | 1 MB (megabyte) | A photo from a phone, compressed |
| 230 | 1 billion | 1 GB (gigabyte) | An hour of HD video, roughly |
| 240 | 1 trillion | 1 TB (terabyte) | A large laptop disk |
| 250 | 1 quadrillion | 1 PB (petabyte) | A big company's data warehouse |
Handy sizes: a character is 1 byte (ASCII), a number (int64) is 8 bytes, a UUID is 16 bytes, a timestamp is 8 bytes. A typical database row is 100 bytes to 1 KB.
Latency numbers every engineer should know
Latency is how long one operation takes. You do not need exact values, just the ladder.
| Operation | Approx time | Takeaway |
|---|---|---|
| L1 CPU cache read | ~1 ns | Basically free |
| Main memory (RAM) read | ~100 ns | Memory is fast; this is why caches live in RAM |
| Compress 1 KB | ~2–10 µs | Cheap; compress before sending over a network |
| Read 1 MB sequentially from RAM | ~10–250 µs | RAM reads are very fast |
| Round trip inside one data centre | ~0.5 ms | Every extra service hop costs this |
| SSD random read | ~100 µs | About 1000x slower than RAM |
| Read 1 MB sequentially from SSD | ~1 ms | Sequential beats random |
| HDD disk seek | ~10 ms | Avoid random reads on spinning disks |
| Read 1 MB sequentially from HDD | ~20 ms | Fine for logs and big scans |
| Round trip across continents | ~150 ms | Why CDNs and regional servers exist |
(ns = nanosecond, a billionth of a second; µs = microsecond, a millionth; ms = millisecond, a thousandth.)
The three lessons: memory is much faster than disk; disk is much faster than a network hop across the world; avoid round trips, especially cross-region ones.
The formulas
Seconds in a day ≈ 86,400 ≈ 100,000 (round up, easier maths)
Average QPS = daily requests / 86,400
≈ daily requests / 100,000
Peak QPS ≈ 2x to 3x average (say "peak factor")
Write QPS = DAU x writes per user per day / 86,400
Read QPS = write QPS x read:write ratio
Storage per day = writes per day x size per item
Storage total = storage per day x 365 x years (x replication factor, often 3)
Bandwidth (ingress) = write QPS x size per item (data coming in)
Bandwidth (egress) = read QPS x size per item (data going out)
Servers needed ≈ peak QPS / QPS one server handles (assume ~1,000 for a simple API)
Cache memory ≈ 20% of daily read data (80/20 rule: 20% of items get 80% of reads)
QPS means queries (or requests) per second. DAU means daily active users.
A fully worked example: a photo-sharing app
Assumptions, stated out loud: 100 million DAU. Each user uploads 1 photo every 10 days (0.1 per day) and views 50 photos per day. Average photo 500 KB. Keep photos for 10 years.
Writes per day = 100M x 0.1 = 10M uploads/day
Write QPS = 10M / 100k = ~100 uploads/sec (peak ~300)
Reads per day = 100M x 50 = 5B views/day
Read QPS = 5B / 100k = ~50,000 views/sec (peak ~150k)
Read:write ratio = 50,000 / 100 = 500 : 1 -> very read-heavy
Storage per day = 10M x 500 KB = 5 TB/day
Storage 10 years = 5 TB x 365 x 10 = ~18 PB (before replication)
Egress bandwidth = 50,000 x 500 KB = 25 GB/sec -> must use a CDN
Metadata = 10M rows/day x ~500 bytes = 5 GB/day -> ~18 TB in 10 years
-> too big for one comfortable DB node, plan to shard
What the numbers tell you, and what to say:
- 500:1 read-heavy → heavy caching and a CDN matter more than write speed.
- 18 PB of photos → object storage like S3 (section 18), not a database.
- 25 GB/s outgoing → no single server can do that; a CDN serves the photos (section 9).
- 18 TB of metadata → shard the metadata database or use a wide-column store.
4. Key Non-Functional Terms
These are the words for "how well" a system works, rather than "what" it does. You will use them in every answer, so you must be able to define each in one sentence.
Features are what a car does (drive, carry five people). Non-functional qualities are how well: top speed, fuel use, how often it breaks down, whether it is safe in a crash.
| Term | One-line meaning | How you improve it |
|---|---|---|
| Scalability | Handling more load by adding resources, without a redesign | Stateless servers, horizontal scaling, sharding, caching |
| Availability | The percentage of time the system is up and answering | Redundancy, failover, no single point of failure, multi-zone |
| Reliability | Working correctly over time, even when parts fail | Retries, replication, testing, graceful degradation |
| Latency | Time for one request to complete (how fast) | Caching, CDN, fewer hops, indexes, closer servers |
| Throughput | How many requests the system completes per second (how much) | More servers, batching, async processing, parallelism |
| Consistency | All readers see the same, latest data | Single leader, synchronous replication, quorums, transactions |
| Durability | Once saved, data is never lost | Write to disk, replicate to several machines, backups |
| Maintainability | Easy for engineers to change and operate | Simple design, clear boundaries, observability |
Availability and the "nines"
Availability is written as a percentage. Each extra 9 cuts allowed downtime by ten.
| Availability | Downtime per year | Per month | Per day | Typical for |
|---|---|---|---|---|
| 99% (two nines) | 3.65 days | ~7.3 hours | ~14.4 min | Internal tools |
| 99.9% (three nines) | 8.76 hours | ~43.8 min | ~1.44 min | Most web apps |
| 99.99% (four nines) | 52.6 min | ~4.4 min | ~8.6 s | Payments, big consumer apps |
| 99.999% (five nines) | 5.26 min | ~26 s | ~0.86 s | Telecom, core infrastructure |
Components in series multiply: if a request needs service A (99.9%) and B (99.9%), total availability is 99.9% x 99.9% ≈ 99.8%. More required hops means lower availability.
Components in parallel add up: two redundant copies each at 99% give 1 − (0.01 x 0.01) = 99.99%. Redundancy is how you buy nines.
Latency vs throughput
Latency is how long one car takes to drive down a road. Throughput is how many cars pass per hour. Widening the road (more lanes) raises throughput but does not make any single car faster. You can have high throughput and bad latency at the same time — for example, a batch system.
Measure latency with percentiles, not averages. p50 is the median (half of requests are faster). p99 means 99% of requests are faster than this; the slowest 1% are slower. Users notice the p99, and a page making 100 calls hits a p99 on almost every load.
Availability vs reliability vs durability
- A system that answers every request with wrong data is available but not reliable.
- A system that is down for an hour but loses nothing is not very available, but is durable.
5. How a Request Travels
When you type a web address, several things happen in order. The browser finds the server's address (DNS), opens a secure connection (TCP + TLS), sends an HTTP request, and the server sends back a response.
DNS (Domain Name System) is the internet's phone book: it turns a name like example.com into an IP address like 93.184.216.34, which is the actual location of a machine.
Client-server means one side asks (the client: a browser or app) and the other side answers (the server).
Sending a letter: look up the address in a directory (DNS), drive to the building (TCP connection), show ID at the door (TLS), hand over the letter (HTTP request), wait for the reply (HTTP response).
The steps in order
- DNS lookup — the browser asks "what is the IP for
api.example.com?" The answer is cached at several levels (browser, OS, internet provider) for a set time called the TTL (time to live). - TCP connection — TCP is the protocol that guarantees bytes arrive in order and complete. Opening one costs a round trip (the "three-way handshake").
- TLS handshake — TLS is the encryption behind the padlock and
https://. It proves the server is who it claims (with a certificate) and agrees a secret key. Costs 1–2 more round trips. - HTTP request — method (
GET,POST…), path, headers, maybe a body. - Server side — load balancer → app server → cache / database → build the response.
- HTTP response — a status code (200 OK, 404 Not Found, 500 Server Error), headers and body.
HTTP in one table
| Method | Meaning | Safe to repeat? |
|---|---|---|
GET | Read something | Yes — no changes |
POST | Create something / trigger an action | No — may create duplicates |
PUT | Replace something completely | Yes — same result each time (idempotent) |
PATCH | Change part of something | Not guaranteed |
DELETE | Remove something | Yes — gone is gone |
Idempotent means doing it twice has the same effect as doing it once. This matters for retries (section 16).
Versions: HTTP/1.1 uses one request at a time per connection. HTTP/2 sends many requests over one connection at once (multiplexing). HTTP/3 runs over QUIC (on UDP) to avoid delays when a packet is lost — good on mobile networks.
6. Vertical vs Horizontal Scaling
Vertical scaling (scaling up) means buying a bigger machine: more CPU, more RAM. Horizontal scaling (scaling out) means adding more machines and splitting the work between them.
A stateless server keeps nothing about a user between requests in its own memory. Any server can handle any request, because all the state (sessions, carts, uploads) lives in a shared store like Redis or a database.
A busy restaurant can hire one super-chef (vertical) or hire more normal chefs (horizontal). One super-chef has limits and if they get sick, the kitchen closes. Many chefs can keep growing, but they need a shared order board — that is the shared state store.
| Vertical (scale up) | Horizontal (scale out) | |
|---|---|---|
| How | Bigger machine | More machines |
| Code changes | None | App must be stateless; need a load balancer |
| Limit | Hard ceiling — the biggest machine you can buy | Nearly unlimited |
| Failure | One machine = single point of failure | One machine dies, others carry on |
| Cost | Big machines get expensive fast | Cheap commodity machines, but more to operate |
| Good for | Early stage, databases (for a while) | Web/app servers, anything at scale |
Making a server stateless
- Sessions → a shared store (Redis) or a signed token the client holds (JWT, section 24).
- Uploaded files → object storage (S3), never the server's local disk.
- In-memory caches → fine for data everyone shares (config), but user-specific data belongs in a shared cache.
- Scheduled jobs → a single scheduler or a queue, so ten servers don't run the same job ten times.
Once servers are stateless you can use auto-scaling: the cloud adds servers when CPU or queue length goes up, and removes them when traffic drops.
7. Load Balancers
A load balancer (LB) sits in front of a group of servers and decides which server gets each request. It spreads the load, and it stops sending traffic to a server that is broken.
The host at a restaurant door: they see which waiters are free and seat you at their table. If a waiter goes home sick, the host simply stops seating people there.
Layer 4 vs Layer 7
The "layers" come from the OSI network model. Layer 4 is the transport layer (TCP/UDP: IP addresses and ports). Layer 7 is the application layer (HTTP: URLs, headers, cookies).
| L4 load balancer | L7 load balancer | |
|---|---|---|
| Looks at | IP and port only | Full HTTP request: path, headers, cookies |
| Speed | Very fast, very cheap | Slower (must read and often decrypt the request) |
| Smart routing | No | Yes: /api to one group, /images to another |
| TLS | Usually passes it through | Usually terminates it (decrypts at the LB) |
| Examples | AWS NLB, HAProxy (TCP mode) | AWS ALB, Nginx, Envoy, HAProxy (HTTP mode) |
Algorithms
| Algorithm | How it works | Use when |
|---|---|---|
| Round robin | 1, 2, 3, 1, 2, 3… | Servers are identical, requests are similar |
| Weighted round robin | Bigger servers get more turns | Mixed server sizes |
| Least connections | Send to the server with the fewest open connections | Requests vary a lot in duration (WebSockets, uploads) |
| Least response time | Send to the fastest-responding server | Performance varies between servers |
| IP hash | Hash of client IP picks the server | Need the same client on the same server, without cookies |
| Consistent hashing | Hash of a key (user ID) on a ring (section 12) | Routing to caches or stateful shards |
Health checks
The LB calls an endpoint like /health every few seconds. After a few failures in a row, the server is removed; after a few passes, it comes back. Active checks are these regular pings; passive checks watch real traffic for errors.
Sticky sessions
Sticky sessions (session affinity) send the same user to the same server every time, usually via a cookie. They make a stateful app work behind an LB, but they cause uneven load and lose the user's state when that server dies. Prefer stateless servers; use stickiness only when you must (for example, a WebSocket connection is naturally sticky).
Is the load balancer a single point of failure?
It can be. Fixes: run two LBs in active-passive (one takes over via a shared floating IP) or active-active mode, or use a managed cloud LB that is already redundant. At the global level, DNS load balancing or GeoDNS sends users to the nearest region.
8. Caching
A cache is a small, fast store that keeps copies of data you read often, so you don't fetch it from the slow source (database, another service) every time. Caches usually live in RAM, which is about 1000x faster than disk.
A cache hit is when the data is in the cache. A cache miss is when it isn't and you must go to the source. The hit ratio is hits divided by all reads; aim for 80–95%+.
Keeping the salt next to the stove instead of in the basement pantry. You still keep a full supply in the pantry (the database), but everyday use comes from the shelf.
Where caches live (layers)
| Layer | Example | Caches |
|---|---|---|
| Browser / client | HTTP cache headers (Cache-Control), local storage | Static files, some API responses |
| CDN | Cloudflare, CloudFront | Images, video, JS, CSS, even whole pages |
| Load balancer / reverse proxy | Nginx, Varnish | Full HTTP responses |
| Application (in-process) | A map in the server's memory | Config, small hot lookups — fast but not shared |
| Distributed cache | Redis, Memcached | Query results, sessions, computed objects — shared by all servers |
| Database | Buffer pool, query cache | Pages of data on disk |
Cache patterns (how reads and writes flow)
| Pattern | How it works | Good | Bad |
|---|---|---|---|
| Cache-aside (lazy loading) | App checks cache; on miss reads DB and fills cache. On write, updates DB and deletes key. | Simple; cache holds only hot data; cache down = still works (slower) | First read is slow; possible stale data between DB write and key delete |
| Read-through | App only talks to the cache; the cache itself loads from DB on a miss. | Cleaner app code | Needs a cache library/provider that supports it |
| Write-through | Every write goes to cache and DB synchronously. | Cache always fresh | Slower writes; caches data nobody may read |
| Write-back (write-behind) | Write to cache only; cache flushes to DB later in batches. | Very fast writes; absorbs spikes | Data loss if cache dies before flushing |
| Write-around | Writes go straight to DB, skipping cache; reads use cache-aside. | Cache not flooded with write-once data | Recent writes always miss the first time |
Eviction — what to throw out when the cache is full
- LRU (Least Recently Used) — drop what hasn't been read for the longest time. The default choice.
- LFU (Least Frequently Used) — drop what is read least often. Better when some items are always popular.
- FIFO (First In, First Out) — drop the oldest entry. Simple, rarely the best.
- TTL (Time To Live) — each entry expires after a set time. Not really eviction, but always set one: it limits how stale data can get.
Invalidation — keeping the cache correct
"There are only two hard things in computer science: cache invalidation and naming things." When the database changes, the cached copy is stale (out of date). Options:
- Delete on write — after updating the DB, delete the key. The next read refills it. Prefer delete over update: updating can race with another writer and leave the wrong value.
- Short TTL — accept up to N seconds of staleness. Simple and robust.
- Event-driven — the DB's change stream (CDC, change data capture) publishes events that delete keys.
- Versioned keys —
user:42:v7; bump the version on change so old keys are simply never read again.
Problems you should name
| Problem | What happens | Fix |
|---|---|---|
| Cache stampede (thundering herd) | A hot key expires; thousands of requests miss at once and all hit the DB | Lock so only one request refills (others wait or get stale value); refresh early before expiry; add random jitter to TTLs |
| Hot key | One key (a celebrity's profile) gets so many reads that one cache node melts | Replicate the key to several nodes (key#1…key#N, pick randomly); local in-process cache for it |
| Cache penetration | Requests for keys that don't exist always miss and hit the DB (often an attack) | Cache the "not found" result briefly; use a Bloom filter (a compact "definitely not present" check) |
| Cache avalanche | Many keys expire at the same moment, or the cache cluster restarts | Jittered TTLs; cache warm-up; redundancy in the cache cluster |
Redis vs Memcached
Redis: rich data types (lists, sets, sorted sets, hashes), persistence, replication, pub/sub, Lua scripts. Memcached: plain key-value strings, multi-threaded, very simple. Default to Redis unless you only need a dumb, huge cache.
9. CDN (Content Delivery Network)
A CDN is a worldwide network of cache servers, called edge servers or points of presence (PoPs), placed close to users. They keep copies of your static files — images, video, JavaScript, CSS — so a user in Tokyo downloads them from Tokyo, not from your server in Virginia.
Your own server, where the original files live, is called the origin.
A publisher prints books in one factory but ships copies to bookshops in every city. Readers buy from their local shop; only when a shop runs out does it order from the factory.
Push vs pull CDN
| Pull CDN | Push CDN | |
|---|---|---|
| How content gets there | Edge fetches from origin on the first request, then caches it | You upload content to the CDN in advance |
| First request | Slow (miss, goes to origin) | Fast (already there) |
| Effort | Low — just point the CDN at your origin | You manage uploads and updates |
| Best for | Sites with lots of content, unpredictable popularity | Large files that rarely change, known in advance (video releases, game patches) |
What to know
- Cache control: the origin sets
Cache-Control: max-age=…to tell the CDN how long to keep a file. - Cache busting: put a version or hash in the filename (
app.3f9a2c.js). A new deploy means a new name, so users never get an old file. Much easier than asking the CDN to purge. - Dynamic content: modern CDNs also speed up API calls by keeping warm connections to the origin, and can run small bits of code at the edge.
- Bonus: CDNs absorb DDoS attacks (section 24) and reduce your bandwidth bill.
- Cost: you pay per GB delivered; stale content is possible until TTL expires.
10. Choosing a Database
SQL databases (relational: PostgreSQL, MySQL) store data in tables with fixed columns, link tables with keys, and support transactions — a group of changes that all succeed or all fail together.
NoSQL is a family of "not only SQL" databases. Each type gives up some SQL features (joins, strict schema, sometimes transactions) to gain something else: easy horizontal scaling, flexible shape, or speed for one access pattern.
SQL is a well-organised filing cabinet with strict forms: every form has the same boxes, and you can cross-reference forms easily. NoSQL is a set of specialised containers: a key rack, a folder box, a huge ledger, a web of strings on a pinboard — each great for one job.
ACID vs BASE
- ACID (typical of SQL): Atomic (all or nothing), Consistent (rules like "balance can't go negative" always hold), Isolated (concurrent transactions don't see each other's half-done work), Durable (committed means saved).
- BASE (typical of many NoSQL): Basically Available, Soft state, Eventually consistent — replicas agree eventually, not instantly.
The database types and when to use each
| Type | Examples | Data looks like | Use when | Avoid when |
|---|---|---|---|---|
| Relational (SQL) | PostgreSQL, MySQL | Tables, rows, foreign keys | Structured data, relationships, transactions: payments, orders, users, inventory | Massive write scale across many machines (possible but hard) |
| Key-value | Redis, DynamoDB, etcd | key → value | Lookups by one key: sessions, caches, carts, feature flags, rate-limit counters | You need to query by anything other than the key |
| Document | MongoDB, Couchbase, Firestore | JSON-like documents | Flexible or nested data read as a whole: product catalogues, user profiles, CMS content | Lots of cross-document joins and transactions |
| Wide-column | Cassandra, HBase, ScyllaDB, Bigtable | Rows with a partition key and sorted columns | Huge write volume, time-ordered data per key: chat messages, activity feeds, IoT, logs | Ad-hoc queries, joins, strong transactions |
| Graph | Neo4j, Amazon Neptune | Nodes and edges | Relationship-heavy queries: friends-of-friends, recommendations, fraud rings | Simple lookups or huge bulk scans |
| Time-series | InfluxDB, TimescaleDB, Prometheus | Timestamp + value + tags | Metrics, monitoring, sensor readings, stock ticks | General app data |
| Search engine | Elasticsearch, OpenSearch | Inverted index over documents | Full-text search, filters, fuzzy matching (section 19) | As the primary source of truth |
| Object storage | S3, GCS, Azure Blob | Files (blobs) by key | Images, video, backups, big files (section 18) | Small, frequently updated records |
How to choose (the questions to ask yourself)
- Do I need transactions across several records? (money, stock levels) → SQL.
- What is the access pattern? Always by one key → key-value. Whole object at once → document. Append-heavy, time-ordered per user → wide-column.
- How much write volume? Tens of thousands of writes per second across regions → Cassandra/DynamoDB style.
- Is the schema stable? Stable → SQL. Changing often or varied → document.
- Are relationships the point? → graph.
Polyglot persistence means using several databases in one system, each for what it does best — for example Postgres for users and orders, Redis for sessions, Cassandra for messages, S3 for photos, Elasticsearch for search.
11. Replication
Replication means keeping copies of the same data on several machines. Each copy is a replica. It gives you three things: durability (a disk dies, data survives), availability (a machine dies, another answers), and read scaling (spread reads across copies).
Important documents kept in three offices. If one office floods, you still have the papers, and staff in all three can read them. The hard part is keeping all three copies up to date when something changes.
Three replication styles
| Style | How it works | Good | Bad | Seen in |
|---|---|---|---|---|
| Leader-follower (primary-replica, master-slave) | One leader takes all writes and streams changes to followers. Reads from any. | Simple, no write conflicts | Leader is a write bottleneck; failover takes seconds | PostgreSQL, MySQL, MongoDB, Redis |
| Multi-leader | Several leaders (often one per region) each accept writes and sync with each other. | Low write latency in every region; survives a region outage | Write conflicts (two people edit the same row) must be resolved | Multi-region setups, offline-first apps, collaborative editing |
| Leaderless | Client writes to several replicas at once and reads from several; quorums decide (section 14). | No failover needed; very available | Weaker consistency; conflict handling needed | Cassandra, DynamoDB, Riak |
Synchronous vs asynchronous
- Synchronous: the leader waits for the follower to confirm before telling the client "saved". Safe (no data loss on failover) but slower, and a slow follower blocks writes.
- Asynchronous: the leader confirms immediately and ships changes in the background. Fast, but if the leader dies the last few writes may be lost.
- Semi-synchronous: one follower is synchronous, the rest async. Common compromise.
Replication lag and its symptoms
Replication lag is the delay before a follower has the latest data. Usually milliseconds, sometimes seconds under load. It causes visible bugs:
- Read-your-own-write fails — you post a comment, the page reloads from a lagging follower, your comment is missing. Fix: read from the leader for a few seconds after the user writes, or read the user's own data from the leader.
- Time going backwards — two reads hit different followers; the second is further behind. Fix: monotonic reads — pin a user to one follower.
Failover
Failover is switching to a replica when the leader dies: detect the failure (heartbeats time out), pick the most up-to-date follower, point writes at it. Danger: split brain — the old leader comes back and both think they are leader. Fixed with fencing (the old leader's writes are rejected) and consensus (section 22).
12. Sharding (Partitioning)
Sharding means splitting one big dataset across several machines, so each machine (a shard) holds only part of the data. Replication copies the same data; sharding splits different data. Real systems do both: each shard is replicated.
The shard key (partition key) is the field used to decide which shard a row lives on — for example user_id.
A library too big for one building. You split it: A–F in building 1, G–M in building 2, and so on. Each building can also keep a spare copy in a warehouse (replication).
Sharding strategies
| Strategy | How | Good | Bad |
|---|---|---|---|
| Range | Rows by key range: users A–F on shard 1, G–M on shard 2; or by date | Range queries are easy ("all orders in March") | Hotspots: all new data lands on the latest range |
| Hash | shard = hash(key) % N | Even spread | Range queries hit every shard; changing N moves almost every key |
| Consistent hashing | Keys and shards on a ring; key goes to the next shard clockwise | Adding a shard moves only ~1/N of keys | More complex; needs virtual nodes for balance |
| Directory (lookup) | A lookup table maps key → shard | Total flexibility; move single tenants | The directory is an extra hop and a possible single point of failure |
| Geographic | By user region: EU users in EU shard | Low latency; data residency laws | Uneven region sizes |
Consistent hashing
With plain hash(key) % N, going from 4 to 5 servers changes the answer for about 80% of keys — a huge, painful data move. Consistent hashing fixes this.
Virtual nodes: with only three points on the ring, one node may get a huge arc and most of the keys. So each physical server is placed at many points (for example 100–200 "virtual nodes"). This evens out load, and a stronger server can simply get more virtual nodes. Used by Cassandra, DynamoDB, and many caches.
Resharding
Resharding is changing how data is split — usually adding shards as data grows. It is expensive: data must be copied while the system keeps serving. Ways to make it easier:
- Consistent hashing so only a fraction moves.
- Start with many logical shards (say 1024) mapped onto a few physical machines. Growing means moving whole logical shards to new machines, never re-splitting data.
- Double-write during migration: write to old and new location, backfill, then switch reads.
Hotspots and the celebrity problem
A hotspot is one shard getting far more traffic than the others — for example a celebrity with 100 million followers when sharding by user_id. Fixes: add a random suffix to hot keys to spread them (celebrity_id#0..9), cache hot items, or give hot tenants their own shard.
What sharding costs you
- Cross-shard queries — a query not on the shard key must ask every shard (scatter-gather) — slow.
- Cross-shard joins and transactions — mostly gone; you design around them (denormalise, sagas).
- Operational complexity — backups, schema changes, monitoring times N.
user_id because almost every query is per-user, and it spreads evenly. The risk is celebrity hotspots, which I'd handle with caching."13. Indexing, Denormalisation, Read Replicas
These are the three cheapest ways to make reads faster before you reach for sharding.
An index is a sorted lookup structure on one or more columns, so the database can jump straight to matching rows instead of reading the whole table (a full table scan).
Denormalisation means deliberately storing the same data in more than one place to avoid joins at read time.
An index is the index at the back of a textbook: go straight to page 214 instead of reading every page. Denormalisation is writing the author's name on every book spine instead of making people look it up in a separate catalogue.
Indexes
- B-tree index — the default. A balanced tree, good for equality (
=) and ranges (>,BETWEEN,ORDER BY). Lookup cost grows slowly (log n). - Hash index — only equality lookups, very fast.
- Composite index — on several columns, e.g.
(user_id, created_at). Follows the leftmost prefix rule: it helps queries onuser_id, oruser_id + created_at, but not oncreated_atalone. - Covering index — the index includes every column the query needs, so the table itself is never read.
- LSM tree (log-structured merge tree) — used by Cassandra, RocksDB. Writes go to memory then are flushed to sorted files; very fast writes, reads may check several files.
The cost: every index slows down writes (it must be updated too) and uses storage. Index the columns you filter, join and sort on — not everything.
Normalisation vs denormalisation
| Normalised | Denormalised | |
|---|---|---|
| Idea | Each fact stored once; join to combine | Copy data where it is read; no joins |
| Reads | Slower (joins) | Faster (one lookup) |
| Writes | Simple, one place | Must update every copy |
| Risk | Join cost at scale | Copies drifting out of sync |
| Use for | Write-heavy, correctness-critical data | Read-heavy views, NoSQL, sharded data |
Related: a materialised view is a precomputed query result stored like a table, refreshed periodically. CQRS (Command Query Responsibility Segregation) means using separate models (even separate databases) for writes and for reads, synced via events.
Read replicas
Followers from section 11 used to serve read traffic. A read-heavy app (most are) can add replicas to multiply read capacity. Remember replication lag: route "read after my own write" to the leader.
The scaling ladder for a database
- Add the right indexes; fix slow queries.
- Add a cache in front (section 8).
- Scale the machine up (vertical).
- Add read replicas.
- Denormalise hot read paths / use materialised views.
- Split by feature (users DB, orders DB) — vertical partitioning.
- Shard (section 12).
14. CAP Theorem, PACELC and Consistency
The CAP theorem says a distributed data store can't guarantee all three at once: Consistency (every read sees the latest write), Availability (every request gets a non-error answer), and Partition tolerance (keeps working when the network between machines breaks).
Networks do break, so P is not optional. The real choice is: when a partition happens, do you stay consistent (refuse some requests) or stay available (maybe return stale data)?
Two bank branches lose their phone line. A customer wants to withdraw. The branch can refuse until the line is back (consistent), or allow it and risk the account going negative (available). It can't be both.
| Choice during a partition | Behaviour | Good for | Examples |
|---|---|---|---|
| CP — consistency | Some requests fail or wait rather than return stale data | Payments, bookings, inventory, leader election | HBase, ZooKeeper, etcd, MongoDB (default), Spanner |
| AP — availability | Always answers, maybe with stale data; syncs later | Social feeds, likes, carts, DNS, product catalogue | Cassandra, DynamoDB (default), CouchDB |
PACELC — the fuller picture
CAP only talks about failures. PACELC adds normal times: "if Partition, choose A or C; Else (normal operation), choose Latency or Consistency." Even with a healthy network, waiting for replicas to agree costs time.
- PA/EL — Cassandra, DynamoDB: available and fast, eventually consistent.
- PC/EC — Spanner, HBase: consistent always, pays in latency.
Consistency models (strongest to weakest)
| Model | Promise | Example need |
|---|---|---|
| Strong / linearisable | Every read sees the latest write, as if there were one copy | Bank balance, seat booking |
| Sequential / causal | Everyone sees related events in the same order (a reply after its question) | Comments and replies, chat |
| Read-your-writes | You always see your own updates (others may not yet) | Editing your profile |
| Monotonic reads | You never see data go back in time | Refreshing a feed |
| Eventual | If writes stop, all replicas converge eventually | Like counts, view counts, DNS |
Quorums (tunable consistency)
In leaderless systems with N replicas, a write waits for W confirmations and a read asks R replicas. A quorum is a majority.
If W + R > N -> every read overlaps at least one replica with the latest write
-> strong-ish consistency
Typical: N = 3, W = 2, R = 2 (2 + 2 > 3) balanced
N = 3, W = 1, R = 1 fast, eventual
N = 3, W = 3, R = 1 fast reads, slow and fragile writes
Conflicts in eventually consistent stores are resolved with last-write-wins (timestamps; simple but can lose data), version vectors (detect concurrent writes and let the app merge), or CRDTs (data types that merge automatically, like counters).
15. Message Queues & Event Streaming
A message queue is a waiting line for work between services. One service (the producer) drops a message in; another (the consumer) picks it up when ready. The producer doesn't wait. This is called asynchronous processing and decoupling — the two sides don't need to be up, fast, or even know about each other at the same time.
The order spike at a restaurant: waiters pin tickets on a rail and go back to customers. Cooks take tickets at their own pace. A rush of orders makes the rail longer, but nobody is turned away and nobody waits at the kitchen door.
Why use a queue
- Absorb spikes — 10x traffic for a minute just makes the queue longer; the database isn't crushed.
- Do slow work later — send emails, resize images, generate reports without making the user wait.
- Survive failures — the consumer is down? Messages wait until it's back.
- Decouple teams and services — the producer doesn't know or care who consumes.
Queue vs publish/subscribe
| Point-to-point queue | Pub/sub (topic) | |
|---|---|---|
| Who gets a message | Exactly one consumer from the group | Every subscriber gets a copy |
| Use for | Distributing work: "resize this image" | Broadcasting events: "order placed" → email, analytics, inventory all react |
| Examples | RabbitMQ queues, AWS SQS | Kafka topics, AWS SNS, Google Pub/Sub, Redis pub/sub |
Kafka in plain words
Apache Kafka is an event streaming platform: a durable, append-only log of events that many consumers can read, and re-read, at their own pace.
- Topic — a named stream of events, like
orders. - Partition — a topic is split into partitions for parallelism. Order is guaranteed within a partition only. Messages with the same key (say
order_id) always go to the same partition, so they stay in order. - Offset — a message's position in the partition. Each consumer remembers its offset, so it can pause, resume or replay.
- Consumer group — consumers sharing the work; each partition is read by one consumer in the group. More partitions = more parallel consumers.
- Retention — messages are kept for a set time (days) even after being read, unlike a classic queue that deletes on ack.
- Replication — each partition is copied to several brokers (Kafka servers).
| RabbitMQ / SQS (message queue) | Kafka (event log) | |
|---|---|---|
| After reading | Message deleted | Message kept; can replay |
| Throughput | High | Very high (millions/sec) |
| Ordering | Per queue (FIFO queues) or best-effort | Per partition |
| Routing | Rich (RabbitMQ exchanges) | Simple: topic + key |
| Best for | Task queues, jobs | Event streams, analytics pipelines, many consumers, event sourcing |
Delivery guarantees
| Guarantee | Meaning | How | Risk |
|---|---|---|---|
| At-most-once | Delivered 0 or 1 times | Ack before processing | Lost messages |
| At-least-once | Delivered 1 or more times | Ack after processing; retry on failure | Duplicates |
| Exactly-once | Effect happens exactly once | At-least-once + idempotent consumer, or transactional systems (Kafka transactions) | Complex, slower |
In practice: use at-least-once delivery with idempotent consumers. Idempotent means processing a message twice has the same effect as once — for example, store processed message IDs and skip repeats, or use "set status = paid" rather than "add 10 to balance".
Dead-letter queue and backpressure
- Dead-letter queue (DLQ) — after N failed attempts, a "poison" message is moved aside so it doesn't block the queue forever. Alert on it; inspect and replay later.
- Backpressure — signalling upstream to slow down when consumers can't keep up. Options: bounded queue that rejects when full, rate-limit producers, auto-scale consumers on queue length, or shed low-priority work.
- Visibility timeout (SQS) — a taken message is hidden for N seconds; if not acked in time, it reappears for another worker.
16. API Design & Communication
An API (Application Programming Interface) is the contract for how one program talks to another: which requests are allowed, what they must contain, and what comes back.
There are two questions: what style of API (REST, GraphQL, gRPC) and how the conversation flows (client asks each time, or server pushes updates).
A restaurant menu is an API. REST is a fixed menu of dishes. GraphQL is "build your own plate" — you list exactly what you want. gRPC is the kitchen's internal shorthand between chefs: fast and precise, but not meant for customers.
REST vs GraphQL vs gRPC
| REST | GraphQL | gRPC | |
|---|---|---|---|
| Shape | Resources at URLs + HTTP verbs | One endpoint; client sends a query describing the fields it wants | Call remote functions defined in a .proto file |
| Format | JSON (text) | JSON | Protocol Buffers (compact binary) |
| Strengths | Simple, cacheable by HTTP, universal | No over-fetching or under-fetching; one round trip for nested data; great for varied clients | Very fast, strongly typed, streaming, code generation |
| Weaknesses | Over/under-fetching; many round trips | Harder caching; expensive queries possible; more server complexity | Not browser-native; not human-readable |
| Use for | Public APIs, most CRUD apps | Mobile + web apps with complex, changing UI needs | Internal service-to-service calls |
Over-fetching = getting fields you don't need. Under-fetching = needing several calls to get everything.
Good REST habits
GET /api/v1/users/42/posts?limit=20&cursor=abc list a user's posts
POST /api/v1/posts create (201 Created)
GET /api/v1/posts/987 read one
PATCH /api/v1/posts/987 partial update
DELETE /api/v1/posts/987 delete (204 No Content)
- Nouns in URLs, verbs are HTTP methods. Plural collections.
- Version the API (
/v1) so you can change it without breaking old clients. - Correct status codes: 200, 201, 204, 400 (bad input), 401 (who are you?), 403 (not allowed), 404, 409 (conflict), 429 (too many requests), 500, 503.
Polling vs long polling vs WebSockets vs SSE
| Technique | How it works | Good | Bad | Use for |
|---|---|---|---|---|
| Short polling | Client asks "anything new?" every N seconds | Dead simple | Wasteful; delay up to N seconds | Rare updates, simple dashboards |
| Long polling | Client asks; server holds the request open until there's news (or timeout), then client asks again | Near real-time with plain HTTP | A connection held per client; reconnect overhead | Fallback when WebSockets aren't possible |
| WebSockets | One persistent, two-way connection | Real-time both directions, low overhead | Stateful connections — harder to load-balance and scale | Chat, multiplayer games, collaborative editing |
| Server-Sent Events (SSE) | One persistent, server-to-client stream over HTTP | Simple, auto-reconnect, works with HTTP infra | One direction only; text only | Live feeds, notifications, stock tickers, streaming AI responses |
| Webhooks | Server calls your URL when something happens | No polling between servers | Receiver must be public and reliable | Payment events (Stripe), GitHub events |
Pagination
| Offset pagination | Cursor (keyset) pagination | |
|---|---|---|
| Request | ?limit=20&offset=40 | ?limit=20&cursor=<last id or timestamp> |
| Good | Simple; jump to page N | Fast at any depth; stable when new items arrive |
| Bad | Slow for deep pages (DB still skips rows); items shift and duplicate when new ones are inserted | No "jump to page 50" |
| Use for | Admin tables, small data | Feeds, infinite scroll, large datasets |
Idempotency keys
Networks fail. A client sends "pay $50", the response is lost, the client retries — did we charge twice? An idempotency key is a unique ID the client generates and sends with the request (header Idempotency-Key: 7f3a…). The server stores the key with the result; a repeat with the same key returns the stored result instead of acting again.
POST /payments Idempotency-Key: 7f3a9c { amount: 50 }
server: key seen before? yes -> return saved response
no -> process, save (key -> response) with a TTL, return
17. Rate Limiting
Rate limiting caps how many requests a client can make in a time window — for example 100 requests per minute per user. Extra requests get 429 Too Many Requests.
It protects against abuse (scraping, brute-force password guessing), keeps one noisy customer from hurting everyone, and controls cost.
A nightclub bouncer letting in a set number of people per minute. The algorithms below are just different rules the bouncer can follow.
The algorithms
| Algorithm | How it works | Good | Bad |
|---|---|---|---|
| Token bucket | A bucket holds up to B tokens and refills at R per second. Each request takes a token; empty bucket = reject. | Allows short bursts (up to B), smooth average (R); tiny memory. Most popular (AWS, Stripe). | Two parameters to tune |
| Leaky bucket | Requests enter a fixed-size queue that drains at a constant rate. Full = reject. | Perfectly smooth output rate; protects a fragile backend | Bursts are delayed or dropped; old requests can starve new ones |
| Fixed window counter | Count per window (per minute, reset on the minute) | Simplest; one counter per user | Edge burst: 100 at 0:59 plus 100 at 1:00 = 200 in two seconds |
| Sliding window log | Store the timestamp of every request; count those in the last 60 s | Exact | Memory heavy (one entry per request) |
| Sliding window counter | Blend current and previous window counts: prev x overlap% + current | Close to exact, cheap memory | An approximation (assumes even spread in the previous window) |
Token bucket (per user), run on each request:
tokens = min(capacity, tokens + (now - last_refill) x rate)
last_refill = now
if tokens >= 1: tokens -= 1; allow
else: reject with 429 and Retry-After header
Sliding window counter example (limit 100/min):
previous minute: 80 requests, current minute: 30, we are 25% into it
estimate = 80 x 0.75 + 30 = 90 -> allow (90 < 100)
Where to put it
- Client side — nice for UX, never trusted (easily bypassed).
- API gateway / load balancer / edge — the usual place; rejects bad traffic before it costs you anything.
- In each service — protects specific expensive endpoints.
- Distributed — with many gateway servers, counters live in a shared Redis so all servers see the same count. Use atomic operations (
INCR+EXPIRE, or a Lua script) to avoid race conditions.
What to limit by
User ID or API key (best), IP address (for anonymous traffic — but many users can share one IP), endpoint (login stricter than reads), and plan tier (free vs paid). Return headers such as X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After.
18. Blob / Object Storage & File Uploads
A blob (binary large object) is any file: an image, video, PDF, backup. Object storage (Amazon S3, Google Cloud Storage, Azure Blob) stores blobs by key in buckets, is nearly unlimited, very durable (S3 advertises eleven nines, 99.999999999%), and cheap.
Databases are bad at big files: they bloat backups, eat memory and slow queries. The rule is: file in object storage, metadata (name, owner, size, key) in the database.
A cloakroom. You hand over a heavy coat (the file) and get a small ticket (the key). Your table only needs the ticket, not the coat.
Presigned URLs — upload straight to storage
Sending a 2 GB video through your app servers wastes their bandwidth and memory. Instead:
- Client asks your API: "I want to upload
video.mp4, 2 GB". - API checks permissions, creates a metadata row (status:
pending), and asks S3 for a presigned URL — a temporary URL with a signature that allows exactly one upload to one key for, say, 15 minutes. - Client uploads directly to S3 using that URL.
- S3 sends an event (or the client calls "upload complete"); the API marks the row
uploadedand queues processing (thumbnails, virus scan, transcoding).
Downloads work the same way: presigned GET URLs for private files, or a CDN in front of public ones.
Chunked (multipart) uploads
Big files are split into chunks (say 5–10 MB each) uploaded separately, even in parallel. If the network drops, only the failed chunk is retried — the upload is resumable. The server (S3 multipart upload) stitches them together at the end. Each chunk can carry a checksum (a short fingerprint of its contents) to detect corruption.
Storage classes
Hot (frequently read, standard price), infrequent access (cheaper storage, pay per read), and archive (Glacier: very cheap, hours to retrieve). Lifecycle rules move old files to cheaper classes automatically.
19. Search
WHERE title LIKE '%phone%' scans every row and can't rank results. Real search uses an inverted index: a map from each word to the list of documents containing it — the reverse of "document → words".
The index at the back of a cookbook: "garlic — pages 12, 45, 88". You don't read every recipe to find garlic; you look it up and go straight to the pages.
How an inverted index is built
Doc 1: "Red running shoes" Doc 2: "Blue shoes for running" Doc 3: "Red dress"
Tokenise, lowercase, stem ("running" -> "run"), drop stop words ("for"):
red -> [1, 3]
run -> [1, 2]
shoe -> [1, 2]
blue -> [2]
dress -> [3]
Query "red shoes" -> red [1,3] AND shoe [1,2] -> doc 1 (then rank)
Ranking uses scores like TF-IDF / BM25: a word counts more if it appears often in this document (term frequency) but rarely across all documents (inverse document frequency) — "the" is worthless, "Bluetooth" is meaningful.
Elasticsearch / OpenSearch
- A distributed search engine built on the Lucene library. Data is split into shards with replicas, across nodes.
- Supports full-text search, filters, fuzzy matching (typos), aggregations (facets like "brand: 12 results").
- Not the source of truth. The main database is. Sync changes to the search index via a queue or CDC (change data capture) stream. The index is near real-time — searchable after about a second.
Typeahead with a trie
A trie (prefix tree) stores words letter by letter; every path from the root spells a prefix. To suggest completions for "ca", walk c → a and collect words below. Store the top-k most popular completions at each node so lookup is instant. Full design in section 34.
20. Monolith vs Microservices
A monolith is one application, deployed as one unit, containing all features. Microservices split the product into many small services (users, orders, payments), each owning its own data, deployed independently, talking over the network.
A monolith is one big department store: everything under one roof, easy to walk between departments, but renovating one floor disturbs everyone. Microservices are a street of specialist shops: each can renovate or open late independently, but you need roads, signs and delivery vans between them.
| Monolith | Microservices | |
|---|---|---|
| Development | Simple to start, one codebase | Each team owns a service |
| Deploy | All or nothing | Independent, frequent |
| Scaling | Scale the whole app | Scale just the hot service |
| Calls between parts | Function calls (fast, reliable) | Network calls (slow, can fail) |
| Data | One database, easy joins and transactions | Database per service; no cross-service joins; sagas for transactions |
| Failure | A bug can take down everything | Failures isolated — if you add timeouts and circuit breakers |
| Operations | Easy | Hard: tracing, discovery, many pipelines |
| Best for | Small teams, early products | Many teams, large scale, parts with very different needs |
A modular monolith — one deployable, but with strict internal boundaries between modules — is often the best of both, and makes a later split easy.
The supporting pieces
- API gateway — the single front door for clients. It routes requests to the right service and handles cross-cutting jobs: authentication, rate limiting, TLS, request logging, response aggregation. Examples: Kong, AWS API Gateway, Nginx, Envoy. A BFF (Backend for Frontend) is a gateway tailored to one client type (mobile vs web).
- Service discovery — services start and stop constantly, with changing IPs. A service registry (Consul, etcd, Eureka, or Kubernetes DNS) keeps the live list; callers look up "where is the payment service right now?"
- Service mesh — a small proxy (a sidecar, e.g. Envoy) runs next to every service and handles networking: retries, timeouts, mutual TLS encryption, metrics, traffic splitting. The app code stays simple. Examples: Istio, Linkerd.
- Communication — synchronous (REST/gRPC: caller waits) for queries that need an answer now; asynchronous (events via Kafka) for "something happened" notifications. Prefer async between services to reduce coupling.
21. Reliability Patterns
In a big system something is always broken: a disk, a server, a network link, a slow dependency. Reliability patterns make sure one failure stays small instead of spreading into a total outage (a cascading failure).
A ship is built with watertight compartments (bulkheads), spare lifeboats (redundancy), and a captain who stops sending crew into a flooded room (circuit breaker). A hole sinks one room, not the ship.
The patterns
| Pattern | What it does | Watch out for |
|---|---|---|
| Redundancy | Two or more of everything important, across availability zones (separate data centres in one region) or regions | Cost; keeping copies in sync |
| Failover | Switch to the standby when the primary dies. Active-passive: standby idle until needed. Active-active: all copies serve traffic. | Detection time; split brain |
| Timeouts | Never wait forever on a call; give up after e.g. 1–2 s | Too long = threads pile up; too short = false failures |
| Retries with exponential backoff + jitter | Retry failed calls, waiting 100 ms, 200 ms, 400 ms… plus a random extra so clients don't retry in sync | Only retry idempotent operations and transient errors; cap attempts; retry storms |
| Circuit breaker | After many failures, stop calling a broken dependency for a while; fail fast instead | Needs a sensible fallback |
| Bulkhead | Separate resource pools (threads, connections) per dependency so one slow dependency can't use up everything | Tuning pool sizes |
| Graceful degradation | Turn off non-essential features under stress: show cached recommendations, hide the "trending" box | Must be designed in advance |
| Load shedding | When overloaded, reject some requests early (503) to protect the rest | Choose what to drop (low priority first) |
| Health checks & auto-restart | Remove and replace broken instances automatically | Distinguish "alive" from "ready" |
Exponential backoff with jitter:
delay = min(cap, base x 2^attempt)
sleep(random(0, delay)) # "full jitter"
attempt 0: up to 100 ms, 1: up to 200 ms, 2: up to 400 ms, 3: up to 800 ms ... cap at 10 s
Circuit breaker states
22. Distributed Systems Essentials
Once data and work live on many machines, simple things become hard: making a unique ID, updating two databases together, deciding who's in charge, locking something, even agreeing what time it is. These are the standard answers.
A group project with members in different cities and no shared room. You need rules for numbering documents, for agreeing on decisions, for who holds the pen, and you can't trust everyone's watch.
Unique ID generation
| Approach | How | Good | Bad |
|---|---|---|---|
| DB auto-increment | The database hands out 1, 2, 3… | Simple, short, ordered | Single point of failure and bottleneck; doesn't work across shards (unless each shard steps by N: 1, 4, 7…) |
| UUID v4 | 128-bit random number generated anywhere | No coordination at all | Long (36 chars); random order hurts B-tree index inserts; not sortable by time |
| UUID v7 / ULID | Timestamp prefix + randomness | No coordination and roughly time-sortable | Still 128 bits |
| Snowflake (Twitter) | 64-bit: 41 bits timestamp (ms) + 10 bits machine ID + 12 bits sequence | Compact, time-sortable, ~4096 IDs per ms per machine, no coordination per ID | Needs unique machine IDs; clock going backwards causes trouble |
| Ticket server / range allocation | A central service hands out blocks (IDs 1–1000 to server A) | Short IDs, little coordination | Central service must be highly available |
Distributed transactions: 2PC vs saga
Placing an order might need to charge payment (payment service), reserve stock (inventory service) and create the order (order service) — three databases. How do they succeed or fail together?
| Two-phase commit (2PC) | Saga | |
|---|---|---|
| How | A coordinator asks everyone "can you commit?" (prepare). If all say yes, it says "commit"; if anyone says no, "abort". | A sequence of local transactions. If a step fails, run compensating actions to undo earlier steps (refund, release stock). |
| Consistency | Strong (atomic) | Eventual; intermediate states are visible |
| Availability | Blocking: if the coordinator dies mid-way, participants hold locks and wait | Non-blocking, services stay independent |
| Styles | — | Choreography: each service reacts to events. Orchestration: a central orchestrator tells each step what to do. |
| Use for | Few participants, same data centre, strict needs | Microservices, long-running business flows |
The outbox pattern solves "update my DB and publish an event" atomically: write the event into an outbox table in the same local transaction, and a separate process publishes outbox rows to the queue.
Leader election and consensus (Raft in plain words)
Consensus means several machines agreeing on one value (who is leader, what the next log entry is) even if some crash. Raft is the most common algorithm:
- Every node starts as a follower. The leader sends regular heartbeats.
- If a follower hears nothing for a random timeout, it becomes a candidate, increases the term (election number) and asks others for votes.
- A candidate that gets votes from a majority becomes leader for that term. Majority means only one leader per term is possible.
- The leader receives writes, appends them to its log, and copies them to followers. An entry is committed once a majority has it.
- That's why clusters have an odd number of nodes: 3 nodes survive 1 failure, 5 survive 2.
You rarely implement Raft; you use a system built on it: etcd, Consul, or ZooKeeper (which uses a similar protocol, ZAB). Paxos is the older, harder-to-explain relative.
Distributed locks
A distributed lock makes sure only one process across many machines does something at a time (e.g. only one worker processes a given payment).
- Redis:
SET lock:order42 <owner> NX PX 30000— set only if not exists, auto-expire in 30 s. Release only if you're still the owner. Simple, but not perfectly safe during failovers. - ZooKeeper / etcd: consensus-backed, safer, slower.
- Fencing tokens: the lock service hands out an increasing number with each lock; the storage rejects writes with an old number. Protects against a paused process that wakes up thinking it still holds the lock.
- Often better: avoid locks with optimistic concurrency — a version column,
UPDATE … WHERE version = 7; if 0 rows change, someone else won, retry.
Clocks
Machine clocks drift and can jump (NTP corrections), so "compare timestamps from two machines" is unreliable for ordering events.
- Lamport clocks — a counter each node increments and attaches to messages; gives a consistent order of cause and effect.
- Vector clocks — one counter per node; can tell whether two events are ordered or truly concurrent (a conflict).
- TrueTime (Google Spanner) — GPS and atomic clocks with a known error bound; waits out the uncertainty to give global ordering.
23. Observability
Observability is how well you can tell what's happening inside the system from the outside — especially why something is broken. It rests on three "pillars": logs, metrics and traces.
A hospital patient: metrics are the heart-rate monitor (numbers over time), logs are the nurse's notes (what happened, when), and a trace is following one blood sample through every lab it visits.
| Pillar | What it is | Answers | Tools |
|---|---|---|---|
| Logs | Timestamped records of events, ideally structured (JSON) with a request ID | "What exactly happened to this request?" | ELK (Elasticsearch, Logstash, Kibana), Loki, Datadog |
| Metrics | Numbers over time: request rate, error rate, latency percentiles, CPU, queue depth | "Is something wrong right now? Since when?" | Prometheus + Grafana, CloudWatch |
| Traces | The path of one request across services, with timing for each step (spans) | "Which service made this slow?" | OpenTelemetry, Jaeger, Zipkin |
What to measure: the RED method for services — Rate, Errors, Duration. The four golden signals (Google SRE): latency, traffic, errors, saturation (how full a resource is).
SLI, SLO, SLA
| Term | Meaning | Example |
|---|---|---|
| SLI — Service Level Indicator | The measurement | % of requests answered successfully in under 300 ms |
| SLO — Service Level Objective | The internal target for the SLI | 99.9% of requests under 300 ms, over 30 days |
| SLA — Service Level Agreement | A promise to customers, with penalties | 99.5% uptime or we refund 10% |
SLA is looser than SLO so you get warned before you owe money. The gap between 100% and the SLO is the error budget: if you have budget left, ship features fast; if it's used up, focus on reliability.
Alerting
- Alert on symptoms users feel (error rate, latency SLO burn), not every CPU spike.
- Every alert must be actionable, with a runbook (step-by-step fix guide). Noisy alerts get ignored.
- Pages (wake someone up) for urgent user impact; tickets for everything else.
24. Security in Design
Authentication (authn) answers "who are you?". Authorisation (authz) answers "what are you allowed to do?". Security in system design is about putting the right checks at the right boxes in your diagram, and protecting data both when it moves and when it sits still.
A hotel: showing your passport at the front desk is authentication. Your key card only opening your own room (and the gym) is authorisation.
Authentication and authorisation
- Sessions — server stores a session in Redis; the browser holds a random session ID in a cookie. Easy to revoke.
- JWT (JSON Web Token) — a signed token containing the user ID and claims. Any service can verify it without a database lookup (stateless). Hard to revoke before it expires, so keep access tokens short-lived (15 min) with a refresh token to get new ones.
- OAuth 2.0 — a standard for letting one app access another on a user's behalf ("Sign in with Google", "allow this app to read your calendar") without sharing the password. OpenID Connect adds login (identity) on top.
- Authorisation models — RBAC (role-based: admin, editor, viewer) and ABAC (attribute-based: "owner of the document, in the same org"). Always check ownership on the server: "can user 7 read order 42?"
- In microservices, authenticate once at the API gateway, pass a verified identity (token) inward; services still authorise.
Protecting data
- In transit — HTTPS/TLS everywhere. TLS encrypts traffic and proves the server's identity via a certificate. Inside the network, mutual TLS (mTLS: both sides show certificates) between services, often via a service mesh.
- At rest — disks, databases and backups encrypted (AES-256), with keys in a KMS (Key Management Service).
- Passwords — never stored; hash with bcrypt/argon2.
- Secrets (API keys, DB passwords) — in a secrets manager (Vault, AWS Secrets Manager), never in code or logs; rotate them.
- Least privilege — every service and person gets only the access it needs.
- PII (personally identifiable information) — minimise, mask in logs, respect laws like GDPR (data residency, right to deletion).
Attacks to name
| Threat | Defence |
|---|---|
| DDoS (Distributed Denial of Service — a flood of traffic from many machines) | CDN / edge protection (Cloudflare, AWS Shield), rate limiting, auto-scaling, WAF |
| SQL injection | Parameterised queries |
| XSS (injected scripts in pages) | Escape output, Content-Security-Policy |
| CSRF (tricking a logged-in browser into sending a request) | SameSite cookies, CSRF tokens |
| Brute-force login | Strict rate limits, lockouts, MFA |
| Broken access control (reading other users' data by changing an ID) | Server-side ownership checks on every request |
A WAF (Web Application Firewall) filters known-bad request patterns at the edge.
25. URL Shortener (TinyURL / bit.ly)
Turn a long link into a short one like sho.rt/aZ3k9. When someone opens the short link, send them to the long one.
It looks tiny, but it tests the basics: ID generation, a read-heavy workload, caching and redirects.
A cloakroom ticket. You hand over a big coat (the long URL), you get a small ticket (the short code). Show the ticket later and you get the coat back.
1. Requirements
- Functional: create a short URL from a long URL; redirect short → long; optional custom alias; optional expiry.
- Non-functional: redirects must be very fast (< 50 ms) and highly available; short codes must be unique and hard to guess in order; reads far outnumber writes (about 100 : 1).
2. Estimates
New URLs: 100M per month → ~40 writes/sec (peak ~200)
Redirects: 100 × writes → ~4,000 reads/sec (peak ~20,000)
Storage: 100M × 12 months × 5 years = 6B rows
6B × ~500 bytes ≈ 3 TB (fits a small sharded DB)
Code length: base62 (a–z, A–Z, 0–9) → 62^7 ≈ 3.5 trillion codes
7 characters is plenty
3. API
POST /api/urls { longUrl, customAlias?, expiresAt? } → 201 { shortUrl }
GET /{code} → 301/302 redirect to longUrl, or 404
DELETE /api/urls/{code} → 204
4. Data model
urls
code VARCHAR(10) PRIMARY KEY -- "aZ3k9x1"
long_url TEXT
user_id BIGINT
created_at TIMESTAMP
expires_at TIMESTAMP NULL
A key-value lookup by code is the only hot query, so either a SQL table or a key-value store (DynamoDB, Cassandra) works.
5. High-level design
6. Deep dives
- Generating the code. Three options:
- Hash the long URL (MD5, take 7 chars) — simple, but collisions need a check-and-retry, and the same URL always gives the same code.
- Counter + base62 — a unique number (e.g. 125) becomes a short string. No collisions. Give each app server a range of numbers (1–1M, 1M–2M…) from a coordinator like ZooKeeper so they never clash and never wait.
- Random 7 chars — check the DB for a clash; fine because clashes are rare in 3.5 trillion.
- 301 vs 302.
301(permanent) lets browsers cache the redirect — less load, but you lose click analytics.302(temporary) sends every click through you — more load, but you can count clicks. - Caching. A small fraction of links get most clicks. Cache-aside in Redis with LRU eviction handles most reads.
- Analytics. Don't slow the redirect down. Push a click event onto a queue (Kafka) and count it asynchronously.
- Expiry. Check
expires_aton read, and clean up with a background job.
7. Bottlenecks & trade-offs
| Concern | Answer |
|---|---|
| Hot links | Cache + CDN edge redirects |
| DB growth | Shard by hash of code |
| ID coordinator down | Servers hold a pre-fetched range, so they keep working for a while |
| Abuse / spam links | Rate-limit creation per user/IP; scan URLs against blocklists |
26. Rate Limiter as a Service
Limit how many requests a user (or IP, or API key) can make in a period — for example 100 per minute. Extra requests get 429 Too Many Requests.
The hard part isn't the algorithm (see section 17). It's making the count shared and fast across many servers.
A club with a strict headcount. Every door (server) must check the same clicker counter, or each door lets in its own 100 people.
1. Requirements
- Functional: limit by user, IP or API key; different rules per endpoint; return 429 with a
Retry-Afterheader. - Non-functional: adds < 5 ms per request; works across many servers; if the limiter fails, the API must still work (fail open).
2. Estimates
API traffic: 50,000 req/sec → 50,000 counter checks/sec
Keys: 10M active users × ~50 bytes ≈ 500 MB (fits in Redis memory)
3. API / interface
// internal call from the gateway, or a middleware in each service
isAllowed(key = "user:42:/api/search", rule = { limit: 100, window: 60s })
→ { allowed: true, remaining: 57, resetAt: 1727340060 }
HTTP response headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 57
Retry-After: 12 (only on 429)
4. Data model
Redis key: rl:{userId}:{endpoint}:{windowStart} → count (TTL = window)
Rules: stored in a config DB / file, cached in each gateway
5. High-level design
6. Deep dives
- Algorithm choice. Token bucket allows short bursts and is the most common answer. Sliding-window counter is accurate and cheap. Fixed window is simplest but lets through 2× at window edges.
- Race conditions. Two servers read "99", both allow, the count ends at 101. Fix: do read-and-increment atomically — Redis
INCR, or a Lua script that runs as one step. - Latency. One Redis round trip is ~1 ms. For extreme scale, keep a small local counter and sync to Redis every few hundred ms — slightly inaccurate, much faster.
- Failure. If Redis is down, fail open (allow requests) for normal APIs; fail closed for things like login attempts or payments.
- Where to put it. At the API gateway for global rules; inside a service for business rules ("3 password resets per hour").
7. Bottlenecks & trade-offs
| Choice | Gain | Cost |
|---|---|---|
| Central Redis | Accurate global limit | Extra network hop; Redis must be highly available |
| Local counters + sync | Near-zero latency | Can overshoot the limit slightly |
| Fail open | API stays up | No protection during the outage |
27. Chat App (WhatsApp / Messenger)
Users send messages one-to-one and in groups. Messages should arrive instantly when the receiver is online, and be waiting for them when they come back online.
The key ideas: a persistent connection (WebSocket) to push messages, a way to find which server a user is connected to, and durable storage so nothing is lost.
A postal service with phone lines. If your friend is on the phone (online), the operator reads the letter to them right away. If not, the letter waits in their mailbox.
1. Requirements
- Functional: 1:1 and group chat (up to ~500 members); sent / delivered / read ticks; online status and last seen; message history on any device; media (images, video).
- Non-functional: low latency (< 200 ms delivery when online); messages never lost; messages in order within a chat; very high availability.
2. Estimates
Users: 500M daily active
Messages: 40 per user per day → 20B/day → ~230,000 msg/sec (peak ~1M)
Storage: 20B × 100 bytes ≈ 2 TB of text per day
Connections: ~100M concurrent WebSockets; ~50K per server → ~2,000 chat servers
3. API
WebSocket (persistent):
→ send { chatId, clientMsgId, text }
← ack { clientMsgId, msgId, timestamp }
← message { chatId, msgId, from, text, timestamp }
→ receipt { msgId, status: "delivered" | "read" }
REST:
GET /chats/{chatId}/messages?before={msgId}&limit=50 (history, paginated)
POST /media → presigned upload URL
4. Data model
messages (Cassandra — partition key = chat_id, clustering key = msg_id)
chat_id, msg_id (time-ordered, e.g. Snowflake), sender_id, body, created_at
chats chat_id, type (direct | group), members
user_inbox user_id → undelivered msg_ids (for offline users)
presence (Redis) user_id → { chatServerId, lastSeen }
Cassandra (wide-column) fits because the main query is "latest messages of chat X", writes are huge, and it scales horizontally.
5. High-level design
6. Deep dives
- Send flow. (1) Sender's app sends over WebSocket with a
clientMsgId. (2) Server A stores the message in Cassandra, then acks the sender (one grey tick). (3) Server A looks up the receiver in presence. (4) If online, it routes the message via the queue to server B, which pushes it; the receiver's app sends a "delivered" receipt (two ticks). (5) If offline, the message stays in the receiver's inbox and a push notification is sent. - Why WebSockets. The server needs to push to the client. Polling wastes battery and bandwidth; a WebSocket is one long-lived two-way connection.
- Ordering. Use a per-chat, time-ordered ID (Snowflake) and sort by it. Kafka partitioned by
chat_idkeeps one chat's messages in order. - Duplicates. Networks retry. The
clientMsgIdis an idempotency key — if the server has already stored it, just ack again. - Groups. For small groups, fan out: write the message once, deliver to each online member's server. For huge groups/channels, members pull instead.
- Presence. The app sends a heartbeat every ~30 s; if heartbeats stop, the user is marked offline after a timeout. Only broadcast presence to contacts who have the chat open, or it floods the system.
- Media. Upload directly to object storage with a presigned URL; the message only carries the file link.
- End-to-end encryption (Signal protocol): the server only stores and forwards ciphertext it cannot read. Mention it; don't design the crypto.
7. Bottlenecks & trade-offs
| Concern | Answer |
|---|---|
| A chat server dies | Clients reconnect (via LB) to another server; presence updates; undelivered messages are still in the store |
| Millions of connections | Chat servers only hold connections; keep them light; scale horizontally |
| Hot group chats | Pull model for very large groups |
| Multi-device | Each device has its own connection and its own "last synced msg_id" |
28. News Feed (Twitter / Instagram)
Users post. Each user's home feed shows recent posts from the people they follow, newest (or most relevant) first.
The whole question is one decision: do you build each feed when a post is written (fan-out on write) or when the feed is read (fan-out on read)?
Newspapers. Fan-out on write = printing and delivering a copy to every subscriber's doorstep in advance. Fan-out on read = every reader walks to every journalist's desk each morning to collect the news.
1. Requirements
- Functional: create a post (text, images); follow/unfollow; see the home feed; like and comment.
- Non-functional: feed loads fast (< 200 ms); a new post can take a few seconds to appear (eventual consistency is fine); very read-heavy.
2. Estimates
Users: 300M daily active, each opens feed ~10×/day → 3B feed reads/day → ~35,000/sec
Posts: each user posts ~0.5/day → 150M/day → ~1,700/sec
Avg follows: 200 → fan-out on write = 1,700 × 200 ≈ 340,000 feed inserts/sec
Feed cache: keep 500 post IDs × 8 bytes per user ≈ 4 KB × 300M ≈ 1.2 TB (Redis cluster)
3. API
POST /posts { text, mediaIds[] }
GET /feed?cursor={lastPostId}&limit=20
POST /users/{id}/follow
DELETE /users/{id}/follow
4. Data model
posts post_id (Snowflake), author_id, text, media_urls, created_at
follows follower_id, followee_id (both directions indexed)
feed cache Redis list feed:{userId} → [post_id, post_id, ...] (latest ~500)
5. High-level design
6. Deep dives — the fan-out decision
| Fan-out on write (push) | Fan-out on read (pull) | |
|---|---|---|
| When work happens | When a post is created | When a feed is opened |
| Feed read speed | Very fast — already built | Slow — query all followees and merge |
| Write cost | Huge for users with millions of followers | Cheap |
| Wasted work | Builds feeds for inactive users | None |
| Best for | Normal users | Celebrities |
The standard answer is hybrid. Push posts from normal users into followers' feeds. For celebrities (say > 100K followers), don't fan out; instead, when a user opens their feed, pull recent posts from the few celebrities they follow and merge them in.
- Store IDs, not posts, in the feed cache. Hydrate from a post cache. Editing a post then updates one place.
- Only active users get pre-built feeds. For someone returning after months, build the feed on demand.
- Ranking. Chronological is simplest. For "relevant" feeds, fetch ~500 candidates, score them (likes, relationship, recency) with a ranking service, return the top 20.
- Pagination. Cursor-based (
?cursor=lastPostId), not offset — new posts arriving would shift offsets. - Media via object storage + CDN.
7. Bottlenecks & trade-offs
- Celebrity posts → hybrid model.
- Fan-out lag → acceptable; the feed is eventually consistent. The author sees their own post immediately (read-your-writes by inserting it client-side or into their own feed first).
- Redis memory → cap each feed at ~500 IDs; older pages fall back to the DB.
29. Notification System
Other services say "tell user 42 their order shipped". The notification system decides how (push, SMS, email, in-app), respects the user's preferences, and delivers reliably — without spamming or sending twice.
A company's mailroom. Departments drop off messages; the mailroom checks how each person likes to be contacted, then hands letters to the right courier (post, phone, email).
1. Requirements
- Functional: send via push (iOS/Android), SMS, email, in-app; templates; user preferences and opt-outs; scheduled and bulk sends; delivery status.
- Non-functional: at-least-once delivery (no silent loss), no duplicates the user notices, soft real-time (seconds), handle spikes (a marketing blast to 10M users).
2. Estimates
Daily: 10M push + 1M SMS + 5M email ≈ 16M/day ≈ 200/sec avg, spikes of 50,000/sec
The spike number matters more than the average — it's why queues are essential here.
3. API
POST /notifications
{ userId, type: "order_shipped", data: { orderId: 4471 },
channels?: ["push", "email"], idempotencyKey: "order-4471-shipped" }
→ 202 Accepted { notificationId }
202, not 200: the request is accepted and will be processed asynchronously.
4. Data model
user_preferences user_id, channel, opted_in, quiet_hours
devices user_id, device_token, platform
templates type, channel, subject, body_with_{placeholders}
notification_log notification_id, user_id, channel, status (queued|sent|failed|opened), attempts
5. High-level design
6. Deep dives
- Why queues. They absorb spikes, let each channel scale separately, and survive provider outages — messages wait instead of being lost.
- Reliability. Workers ack the queue only after the provider accepts the message. Failures retry with exponential backoff; after N attempts they go to a dead-letter queue for inspection.
- No duplicates. Retries mean at-least-once, so deduplicate: store the
idempotencyKey(or notificationId) in Redis with a TTL and skip if already sent. - Preferences & quiet hours are checked before queueing. Legal requirement for marketing email: honour unsubscribes.
- Rate limiting per user — nobody wants 40 pushes an hour. Group or digest low-priority ones.
- Priority. Separate high-priority queues (OTP codes, security alerts) from bulk marketing.
- Tracking. Providers call back webhooks for delivered/bounced/opened; update the log.
30. File Storage & Sync (Dropbox / Google Drive)
Users upload files, and the files appear on all their devices. When a file changes, only the changed part is uploaded, and every other device is told to update.
Key ideas: split files into chunks, store chunks in object storage, keep metadata (which chunks make up which file version) in a database, and notify other devices.
A book stored as numbered pages in a warehouse. The catalogue (metadata) says "Book v3 = pages 1, 2, 7, 4". Edit one page, and you only ship that one page and update the catalogue.
1. Requirements
- Functional: upload/download files; sync across devices; version history; share with others; offline edits that sync later.
- Non-functional: never lose data (very high durability); large files (up to 50 GB); save bandwidth; sync within seconds.
2. Estimates
Users: 50M, avg 10 GB each → 500 PB total (object storage, not a DB)
Uploads: ~2M file changes/day → ~25/sec avg, but each can be large
Chunk: 4 MB → a 1 GB file = 256 chunks
3. API
POST /files/upload-session { path, size, chunkHashes[] }
→ { missingChunks[], presignedUrls[] } // only upload what the server lacks
PUT {presignedUrl} (client → object storage directly)
POST /files/commit { path, chunkHashes[], baseVersion }
GET /files/changes?cursor= → list of changes since cursor (sync)
GET /files/{id}/download → chunk URLs
4. Data model
files file_id, owner_id, path, latest_version, is_deleted
file_versions file_id, version, chunk_hashes[], size, modified_by, created_at
chunks chunk_hash (SHA-256) → object storage key, ref_count
changes user_id, cursor (increasing), file_id, version // sync journal
5. High-level design
6. Deep dives
- Chunking + dedup. Each chunk is named by its content hash. If the server already has that hash (from any user, or an earlier version), it isn't uploaded again. Editing one paragraph of a 1 GB file re-uploads one 4 MB chunk.
- Resumable uploads. A dropped connection only loses the chunk in flight.
- Presigned URLs. Clients upload directly to S3, so app servers never carry the heavy bytes.
- Metadata consistency. Use a SQL database with transactions: committing a new version must be all-or-nothing.
- Conflicts. Two devices edit offline. The commit includes
baseVersion; if it's not the latest, the server rejects it and the client saves a "conflicted copy" rather than silently overwriting. - Sync. Each device keeps a cursor into the
changesjournal and asks "what changed since cursor X?". A long-poll or WebSocket just says "something changed, go ask". - Durability. Object storage replicates across zones (S3 promises 11 nines). Old versions are kept; chunks are deleted only when no version references them (
ref_count= 0).
31. Video Streaming (YouTube / Netflix)
Creators upload a video. It is converted into many sizes and qualities, cut into small segments, and pushed to CDNs near viewers. The player picks a quality that suits the viewer's internet speed and switches as it changes.
A restaurant that cooks every dish in small, medium and large portions ahead of time, and keeps them in fridges near every neighbourhood. You get served from the nearest fridge, in the size your appetite (bandwidth) can handle right now.
1. Requirements
- Functional: upload video; watch on any device; multiple resolutions; search; view counts, likes.
- Non-functional: smooth playback with fast start (< 2 s) worldwide; uploads can take minutes to process; huge storage and bandwidth; very read-heavy.
2. Estimates
Uploads: 500 hours of video per minute
Storage: 1 hour ≈ 1 GB raw → ~3 GB across all renditions
500 × 60 × 24 × 3 GB ≈ 2 PB per day
Viewing: 1B hours watched/day; bandwidth is the #1 cost → CDN is mandatory
3. API
POST /videos { title, description } → { videoId, uploadUrl (presigned, resumable) }
GET /videos/{id} → metadata + manifest URL
GET {cdn}/videos/{id}/master.m3u8 → list of qualities
GET {cdn}/videos/{id}/720p/seg_0042.ts → 4-second segment
4. Data model
videos video_id, owner_id, title, status (uploading|processing|ready), duration, created_at
renditions video_id, resolution, bitrate, manifest_path
views counted in a stream processor, stored as aggregates (not one row per view)
5. High-level design
6. Deep dives
- Transcoding (converting to different formats/sizes) is slow and CPU-heavy, so it's a queue of jobs. Split the video into pieces and transcode them in parallel on many workers; stitch the results. Status goes processing → ready.
- Adaptive bitrate streaming (HLS or DASH). Each quality is cut into ~4-second segments. The manifest file lists them. The player measures download speed and picks the best quality it can sustain — switching mid-video without stopping.
- CDN. Popular videos are cached at edges worldwide. Netflix even places servers inside ISPs. The long tail of rarely watched videos is served from origin or regional caches.
- Resumable uploads for large files — upload in parts, retry just the failed part.
- View counts — don't write one DB row per view. Stream events through Kafka, count in batches, and accept that counts lag slightly.
- Cost. Bandwidth and storage dominate. Keep fewer renditions for unpopular videos; use cheaper cold storage for old ones.
32. Ride Sharing (Uber / Lyft)
Riders request a ride; the system finds nearby available drivers fast, matches one, and both see each other's live location until the trip ends.
The special problem is location: millions of drivers send their position every few seconds, and we need "which drivers are within 2 km of this point?" in milliseconds. The answer is a geospatial index — geohash or quadtree.
A city map divided into a grid of numbered squares. To find nearby taxis you don't check every taxi in the city — you look in your square and the eight squares around it.
1. Requirements
- Functional: rider requests a ride; see nearby drivers; match a driver; live tracking; fare estimate; payment; ratings.
- Non-functional: matching in seconds; location updates in near real time; a driver must never be matched to two riders; high availability.
2. Estimates
Active drivers: 5M, location update every 4 s → 1.25M writes/sec
Ride requests: 20M/day → ~250/sec (peak ~2,000)
Location data: mostly in memory; only the latest position per driver matters for matching
The location write rate is the headline number. It rules out writing every update into a normal SQL database.
3. API
PUT /drivers/me/location { lat, lng } (every 4 s, often over WebSocket)
POST /rides { pickup, dropoff } → { rideId, fareEstimate }
GET /rides/{id} → status, driver, driver location
POST /rides/{id}/accept (driver)
WebSocket: live location + status updates for an active trip
4. Data model
driver_locations (Redis GEO / in-memory) driver_id → lat, lng, geohash, status, updated_at
rides (SQL) ride_id, rider_id, driver_id, status, pickup, dropoff, fare, timestamps
trip_path ride_id, points[] (written in batches, for receipts and disputes)
5. High-level design
6. Deep dives
- Geohash. Encodes a lat/lng into a short string like
tdr1w. Nearby points share a prefix. Longer string = smaller cell. To find drivers near a rider: take the rider's cell plus its 8 neighbours, list drivers in those cells, then sort by real distance or ETA. Redis has this built in (GEOADD,GEOSEARCH). - Quadtree. A tree that splits the map into four squares, and keeps splitting busy squares until each holds a manageable number of drivers. Adapts to density (Manhattan gets tiny cells, the countryside huge ones). Geohash is simpler to shard; quadtree adapts better.
- Handling 1.25M updates/sec. Keep only the latest position in memory; shard the geo index by region/city. Write the full path to durable storage in batches, not per update.
- No double booking. When offering a ride to a driver, atomically set their status from
availabletooffered(a RedisSET NXlock or a conditional DB update). If they don't accept in ~10 s, release and try the next driver. - Trip state machine: requested → matched → driver arriving → in progress → completed / cancelled. Every transition is validated and stored.
- Surge pricing — compare demand (requests) vs supply (free drivers) per geohash cell every minute.
- Payments go through an external provider with idempotency keys so a retried charge never bills twice.
33. Web Crawler
Start from some seed URLs, download each page, pull out its links, and add new links to the to-do list. Repeat for billions of pages — politely, without downloading the same thing twice.
Exploring a library by following every "see also" reference, with a notebook of shelves already visited, and a rule not to bother any one librarian more than once a second.
1. Requirements
- Functional: crawl pages from seeds; store page content for indexing; re-crawl pages periodically.
- Non-functional: scale to billions of pages; polite (obey
robots.txt, don't overload any site); robust to bad HTML, traps and slow servers; avoid duplicates.
2. Estimates
Target: 1B pages per month → ~400 pages/sec
Page size: ~500 KB → 500 TB per month of raw HTML (compress, store in object storage)
3. Components (the "API" here is the pipeline)
Seed URLs → URL Frontier (queue) → Fetcher → Parser → Link extractor
^ |
|---- URL filter + "seen?" check ---------|
Fetched pages → content "seen?" check → object storage → indexer
4. Data
URL frontier priority queues + per-host queues
seen URLs Bloom filter / hash set of normalised URLs (billions)
seen content hash (SimHash) of page content, to skip mirrors and duplicates
pages object storage: url, fetched_at, content (compressed)
robots cache host → robots.txt rules, crawl delay
5. Deep dives
- URL frontier is the heart. Two levels: priority queues (important/frequently changing pages first) feed per-host queues. Each worker takes one host at a time and waits between requests to that host — that's politeness.
- Dedup URLs. Normalise first (lowercase host, remove fragments and tracking params), then check a Bloom filter — a compact structure that answers "definitely new" or "probably seen". Billions of URLs in a few GB of memory.
- Dedup content. Many URLs serve the same page. Hash the content; skip if seen.
- DNS is a hidden bottleneck — cache DNS results.
- Traps. Infinite calendars or
?page=1,2,3…loops. Cap URL length and depth per site. - Distribution. Partition by host (hash of hostname → worker), so one worker owns each site and politeness is easy.
- Re-crawling. Pages that change often get crawled more often; track change frequency per URL.
34. Search Autocomplete (Typeahead)
As the user types "new y", suggest the top 5 completions — "new york", "new year", "new york times" — in under ~100 ms, on every keystroke.
The trick: precompute the top suggestions for every prefix, so a lookup is just "fetch the answer", not "search and sort".
A shop assistant who has memorised the five best-selling items for every first few letters, instead of walking the aisles each time you start a sentence.
1. Requirements
- Functional: top-k (e.g. 5) suggestions per prefix, ranked by popularity; optionally personalised and trending.
- Non-functional: < 100 ms end to end; very high read rate; suggestions can be minutes to hours stale.
2. Estimates
Searches: 5B/day, ~6 keystrokes each → 30B lookups/day → ~350,000/sec
(debouncing on the client cuts this a lot)
3. API
GET /suggest?q=new%20y&limit=5 → ["new york", "new year", "new york times", ...]
4. Data structure — the trie
(root)
└ n
└ e
└ w top5: [new york, new year, news, ...]
└ " "
└ y top5: [new york, new year, new york times, ...]
A trie (prefix tree) has one node per character. The key optimisation is to store the top-k results in each node, so answering a prefix is a walk down a few nodes — no sorting at request time.
5. Design
- Two paths. The serve path reads a prebuilt trie from memory (or a key-value store of
prefix → top5in Redis). The build path runs offline: collect search logs → count query frequency (e.g. hourly/daily with Spark) → build a new trie → swap it in. - Sharding. Split by prefix range (a–f, g–m…), balanced by traffic rather than alphabet, since some letters are far more common.
- Caching. Short prefixes ("a", "th") are requested constantly — cache them at the CDN and in the browser.
- Client tricks. Debounce (wait ~150 ms after typing stops), cancel stale requests, and reuse results: if you already have suggestions for "new", filter them locally for "new y".
- Trending. A streaming job (Kafka + Flink) boosts queries spiking in the last few minutes, merged with the daily results.
- Filtering. Remove offensive or dangerous suggestions before publishing the trie.
Cheat Sheet — If You Hear X, Reach for Y
Interviewers drop hints. Each phrase below points to a standard tool. Name the tool and the reason.
| If you hear… | Reach for… | Because… |
|---|---|---|
| "Read-heavy", "slow queries", "same data requested again" | Cache (Redis) + read replicas | Serve repeats from memory; spread reads |
| "Global users", "images/videos", "static files" | CDN + object storage | Serve bytes close to the user |
| "Too much traffic for one server" | Stateless servers + load balancer | Scale horizontally |
| "Too much data for one database" | Sharding (consistent hashing) | Split data across machines |
| "Slow task", "email", "video processing", "spikes" | Message queue + workers | Do it asynchronously, absorb bursts |
| "Many services need the same event" | Pub/sub (Kafka) | One event, many consumers |
| "Real-time", "chat", "live updates" | WebSockets (or SSE for one-way) | Server can push |
| "Money", "bookings", "inventory" | SQL with transactions, idempotency keys | Strong consistency, no double charges |
| "Flexible schema", "huge write volume" | NoSQL (document / wide-column) | Scales writes, easy schema change |
| "Relationships", "friends of friends" | Graph database | Traversals are cheap |
| "Search text", "fuzzy matching" | Elasticsearch (inverted index) | Full-text search |
| "Autocomplete" | Trie with top-k per node | Precomputed prefix answers |
| "Nearby", "location" | Geohash / quadtree, Redis GEO | Spatial lookups |
| "Abuse", "too many requests" | Rate limiter (token bucket) | Protect the system |
| "Unique IDs across servers" | Snowflake IDs | Unique, time-sortable, no coordination |
| "Transaction across services" | Saga pattern | Local steps + compensations |
| "A dependency keeps failing" | Circuit breaker, timeouts, retries with backoff | Fail fast, don't cascade |
| "Have we seen this before?" at huge scale | Bloom filter | Tiny memory, no false negatives |
| "Leaderboard", "top N" | Redis sorted set | Ranked reads in O(log n) |
| "Analytics", "counts", "metrics over time" | Stream processing + columnar / time-series DB | Aggregate, don't store every row |
Trade-offs to Mention
There's no perfect design — only choices with costs. Saying the cost out loud is what makes an answer sound senior.
| Choice | You gain | You pay |
|---|---|---|
| SQL vs NoSQL | Joins, transactions, strong consistency | Harder horizontal scaling |
| Strong vs eventual consistency | Always-correct reads | Higher latency, lower availability during partitions |
| Cache | Speed, less DB load | Stale data, invalidation complexity |
| Normalise vs denormalise | No duplication, easy writes | Slower reads (joins) |
| Sync vs async processing | Simple, immediate result | Slow requests, tight coupling |
| Push (fan-out on write) vs pull | Fast reads | Expensive writes for popular users |
| Monolith vs microservices | Simple deploys, easy debugging | Harder to scale teams and parts independently |
| Sync vs async replication | No data loss on failover | Slower writes |
| Polling vs WebSockets | Simple, stateless | Wasted requests, delay |
| Batch vs stream processing | Simple, cheap, accurate | Results are hours old |
| Vertical vs horizontal scaling | No code changes | Hard ceiling, single point of failure |
Numbers to Remember
| What | Roughly |
|---|---|
| Seconds in a day | ~86,400 → use 100,000 for quick maths |
| 1M requests/day | ≈ 12 per second |
| 1B requests/day | ≈ 12,000 per second |
| Peak traffic | 2–5× the average |
| Memory read | ~100 ns |
| SSD read | ~100 µs |
| Network round trip, same data centre | ~0.5 ms |
| Network round trip, across continents | ~150 ms |
| Redis operation | < 1 ms; ~100K ops/sec per node |
| One well-tuned SQL database | ~1–10K queries/sec (more with replicas) |
| One app server | ~1–10K simple requests/sec |
| WebSocket connections per server | ~50K–100K+ |
| Kafka | ~1M messages/sec per cluster is ordinary |
| Availability: 99.9% / 99.99% / 99.999% | ~8.8 hours / 52 minutes / 5 minutes of downtime per year |
| 1 char = 1 byte (ASCII); 1 KB = 10³, 1 MB = 10⁶, 1 GB = 10⁹, 1 TB = 10¹², 1 PB = 10¹⁵ |
Common Mistakes in Interviews
- Jumping into drawing before asking about requirements and scale. Spend the first 5 minutes asking.
- Staying silent. They grade your thinking. Talk through every choice.
- Buzzword soup — "Kafka, Kubernetes, microservices" with no reason. Every box needs a "because".
- Over-engineering a system for 1,000 users. Design for the stated scale, then explain how you'd grow.
- No numbers. Even rough estimates show you know what's big and what's small.
- Ignoring failure. Always say what happens when a server, DB or dependency dies.
- Only the happy path. Mention retries, duplicates, timeouts, and hot spots.
- Single points of failure left in the diagram — one DB, one LB. Show replicas.
- Not driving. Don't wait to be asked; propose the next step ("Shall I go deeper on the feed generation?").
- Defending a choice to the death. If the interviewer pushes back, weigh their point — adapting is a plus.
Interview Q&A — quick answers
Fundamentals
- Vertical vs horizontal scaling? Bigger machine vs more machines. Vertical is simple but has a ceiling and one point of failure; horizontal needs stateless servers and a load balancer but scales far.
- Why make servers stateless? So any server can handle any request. Then you can add, remove or replace servers freely. Keep session data in Redis or a token.
- Latency vs throughput? Latency is how long one request takes; throughput is how many requests per second you handle.
- What does 99.99% availability mean? About 52 minutes of downtime a year. Each extra nine costs much more.
- What is a single point of failure? A component whose failure takes down the whole system. Remove it with redundancy.
- L4 vs L7 load balancer? L4 routes by IP/port (fast, blind to content); L7 reads HTTP (can route by path, header, cookie).
Caching & data
- Cache-aside? App checks cache; on a miss it reads the DB and fills the cache. The most common pattern.
- Write-through vs write-back? Write-through writes cache and DB together (safe, slower). Write-back writes cache first and DB later (fast, can lose data).
- How do you invalidate a cache? TTLs plus deleting the key on update. Delete rather than update to avoid races.
- Cache stampede? A popular key expires and thousands of requests hit the DB at once. Fix with a lock so one request refills, or refresh early.
- SQL or NoSQL? SQL for relationships, transactions and correctness. NoSQL for huge scale, simple access patterns and flexible schemas.
- Replication vs sharding? Replication copies the same data (more reads, fault tolerance). Sharding splits different data across machines (more writes, more storage).
- What is consistent hashing? Servers and keys on a ring; a key goes to the next server clockwise. Adding or removing a server moves only ~1/n of keys.
- What is replication lag? Followers are behind the leader, so a read right after a write may return old data. Fix with read-your-writes (read from the leader for that user briefly).
- What is an index's cost? Faster reads, slower writes, more storage.
Distributed systems
- CAP theorem? During a network partition you must choose consistency or availability. Partitions will happen, so it's really C vs A when things break.
- PACELC? If Partition, choose A or C; Else, choose Latency or Consistency.
- Eventual consistency? All copies will match if writes stop — but reads may briefly be stale.
- Queue vs pub/sub? A queue gives each message to one consumer (work sharing); pub/sub gives each message to every subscriber (broadcast).
- At-least-once vs exactly-once? At-least-once may duplicate; make consumers idempotent. True exactly-once is expensive and rare.
- What is idempotency? Doing it twice has the same effect as once. Essential for retries — use idempotency keys.
- 2PC vs saga? 2PC locks all participants until everyone agrees (consistent, slow, fragile). A saga runs local transactions and undoes finished steps with compensating actions if a later one fails.
- How do you generate unique IDs at scale? Snowflake: timestamp + machine ID + sequence — unique, sortable, no central coordinator.
- What is a circuit breaker? After repeated failures it stops calling a dependency for a while and fails fast, so one bad service doesn't take everything down.
- Why add jitter to retries? So thousands of clients don't retry at the same instant and overload the recovering service again.
Communication & APIs
- REST vs GraphQL vs gRPC? REST: simple, cacheable, public APIs. GraphQL: client picks fields, good for varied frontends. gRPC: fast binary calls between internal services.
- Polling vs long polling vs WebSocket vs SSE? Polling asks repeatedly; long polling holds the request until data arrives; WebSocket is a two-way open connection; SSE is a one-way server-to-client stream.
- Offset vs cursor pagination? Offset is simple but slow on deep pages and skips/duplicates when data changes; cursor ("after id X") is stable and fast.
- What does an API gateway do? One entry point: routing, auth, rate limiting, logging, SSL termination.
- Why presigned URLs for uploads? The client uploads straight to object storage, so your servers don't carry large files.
Case-study one-liners
- URL shortener: counter ranges + base62, cache-aside redirects, 301 vs 302 for analytics.
- Chat: WebSockets, presence map to route to the right server, store then deliver, Snowflake ordering.
- News feed: hybrid fan-out — push for normal users, pull for celebrities.
- Notifications: per-channel queues, retries + dead-letter queue, dedupe with idempotency keys.
- Dropbox: content-hashed chunks, upload only missing chunks, metadata in SQL.
- YouTube: transcode to many qualities, HLS segments, CDN, adaptive bitrate.
- Uber: in-memory geohash index, lock the driver when matching.
- Crawler: URL frontier with per-host politeness, Bloom filter for seen URLs.
- Autocomplete: trie with top-k stored per node, rebuilt offline, cached at the edge.
Written-Exam Answer Template
In a written exam no one can ask you follow-up questions, so your page must show the whole thought process. Use headings — examiners skim for them.
Q: Design a ______.
1. Requirements
Functional: 3–5 bullet points (what the user can do)
Non-functional: scale, latency, availability, consistency
Assumptions: "Assume 10M daily users, read:write = 100:1"
2. Estimates
QPS (avg and peak), storage per year, bandwidth — show the working
3. API
3–5 endpoints with method, path, request and response
4. Data model
Main tables/collections with key fields; SQL or NoSQL and WHY
5. High-level design
A labelled box-and-arrow diagram:
client → LB → services → cache → DB / queue / object storage / CDN
One sentence describing the main read path and write path
6. Deep dive (2–3 components)
The hardest part of THIS problem (e.g. fan-out, matching, ID generation)
7. Scaling & reliability
Sharding, replication, caching, queues; what happens when X fails
8. Trade-offs
2–3 "I chose A over B because…, the cost is…"
9. Summary
3 lines: the core idea of the design
Learning Path
- Learn the framework (section 2) until you can run it without notes.
- Practise estimates — do the maths for 3 systems out loud with round numbers.
- Master the building blocks — for each, be able to say what it is, when to use it, and its cost.
- Do the case studies — URL shortener first, then chat, news feed, and notifications. Draw each from memory.
- Mock interviews — 45 minutes, timer on, talking out loud. Record yourself once; you'll spot silences and hand-waving.
- Read real architectures — engineering blogs from Netflix, Uber, Discord, Slack and Instagram show these patterns in production.
- Night before — read the cheat sheet, trade-offs, numbers and case-study one-liners on this page. Nothing new.