System Design — Concept Notes

Interview Q&A

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.

Part 1 — How to Answer

1. What Interviewers Actually Test

In plain words

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

SignalWhat it looks like when you do it well
Problem scopingYou ask questions before drawing. You write down what is in and out of scope.
Structured thinkingYou follow a clear order (the framework in section 2) instead of jumping around.
Technical depthYou can go deep on one or two parts: how the cache is invalidated, how the data is sharded.
Trade-off reasoningEvery choice comes with "I chose X over Y because…, the cost is…".
Scale awarenessYou use rough numbers to justify choices. You know when one database is enough and when it is not.
CommunicationYou 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 interviewWritten exam
Ask clarifying questionsWrite "Assumptions:" as a list at the top
Draw and talk at the same timeDraw a labelled diagram, then explain each box in numbered points
Interviewer steers you to the deep diveYou pick 1–2 deep dives yourself and label them clearly
Trade-offs mentioned as you goA separate "Trade-offs" heading, ideally a small table
Time: 35–45 minutesTime: split marks by headings; do not spend it all on one part

How to structure a written answer

  1. Assumptions and requirements — functional (what it does) and non-functional (how well), as bullet points.
  2. Estimates — 3–5 lines of numbers: users, requests per second, storage.
  3. API — 3–5 endpoints.
  4. Data model — tables or collections with key fields.
  5. Architecture diagram — boxes and arrows, every box labelled, numbered arrows for the main flow.
  6. Component explanation — one short paragraph per box, following the arrow numbers.
  7. Scaling and deep dives — the hard part of this problem, explained properly.
  8. Trade-offs and alternatives — a table: choice, why, cost.
  9. Conclusion — two sentences summarising the design.

The full template is in the revision section: Written-exam template.

Say this in the interview: "Before I design anything, I'd like to spend a few minutes on requirements and scale, so I build the right thing." That one sentence shows maturity and buys you time to think.

2. The Step-by-Step Framework

In plain words

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.

1 Requirements 5 min 2 Estimates 3 min 3 API 3 min 4 Data model 4 min 5 High-level 10 min 6 Deep dive 10–12 min 7 Bottlenecks 5 min 8 Wrap-up 2 min Total about 45 minutes. Steps 1 and 6 decide most of your score.
The framework as a loop around the board. Spend real time on requirements and the deep dive; keep estimates and API short.

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.

Rule of thumb: if the interviewer interrupts, follow them. The framework is your default path, not a script to finish at any cost. An interviewer steering you to a topic is telling you where the points are.

3. Back-of-the-Envelope Estimation

In plain words

"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)

PowerApprox valueNameFeel for it
2101 thousand1 KB (kilobyte)A short text message with metadata
2201 million1 MB (megabyte)A photo from a phone, compressed
2301 billion1 GB (gigabyte)An hour of HD video, roughly
2401 trillion1 TB (terabyte)A large laptop disk
2501 quadrillion1 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.

OperationApprox timeTakeaway
L1 CPU cache read~1 nsBasically free
Main memory (RAM) read~100 nsMemory is fast; this is why caches live in RAM
Compress 1 KB~2–10 µsCheap; compress before sending over a network
Read 1 MB sequentially from RAM~10–250 µsRAM reads are very fast
Round trip inside one data centre~0.5 msEvery extra service hop costs this
SSD random read~100 µsAbout 1000x slower than RAM
Read 1 MB sequentially from SSD~1 msSequential beats random
HDD disk seek~10 msAvoid random reads on spinning disks
Read 1 MB sequentially from HDD~20 msFine for logs and big scans
Round trip across continents~150 msWhy 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.
Say this in the interview: "I'll round 86,400 to 100,000 seconds to keep the maths simple — I only need the order of magnitude." Then always finish each number with a decision: "so we need a CDN", "so one Postgres is enough". A number without a decision earns nothing.

4. Key Non-Functional Terms

In plain words

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.

TermOne-line meaningHow you improve it
ScalabilityHandling more load by adding resources, without a redesignStateless servers, horizontal scaling, sharding, caching
AvailabilityThe percentage of time the system is up and answeringRedundancy, failover, no single point of failure, multi-zone
ReliabilityWorking correctly over time, even when parts failRetries, replication, testing, graceful degradation
LatencyTime for one request to complete (how fast)Caching, CDN, fewer hops, indexes, closer servers
ThroughputHow many requests the system completes per second (how much)More servers, batching, async processing, parallelism
ConsistencyAll readers see the same, latest dataSingle leader, synchronous replication, quorums, transactions
DurabilityOnce saved, data is never lostWrite to disk, replicate to several machines, backups
MaintainabilityEasy for engineers to change and operateSimple design, clear boundaries, observability

Availability and the "nines"

Availability is written as a percentage. Each extra 9 cuts allowed downtime by ten.

AvailabilityDowntime per yearPer monthPer dayTypical for
99% (two nines)3.65 days~7.3 hours~14.4 minInternal tools
99.9% (three nines)8.76 hours~43.8 min~1.44 minMost web apps
99.99% (four nines)52.6 min~4.4 min~8.6 sPayments, big consumer apps
99.999% (five nines)5.26 min~26 s~0.86 sTelecom, 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.
Say this in the interview: "For this system I'd prioritise availability and low read latency over strong consistency — a user seeing a like count one second late is fine." Naming the priority early lets you justify every later choice with it.
Part 2 — Building Blocks

