SSystem Design

Block E · Topics 21–27

Case studies

Seven complete designs. Read them for the reasoning chain — requirement → number → constraint → component — not for the diagrams. In an interview you will be asked to produce that chain live, and a memorised picture cannot survive the first follow-up.

21

Feed Systems

fan-out on writefan-out on readhybridranking
The core question

When Alice posts, do you immediately write that post into all 500 of her followers' inboxes (fan-out on write), or do you write it once and assemble each follower's feed when they open the app (fan-out on read)? Everything else in a feed design is downstream of this one choice.

Scope and numbers

500M DAU · 2 posts/user/day · 30 feed opens/user/day · avg 200 follows

WRITES   1B posts/day    ≈  11,500 /s   → peak ×3 ≈ 35,000 /s
READS    15B opens/day   ≈  175,000 /s  → peak ×3 ≈ 500,000 /s   # 15:1

FAN-OUT ON WRITE cost:  11,500 posts/s × 200 followers
                     =  2.3M inbox writes/s          # large but linear
FAN-OUT ON READ cost:   500,000 reads/s × fetch 200 authors' posts + merge
                     =  100M row reads/s             # hopeless

Conclusion: default to fan-out on WRITE. Reads are 15× writes and a read
must be fast; a write can be slow and asynchronous. Precompute the expensive
side. But 2.3M writes/s is an average — the tail is what breaks it.
Fan-out on WRITE Alice posts 200 followers inbox:bob inbox:carol inbox:… ×200 read = ONE lookup ✓ write = 200 writes celebrity = 100M writes ✗ Fan-out on READ Bob opens the app posts by alice posts by dan … ×200 authors write = ONE write ✓ read = 200 queries + merge ✗ always fresh ✓ HYBRID — what everyone actually ships · Normal accounts (<10k followers): push to inboxes on write. · Celebrities (>10k): do NOT fan out. Their posts are pulled at read time. · Read = precomputed inbox ∪ live pull from the few celebrities you follow, merged. The celebrity problem: one post × 100M followers = 100M writes, a multi-hour fan-out, and a hot partition. So don't. A user follows ~200 accounts but only a handful of celebrities — the pull is cheap.
Fan-out on write for the many, fan-out on read for the few. The hybrid exists because follower counts are power-law distributed, not normal.

Data model

posts       PK post_id (Snowflake: time-sortable)
            author_id, text, media_keys[], created_at
            # sharded by post_id; the single source of truth

follows     PK (follower_id, followee_id)            # "who do I follow"
            GSI (followee_id, follower_id)           # "who follows me" — the fan-out list

inbox       Redis list/zset per user, capped at ~800 post IDs
            key inbox:{user_id}  →  [post_id, …]     # IDs only, not content
            # Store IDs, hydrate content from a shared post cache. 500M users
            # × 800 IDs × 8 bytes ≈ 3 TB — a sharded Redis fleet, not one node.
            # Storing full posts here would be 100× that and duplicated.

Design points that earn credit

  • Cap the inbox. Nobody scrolls past ~800 items. Trim on write. Deep pagination falls back to a slower query path against posts.
  • Inactive users. Fanning out to accounts that haven't opened the app in 90 days is pure waste — often the majority of your writes. Mark them dormant and generate on read when they return.
  • Fan-out is async. The post write returns as soon as posts is durable; fan-out is a queued job. The author sees their own post immediately by merging their own recent posts client-side or in the API (read-your-writes without waiting for 200 inbox writes).
  • Ranking is the same funnel as search: retrieve candidates from the inbox plus pulls, filter (blocked, muted, already seen), score with a model over recency, affinity, engagement probability and media type, then diversify so one author can't take the whole screen.
  • Pagination must be cursor-based — an offset in a feed that receives new items at the top guarantees duplicates and skips.
  • Deletes and privacy changes after fan-out: don't chase 200 inboxes. Filter at hydration time — the inbox holds IDs, and a deleted or now-private post simply isn't returned.
Say this

