SSystem Design

Reference

Cheat sheet

Everything you should be able to recall without thinking. Print it (Ctrl/⌘+P — the layout is print-styled) and read it on the way to the interview.

Latency numbers every programmer should know

OperationTimeRelativeImplication
L1 cache reference0.5 nsIn-process work is essentially free compared with anything crossing a wire.
Branch mispredict5 ns10×
Main memory reference100 ns200×
Compress 1 KB (Snappy)3 µs6,000×Compression is cheaper than transmitting the extra bytes.
Send 1 KB over 1 Gbps10 µs20,000×Payload size matters, but round trips matter more.
SSD random read100 µs200,000×A disk read is ~1,000× a memory read. This is what a cache saves you.
Read 1 MB sequentially from memory250 µs500,000×Sequential beats random by an order of magnitude — the reason LSM trees and log-structured designs exist.
Read 1 MB sequentially from SSD1 ms2M×
Round trip in the same datacentre0.5 ms1M×Your budget for a cache lookup or an internal call.
Disk seek (spinning)10 ms20M×Why nobody puts a hot index on spinning disk.
Read 1 MB sequentially from disk20 ms40M×
Round trip California ↔ Netherlands150 ms300M×Speed of light. No engineering fixes it — only moving the data closer.
The three ratios that matter

Memory is ~1,000× faster than SSD. Same-datacentre network is ~300× faster than cross-continent. Sequential is ~10–100× faster than random. Almost every optimisation in this guide is an application of one of those three.

Capacity formulas

── TRAFFIC ──────────────────────────────────────────────────────
seconds/day        = 86,400            # round to 100,000
QPS                = daily / 86,400
1M/day             ≈ 12 QPS
1B/day             ≈ 12,000 QPS
peak QPS           = avg × 2 to 10     # use ×3 by default

── STORAGE ──────────────────────────────────────────────────────
daily bytes        = writes/day × bytes/write
5-year storage     = daily × 365 × 5 × replication (×3)
                     + indexes (often +30–100% of table size)

── BANDWIDTH ────────────────────────────────────────────────────
bandwidth          = QPS × payload     # reads and writes separately
                     >1 GB/s egress → it is a CDN problem, not an origin one

── FLEET ────────────────────────────────────────────────────────
servers            = peak QPS / per-server QPS × 1.5   # headroom
app server         ≈ 5k–15k QPS (I/O bound), hundreds (CPU bound)
Redis node         ≈ 100k ops/s
Postgres primary   ≈ 10k–50k simple writes/s
Kafka broker       ≈ 100k+ msgs/s, ~10 MB/s per partition
one gateway node   ≈ 50k–100k WebSocket connections

── CACHE ────────────────────────────────────────────────────────
DB QPS             = total QPS × (1 − hit rate)
memory needed      = items × avg size × 1.2
95% → 99% hit rate = 5× less database load   # the last points matter most

── AVAILABILITY ─────────────────────────────────────────────────
99%      3d 15h/yr      99.9%    8h 46m/yr
99.95%   4h 23m/yr      99.99%   52m/yr        99.999%  5m 15s/yr
series (dependencies)   A₁ × A₂ × … → 5 × 99.9% = 99.5%
parallel (redundancy)   1 − (1−A)ⁿ  → 2 × 99%   = 99.99%

── QUORUM ───────────────────────────────────────────────────────
W + R > N          → reads see the latest write
N=3, W=2, R=2      → balanced, survives one failure

Units and sizes to reason with

1 KBa small JSON row, a tweet, a log line
100 KBa web page's HTML, a thumbnail
2 MBa phone photo
50 MBa 5-minute 1080p video
1 GB1M rows × 1 KB
1 TB1B rows × 1 KB — still one machine
~64 bitSnowflake ID: 41 time + 10 machine + 12 seq
62⁷ = 3.5T7-char base62 keyspace
2³² = 4.3BIPv4 space; also a common hash ring size
10 KB–50 KBmemory per idle WebSocket connection

Decision trees

Which datastore?

Do you need transactions across multiple rows/entities?
├─ YES → relational (Postgres). Past one node → shard, or Spanner/Cockroach.
└─ NO  → What is the access pattern?
         ├─ purely by key, huge scale       → key-value (DynamoDB, Redis)
         ├─ nested docs, flexible schema    → document (MongoDB)
         ├─ write-heavy, time-ordered rows  → wide-column (Cassandra)
         ├─ multi-hop relationships         → graph (Neo4j)
         ├─ metrics over time               → time-series (Prometheus, Timescale)
         └─ relevance-ranked text           → search (Elasticsearch) — as a
                                              DERIVED index, never the truth

Sync or async?

Does the user need the result on this screen, now?
├─ YES → synchronous. Then: can it be slow?
│        ├─ no  → cache it, precompute it, or make the dependency optional
│        └─ yes → timeout + circuit breaker + a degraded fallback
└─ NO  → asynchronous. Then: does anyone else care about the event?
         ├─ one worker should do it   → queue
         └─ several systems, + replay → log (Kafka)

Fan-out on write or on read?