5. How a Request Travels

In plain words

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).

Client browser / app DNS name -> IP CDN images, JS, CSS Load balancer spreads traffic App server 1 App server 2 App server 3 Cache Redis, in memory Database source of truth 1 2a 2b 3 4 hit? 5 miss 1 look up IP · 2a static files from the CDN · 2b API calls to the load balancer 3 pick a server · 4 try the cache · 5 on a miss, read the database
The standard web architecture. Almost every case study starts from this picture and adds pieces (queues, object storage, search) as needed.

The steps in order

  1. 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).
  2. TCP connection — TCP is the protocol that guarantees bytes arrive in order and complete. Opening one costs a round trip (the "three-way handshake").
  3. 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.
  4. HTTP request — method (GET, POST…), path, headers, maybe a body.
  5. Server side — load balancer → app server → cache / database → build the response.
  6. HTTP response — a status code (200 OK, 404 Not Found, 500 Server Error), headers and body.

HTTP in one table

MethodMeaningSafe to repeat?
GETRead somethingYes — no changes
POSTCreate something / trigger an actionNo — may create duplicates
PUTReplace something completelyYes — same result each time (idempotent)
PATCHChange part of somethingNot guaranteed
DELETERemove somethingYes — 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.

Say this in the interview: "Static assets go through a CDN, API calls go through a load balancer to stateless app servers, which read through a cache in front of the database." That sentence is the skeleton of 80% of designs — say it, draw it, then customise.

6. Vertical vs Horizontal Scaling

In plain words

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)
HowBigger machineMore machines
Code changesNoneApp must be stateless; need a load balancer
LimitHard ceiling — the biggest machine you can buyNearly unlimited
FailureOne machine = single point of failureOne machine dies, others carry on
CostBig machines get expensive fastCheap commodity machines, but more to operate
Good forEarly 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.

Rule of thumb: scale the app tier horizontally from day one (it is easy once stateless). Scale the database vertically as long as you can (it is simpler), then add read replicas, then shard only when forced to.

7. Load Balancers

In plain words

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.

Clients many requests Load balancer algorithm: least conn health checks every 5s Server 1 healthy · 12 conns Server 2 healthy · 9 conns Server 3 ✗ failed /health · removed next no traffic sent
Least-connections sends the next request to Server 2 (fewest open connections). Server 3 failed its health check, so it gets nothing until it recovers.

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 balancerL7 load balancer
Looks atIP and port onlyFull HTTP request: path, headers, cookies
SpeedVery fast, very cheapSlower (must read and often decrypt the request)
Smart routingNoYes: /api to one group, /images to another
TLSUsually passes it throughUsually terminates it (decrypts at the LB)
ExamplesAWS NLB, HAProxy (TCP mode)AWS ALB, Nginx, Envoy, HAProxy (HTTP mode)

Algorithms

AlgorithmHow it worksUse when
Round robin1, 2, 3, 1, 2, 3…Servers are identical, requests are similar
Weighted round robinBigger servers get more turnsMixed server sizes
Least connectionsSend to the server with the fewest open connectionsRequests vary a lot in duration (WebSockets, uploads)
Least response timeSend to the fastest-responding serverPerformance varies between servers
IP hashHash of client IP picks the serverNeed the same client on the same server, without cookies
Consistent hashingHash 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.

Say this in the interview: "I'll put an L7 load balancer in front of stateless app servers, using least-connections and health checks. The LB itself is redundant — a managed one, or an active-passive pair." That covers algorithm, health, and single point of failure in one breath.

8. Caching

In plain words

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)

LayerExampleCaches
Browser / clientHTTP cache headers (Cache-Control), local storageStatic files, some API responses
CDNCloudflare, CloudFrontImages, video, JS, CSS, even whole pages
Load balancer / reverse proxyNginx, VarnishFull HTTP responses
Application (in-process)A map in the server's memoryConfig, small hot lookups — fast but not shared
Distributed cacheRedis, MemcachedQuery results, sessions, computed objects — shared by all servers
DatabaseBuffer pool, query cachePages of data on disk
App server owns the logic Cache (Redis) key -> value, TTL Database source of truth 1 GET key 2 hit: value 3 miss: SELECT rows 4 SET key with TTL On write: UPDATE the database, then DELETE the cache key (next read refills it)
Cache-aside (lazy loading) — the most common pattern. The cache only ever holds data that someone actually asked for.

Cache patterns (how reads and writes flow)

PatternHow it worksGoodBad
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-throughApp only talks to the cache; the cache itself loads from DB on a miss.Cleaner app codeNeeds a cache library/provider that supports it
Write-throughEvery write goes to cache and DB synchronously.Cache always freshSlower 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 spikesData loss if cache dies before flushing
Write-aroundWrites go straight to DB, skipping cache; reads use cache-aside.Cache not flooded with write-once dataRecent 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

