Foundations
Part 1System Design Fundamentals
HLD vs LLD. High-level design is the what: components, how they interact and why. Low-level design is the how: schemas, API contracts, algorithms, error handling. Interviews start high-level, then deep-dive one or two areas.
The four-step framework. Clarify requirements (3–5 min), estimate scale (5–7), draw the high-level design (15–20), then deep dive (10–15): schema, API, critical algorithms, edge cases and failures.
Functional vs non-functional. Functional requirements say what the system does; non-functional ones say how well: uptime, latency, throughput, consistency. The non-functional ones drive architecture, technology and cost.
Sync vs async. Sync is simple with immediate feedback, but the client blocks and services couple. Async through a queue decouples, absorbs spikes and allows retries, at the cost of eventual consistency and harder debugging.
API styles. REST for CRUD and public APIs, GraphQL when clients ask for exactly the data they need, gRPC (binary Protocol Buffers) for fast internal calls, WebSocket for server push.
| Synchronous | Asynchronous | |
|---|---|---|
| Feedback | Immediate | Later (ACK on receipt) |
| Coupling | Tight | Loose |
| Traffic spike | Client waits | Queue buffers it |
| Use for | Login, checkout, balance check | Video processing, emails, order fan-out |
- Designing before clarifying scope, scale and the read/write ratio.
- Skipping non-functional requirements: they decide the architecture.
- Diving into low-level detail before the high-level picture is agreed.
Part 2Infrastructure Building Blocks
CDN. Edge servers (PoPs) serve content near users: ~5–10 ms from a Mumbai PoP versus ~150–200 ms to a US origin. Cache static assets and identical-for-everyone responses; never user-specific data.
Versioned URLs. Put a content hash in the filename (app.a1b2c3d4.js) and cache it for a year as immutable. A deploy changes the URL, so users get fresh files with zero purge delay.
no-cache doesn't mean "don't cache". no-cache caches but revalidates (ETag, 304 Not Modified); no-store never caches; private keeps it out of the CDN; s-maxage gives the CDN its own TTL.
L4 vs L7 load balancing. L4 sees IP and port: very fast (<100 μs added), any protocol. L7 sees the whole HTTP request: routes by path, host, header or cookie and terminates TLS, adding 1–5 ms.
Shallow vs deep health checks. Point the load balancer at a shallow liveness check. A deep check that queries the database marks every server unhealthy when the DB is down; use deep checks for monitoring.
Stateless servers scale. Keep no session in server memory: sessions in Redis, data in PostgreSQL, files in S3. Then any server takes any request and a crash loses nothing.
| Monolith | Microservices | |
|---|---|---|
| Scaling | Everything together | Per service |
| Failures | One affects all | Isolated |
| Transactions | ACID is easy | Distributed, hard |
| Start here when | Small team, new product | Clear boundaries, strong DevOps |
- Caching user-specific or authenticated responses at the CDN.
- A deep health check behind the load balancer: one database outage removes every server.
- Microservices for a small team with unclear requirements; start with a monolith.
Part 3Back-of-the-Envelope Estimation
Per day to per second. A day is 86,400 seconds; round to 100,000. So X per day ÷ 100,000 ≈ X per second: 1 billion a day is about 10,000 a second.
The four estimates. Traffic (RPS, read/write split, peak), storage (objects × size × retention × replication), bandwidth (RPS × payload, ingress and egress) and infrastructure (servers, replicas, cache).
Latency ratios matter. L1 cache 0.5 ns, memory 100 ns, SSD random read 150 μs, HDD seek 10 ms; datacenter round trip 0.5 ms, cross-continent 150 ms. Cache aggressively, batch, and minimize round trips.
Media dominates storage. For a Twitter-like service, text is 30 GB a day but images and videos add ~7 TB a day: media is 99%+ of storage. Egress for images is why the CDN is essential.
Formulas to say out loud. RPS = daily actions ÷ 100,000. API servers = peak RPS ÷ 5,000 ÷ 0.7. Read replicas = read RPS ÷ 30,000. Cache = hot data: 20% of the total, or the last N days.
| Do | Don't | |
|---|---|---|
| Numbers | "~12,000, call it 10K" | "12,453 per second" |
| Traffic | Design for peak, 2–3x | Design for the average |
| Time | Approximate, move on | Spend over 5 minutes |
| Result | Tie numbers to decisions | Skip the "so what" |
- Forgetting growth: 100 GB now can be 1 TB in a year at 10x growth.
- Forgetting replication (500 GB × 3 = 1.5 TB) or that images make posts 100x bigger.
- Mixing megabytes and megabits: MB/s × 8 = Mbps.
Part 4Networking Fundamentals for System Design
TCP guarantees, at a price. TCP gives reliable, ordered delivery with flow and congestion control, but the handshake costs 1 RTT before the first request and one lost packet blocks everything behind it.
UDP when late data is useless. No handshake, an 8-byte header, message boundaries kept, no reliability. Use it for real-time video, voice, gaming, DNS and as QUIC's base.
HTTP/1.1 → 2 → 3. HTTP/1.1 sends one request at a time per connection. HTTP/2 multiplexes streams and compresses headers over one TCP connection. HTTP/3 runs QUIC over UDP: a lost packet only blocks its own stream.
Reuse connections. Each new connection pays TCP and TLS handshakes and starts in slow start. Keep-alive and connection pools avoid that; short connections also pile up in TIME_WAIT and exhaust ephemeral ports.
Scaling WebSockets. Connections are stateful, so the load balancer needs stickiness, and users on different servers reach each other through a pub/sub backbone (Redis, Kafka). Use SSE for one-way server push.
| gRPC | REST | |
|---|---|---|
| Format | Protobuf, binary | JSON, text |
| Browser support | Limited (grpc-web) | Native |
| Streaming | Full | Limited |
| Use for | Internal microservices | Public APIs, browsers |
- Opening a new connection per request instead of pooling: handshakes, slow start, port exhaustion.
- WebSocket for request-response or one-way push, where HTTP or SSE is simpler.
- Assuming HTTP/2 solved head-of-line blocking: it still rides one TCP connection.
Part 5Operating System Concepts for System Design
Processes isolate, threads share. A process owns its memory and file descriptors, so one crash kills only itself. Threads share the heap and descriptors with their own stacks: cheap to create and switch, but one crash kills them all.
Event loops scale connections. Thread-per-connection doesn't scale past ~10K connections. epoll registers descriptors once and returns only the ready ones, so one thread serves thousands (Node.js, nginx, Redis).
Context switches cost more than they look. Saving registers is cheap; the TLB flush, cold CPU caches and pipeline flush are not. Use async I/O, thread pools, batching and CPU affinity to switch less.
Write-ahead logging. Append the change to a sequential log and fsync it before touching data files; on a crash, replay the log. Sequential HDD writes run ~100 MB/s versus ~1 MB/s random.
Containers are namespaces plus cgroups. Namespaces isolate what a process sees (PID, network, mounts, users); cgroups limit what it uses (CPU quota, memory). Exceed the memory limit and the container is OOM killed.
| Mutex | Semaphore | RWLock | |
|---|---|---|---|
| Lets in | One thread | N threads | Many readers or one writer |
| Ownership | Locker unlocks | None | Per lock mode |
| Use for | Shared data | Pools, rate limits | Read-heavy data |
- Thread-per-connection for a million connections instead of an event loop.
- Taking locks in different orders across threads: a deadlock waiting to happen.
- Running a database that caches pages itself without O_DIRECT, so data is cached twice.
Part 6System Design Terminology & Theory
CAP is C or A during a partition. Partitions will happen, so P isn't optional. During one, a CP system refuses what it can't confirm (ZooKeeper, etcd); an AP system answers with possibly stale data (Cassandra, DNS).
PACELC. Even with no partition, strong consistency costs latency: synchronous replication waits for the slowest replica. DynamoDB and Cassandra are PA/EL; HBase is PC/EC.
Isolation levels. Read uncommitted allows dirty reads; read committed (PostgreSQL's default) allows non-repeatable reads; repeatable read (MySQL's default) allows phantoms; serializable allows none.
Consistency models. Linearizability respects real-time order; sequential keeps only each process's program order; causal keeps cause before effect (vector clocks); eventual only promises convergence.
Delivery semantics. At-most-once may lose messages; at-least-once may duplicate them, so consumers must be idempotent; exactly-once needs idempotent producers and consumers, or transactional messaging.
Error budgets. A 99.9% SLO allows 43.2 minutes of downtime in a 30-day month. Budget left: ship fast. Budget gone: freeze features and fix reliability. Keep the SLA looser than the SLO.
| ACID | BASE | |
|---|---|---|
| Consistency | Strong | Eventual |
| Approach | Pessimistic: lock early | Optimistic: resolve later |
| Scaling | Mainly vertical | Horizontal |
| Use for | Payments, inventory, accounts | Feeds, catalogs, sessions |
- "Pick 2 of 3" in CAP: partition tolerance is a given, not a choice.
- Assuming exactly-once delivery is free instead of making consumers idempotent.
- Saga compensations that aren't idempotent: they may run more than once.
Part 7API Design for System Design
Resources, not verbs. Model things as plural nouns with a few standard methods: Get, List, Create, Update, Delete. A real verb stays rare and attached to a resource: POST /v1/orders/{id}:cancel.
Paginate with cursors from day one. Adding pagination later breaks clients. Keyset on (sort key, unique ID) with an opaque token costs the same on every page; offset walks every skipped row and duplicates or skips rows under writes.
Additive changes only. Within a version, add endpoints, optional request fields and response fields. Renames, removals, type changes, new required fields and changed defaults break clients. Clients ignore unknown fields.
Idempotency keys on POST. The client sends one key per logical operation and reuses it on every retry. A repeat returns the original response; the same key with a different body is an error.
Errors are a contract. Clients branch on a stable code; people read the message. RFC 9457 problem details, plus the field and whether to retry. One shape for every error, including the gateway's 429s.
Slow work becomes a resource. Return 202 with a status resource to poll, whose terminal states never change. Servers also get a webhook, mobile apps a push. Big results are a link to object storage.
limit, most GitHub per_page| Offset | Cursor | |
|---|---|---|
| Cost | Grows with depth | Constant per page |
| Concurrent writes | Duplicates and gaps | Stable |
| Jump to page 50 | Yes | No |
| Fits | Small bounded lists | Feeds, histories, public APIs |
- POSTs without an idempotency key: every timeout on an order or payment can become a duplicate.
- Renaming or retyping a field inside a version instead of adding alongside.
- Slow work inside the request instead of
202and an operation.
Part 8Databases in Depth
A commit is one sequential write. Every change is appended to the WAL first; a commit is durable once its log record is flushed, while changed pages are written later. Group commit lets one fsync cover many transactions.
Composite index order. Equality columns first, then the one range or sort column. (user_id, created_at, id) reads 20 adjacent entries for a user's latest orders; INCLUDE the payload to make it covering.
Lost update and write skew. Read-then-write code loses updates on both PostgreSQL's and InnoDB's defaults: use a guarded atomic update or FOR UPDATE. Write skew needs Serializable or a constraint like UNIQUE (customer_id, coupon_code).
Where MVCC keeps old versions. PostgreSQL writes a new tuple in the heap and VACUUM removes the old one. InnoDB updates in place and keeps old versions in undo, which purge discards. A long transaction blocks both.
Optimistic concurrency. Each row has a version; the update runs WHERE version = seen. Zero rows means someone changed it: reload and decide again. It spans a user's think time, and maps to ETag and 412.
Time-ordered IDs. UUIDv4 inserts at random index leaves, so the whole index becomes the working set. UUIDv7 and Snowflake IDs sort by time and keep inserts at the index's right edge.
shared_buffers, dedicated server| B-tree | LSM-tree | |
|---|---|---|
| Writes | In place, random, on commit path | Appends, merged in background |
| Reads | ~1 disk read, predictable | Several files; scans cost more |
| Best for | Read-heavy OLTP, ranges | Messages, events, metrics |
| Examples | PostgreSQL, InnoDB | RocksDB, Cassandra, ScyllaDB |
- Trusting the default isolation level with read-then-write code: neither prevents lost updates.
- An index for every slow query. Each taxes every write; read
EXPLAINfirst. - Long transactions on the primary: they block vacuum and purge. Run reports on a replica.