"Reads outnumber writes 15 to 1 and a read has to be fast, so I precompute: fan-out on write into a per-user Redis inbox holding post IDs, capped at 800. That breaks for celebrities — a 100M-follower account would need 100M writes per post — so accounts over about 10,000 followers are excluded from fan-out and pulled at read time instead, then merged. I also skip fan-out for dormant users, which is most of the write volume. The cost of the hybrid is a more complex read path and a small consistency window; the alternative is a fan-out that takes hours and hot-spots a shard."

22

URL Shortener

key generationredirectsexpiryanalytics
Why it's asked

It looks trivial and isn't. It exercises key generation without coordination, an extreme read:write ratio, cache design, and the difference between a 301 and a 302 — and every one of those has a defensible tradeoff. It's the best 30-minute warm-up question in the field.

Numbers

100M new URLs/day  ≈ 1,200 /s   (peak ~3,500 /s)
10:1 read ratio    ≈ 12,000 /s redirects (peak ~35,000 /s)
Storage: 500 bytes/row × 100M/day × 365 × 5  ≈  90 TB over 5 years

Key space with a 7-character base62 alphabet:
  62^7  =  3.5 trillion   →  at 100M/day, ~95 years of keys
  62^6  =  56 billion     →  ~1.5 years. Not enough. Use 7.

Key generation — the real question

ApproachHowTradeoff
Hash the URL, take 7 charsbase62(md5(url))[:7]Deterministic and dedupes identical URLs — but collisions are certain at scale, so you need a check-and-retry loop, which is a read before every write
Random 7 charsGenerate, insert with a unique constraint, retry on conflictUnguessable; collision probability is tiny until the space fills. Simple and good.
Counter + base62Global counter → encodeZero collisions by construction, shortest keys. But the counter is a coordination point, and keys are sequential — enumerable and they leak volume.
Ranged counter (the answer)A Zookeeper/DB allocator hands each app server a block of 1M IDs; the server allocates locallyNo per-request coordination, no collisions, survives server death (you lose an unused block — irrelevant against 3.5 trillion). Add a scramble/Feistel permutation over the counter so keys aren't guessable.

Architecture and the redirect

GET /aB3xK9z
  → edge cache / CDN            # hot links: most traffic never reaches you
  → Redis  short_code → long_url  (hit rate >95%; hot links are power-law)
  → DB miss path: key-value store, partitioned by short_code
  → 302 Found, Location: <long_url>
  → fire-and-forget click event onto Kafka   # NEVER block the redirect

# 301 vs 302 — a genuine tradeoff, and interviewers ask:
#   301 permanent → browsers cache it → fastest for users, near-zero
#                   load on you, but you LOSE all analytics after the
#                   first hit and can never change or revoke the target.
#   302 found     → every click comes to you → analytics, expiry,
#                   revocation, A/B destinations. Costs traffic.
# Almost every real shortener chooses 302, because the product IS the
# analytics. Say that reasoning, not just the number.

The rest of it

  • Store: a simple key-value workload — DynamoDB/Cassandra partitioned on short_code, which is uniformly distributed by construction, so no hotspots. Row: {short_code, long_url, owner_id, created_at, expires_at, is_active}.
  • Expiry: a TTL column plus a lazy check on read (expired → 410 Gone), with a background sweeper to reclaim storage. Never rely on the sweeper for correctness.
  • Analytics: click events to Kafka → stream aggregation into per-link counters (Redis HINCRBY for live counts, a columnar warehouse for the dimensional stuff). Use HyperLogLog for unique-visitor estimates — exact distinct counts at this volume are not worth the cost.
  • Custom aliases: a separate namespace check, reserved-word list, and a unique constraint. Rate-limit alias creation hard — it's a squatting vector.
  • Abuse: this is a phishing and malware delivery system by default. Check destinations against a safe-browsing API at creation and periodically after (attackers point at a benign URL first and swap it), show an interstitial for suspicious targets, and rate-limit per account and per IP.
Say this