ProblemWhat happensFix
Cache stampede (thundering herd)A hot key expires; thousands of requests miss at once and all hit the DBLock so only one request refills (others wait or get stale value); refresh early before expiry; add random jitter to TTLs
Hot keyOne key (a celebrity's profile) gets so many reads that one cache node meltsReplicate the key to several nodes (key#1…key#N, pick randomly); local in-process cache for it
Cache penetrationRequests 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 avalancheMany keys expire at the same moment, or the cache cluster restartsJittered 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.

Say this in the interview: "I'll use cache-aside with Redis, a TTL of a few minutes, and delete-on-write. For hot keys I'd add a small local cache, and to avoid a stampede I'd let only one request refill an expired key." Then mention the trade-off: "the cost is a short window of staleness, which is acceptable here."

9. CDN (Content Delivery Network)

In plain words

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 CDNPush CDN
How content gets thereEdge fetches from origin on the first request, then caches itYou upload content to the CDN in advance
First requestSlow (miss, goes to origin)Fast (already there)
EffortLow — just point the CDN at your originYou manage uploads and updates
Best forSites with lots of content, unpredictable popularityLarge 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.
Rule of thumb: any time a design serves images, video or files to many users, say "served from object storage through a CDN". If your estimate shows GB/s of outgoing traffic, a CDN is not optional.
Part 2 — Data

10. Choosing a Database

In plain words

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

TypeExamplesData looks likeUse whenAvoid when
Relational (SQL)PostgreSQL, MySQLTables, rows, foreign keysStructured data, relationships, transactions: payments, orders, users, inventoryMassive write scale across many machines (possible but hard)
Key-valueRedis, DynamoDB, etcdkey → valueLookups by one key: sessions, caches, carts, feature flags, rate-limit countersYou need to query by anything other than the key
DocumentMongoDB, Couchbase, FirestoreJSON-like documentsFlexible or nested data read as a whole: product catalogues, user profiles, CMS contentLots of cross-document joins and transactions
Wide-columnCassandra, HBase, ScyllaDB, BigtableRows with a partition key and sorted columnsHuge write volume, time-ordered data per key: chat messages, activity feeds, IoT, logsAd-hoc queries, joins, strong transactions
GraphNeo4j, Amazon NeptuneNodes and edgesRelationship-heavy queries: friends-of-friends, recommendations, fraud ringsSimple lookups or huge bulk scans
Time-seriesInfluxDB, TimescaleDB, PrometheusTimestamp + value + tagsMetrics, monitoring, sensor readings, stock ticksGeneral app data
Search engineElasticsearch, OpenSearchInverted index over documentsFull-text search, filters, fuzzy matching (section 19)As the primary source of truth
Object storageS3, GCS, Azure BlobFiles (blobs) by keyImages, video, backups, big files (section 18)Small, frequently updated records

How to choose (the questions to ask yourself)

  1. Do I need transactions across several records? (money, stock levels) → SQL.
  2. What is the access pattern? Always by one key → key-value. Whole object at once → document. Append-heavy, time-ordered per user → wide-column.
  3. How much write volume? Tens of thousands of writes per second across regions → Cassandra/DynamoDB style.
  4. Is the schema stable? Stable → SQL. Changing often or varied → document.
  5. 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.

Say this in the interview: "I'll default to PostgreSQL for the core data because we need transactions and the relationships are clear. Messages are append-heavy and read by conversation, so I'd put them in Cassandra partitioned by conversation ID." Always tie the choice to the access pattern, never to fashion.

11. Replication

In plain words

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.

App writes Leader accepts all writes Follower 1 sync copy Follower 2 async copy, may lag App reads write sync: wait for ack async: don't wait read If the leader dies, a follower is promoted (failover). Async followers may lose the last few writes.
Leader-follower with one synchronous and one asynchronous follower ("semi-synchronous") — a common real-world compromise between safety and speed.

Three replication styles

StyleHow it worksGoodBadSeen in
Leader-follower (primary-replica, master-slave)One leader takes all writes and streams changes to followers. Reads from any.Simple, no write conflictsLeader is a write bottleneck; failover takes secondsPostgreSQL, MySQL, MongoDB, Redis
Multi-leaderSeveral leaders (often one per region) each accept writes and sync with each other.Low write latency in every region; survives a region outageWrite conflicts (two people edit the same row) must be resolvedMulti-region setups, offline-first apps, collaborative editing
LeaderlessClient writes to several replicas at once and reads from several; quorums decide (section 14).No failover needed; very availableWeaker consistency; conflict handling neededCassandra, 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).

Say this in the interview: "Single leader with two followers across availability zones: one synchronous for durability, one async for read scaling. Reads that must see the user's own writes go to the leader." Mentioning replication lag and read-your-writes unprompted is a strong signal.

12. Sharding (Partitioning)

In plain words

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

StrategyHowGoodBad
RangeRows by key range: users A–F on shard 1, G–M on shard 2; or by dateRange queries are easy ("all orders in March")Hotspots: all new data lands on the latest range
Hashshard = hash(key) % NEven spreadRange queries hit every shard; changing N moves almost every key
Consistent hashingKeys and shards on a ring; key goes to the next shard clockwiseAdding a shard moves only ~1/N of keysMore complex; needs virtual nodes for balance
Directory (lookup)A lookup table maps key → shardTotal flexibility; move single tenantsThe directory is an extra hop and a possible single point of failure
GeographicBy user region: EU users in EU shardLow latency; data residency lawsUneven 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.

