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 # hopelessConclusion: 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 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
Approach
How
Tradeoff
Hash the URL, take 7 chars
base62(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 chars
Generate, insert with a unique constraint, retry on conflict
Unguessable; collision probability is tiny until the space fills. Simple and good.
Counter + base62
Global counter → encode
Zero 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 locally
No 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
Technique
How
Tradeoff
Geohash
Recursively 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.
Quadtree
Tree that subdivides dense regions further
Adapts to density — dense in Mumbai, coarse in the desert. Rebalancing is work.
Google S2
Projects the sphere onto a cube, Hilbert curve for locality
Excellent locality and precise region coverage; more machinery.
Uber H3
Hexagonal grid
Hexagons 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.
System
Scarce resource
Defining decision
Hardest part
YouTube
Bandwidth & storage
Push everything to the CDN; transcode once, serve billions of times
Segment-parallel transcoding; cache hit rate on a long tail of unpopular videos
WhatsApp
Concurrent connections
Persist before push; delete after delivery
Holding millions of sockets cheaply; multi-device sync under E2E encryption
Uber
Location write throughput
Geospatial index in memory; region-sharded everything
Matching quality and exactly-once dispatch
Dropbox
Upload bandwidth
Chunk, hash, dedupe — send only what's new
Conflict resolution across devices; metadata scale
Twitter
Read fan-out
Precompute timelines; special-case celebrities
The 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.