"7-character base62 gives 3.5 trillion keys, which is a century of headroom. I'd generate from a counter rather than a hash so there are no collisions and no read before every write — but rather than a global counter per request, each app server leases a block of a million IDs from a central allocator and allocates locally, so there's no coordination on the hot path. I'd run the counter through a Feistel permutation so codes aren't sequentially guessable. Redirects are 302 not 301: 301 would be faster and cheaper but we'd lose every click after the first, and click analytics is the actual product."

23

Chat System

deliveryorderingpresenceunread countsoffline
Why it's hard

Messages must never be lost, must appear in a sensible order, must reach devices that are currently offline, and must arrive in under a second when online. Those four requirements pull against each other, and the naive "push over the socket" design fails all but the last.

Architecture

Client ──WebSocket──▶ Gateway (stateful, holds sockets)
                        │
                        ▼
                     Chat service ──▶ 1. PERSIST first, assign seq
                        │                (durability before delivery)
                        ├──▶ 2. Registry: which gateways hold the recipients?
                        ├──▶ 3. Online  → push via those gateways
                        └──▶ 4. Offline → push notification (APNs/FCM)
                                          + it waits in storage

Reconnect: client sends its last seen seq per conversation
           → server returns everything after it (gap backfill)
Persist before you push

If you push over the socket and then write, a crash between the two loses the message for a user who has already seen it — or worse, the recipient's client shows it and it disappears on next sync. Durability first, delivery second, always.

Ordering

Global ordering is meaningless and expensive. What users perceive is ordering within a conversation. Assign a monotonic per-conversation sequence number server-side — the conversation's owning shard is a single writer, so it can hand out sequence numbers without coordination.

Do not order by client timestamp: device clocks are wrong, sometimes by hours, and users will happily set them wrong on purpose. Keep the client timestamp for display only; order by server sequence. For optimistic local echo, the client shows the message immediately with a temporary ID and reconciles when the server's assigned seq comes back.

Data model

messages         PK conversation_id, SK seq        # sharded BY conversation_id
                 sender_id, body(encrypted), created_at, attachments[]
                 # "last 50 messages in this conversation" = one partition read

conversations    PK conversation_id → {type, member_ids[], last_seq, updated_at}

user_conversations  PK user_id, SK updated_at DESC   # the chat list, sorted
                    → conversation_id, last_read_seq, muted

# Unread count = conversation.last_seq − user.last_read_seq
# A subtraction, not a counter. No increments to keep in sync across
# devices, no drift, and marking read is one write of one number.
# This is the answer interviewers are listening for.

Group chat and fan-out

Small groups (<500): write once to the conversation partition, fan out delivery to online members. Large groups / channels (100k+): the same celebrity problem as feeds — don't fan out per member; members pull from the conversation on open, and you push only a lightweight "there's activity" signal. Per-member unread state at that size is also too expensive to maintain eagerly, which is exactly why large channels in real products show coarser badges.

The details that get asked

  • Multi-device. A message must reach every one of a user's devices, and read state must sync between them. Track a per-device cursor as well as the per-user one.
  • Delivery receipts. Sent (server has it) → delivered (device acked) → read (user opened). Each is a separate small write; batch and debounce them or read receipts become more traffic than messages.
  • Typing indicators and presence are ephemeral: never persist them, use a TTL'd key, and accept loss.
  • End-to-end encryption (Signal protocol) changes the design: the server stores ciphertext and cannot search, generate previews, or do server-side moderation; key exchange, per-device keys and multi-device fan-out all become client responsibilities.
  • Media goes to object storage via presigned URLs (topic 20); the message carries a reference, not bytes.
  • Retention drives cost more than anything else. WhatsApp's original design deleted messages from the server once delivered — a storage decision that shaped the whole product.
Say this

"WebSocket gateways that hold connections and nothing else, with a registry mapping user to gateway. A message is persisted with a per-conversation sequence number before any delivery attempt, because the socket is a fast path, not a durable one — on reconnect the client sends its last seen sequence and we backfill the gap. Sharded by conversation_id, so loading a chat is a single-partition read and the shard is a single writer that can assign sequence numbers without coordination. Unread count is last_seq − last_read_seq, a subtraction rather than a counter, so it can't drift across devices."