Node A Node B Node C Node D (new) k1 → B, then D k2 → C k3 → A walk clockwise How it works 1 Hash each node onto the ring 2 Hash each key onto the ring 3 Walk clockwise; first node owns it Add node D: only keys between A and D move (k1). Others stay. Virtual nodes: each server sits at 100+ points so load is even
Adding or removing a node only moves the keys in one arc of the ring — about 1/N of the data — instead of reshuffling everything.

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.
Rule of thumb for choosing a shard key: (1) high cardinality (many different values), (2) spreads writes evenly, (3) most queries include it. Say out loud: "I'll shard by 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

In plain words

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 on user_id, or user_id + created_at, but not on created_at alone.
  • 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

NormalisedDenormalised
IdeaEach fact stored once; join to combineCopy data where it is read; no joins
ReadsSlower (joins)Faster (one lookup)
WritesSimple, one placeMust update every copy
RiskJoin cost at scaleCopies drifting out of sync
Use forWrite-heavy, correctness-critical dataRead-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

  1. Add the right indexes; fix slow queries.
  2. Add a cache in front (section 8).
  3. Scale the machine up (vertical).
  4. Add read replicas.
  5. Denormalise hot read paths / use materialised views.
  6. Split by feature (users DB, orders DB) — vertical partitioning.
  7. Shard (section 12).
Say this in the interview: "Before sharding, I'd exhaust indexes, caching and read replicas — sharding is a one-way door that makes joins and transactions much harder." Interviewers love hearing that you don't jump to the most complex tool.

14. CAP Theorem, PACELC and Consistency

In plain words

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 partitionBehaviourGood forExamples
CP — consistencySome requests fail or wait rather than return stale dataPayments, bookings, inventory, leader electionHBase, ZooKeeper, etcd, MongoDB (default), Spanner
AP — availabilityAlways answers, maybe with stale data; syncs laterSocial feeds, likes, carts, DNS, product catalogueCassandra, 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)

ModelPromiseExample need
Strong / linearisableEvery read sees the latest write, as if there were one copyBank balance, seat booking
Sequential / causalEveryone sees related events in the same order (a reply after its question)Comments and replies, chat
Read-your-writesYou always see your own updates (others may not yet)Editing your profile
Monotonic readsYou never see data go back in timeRefreshing a feed
EventualIf writes stop, all replicas converge eventuallyLike 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).

Say this in the interview: "Since network partitions will happen, the real choice is C or A during one. For the payment ledger I'd choose consistency; for the feed and like counts, availability with eventual consistency." Picking per feature rather than per system is the senior answer.
Part 2 — Communication

15. Message Queues & Event Streaming

In plain words

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.

Producer A Producer B Message queue messages wait here, in order Worker 1 Worker 2 Dead-letter queue for humans to inspect publish & move on pull, process, ack after N failed tries The queue absorbs spikes: producers never wait, workers go at their own pace, add workers to go faster.
A work queue with a dead-letter queue. The message is removed only after a worker acknowledges it, so a crashed worker's message is delivered again.

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 queuePub/sub (topic)
Who gets a messageExactly one consumer from the groupEvery subscriber gets a copy
Use forDistributing work: "resize this image"Broadcasting events: "order placed" → email, analytics, inventory all react
ExamplesRabbitMQ queues, AWS SQSKafka 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 readingMessage deletedMessage kept; can replay
ThroughputHighVery high (millions/sec)
OrderingPer queue (FIFO queues) or best-effortPer partition
RoutingRich (RabbitMQ exchanges)Simple: topic + key
Best forTask queues, jobsEvent streams, analytics pipelines, many consumers, event sourcing

Delivery guarantees

GuaranteeMeaningHowRisk
At-most-onceDelivered 0 or 1 timesAck before processingLost messages
At-least-onceDelivered 1 or more timesAck after processing; retry on failureDuplicates
Exactly-onceEffect happens exactly onceAt-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.
Say this in the interview: "The upload API just writes the job to a queue and returns 202 Accepted. Workers process at-least-once and are idempotent, keyed on the job ID. Failed jobs go to a dead-letter queue with an alert." That covers async, guarantees, idempotency and failure in four lines.

16. API Design & Communication

In plain words

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

RESTGraphQLgRPC
ShapeResources at URLs + HTTP verbsOne endpoint; client sends a query describing the fields it wantsCall remote functions defined in a .proto file
FormatJSON (text)JSONProtocol Buffers (compact binary)
StrengthsSimple, cacheable by HTTP, universalNo over-fetching or under-fetching; one round trip for nested data; great for varied clientsVery fast, strongly typed, streaming, code generation
WeaknessesOver/under-fetching; many round tripsHarder caching; expensive queries possible; more server complexityNot browser-native; not human-readable
Use forPublic APIs, most CRUD appsMobile + web apps with complex, changing UI needsInternal 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