reads ≫ writes and the audience is bounded?     → fan-out on WRITE
writes ≫ reads, or the audience is enormous?    → fan-out on READ
power-law audience (a few huge accounts)?       → HYBRID:
      push for normal accounts, pull for celebrities, merge at read

Which consistency?

Is being wrong for 2 seconds a bug or a shrug?
├─ BUG (money, inventory, auth, locks, uniqueness) → strong/linearizable,
│    keep it in one region, accept the latency
├─ ORDER MATTERS but global order doesn't (replies, chat) → causal
└─ SHRUG (likes, views, feeds, presence) → eventual + session guarantees
     (read-your-writes and monotonic reads remove most visible weirdness
      for almost no cost)

Where does the bottleneck go next?

Slow reads       → index → cache → read replica → denormalise → CDN
Slow writes      → batch → async → partition → change the storage engine
Too much data    → tier/archive → partition → compress → sample
Too many conns   → pooling → gateway tier → protocol change
Too slow overall → measure first. It is almost never where you think.

Pattern index — problem to pattern

ProblemPatternTopic
Same expensive answer computed repeatedlyCache-aside with jittered TTL4
Hot key expires and floods the DBSingle-flight + jitter + early refresh4
Queries for keys that don't existNegative caching, Bloom filter4
One node can't hold the writesHash partitioning + logical shards6
Adding a node reshuffles everythingConsistent hashing + virtual nodes6
Sequential keys hotspot one shardHash or bucket-prefix the key6
User can't see their own writeRead-your-writes: route to leader briefly8
Data flickers between valuesMonotonic reads: pin session to a replica8
DB write and event publish must both happenTransactional outbox (+ CDC)10
Duplicates from at-least-once deliveryIdempotency key / dedup on natural ID14
Retry might double-chargeIdempotency key passed downstream too14
Retries amplify into an outageBackoff + full jitter + retry budget13
A dying dependency drags you downCircuit breaker + bulkhead + fallback13
One tenant starves everyonePer-tenant rate limit, shuffle sharding12
Producers outrun consumersBackpressure: shed, throttle, or buffer11
One bad message blocks the pipelineMax attempts → dead-letter queue11
Two nodes both think they're leaderQuorum election + fencing tokens15
Concurrent edits to one rowOptimistic concurrency (version check)15
Multi-service transactionSaga with compensating actions26
Celebrity with 100M followersHybrid fan-out21
Expensive ranking over a huge corpusRetrieve → filter → re-rank funnel18
Uploads consuming app serversPresigned URL, direct to object storage20
Big upload over a flaky linkMultipart + resume from part list20
Message lost when the socket dropsPersist first; backfill from a cursor19
Unread counts drifting across deviceslast_seq − last_read_seq23
Balances that must be auditableImmutable double-entry ledger26
Unknown outcome after a timeoutLeave pending, reconcile — never guess26
Nightly job on ten instancesLease, or leader election + fencing15
Unique IDs without coordinationSnowflake, or leased ID blocks22

Sensible defaults to reach for

DecisionDefaultChange it when
DatabasePostgresWrites exceed one node, or the access pattern is purely key-based at huge scale
CacheRedis, cache-aside, 5 min jittered TTLFreshness requirement forbids staleness
Load balancingL7, least connections, shallow health checksNon-HTTP protocol or extreme throughput
SessionsRedis or signed JWT — never server memoryPractically never
API styleREST at the edge, gRPC internallyMany client shapes → consider GraphQL
PaginationCursorSmall admin tables where users pick page numbers
Delivery semanticsAt-least-once + idempotent consumersLoss is preferable to duplication (telemetry)
OrderingPer-key, via the partition keyAlmost never need global
Retries3 attempts, exponential + full jitter, outermost layer onlyNon-idempotent without a key → don't retry
Rate limitingToken bucket in Redis, per API keyDownstream can't burst → leaky bucket
ReplicationLeader + semi-sync replica in another AZMulti-region writes required → multi-leader
Shard countOver-partition: 1,024 logical shardsSmall enough that one node is fine forever
BlobsObject storage + CDN, metadata in the DBNever put blobs in a relational database
RealtimeSSE if server→client only, else WebSocketClient must push → WebSocket
IDsSnowflake / UUIDv7 (time-ordered)Public and must be unguessable → random + rate limits
MoneyInteger minor units, immutable ledgerNever floats. Never a mutable balance column.

The 60-second opener, memorised

Word for word

"Before I design anything, let me pin down scope. Functionally, I think the core is [3 things] — is there anything you'd add, and is [X] out of scope? On the non-functional side I want four numbers: roughly how many daily active users, the read-to-write ratio, the latency target, and how stale data is allowed to be. …Good. So that's about [N] writes per second and [M] reads at peak, which tells me the write path is straightforward and the read path is where the engineering goes. I'll sketch the API, then the data model, then draw the system, and we can go deep wherever you'd like."

And the closer

"To summarise: [three sentences]. The two tradeoffs I deliberately made were [staleness for read latency] and [complexity in the read path to avoid celebrity fan-out]. With more time I'd look at the migration path from the existing system and the cost model, and I've deliberately deferred multi-region until there's a latency requirement that justifies it."