24

Video Streaming

transcodingHLS/DASHadaptive bitrateCDNDRM
The shape of the problem

Video is two systems with almost nothing in common: an upload and processing pipeline that is throughput-bound and can take minutes, and a playback path that is bandwidth-bound and must start in under two seconds. Design them separately and say so.

Ingest and transcoding

upload (presigned, multipart, resumable)  →  raw object storage
   ▼
split into segments (2–10s GOP-aligned chunks)
   ▼
FAN OUT: transcode every segment × every rendition, in parallel
   240p 400kbps · 480p 1Mbps · 720p 2.5Mbps · 1080p 5Mbps · 4K 15Mbps
   (plus codecs: H.264 for compatibility, AV1/HEVC for efficiency)
   ▼
package into HLS/DASH · generate manifests · thumbnails · captions
   ▼
publish to object storage → CDN

# Why segment-level parallelism matters: a 2-hour film is one job that
# takes hours, or 3,600 independent 2-second jobs that finish in minutes
# across a worker fleet. Segments are also independently retryable.
# Storage cost: all renditions together ≈ 2–3× the original.

Adaptive bitrate — how playback actually works

The server is dumb; the client decides. The player fetches a manifest listing every rendition, then requests segments one at a time over plain HTTP, choosing the next segment's quality from measured throughput and how full its buffer is. Congestion → drop to 480p mid-stream; recovery → climb back. Because it's ordinary HTTP GETs of static files, the whole thing caches perfectly at a CDN — which is the entire reason HLS/DASH beat custom streaming protocols.

master.m3u8
  ├─ 240p/index.m3u8  → seg0.ts, seg1.ts, …
  ├─ 720p/index.m3u8  → seg0.ts, seg1.ts, …
  └─ 1080p/index.m3u8 → seg0.ts, seg1.ts, …

# Startup trick: begin at a LOW rendition so playback starts in <2s,
# then climb once the buffer is healthy. Users abandon on startup delay
# far more readily than they complain about the first five seconds
# looking soft. Buffer target ~30s: too small rebuffers, too large
# wastes bandwidth on video nobody watches.

Delivery

  • The CDN is the system. Origin serves the CDN, not users. Aim for >95% offload; a popular title should exist in every PoP. Some providers push hardware into ISP networks (Netflix Open Connect) so bytes never cross the public internet.
  • Prewarm for a known launch — pushing a new season to edges before release beats 10 million simultaneous cache misses.
  • Live is different: segments are produced continuously, latency budget is seconds not minutes, the manifest is a sliding window, and you cannot pre-transcode. LL-HLS/DASH with partial segments gets to ~2–5 s; WebRTC gets sub-second but doesn't scale the same way.

DRM and access control

Three tiers, and knowing which is which is the whole point: signed URLs (expiring, IP/token bound) stop casual link sharing; AES-128 / SAMPLE-AES encryption with a key server stops trivial download; real DRM (Widevine, FairPlay, PlayReady) enforces hardware-backed decryption and output protection, and is the only thing studios accept. You need all three formats to cover Chrome/Android, Safari/iOS and Edge, so packaging is per-DRM. State plainly that DRM raises cost and complexity and is a licensing requirement rather than a security absolute.

Say this

"Two pipelines. Upload goes straight to object storage via presigned multipart, then a fan-out transcode: the video is split into two-second GOP-aligned segments and every segment × rendition is an independent job, so a two-hour film is thousands of parallel jobs instead of one long one. Playback is HLS with adaptive bitrate — the client picks quality from throughput and buffer level, and because it's just HTTP GETs of static segments the CDN caches everything, which is the only way 21 GB/s of egress is affordable. I'd start playback at a low rendition to get under two seconds to first frame and climb from there."

25

Ride Sharing

geospatial indexmatchinglive locationsurge
The core problem

"Find drivers near this point" is not a query a B-tree can answer — two dimensions have no single sort order. And the data is extraordinarily write-heavy: every driver reports a new location every few seconds, forever.

Numbers