TechniqueHow it worksGoodBadUse for
Short pollingClient asks "anything new?" every N secondsDead simpleWasteful; delay up to N secondsRare updates, simple dashboards
Long pollingClient asks; server holds the request open until there's news (or timeout), then client asks againNear real-time with plain HTTPA connection held per client; reconnect overheadFallback when WebSockets aren't possible
WebSocketsOne persistent, two-way connectionReal-time both directions, low overheadStateful connections — harder to load-balance and scaleChat, multiplayer games, collaborative editing
Server-Sent Events (SSE)One persistent, server-to-client stream over HTTPSimple, auto-reconnect, works with HTTP infraOne direction only; text onlyLive feeds, notifications, stock tickers, streaming AI responses
WebhooksServer calls your URL when something happensNo polling between serversReceiver must be public and reliablePayment events (Stripe), GitHub events

Pagination

Offset paginationCursor (keyset) pagination
Request?limit=20&offset=40?limit=20&cursor=<last id or timestamp>
GoodSimple; jump to page NFast at any depth; stable when new items arrive
BadSlow for deep pages (DB still skips rows); items shift and duplicate when new ones are insertedNo "jump to page 50"
Use forAdmin tables, small dataFeeds, 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
Say this in the interview: "Public API is REST with cursor pagination; internal services talk gRPC. Real-time updates go over WebSockets. Any request that creates or charges something takes an idempotency key so client retries are safe."

17. Rate Limiting

In plain words

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

AlgorithmHow it worksGoodBad
Token bucketA 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 bucketRequests enter a fixed-size queue that drains at a constant rate. Full = reject.Perfectly smooth output rate; protects a fragile backendBursts are delayed or dropped; old requests can starve new ones
Fixed window counterCount per window (per minute, reset on the minute)Simplest; one counter per userEdge burst: 100 at 0:59 plus 100 at 1:00 = 200 in two seconds
Sliding window logStore the timestamp of every request; count those in the last 60 sExactMemory heavy (one entry per request)
Sliding window counterBlend current and previous window counts: prev x overlap% + currentClose to exact, cheap memoryAn 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.

Say this in the interview: "Token bucket at the API gateway, keyed by user ID, counters in Redis with an atomic Lua script. If Redis is unreachable I'd fail open — allow requests — so the limiter can't take the whole API down." (Fail closed instead for security-critical limits like login attempts.) Full case study in section 26.

18. Blob / Object Storage & File Uploads

In plain words

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:

  1. Client asks your API: "I want to upload video.mp4, 2 GB".
  2. 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.
  3. Client uploads directly to S3 using that URL.
  4. S3 sends an event (or the client calls "upload complete"); the API marks the row uploaded and 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.

Rule of thumb: "Files never go through the app servers or into the database. Presigned URL for upload, multipart for anything over ~100 MB, metadata in SQL, CDN for downloads, async workers for processing."

19. Search

In plain words

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.

Say this in the interview: "Postgres stays the source of truth. Changes flow through Kafka into Elasticsearch, which serves search queries. Results may lag by a second or two — acceptable for search." For small apps, mention Postgres full-text search is enough and avoids a new system.
Part 2 — Architecture & Operations

20. Monolith vs Microservices

In plain words

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.

MonolithMicroservices
DevelopmentSimple to start, one codebaseEach team owns a service
DeployAll or nothingIndependent, frequent
ScalingScale the whole appScale just the hot service
Calls between partsFunction calls (fast, reliable)Network calls (slow, can fail)
DataOne database, easy joins and transactionsDatabase per service; no cross-service joins; sagas for transactions
FailureA bug can take down everythingFailures isolated — if you add timeouts and circuit breakers
OperationsEasyHard: tracing, discovery, many pipelines
Best forSmall teams, early productsMany 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.
Say this in the interview: "I'd start with a modular monolith and split out services only where there's a clear reason — a part that needs to scale separately, or a separate team." In a design interview it's fine to draw services as separate boxes, but show you know the costs: network failures, distributed data, and operational load.

21. Reliability Patterns

In plain words

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

PatternWhat it doesWatch out for
RedundancyTwo or more of everything important, across availability zones (separate data centres in one region) or regionsCost; keeping copies in sync
FailoverSwitch to the standby when the primary dies. Active-passive: standby idle until needed. Active-active: all copies serve traffic.Detection time; split brain
TimeoutsNever wait forever on a call; give up after e.g. 1–2 sToo long = threads pile up; too short = false failures
Retries with exponential backoff + jitterRetry failed calls, waiting 100 ms, 200 ms, 400 ms… plus a random extra so clients don't retry in syncOnly retry idempotent operations and transient errors; cap attempts; retry storms
Circuit breakerAfter many failures, stop calling a broken dependency for a while; fail fast insteadNeeds a sensible fallback
BulkheadSeparate resource pools (threads, connections) per dependency so one slow dependency can't use up everythingTuning pool sizes
Graceful degradationTurn off non-essential features under stress: show cached recommendations, hide the "trending" boxMust be designed in advance
Load sheddingWhen overloaded, reject some requests early (503) to protect the restChoose what to drop (low priority first)
Health checks & auto-restartRemove and replace broken instances automaticallyDistinguish "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

