A guided route from the basics to senior system-design fluency. Each step is a real simulator you operate β work top to bottom, or jump anywhere.
How the web works before any database or cluster.
Request/response, status codes, caching, REST vs RPC vs GraphQL.
Interleaving on one core vs real overlap on many; speedup and Amdahl's law.
Latency hierarchy, throughput, Little's Law, and nines β downtime.
Why string length is ambiguous and how to sort names right: character sets vs encodings, UTF-8 vs UTF-16, code points vs grapheme clusters, normalization, and collation.
The structures the rest of the stack is built on.
Find any key in one step by hashing it to a slot; probe past collisions, grow and rehash to stay fast.
How a database finds any row β or any range β in just a few disk reads.
Keep the smallest (or largest) always on top; push and pop in O(log n).
The hash map plus doubly-linked list that makes an LRU cache O(1): a hit splices the touched node to the head, and a full cache evicts the tail.
The structure behind search: map each word to the sorted list of documents that contain it, then AND two queries by intersecting their lists.
How databases actually persist and find data on disk.
A key-value store that finds any value in a single disk read.
How write-heavy databases keep up: buffer writes in memory, flush to disk in batches.
How a database survives a crash mid-write: log first, apply second, replay to recover.
Consistent reads without locks: keep old versions so readers and writers never block.
Don't model the data β model the queries: shape the schema so each read is a cheap lookup.
How a login stores a password safely: never in plaintext, but a salted, deliberately slow hash with a tunable work factor and a constant-time compare, so a leaked table resists cracking.
Make reads fast β and keep the cache honest.
cache-aside, write-through/back/around, TTL, stampede.
Serve content from a cache near every user; keep the hit ratio high and copies fresh.
Concurrency and traffic control on a single node.
How the OS keeps files in RAM: fault pages in on read, defer writes as dirty pages, write them back, and pay the real cost only when evicting a dirty page.
Bound the queue and shed the excess so an overload stays fast instead of falling over.
Make a payment safe to retry: idempotency keys, a dedup store, and backoff so one charge stays one charge.
One slow dependency shouldn't sink the whole service: give each dependency its own bounded thread pool or concurrency slot, so a slowdown floods one compartment instead of draining the shared pool and cascading into a total outage.
How many workers should the pool hold, and what happens when they're all busy: reuse a capped set of threads draining a bounded queue, size it (CPU-bound β ~cores; I/O-bound β Goetz's formula / Little's Law), and pick a saturation policy (reject, caller-runs, block) when it fills.
Secure, multiplex, and speed up the connections between services.
How two strangers agree on a secret over a wire everyone can read.
One connection, many requests at once β and why a lost packet still matters.
Define a service in a .proto, generate the client and server stubs, and send compact binary over HTTP/2 β plus when to pick it over REST or GraphQL.
Design an API that survives clients you don't control: resource modelling and the safe/idempotent verb table, cursor vs offset pagination, versioning and backward compatibility, machine-readable error contracts, and REST vs RPC vs GraphQL.
How chat pushes a message instantly: upgrade one HTTP connection to a persistent two-way pipe, then scale to a million open connections with a connection-server tier and a pub/sub backplane.
In a cluster where instances come and go every minute, how one service finds a healthy instance of another: a live registry, health checks, and client-side vs server-side lookup.
Two kinds of load balancer, split by how deep they look: an L4 balancer forwards TCP by IP and port without reading the bytes; an L7 balancer terminates the connection, reads the HTTP request, and routes per request by path, header, or cookie β plus TLS termination, sticky sessions, and health checks compared at each layer.
Spread data across machines and find it again.
The ring that moves minimal data when nodes change.
Split one dataset across many machines by hash, range, or a directory β keep load even and grow without moving everything.
XOR distance + k-buckets; O(log n) lookups, no coordinator.
You added a node (or lost one) β how the data moves to match without downtime: fixed partitions and vnodes keep the move small, then a live assign β stream β dual-write β verify β cutover migration moves it under live traffic, throttled so it never starves real requests.
Encode a location as a short string; nearby points share a prefix, so 'find things near me' becomes a prefix scan.
Find the nearest embedding to a query without scanning them all: a layered graph you descend greedily, trading a little recall for a big speed-up.
Round-robin, least-conn, weighted, power-of-two-choices.
Store a table by column so an analytical query reads only the fields it needs and skips row groups whose min/max cannot match.
Keep copies in sync when clocks and networks lie.
Why clocks drift and how NTP-style sync corrects them.
Order events by cause, not time; detect the concurrency Lamport clocks miss.
Epidemic dissemination / anti-entropy with no coordinator.
Replicas take writes independently, then merge to the same state regardless of order β no coordination, no lost update.
Leaderless replicas drift apart β the two mechanisms that pull them back: read repair heals on the read path (hot keys, immediate), anti-entropy sweeps the whole dataset in the background with Merkle trees (cold keys, complete), plus hinted handoff as the third leg. Dynamo/Cassandra.
The consensus algorithm everyone cites and nobody implements: how a set of unreliable machines agrees on one value β three roles, two phases, and the single safety rule (adopt the highest already-accepted value) that stops two values from both winning β then Multi-Paxos, and why Raft repackages the same guarantee to be buildable.
Live copies decided by two dials: who accepts a write (single-leader / multi-leader / leaderless) and how long the leader waits before acking (sync / async / semi-sync) β the durability-vs-latency trade, replication lag and stale reads, and the failover that can lose acknowledged writes.
Mutual exclusion across machines: a lease auto-releases a dead holder, but a holder that pauses past its lease makes two holders at once β so a monotonic fencing token, enforced by the resource, is what actually keeps it safe.
What consistency does this feature actually need? The spectrum from linearizable through causal to eventual, the client-centric session guarantees, and CAP's honest successor PACELC β used as a decision tool: a bank balance needs linearizable, a like count is fine with eventual, and strong consistency you don't need costs latency and availability.
Your users are global but your database is in one region: the speed-of-light wall (a cross-region write costs tens to 100+ ms, so you can't keep every region strongly consistent for free), active-passive (one write region, simple, async failover loses the RPO window) vs active-active (every region writes β cross-region conflicts you resolve with LWW/CRDTs/home regions), latency-based geo routing, split-brain, and data residency as a hard constraint.
How a group of equal machines pick one leader with no human involved β and how that leader keeps proving it still leads. A renewable lease grants leadership for a bounded time; miss the renewal and you step down. A majority vote plus the lease stop split-brain, but a frozen leader can still overlap, so correctness-critical work needs a fencing token. The real trade is failover speed vs false positives.
Make many services agree β or undo together.
Reed-Solomon k-of-n durability: split into k data + m parity shards, lose up to m and rebuild.
Atomic commit across services β and its blocking flaw.
Multi-service transactions via compensating rollback.
Add a pre-commit phase so a coordinator crash no longer freezes 2PC β and why a network partition brings the block back as split-brain.
Publish an event and change the database without losing either: write the event to an outbox table in the same transaction as the business change, relay it to the broker at-least-once, and dedup on the consumer so the effect happens effectively-once.
The ladder from Read Uncommitted to Serializable, and the anomaly each rung lets through.
Decouple producers from consumers with a durable log.
Partitioned log: consumer groups, offsets, rebalancing.
The replicated log under a partition: leader and follower replicas, the in-sync set, the high-water mark that gates readable records, and a clean leader failover.
Group events by when they happened, not when they arrived: tumbling, sliding, and session windows, with a watermark deciding when a window is done.
One message published once, fanned out to many independent subscriptions: at-least-once delivery with per-subscription ack, nack, redelivery, and dead-lettering.
Tail a database's write-ahead log and stream every committed row change as an ordered event, at least once, with a resumable checkpoint.
Trade a little accuracy for a lot of memory.
Estimate how often each item appears in a stream, in a tiny fixed grid of counters.
Approximate membership you can delete from β fingerprints in buckets, with a cuckoo relocation to pack them tight.
Estimate p50/p90/p99 of a huge number stream from a small, mergeable set of centroids.
Estimate how similar two sets are from tiny signatures, then band them into buckets so near-duplicates collide without comparing every pair.
See inside a running system β metrics, traces, and health.
The metric toolkit: counters, gauges, and histograms; why the average hides the tail, why you can't average a p99, and the labels that blow up cardinality.
One request fans out across dozens of services and no single service's logs can tell you which hop was slow. Distributed tracing follows that request end-to-end: a trace id shared across every call, spans that record each unit of work and nest into a tree, context carried in the W3C traceparent header, and head- vs tail-based sampling to survive the volume.
How the system knows an instance is dead before it routes a user there: liveness vs readiness, active probes vs passive checks, heartbeats and the missed-heartbeat timeout, gossip-based failure detection at scale, and the cascade when an over-aggressive check evicts a healthy fleet under load.
You can't store every trace or log at scale, so you keep a fraction: head-based sampling (decide at the start, cheap) vs tail-based (decide after the outcome, keep every error), reservoir sampling for a fixed-size uniform sample of an unbounded stream, scaling a sampled count back up and why that gets jumpy on rare errors, and cardinality control as the metrics-side cousin.
Trust, identity, and secrecy on the wire and at rest.
How a login stays logged in at scale: server-side sessions vs signed stateless tokens (JWT), the revocation trade, and who's allowed to do what.
Delegated access without password-sharing: the four roles, the authorization code flow, PKCE, scopes, and why an OAuth access token is not proof of identity β that's OIDC's ID token.
If someone steals the disk or a backup, is the data readable? Symmetric ciphers (AES-GCM), the key-management problem, envelope encryption (DEK wrapped by a KEK in a KMS), key rotation, and where encryption sits β full-disk vs database vs field-level.
A per-key rate limiter stops one caller, but distributed abuse (credential stuffing, scraping, card testing) spreads across a million IPs and mimics real users. The layered answer: IP/ASN reputation and device fingerprinting, challenges and proof-of-work, behavioral scoring, and a WAF at the edge β framed as raising the attacker's cost, not a solvable problem.
Compose the primitives into a whole interview answer.
A twelve-minute composition read: assemble a working rate limiter from primitives you already know β rate limiting, a shared counter, consistent hashing. For the full interactive interview, open the Design a Rate Limiter board.
The canonical warm-up: keep one global counter correct and fast across a fleet. The race two servers run over the last token is the whole problem; the algorithm is a detail next to it.
The canonical interview opener: short codes, a read-heavy redirect path, and the sharding decision β argued against real numbers.
WhatsApp/Messenger, as one routing fabric you operate: persistent connections, a directory to find the recipient, offline inboxes, group fan-out, and surviving a gateway dying.
Twitter/X, decided by one fork: build every timeline when the post is written, or when it is read. Fifty reads per write says precompute β until one account with fifty million followers says otherwise.
One event, one buzz β out of parts that are each unreliable. The event arrives at-least-once and every channel gateway is a best-effort black box, so effectively-once delivery has to be built at the edges: an outbox, an idempotency key, backoff, and backpressure.
Uber/Lyft, decided by one firehose: a million driver locations a second, each stale in four. That write rate is what forces the design β live location lives in memory, non-durable, and 'who's nearby' becomes a spatial-index query, not a scan. The matching everyone reaches for first is the easy part.
Common Crawl scale, decided by one queue: a colossal fetch rate that must stay gentle on every host. A flat queue cannot be polite, so the frontier gains structure β one back queue per host, a heap that knows who is due β and 'have I fetched this URL?' becomes a Bloom filter, ~40Γ smaller than the raw strings.
YouTube/Netflix, decided by one wall: the same video served to a huge audience is terabits per second of egress, while storing the library is only tens of terabytes. That flips the design outward to a CDN edge tier of caches near the viewer, a client-pull ABR ladder, and an async transcode pipeline behind a durable blob store.
Redis/Memcached at fleet scale, decided by one wall: a single wildly popular key hashes to one shard, and no shard count spreads it β so hot-shard replication and an app-local near cache serve the hot key, while a consistent-hash ring, invalidate-on-commit, LFU eviction, and leases keep the hit ratio high. Storing key-value pairs fast is the easy part.
Prometheus/Datadog scale, decided by one wall: ingesting the firehose is a shardable append, but keeping every raw sample at fleet cardinality for years is a petabyte-a-year cost no disk buys down β so cardinality control bounds the label dimension and a rollup pipeline tiers data by age, while a hot tier and a bounded query engine keep reads fast and safe. Swallowing the firehose is the easy part.
Cron/Airflow at scale, decided by one wall: accepting and running a task are both cheap, but finding which of hundreds of millions of pending tasks are due each instant is impossible to poll for β so a durable time-bucketed index fronted by a timing wheel makes 'what's due now' a cheap pop, sharded to survive a node loss, while lease-and-ack dispatch and an idempotency key run each due task once even across worker crashes. Finding what's due is the hard part.
Ad-tech firehose scale, decided by one fork: counting clicks is a windowed sum, but a dashboard needs a fast number and billing needs an exact one, and at ten thousand clicks a second those two cannot come from one path. So every click is appended to a durable log, a stream aggregator serves a fast provisional count, and a batch job replays the log to recompute the exact billable count and override it β while a windowed dedup set counts each click once. Counting is the easy part.