1M active drivers, location ping every 4s   →  250,000 writes/s
100k ride requests/min                       →  ~1,700 matches/s
# The location firehose is 150× the matching load. That asymmetry
# decides the storage: an in-memory geospatial index, not a database.
# Losing a location update is fine — another arrives in 4 seconds.

Geospatial indexing

TechniqueHowTradeoff
GeohashRecursively subdivide the globe; encode the cell as a string. Shared prefix ⇒ nearby.Simple, string-prefix searchable in any KV store. Boundary problem: two points either side of a cell edge share no prefix, so you must query the 8 neighbours too.
QuadtreeTree that subdivides dense regions furtherAdapts to density — dense in Mumbai, coarse in the desert. Rebalancing is work.
Google S2Projects the sphere onto a cube, Hilbert curve for localityExcellent locality and precise region coverage; more machinery.
Uber H3Hexagonal gridHexagons have uniform distance to all 6 neighbours (squares don't — diagonals are 1.41× further), which makes ring expansion and demand aggregation clean. Built for exactly this problem.
# The practical shape, in Redis
GEOADD  drivers:h3:{cell}  <lon> <lat> driver_42     # or a sorted set per cell
# Search = my cell + its ring of neighbours, expanding outward until
# enough candidates are found. Filter by vehicle type, rating, availability.
# Only current location matters — it's a cache, not a ledger.

# Location HISTORY (for billing, disputes, replay) is a separate,
# append-only pipeline: pings → Kafka → columnar/time-series store.
# Never conflate "where is this driver now" with "where has this driver been".

Matching

Naive nearest-driver is wrong in two ways: straight-line distance ignores rivers, one-way streets and traffic (use ETA from a routing engine, not Euclidean distance), and greedy per-request assignment produces globally worse outcomes than batching. Real systems accumulate requests over a short window (a few seconds) and solve a small assignment problem — a rider two seconds later may be a much better match for the driver you were about to dispatch.

Then the concurrency problem: one driver must not be offered to two riders. The clean answer is an atomic state transition on the driver record — UPDATE drivers SET state='OFFERED', ride_id=? WHERE id=? AND state='AVAILABLE' — zero rows updated means someone else won. Add a short offer timeout that returns the driver to the pool if they don't accept.

The rest of the design

  • Live tracking: the rider needs the driver's position during the trip. Push over a WebSocket at 1–4 s, and interpolate/snap-to-road on the client so a jittery GPS trace looks smooth.
  • Trip state machine: requested → matched → accepted → arrived → started → completed → paid. Persist every transition with idempotency keys — the driver's phone will lose signal mid-trip and replay events.
  • Surge: compute a demand/supply ratio per H3 cell over a rolling window and multiply. Publish the price at request time and honour it — changing it mid-flow is a support nightmare. Smooth across neighbouring cells so the map doesn't show a hard price cliff across a street.
  • Regional isolation: a ride is inherently local, so shard by city or region. It's a natural bulkhead — an incident in one city doesn't touch another — and it keeps latency low. Cross-region traffic is essentially nil.
  • Payments are a separate concern with their own guarantees — see topic 26.
Say this

"Driver locations are 250,000 writes a second and 150× the matching load, so they live in an in-memory geospatial index — H3 cells in Redis — not in a database. Losing a ping is harmless; another arrives in four seconds. Matching searches my cell then expanding rings, ranks by ETA from the routing engine rather than straight-line distance, and batches requests over a few seconds because greedy assignment is globally worse. The double-booking risk is handled with a conditional update on the driver's state, so exactly one dispatch can win. Trip history is a completely separate append-only pipeline into a time-series store."

26

Payments

double-entry ledgeridempotencyreconciliationsagas
Different rules apply

Everywhere else in this guide, availability usually wins. Here it doesn't. Money must never be created, destroyed or double-spent, every number must be explainable years later, and "eventually consistent" is a compliance problem. Payments is where you demonstrate that you know when not to reach for the scalable answer.

Double-entry, and why a balance column is a bug

# WRONG — destroys history, races under concurrency, unauditable
UPDATE accounts SET balance = balance - 100 WHERE id = 'alice'
UPDATE accounts SET balance = balance + 100 WHERE id = 'bob'

# RIGHT — an immutable ledger. Entries are facts, never updated.
entries(id, transaction_id, account_id, direction, amount, currency, created_at)

txn t_881:
  t_881 | alice | DEBIT  | 100 | INR
  t_881 | bob   | CREDIT | 100 | INR
# INVARIANT: for every transaction, sum(debits) == sum(credits).
# Both rows commit in ONE transaction, or neither does.
# Balance = SUM(entries) — derived, never stored as truth.
# Snapshot balances nightly for performance; the snapshot is a CACHE
# and must always be re-derivable from the entries.

Money is stored in the smallest unit as an integer (paise, cents) — never a float, because 0.1 + 0.2 ≠ 0.3 and that error compounds across millions of rows. Every amount carries a currency; never mix currencies in one account.

The charge flow

1. Client sends an Idempotency-Key with the charge request.
2. Claim the key (unique constraint) → state PENDING, amount authorised.
3. Ledger: DEBIT user_pending / CREDIT merchant_pending  (one txn)
4. Call the PSP, passing the SAME idempotency key downstream.
5. Outcome:
     success → ledger entries moving pending → settled
     failure → reversing entries (NEVER delete the originals)
     TIMEOUT/UNKNOWN → do NOT retry blindly. Leave PENDING and
                       reconcile against the PSP. Guessing here is
                       how you double-charge a customer.
6. Emit an event via the transactional outbox for downstream systems.
The unknown-outcome case is the interview

A network timeout on the PSP call means the charge may or may not have happened. There is no local information that resolves it. The only correct moves are: keep the row PENDING with the idempotency key, query the PSP for that key's status (idempotent, safe), and reconcile from their settlement file. Anything else — retrying, assuming failure, assuming success — creates a double charge or a lost payment. Say this out loud; most candidates don't.

Distributed transactions: sagas

Reserving inventory, charging a card, and creating a shipment span three services with three databases. Two-phase commit gives you atomicity but blocks on the coordinator and takes locks across services — usually unacceptable. A saga instead runs a sequence of local transactions, each with a compensating action:

reserve inventory  →  charge card  →  create shipment
      ↓ compensate       ↓ compensate      ↓ compensate
  release stock       refund charge     cancel shipment

# Sagas give ATOMICITY-ish, not ISOLATION: intermediate states are
# visible (stock reserved but not yet paid). Design for that — reserved
# stock is a real, visible state with its own timeout, not a bug.
# Orchestrated (a central coordinator, easier to reason about and debug)
# vs choreographed (services react to events, looser but harder to trace).
# Compensations must be idempotent too — they get retried like everything else.

Reconciliation

Assume drift. Every day, fetch the PSP's settlement file and compare it to your ledger, three ways: in ours but not theirs (did we lose a call?), in theirs but not ours (did we miss a webhook?), and present in both with different amounts (fees, partial captures, currency conversion). Anything unmatched goes to a suspense account and a human queue. A payments system without automated daily reconciliation is a payments system that is quietly wrong, and mentioning reconciliation unprompted is one of the strongest signals in this question.

Also worth naming
  • Webhooks are unreliable and unordered. Verify signatures, dedupe on event ID, tolerate out-of-order arrival, and poll as a backstop.
  • Auth vs capture. Authorise at order time, capture at shipment; authorisations expire, and that expiry is a real state to handle.
  • Refunds and chargebacks are new transactions with new entries, never edits or deletions of the original.
  • Audit and retention. Append-only, immutable, typically 7+ years, with restricted access and full audit logging. PCI-DSS means you never store raw card numbers — you store a token from the PSP.
Say this

"An immutable double-entry ledger — every transaction writes balanced debit and credit entries in a single database transaction, and a balance is a sum over entries, never a mutable column. Amounts are integers in the smallest currency unit. Every charge carries an idempotency key that I also pass to the payment processor, so a retry can't create a second charge anywhere in the chain. If the processor call times out I leave the record pending and reconcile rather than guessing, because guessing is how you double-charge. Across services I'd use a saga with explicit compensating transactions rather than two-phase commit, and I'd run automated daily reconciliation against the processor's settlement file with a suspense account for anything that doesn't match."

27

Large-Scale Design

YouTubeWhatsAppUberDropboxTwitter
How to use this section

These five are the most-asked prompts. Don't memorise them — read across them. Each is a different answer to "what is the scarce resource here?", and recognising which resource a new prompt is really about is the transferable skill.

SystemScarce resourceDefining decisionHardest part
YouTubeBandwidth & storagePush everything to the CDN; transcode once, serve billions of timesSegment-parallel transcoding; cache hit rate on a long tail of unpopular videos
WhatsAppConcurrent connectionsPersist before push; delete after deliveryHolding millions of sockets cheaply; multi-device sync under E2E encryption
UberLocation write throughputGeospatial index in memory; region-sharded everythingMatching quality and exactly-once dispatch
DropboxUpload bandwidthChunk, hash, dedupe — send only what's newConflict resolution across devices; metadata scale
TwitterRead fan-outPrecompute timelines; special-case celebritiesThe power-law follower distribution

YouTube — the two-pipeline shape

Covered in full at topic 24. The three sentences that matter: ingest and playback are separate systems with separate scaling properties; transcoding parallelises at segment level so a long video isn't a long job; and playback is static HTTP segments so the CDN absorbs essentially all of it. The interesting follow-up is the long tail — most videos are watched almost never, so you cannot keep everything hot at every edge. Popular content is pushed to edges; the tail is pulled on demand and may be stored in cheaper tiers or, for the very coldest, kept in fewer renditions and transcoded on the fly.

WhatsApp — do less, hold more connections

The famous fact (millions of connections per server on Erlang/BEAM) is a consequence of a design choice, not a trick: keep the server tier thin and stateless about business logic, put almost everything in the client, and don't store what you don't have to. Messages were historically deleted from the server once delivered, which removed an entire storage tier from the problem. Groups are capped, which bounds fan-out by design — a product decision doing architectural work. E2E encryption then forces media, search, previews and backup to be client-side concerns. Detail at topic 23.

Uber — geography as the shard key

Covered at topic 25. The generalisable idea: when the workload has a natural locality (a ride never spans continents), shard on it. You get low latency, a natural bulkhead, and regulatory data residency for free. Compare with Twitter, where every user can follow every other user and there is no locality to exploit — which is exactly why Twitter's problem is harder.

Dropbox — sync is the hard part, storage isn't

File → split into 4 MB blocks → hash each block (SHA-256)
     → ask the server which hashes it already has
     → upload ONLY the missing blocks
     → commit a new file version = an ordered list of block hashes

# Consequences worth stating:
#  · Editing 1 byte of a 1 GB file uploads ~4 MB, not 1 GB.
#  · A file everyone has (a popular PDF) uploads instantly.
#  · Metadata (namespace, versions, block lists) is a DIFFERENT and
#    harder database problem than the blocks — it's transactional,
#    high-QPS, and must be strongly consistent. Blocks are trivially
#    scalable object storage.

Sync protocol: each client holds a cursor into a per-namespace change
journal; long-poll for changes; apply, then advance the cursor.

Conflicts: two devices edit offline. Do NOT silently pick a winner —
create "file (conflicted copy from Hetav's laptop)". Losing a user's
work is far worse than showing them two files.

Twitter — the canonical fan-out problem

Full treatment at topic 21. The shape: hybrid fan-out, home timelines precomputed in Redis, celebrities pulled at read time, ranking as a retrieve-then-rerank funnel. The extra pieces specific to Twitter are search over a real-time stream (a separate inverted index with very fast indexing) and trends (streaming aggregation over a sliding window with per-region cuts).

The transferable question

Given any new prompt, ask: what is scarce here? Bandwidth, connections, write throughput, read fan-out, storage, or correctness. Name it out loud, and the architecture follows — because every technique in this guide is a way of spending one abundant resource to save a scarce one.