CLOSED requests flow normally OPEN fail fast, no calls made HALF-OPEN let one test call through failures pass threshold (e.g. 50% in 10 s) after cool-down (30 s) test call succeeds test fails
Like the fuse box at home: when too many failures happen, the breaker trips so a broken dependency stops dragging the whole system down.
Say this in the interview: "Every outbound call gets a timeout, bounded retries with exponential backoff and jitter — only for idempotent calls — and a circuit breaker with a fallback. That stops one slow dependency from cascading." Name all three together; they work as a set.

22. Distributed Systems Essentials

In plain words

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

ApproachHowGoodBad
DB auto-incrementThe database hands out 1, 2, 3…Simple, short, orderedSingle point of failure and bottleneck; doesn't work across shards (unless each shard steps by N: 1, 4, 7…)
UUID v4128-bit random number generated anywhereNo coordination at allLong (36 chars); random order hurts B-tree index inserts; not sortable by time
UUID v7 / ULIDTimestamp prefix + randomnessNo coordination and roughly time-sortableStill 128 bits
Snowflake (Twitter)64-bit: 41 bits timestamp (ms) + 10 bits machine ID + 12 bits sequenceCompact, time-sortable, ~4096 IDs per ms per machine, no coordination per IDNeeds unique machine IDs; clock going backwards causes trouble
Ticket server / range allocationA central service hands out blocks (IDs 1–1000 to server A)Short IDs, little coordinationCentral 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
HowA 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).
ConsistencyStrong (atomic)Eventual; intermediate states are visible
AvailabilityBlocking: if the coordinator dies mid-way, participants hold locks and waitNon-blocking, services stay independent
Styles—Choreography: each service reacts to events. Orchestration: a central orchestrator tells each step what to do.
Use forFew participants, same data centre, strict needsMicroservices, 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:

  1. Every node starts as a follower. The leader sends regular heartbeats.
  2. If a follower hears nothing for a random timeout, it becomes a candidate, increases the term (election number) and asks others for votes.
  3. A candidate that gets votes from a majority becomes leader for that term. Majority means only one leader per term is possible.
  4. The leader receives writes, appends them to its log, and copies them to followers. An entry is committed once a majority has it.
  5. 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.
Rule of thumb: Snowflake-style IDs for sortable unique IDs; sagas plus the outbox pattern for cross-service workflows; etcd/ZooKeeper when you need a leader; optimistic locking before distributed locks; never trust wall-clock time to order events across machines.

23. Observability

In plain words

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.

PillarWhat it isAnswersTools
LogsTimestamped records of events, ideally structured (JSON) with a request ID"What exactly happened to this request?"ELK (Elasticsearch, Logstash, Kibana), Loki, Datadog
MetricsNumbers over time: request rate, error rate, latency percentiles, CPU, queue depth"Is something wrong right now? Since when?"Prometheus + Grafana, CloudWatch
TracesThe 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

TermMeaningExample
SLI — Service Level IndicatorThe measurement% of requests answered successfully in under 300 ms
SLO — Service Level ObjectiveThe internal target for the SLI99.9% of requests under 300 ms, over 30 days
SLA — Service Level AgreementA promise to customers, with penalties99.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.
Say this in the interview: "Each service emits structured logs with a request ID, RED metrics to Prometheus, and OpenTelemetry traces. We alert on SLO burn rate — for example, error rate eating the monthly budget too fast — with a runbook per alert." Mention this in the wrap-up of every design.

24. Security in Design

In plain words

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

ThreatDefence
DDoS (Distributed Denial of Service — a flood of traffic from many machines)CDN / edge protection (Cloudflare, AWS Shield), rate limiting, auto-scaling, WAF
SQL injectionParameterised 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 loginStrict 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.

Say this in the interview: "TLS everywhere and mTLS between services, encryption at rest with KMS-managed keys, auth at the gateway with short-lived JWTs, per-request ownership checks, secrets in a vault, and a CDN with DDoS protection at the edge." One sentence, and security is covered in your wrap-up.
Part 3 — Case Studies
How to use these. Every case study follows the same eight steps as the framework in section 2: Requirements → Estimates → API → Data model → High-level design → Deep dives → Bottlenecks & trade-offs → Summary to say out loud. Learn the shape, not the exact numbers. In the interview, the same shape works for questions you have never seen.

25. URL Shortener (TinyURL / bit.ly)

In plain words

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

Client Load balancer App servers stateless encode base62 ID generator counter ranges / Snowflake Redis cache code → long URL (hot) Database sharded by code read path: cache hit → redirect; miss → DB → fill cache → redirect
Writes need a unique ID; reads are a single key lookup served mostly from cache.

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.
    Counter ranges are the usual best answer. Mention that sequential codes are guessable; shuffle the bits if that matters.
  • 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_at on read, and clean up with a background job.

7. Bottlenecks & trade-offs

ConcernAnswer
Hot linksCache + CDN edge redirects
DB growthShard by hash of code
ID coordinator downServers hold a pre-fetched range, so they keep working for a while
Abuse / spam linksRate-limit creation per user/IP; scan URLs against blocklists
Say this out loud: "It's a read-heavy key-value problem. I'll generate codes from a counter encoded in base62, with each server taking ID ranges so there are no collisions. Redirects hit a Redis cache first, then a DB sharded by code. I'll use 302 if we need analytics, and send click events to Kafka so the redirect stays fast."

