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
| Operation | Time | Relative | Implication |
|---|---|---|---|
| L1 cache reference | 0.5 ns | 1× | In-process work is essentially free compared with anything crossing a wire. |
| Branch mispredict | 5 ns | 10× | |
| Main memory reference | 100 ns | 200× | |
| Compress 1 KB (Snappy) | 3 µs | 6,000× | Compression is cheaper than transmitting the extra bytes. |
| Send 1 KB over 1 Gbps | 10 µs | 20,000× | Payload size matters, but round trips matter more. |
| SSD random read | 100 µs | 200,000× | A disk read is ~1,000× a memory read. This is what a cache saves you. |
| Read 1 MB sequentially from memory | 250 µs | 500,000× | Sequential beats random by an order of magnitude — the reason LSM trees and log-structured designs exist. |
| Read 1 MB sequentially from SSD | 1 ms | 2M× | |
| Round trip in the same datacentre | 0.5 ms | 1M× | Your budget for a cache lookup or an internal call. |
| Disk seek (spinning) | 10 ms | 20M× | Why nobody puts a hot index on spinning disk. |
| Read 1 MB sequentially from disk | 20 ms | 40M× | |
| Round trip California ↔ Netherlands | 150 ms | 300M× | Speed of light. No engineering fixes it — only moving the data closer. |
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
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
| Problem | Pattern | Topic |
|---|---|---|
| Same expensive answer computed repeatedly | Cache-aside with jittered TTL | 4 |
| Hot key expires and floods the DB | Single-flight + jitter + early refresh | 4 |
| Queries for keys that don't exist | Negative caching, Bloom filter | 4 |
| One node can't hold the writes | Hash partitioning + logical shards | 6 |
| Adding a node reshuffles everything | Consistent hashing + virtual nodes | 6 |
| Sequential keys hotspot one shard | Hash or bucket-prefix the key | 6 |
| User can't see their own write | Read-your-writes: route to leader briefly | 8 |
| Data flickers between values | Monotonic reads: pin session to a replica | 8 |
| DB write and event publish must both happen | Transactional outbox (+ CDC) | 10 |
| Duplicates from at-least-once delivery | Idempotency key / dedup on natural ID | 14 |
| Retry might double-charge | Idempotency key passed downstream too | 14 |
| Retries amplify into an outage | Backoff + full jitter + retry budget | 13 |
| A dying dependency drags you down | Circuit breaker + bulkhead + fallback | 13 |
| One tenant starves everyone | Per-tenant rate limit, shuffle sharding | 12 |
| Producers outrun consumers | Backpressure: shed, throttle, or buffer | 11 |
| One bad message blocks the pipeline | Max attempts → dead-letter queue | 11 |
| Two nodes both think they're leader | Quorum election + fencing tokens | 15 |
| Concurrent edits to one row | Optimistic concurrency (version check) | 15 |
| Multi-service transaction | Saga with compensating actions | 26 |
| Celebrity with 100M followers | Hybrid fan-out | 21 |
| Expensive ranking over a huge corpus | Retrieve → filter → re-rank funnel | 18 |
| Uploads consuming app servers | Presigned URL, direct to object storage | 20 |
| Big upload over a flaky link | Multipart + resume from part list | 20 |
| Message lost when the socket drops | Persist first; backfill from a cursor | 19 |
| Unread counts drifting across devices | last_seq − last_read_seq | 23 |
| Balances that must be auditable | Immutable double-entry ledger | 26 |
| Unknown outcome after a timeout | Leave pending, reconcile — never guess | 26 |
| Nightly job on ten instances | Lease, or leader election + fencing | 15 |
| Unique IDs without coordination | Snowflake, or leased ID blocks | 22 |
Sensible defaults to reach for
| Decision | Default | Change it when |
|---|---|---|
| Database | Postgres | Writes exceed one node, or the access pattern is purely key-based at huge scale |
| Cache | Redis, cache-aside, 5 min jittered TTL | Freshness requirement forbids staleness |
| Load balancing | L7, least connections, shallow health checks | Non-HTTP protocol or extreme throughput |
| Sessions | Redis or signed JWT — never server memory | Practically never |
| API style | REST at the edge, gRPC internally | Many client shapes → consider GraphQL |
| Pagination | Cursor | Small admin tables where users pick page numbers |
| Delivery semantics | At-least-once + idempotent consumers | Loss is preferable to duplication (telemetry) |
| Ordering | Per-key, via the partition key | Almost never need global |
| Retries | 3 attempts, exponential + full jitter, outermost layer only | Non-idempotent without a key → don't retry |
| Rate limiting | Token bucket in Redis, per API key | Downstream can't burst → leaky bucket |
| Replication | Leader + semi-sync replica in another AZ | Multi-region writes required → multi-leader |
| Shard count | Over-partition: 1,024 logical shards | Small enough that one node is fine forever |
| Blobs | Object storage + CDN, metadata in the DB | Never put blobs in a relational database |
| Realtime | SSE if server→client only, else WebSocket | Client must push → WebSocket |
| IDs | Snowflake / UUIDv7 (time-ordered) | Public and must be unguessable → random + rate limits |
| Money | Integer minor units, immutable ledger | Never floats. Never a mutable balance column. |
The 60-second opener, memorised
"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."
"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."