26. Rate Limiter as a Service

In plain words

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-After header.
  • 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

Client API gateway rate-limit middleware rules cached locally Redis cluster INCR + EXPIRE in a Lua script Backend services only allowed requests Rules store (config) over limit → 429
All gateways share one counter store, so the limit is global rather than per server.

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

ChoiceGainCost
Central RedisAccurate global limitExtra network hop; Redis must be highly available
Local counters + syncNear-zero latencyCan overshoot the limit slightly
Fail openAPI stays upNo protection during the outage
Say this out loud: "A token-bucket limiter in the API gateway, with counters in a shared Redis cluster updated atomically by a Lua script. Rules are cached in each gateway. Over-limit requests get 429 with Retry-After. If Redis fails we fail open for normal endpoints and closed for sensitive ones like login."

27. Chat App (WhatsApp / Messenger)

In plain words

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

Sender Receiver Chat server A holds WebSocket Chat server B holds WebSocket Queue Kafka Message store Cassandra, by chat_id Presence (Redis) user → which chat server Push notifications APNs / FCM if offline offline
Save first, then deliver. The presence store tells server A which server holds the receiver's connection.

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_id keeps one chat's messages in order.
  • Duplicates. Networks retry. The clientMsgId is 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

ConcernAnswer
A chat server diesClients reconnect (via LB) to another server; presence updates; undelivered messages are still in the store
Millions of connectionsChat servers only hold connections; keep them light; scale horizontally
Hot group chatsPull model for very large groups
Multi-deviceEach device has its own connection and its own "last synced msg_id"
Say this out loud: "Clients keep a WebSocket to a stateless-ish chat server. A message is stored in Cassandra partitioned by chat_id first, then acked, then routed: presence in Redis tells us which server holds the receiver, and Kafka carries it there. Offline users get a push notification and sync on reconnect using their last seen msg_id. Snowflake IDs keep order; a client message ID makes retries safe."

28. News Feed (Twitter / Instagram)

In plain words

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

Author Post service Posts DB + post cache Queue Fan-out workers Follow graph Feed cache (Redis) feed:{userId} → post IDs Reader Feed service merge + hydrate + rank
Post IDs are pushed into followers' feeds at write time. At read time the feed service fetches IDs, adds celebrity posts, and fills in the post content from cache.

6. Deep dives — the fan-out decision

Fan-out on write (push)Fan-out on read (pull)
When work happensWhen a post is createdWhen a feed is opened
Feed read speedVery fast — already builtSlow — query all followees and merge
Write costHuge for users with millions of followersCheap
Wasted workBuilds feeds for inactive usersNone
Best forNormal usersCelebrities

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.
Say this out loud: "Hybrid fan-out. On post, a worker pushes the post ID into each follower's Redis feed list — except for celebrity accounts, whose posts are pulled and merged at read time. Feeds store IDs only and are hydrated from a post cache, paginated with a cursor. It's eventually consistent, which is fine for a feed."

29. Notification System

In plain words

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

Services orders, auth… Notification svc prefs · dedupe rate limit · template push queue SMS queue email queue workers → APNs / FCM workers → Twilio workers → SES / SendGrid failures → retry with backoff → dead-letter queue
One queue per channel, so a slow email provider can't hold up urgent push notifications.

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.
Say this out loud: "Services call one notification API that returns 202. It checks preferences, deduplicates with an idempotency key, renders the template and puts the job on a per-channel, per-priority queue. Channel workers call APNs/FCM, Twilio and SES, retry with backoff, and send permanent failures to a dead-letter queue. Delivery status is logged from provider webhooks."

30. File Storage & Sync (Dropbox / Google Drive)

In plain words

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

Device A chunk + hash Metadata service which chunks missing? Metadata DB SQL: files, versions Object storage (S3) chunks by hash Notification svc long poll / WebSocket Device B pulls changes 1 2 upload missing 3 notify 4 "you have changes" → Device B fetches change list + new chunks
Bytes go straight to object storage; the servers only handle small metadata requests.

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 changes journal 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).
Say this out loud: "Files are split into 4 MB content-hashed chunks. The client asks the metadata service which hashes are missing and uploads only those, straight to S3 with presigned URLs. Committing a version is a SQL transaction with an optimistic-lock base version to detect conflicts. Other devices get a push that something changed and pull from a change journal using their cursor."

31. Video Streaming (YouTube / Netflix)

In plain words

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

Creator Raw storage S3 original Job queue Transcoding workers 1080p · 720p · 480p · 240p 4-second segments + manifest Processed storage segments by quality CDN edges near the viewer Viewer adaptive player API + metadata DB manifest
Upload once, transcode in parallel, serve segments from the edge.

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.
Say this out loud: "Upload the raw file to object storage via a resumable presigned URL. An event creates transcoding jobs on a queue; workers produce several resolutions, each cut into short HLS segments with a manifest. Segments are served from a CDN, and the player does adaptive bitrate — switching quality as bandwidth changes. Metadata lives in a DB; view counts are aggregated from a stream."

32. Ride Sharing (Uber / Lyft)

In plain words

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

Drivers Location service 1.25M updates/sec Geo index (in memory) geohash cell → driver IDs Rider Ride service trip state machine Matching service nearest + ETA + lock driver Rides DB (SQL) WebSocket gateway → both apps query nearby
Hot, fast-changing location data lives in memory; durable trip records live in SQL.

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 available to offered (a Redis SET NX lock 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.
Say this out loud: "Drivers stream locations every few seconds to a location service that keeps only the latest position in an in-memory geo index — geohash cells in Redis, sharded by city. A ride request goes to matching, which searches the rider's cell and its neighbours, ranks by ETA, and atomically locks the chosen driver to prevent double booking. Trips are a state machine stored in SQL, and both apps get live updates over WebSockets."

33. Web Crawler

In plain words

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.
Say this out loud: "A URL frontier with priority queues feeding per-host queues for politeness, partitioned across workers by hostname. Fetchers respect a cached robots.txt, parsers extract links, and new URLs are normalised and checked against a Bloom filter before being enqueued. Content is hashed to skip duplicates and stored compressed in object storage for the indexer."

34. Search Autocomplete (Typeahead)

In plain words

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 → top5 in 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.
Say this out loud: "Precompute. An offline pipeline aggregates search logs into query frequencies and builds a trie where every node already stores its top 5 completions. The serving layer holds the trie in memory, sharded by prefix range, with short prefixes cached at the CDN. The client debounces keystrokes. A streaming job adds trending queries between rebuilds."
Revision — the night before

Cheat Sheet — If You Hear X, Reach for Y

In plain words

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 replicasServe repeats from memory; spread reads
"Global users", "images/videos", "static files"CDN + object storageServe bytes close to the user
"Too much traffic for one server"Stateless servers + load balancerScale horizontally
"Too much data for one database"Sharding (consistent hashing)Split data across machines
"Slow task", "email", "video processing", "spikes"Message queue + workersDo 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 keysStrong consistency, no double charges
"Flexible schema", "huge write volume"NoSQL (document / wide-column)Scales writes, easy schema change
"Relationships", "friends of friends"Graph databaseTraversals are cheap
"Search text", "fuzzy matching"Elasticsearch (inverted index)Full-text search
"Autocomplete"Trie with top-k per nodePrecomputed prefix answers
"Nearby", "location"Geohash / quadtree, Redis GEOSpatial lookups
"Abuse", "too many requests"Rate limiter (token bucket)Protect the system
"Unique IDs across servers"Snowflake IDsUnique, time-sortable, no coordination
"Transaction across services"Saga patternLocal steps + compensations
"A dependency keeps failing"Circuit breaker, timeouts, retries with backoffFail fast, don't cascade
"Have we seen this before?" at huge scaleBloom filterTiny memory, no false negatives
"Leaderboard", "top N"Redis sorted setRanked reads in O(log n)
"Analytics", "counts", "metrics over time"Stream processing + columnar / time-series DBAggregate, don't store every row

Trade-offs to Mention

In plain words

There's no perfect design — only choices with costs. Saying the cost out loud is what makes an answer sound senior.

ChoiceYou gainYou pay
SQL vs NoSQLJoins, transactions, strong consistencyHarder horizontal scaling
Strong vs eventual consistencyAlways-correct readsHigher latency, lower availability during partitions
CacheSpeed, less DB loadStale data, invalidation complexity
Normalise vs denormaliseNo duplication, easy writesSlower reads (joins)
Sync vs async processingSimple, immediate resultSlow requests, tight coupling
Push (fan-out on write) vs pullFast readsExpensive writes for popular users
Monolith vs microservicesSimple deploys, easy debuggingHarder to scale teams and parts independently
Sync vs async replicationNo data loss on failoverSlower writes
Polling vs WebSocketsSimple, statelessWasted requests, delay
Batch vs stream processingSimple, cheap, accurateResults are hours old
Vertical vs horizontal scalingNo code changesHard ceiling, single point of failure

Numbers to Remember

WhatRoughly
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 traffic2–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 plain words

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
Marks tip: a neat diagram plus explicit trade-offs usually earns more marks than extra components. If time is short, write the headings and one line under each rather than finishing only half.

Learning Path

  1. Learn the framework (section 2) until you can run it without notes.
  2. Practise estimates — do the maths for 3 systems out loud with round numbers.
  3. Master the building blocks — for each, be able to say what it is, when to use it, and its cost.
  4. Do the case studies — URL shortener first, then chat, news feed, and notifications. Draw each from memory.
  5. Mock interviews — 45 minutes, timer on, talking out loud. Record yourself once; you'll spot silences and hand-waving.
  6. Read real architectures — engineering blogs from Netflix, Uber, Discord, Slack and Instagram show these patterns in production.
  7. Night before — read the cheat sheet, trade-offs, numbers and case-study one-liners on this page. Nothing new.
The mindset that matters: a system design interview is a conversation, not a quiz. There's no single right answer. Clarify, estimate, draw something simple that works, then improve it — and say out loud why each choice beats the alternative. Do that calmly and you'll do well.