Scaling up the system design roadmap
Resolving 85 modules across 13 phases
0%
System design roadmapBackend Engineering Guides
0 of 85 done
Focus mode on
00 to 84Bold ideas
Brighter designs
Bigger systems

System design,from first array to Netflix scale

Start with Big O and the handful of data structures every system is built from, learn the algorithms hiding inside caches and load balancers, then scale, store, cache, queue and replicate until you can design Bitly, Twitter, WhatsApp, Netflix, Uber and a payment system out loud.

Best for: Interviews, Architecture reviews, Curious engineers

Phase 00, module 00

Before you start

The system design dictionary

Every word this guide uses, explained in plain words, in technical terms, and with an everyday comparison.

At a glance

Interview ready85 modules13 phases55 dictionary wordsKeyboard first ?

The building blocks you will use

ServiceDatabaseCacheQueueCDNLoad balancer

Best way to use it

  • New to system design

    Start at module 01 and go in order. Each module leans on the one before it.

  • Prepping an interview

    Open the menu with M and jump straight to the phase you need.

  • Track it

    Press D to mark a module done. Progress stays in this browser.

  • Focus on one

    Press O to read one module alone, then ← → to move between them.

Module 00 of 84, phase 00

A word for everything

The system design dictionary

A catalog of every keyword in this guide. Each entry says what the word means in plain language, what it means technically, and what it is like in everyday life, then links to the module that teaches it.

In detail

Use it as a reference rather than a lesson. Filter by kind to see only data structures, storage words or distributed systems terms, or type to find one. Come back whenever a case study uses a word you have not met yet.

Big O

Algorithms
In plain words
How much slower something gets as the input grows.
Technically
Asymptotic upper bound on time or space as a function of input size n, ignoring constants.
Learn it in module 02Big O and complexity

Hash table

Algorithms
In plain words
A lookup that jumps straight to the answer.
Technically
Array of buckets indexed by hash(key) mod size, giving average O(1) get and put.
Learn it in module 04Hash tables

Tree

Algorithms
In plain words
Data arranged as branches from a single root.
Technically
Hierarchical nodes; balanced search trees give O(log n) search, insert and range scans.
Learn it in module 07Trees, BSTs and B-trees

Heap

Algorithms
In plain words
A pile that always keeps the smallest or biggest on top.
Technically
A complete binary tree with the heap property, giving O(1) peek and O(log n) push and pop.
Learn it in module 08Heaps and priority queues

Graph

Algorithms
In plain words
Things connected to other things.
Technically
Vertices and edges, stored as adjacency lists, explored with BFS, DFS or Dijkstra.
Learn it in module 09Graphs, BFS and DFS

Trie

Algorithms
In plain words
A tree of letters for finding words by their start.
Technically
A prefix tree where each edge is a character, giving lookups in O(length of key).
Learn it in module 10Tries and prefix search

Consistent hashing

Algorithms
In plain words
Spreading keys over servers so adding one moves very little.
Technically
Keys and nodes mapped onto a hash ring; each key belongs to the next node clockwise, with virtual nodes for balance.
Learn it in module 12Consistent hashing

Bloom filter

Algorithms
In plain words
A tiny memory that can say 'definitely not' or 'maybe'.
Technically
A bit array with k hash functions; false positives are possible, false negatives are not.
Learn it in module 15Bloom filters and probabilistic structures

LRU cache

Algorithms
In plain words
A small store that throws out whatever was used longest ago.
Technically
Hash map plus doubly linked list giving O(1) get, put and eviction of the least recently used entry.
Learn it in module 14LRU and LFU caches

Token bucket

Algorithms
In plain words
A limit that allows short bursts but a steady average.
Technically
Tokens refill at rate r up to capacity b; each request spends one token or is rejected.
Learn it in module 13Rate limiting algorithms

Snowflake ID

Algorithms
In plain words
A unique number made without asking anyone.
Technically
A 64 bit id of timestamp, machine id and sequence bits, roughly sortable by time.
Learn it in module 19Unique ID generation

Base62

Algorithms
In plain words
Writing numbers with letters and digits to make them short.
Technically
Positional encoding with alphabet 0-9a-zA-Z; seven characters hold about 3.5 trillion values.
Learn it in module 20Base62 encoding and short codes

Requirements

Fundamentals
In plain words
What the system must do, and how well.
Technically
Functional requirements describe behaviour; non functional describe scale, latency, availability and consistency.
Learn it in module 22Functional and non functional requirements

QPS

Fundamentals
In plain words
How many requests arrive each second.
Technically
Queries per second, estimated from daily volume divided by 86,400 and multiplied by a peak factor.
Learn it in module 23Back of the envelope estimation

Horizontal scaling

Fundamentals
In plain words
Adding more machines instead of a bigger one.
Technically
Scaling out across identical stateless nodes behind a load balancer.
Learn it in module 25Vertical and horizontal scaling

Availability

Fundamentals
In plain words
How often the system is up when you need it.
Technically
Fraction of time serving successfully, expressed in nines; 99.9% allows about 8.8 hours down a year.
Learn it in module 26Availability, reliability and SLOs

CAP theorem

Fundamentals
In plain words
During a network split you must choose: correct or available.
Technically
Under a partition, a distributed store can provide consistency or availability, not both.
Learn it in module 28CAP and PACELC

Eventual consistency

Fundamentals
In plain words
Everyone sees the same thing, just not instantly.
Technically
Replicas converge if no new writes arrive; reads may be stale meanwhile.
Learn it in module 29Consistency models

Single point of failure

Fundamentals
In plain words
One part that, if it breaks, breaks everything.
Technically
A component without redundancy whose failure causes total outage.
Learn it in module 27Reliability and fault tolerance

DNS

Networking
In plain words
The internet's phone book.
Technically
Hierarchical, cached resolution of names to IP addresses, also used for geo routing and failover.
Learn it in module 31DNS

CDN

Networking
In plain words
Copies of files kept close to users around the world.
Technically
Edge servers caching static and media content near users, reducing latency and origin load.
Learn it in module 33Content delivery networks

Load balancer

Networking
In plain words
Spreads requests across many servers.
Technically
Layer 4 or 7 proxy distributing traffic with health checks and algorithms like least connections.
Learn it in module 34Load balancers

API gateway

Networking
In plain words
One front door for many services.
Technically
Reverse proxy adding authentication, rate limiting, routing and request shaping.
Learn it in module 35Reverse proxies and API gateways

REST, GraphQL, gRPC

Networking
In plain words
Three ways for programs to ask each other for things.
Technically
Resource oriented HTTP, client specified graph queries, and typed binary RPC over HTTP/2.
Learn it in module 36REST, GraphQL and gRPC

Index

Data
In plain words
A shortcut that finds rows without reading them all.
Technically
A B-tree or LSM structure on columns that turns scans into logarithmic lookups.
Learn it in module 39Indexes, B-trees and LSM trees

Transaction

Data
In plain words
Changes that all happen together or not at all.
Technically
ACID unit of work with isolation levels controlling concurrent anomalies.
Learn it in module 40Transactions, ACID and isolation

Replication

Data
In plain words
Keeping copies of data on several machines.
Technically
Leader follower or multi leader copying of writes, synchronous or asynchronous.
Learn it in module 41Replication

Sharding

Data
In plain words
Splitting one big database into many smaller ones.
Technically
Horizontal partitioning of rows across nodes by a shard key using range, hash or directory.
Learn it in module 42Sharding and partitioning

Object storage

Data
In plain words
Endless cheap shelves for files.
Technically
Flat namespace of immutable objects with metadata, accessed over HTTP, such as S3.
Learn it in module 44Blob and object storage

Inverted index

Data
In plain words
A list of which documents contain each word.
Technically
Mapping from term to posting list of document ids, intersected to answer queries.
Learn it in module 45Search and inverted indexes

Cache aside

Messaging
In plain words
Check the fast copy first, fill it on a miss.
Technically
Application reads cache, falls back to the database, then populates the cache with a TTL.
Learn it in module 47Cache strategies and invalidation

Message queue

Messaging
In plain words
A waiting line for work.
Technically
Durable broker holding messages until a consumer processes and acknowledges them.
Learn it in module 48Message queues

Pub sub

Messaging
In plain words
One message, many listeners.
Technically
Topics fan messages out to independent subscriber groups; logs like Kafka keep them replayable.
Learn it in module 49Publish subscribe and logs

Idempotency

Messaging
In plain words
Doing something twice has the same effect as once.
Technically
Operations designed so retries are safe, usually via an idempotency key and stored result.
Learn it in module 52Idempotency and delivery guarantees

Stream processing

Messaging
In plain words
Handling data as it flows rather than in batches.
Technically
Continuous computation over unbounded event streams with windows and state.
Learn it in module 51Batch and stream processing

Vector clock

Distributed
In plain words
A way to tell which change came first, or if both happened at once.
Technically
Per node counters attached to versions to detect causality and conflicts.
Learn it in module 53Time, clocks and ordering

Saga

Distributed
In plain words
A long task split into steps that can each be undone.
Technically
Sequence of local transactions with compensating actions instead of a distributed lock.
Learn it in module 55Distributed transactions and sagas

Service discovery

Distributed
In plain words
Finding where a service lives right now.
Technically
A registry of healthy instances, via Consul, etcd or Kubernetes DNS.
Learn it in module 56Service discovery and configuration

RPO and RTO

Distributed
In plain words
How much data and time you can afford to lose in a disaster.
Technically
Recovery point objective and recovery time objective for failover planning.
Learn it in module 58Multi region and disaster recovery

SLO

Operations
In plain words
The reliability target you promise yourself.
Technically
Service level objective on an SLI, such as 99.9% of requests under 300 ms, with an error budget.
Learn it in module 61Logs, metrics, traces and SLOs

Distributed tracing

Operations
In plain words
Following one request through every service.
Technically
Propagated trace and span ids recorded by each hop, visualised as a timeline.
Learn it in module 61Logs, metrics, traces and SLOs

Geohash

Designs
In plain words
Turning a location into a short code for nearby searches.
Technically
Interleaved latitude and longitude bits encoded in base32; shared prefixes mean nearby cells.
Learn it in module 17Geohash, quadtrees and spatial indexes

Double entry ledger

Designs
In plain words
Every amount leaves one account and enters another.
Technically
Append only debit and credit rows per transaction that always sum to zero.
Learn it in module 78Design a payment system

Phase 01, modules 01 to 11

Counting the cost

Complexity and data structures

How to measure work, and the handful of structures every large system is quietly built from.

Module 01 of 84, phase 01

Small pieces, giant systems

Why data structures matter for system design

Every big system is a few simple data structures at enormous scale. A cache is a hash table, a message queue is a queue, a database index is a tree, and a feed ranks items with a heap.

In detail

System design interviews rarely ask you to code a red black tree, but they constantly ask why a choice is fast or slow. Knowing what each structure costs lets you explain why Redis answers in a millisecond, why an index makes a query fast, and why a queue protects a service from spikes. Start here and every later module will feel like a familiar shape wearing a bigger coat.

structures-in-systems.ts
TypeScript
const cache = new Map<string, string>(); // hash table -> Redis, Memcachedconst jobs: string[] = []; // queue -> Kafka, SQS, RabbitMQconst topK = [...scores].sort((a, b) => b[0] - a[0]).slice(0, 2); // heap -> trending topicsconst ordersByTime = orders.sort((a, b) => a[0] - b[0]); // sorted -> database B-tree index
Why it matters Each line maps to a production system you will design later in this guide.

Module 02 of 84, phase 01

Counting steps, not seconds

Big O and complexity

Big O describes how the work grows as the input grows. O(1) stays flat, O(log n) grows slowly, O(n) grows in step, and O(n squared) explodes.

In detail

We ignore constants and keep the fastest growing term, because at a million users only the shape of the curve matters. Time complexity counts steps; space complexity counts memory. In system design the same idea scales up: a full table scan is O(n) per request, an index lookup is O(log n), a cache hit is O(1). Amortised cost means an operation is usually cheap even if it is occasionally expensive, like a dynamic array resizing.

input size nworkO(1)O(log n)O(n)O(n log n)O(n²)
Steps needed as n grows
Complexityn = 10n = 1,000n = 1 millionExample
O(1)111Hash lookup
O(log n)31020Binary search, B-tree
O(n)101,0001,000,000Scan a list
O(n log n)3310,00020,000,000Good sort
O(n²)1001,000,0001012, hoursCompare every pair

Cost of each operation

  • Hash lookupO(1)
  • Binary searchO(log n)
  • Scan a listO(n)
  • Good sortO(n log n)
  • Compare all pairsO(n^2)
complexity.ts
TypeScript
/** O(n) time, O(1) space: check every item. */export function containsLinear(items: number[], x: number): boolean {	for (const item of items) {		if (item === x) return true;	}	return false;}
Why it matters The same question asked three ways costs n, log n or 1 steps. Choosing the structure is choosing the curve.

Module 03 of 84, phase 01

Seats in a row

Arrays and strings

An array stores items side by side in memory, so reading any position is instant but inserting in the middle shifts everything after it.

In detail

Because elements are contiguous, the CPU cache loves arrays: scanning them is the fastest loop in computing. Dynamic arrays (Python lists, Java ArrayList) double their capacity when full, so appends are O(1) amortised. Strings are arrays of characters; building strings in a loop by concatenation can be O(n squared), which is why we join lists. Two pointer and sliding window techniques solve many array problems in one pass, and the same sliding window idea reappears in rate limiting.

index lookup jumps straight to a slot
  1. 17[0]
  2. 4[1]
  3. 42[2]
  4. 8[3]
  5. 23[4]
  6. 15[5]
  7. 9[6]

Cost of each operation

  • Read by indexO(1)
  • AppendO(1)
  • Insert in middleO(n)
  • Search unsortedO(n)
sliding-window.ts
TypeScript
let window = nums.slice(0, k).reduce((a, b) => a + b, 0);let best = window;for (let i = k; i < nums.length; i++) {	window += nums[i] - nums[i - k]; // slide: add the new item, drop the oldest	best = Math.max(best, window);}return best;
Why it matters Sliding windows turn repeated sums into one pass, the same trick a rate limiter uses over time.

Module 04 of 84, phase 01

A coat check for data

Hash tables

A hash table turns a key into a bucket number with a hash function, so finding a value takes the same time whether you store ten items or ten million.

In detail

A good hash function spreads keys evenly. When two keys land in the same bucket (a collision) the table chains them in a small list or probes to the next slot. When it fills past a load factor it resizes and rehashes. Python dicts, Java HashMaps, Redis, Memcached and every database hash index use this idea. At system scale the same trick partitions data across machines, which is exactly where consistent hashing comes in.

Keys hashed into 5 buckets; bucket 3 shows a collision chained in one slot
  • user:42hash() % 5 = 0
  • cart:7hash() % 5 = 3
  • post:9hash() % 5 = 1
  • user:13hash() % 5 = 3
hash
  1. 0user:42
  2. 1post:9
  3. 2empty
  4. 3cart:7user:13
  5. 4empty

Cost of each operation

  • GetO(1)
  • PutO(1)
  • DeleteO(1)
  • Worst case, all collideO(n)
tiny-hashmap.ts
TypeScript
return this.buckets[hash(key) % this.buckets.length];for (const [k, v] of this.bucket(key)) {
Why it matters Resizing rehashes every key. A distributed cache cannot afford that, which is the whole motivation for consistent hashing.

Module 05 of 84, phase 01

A treasure hunt of pointers

Linked lists

A linked list stores each item with a pointer to the next one. Inserting or removing next to a node you already hold is instant, but finding the fifth item means walking from the start.

In detail

Singly linked lists point forward; doubly linked lists point both ways, which lets you remove a node in O(1) when you have it. That property is why an LRU cache pairs a hash map with a doubly linked list. Linked lists use more memory per item and are unfriendly to CPU caches, so in practice arrays win most scans, but the pointer idea reappears everywhere: log segments, skip lists in Redis sorted sets, and blockchain style hash chains.

Each node points to the next; the last points to nothing
  1. Anext
  2. Bnext
  3. Cnext
  4. Dnext
  5. null

Cost of each operation

  • Access by positionO(n)
  • Insert at headO(1)
  • Unlink known nodeO(1)
  • SearchO(n)
doubly-linked.ts
TypeScript
node.prev.next = node.next;node.next.prev = node.prev;
Why it matters push_front, unlink and pop_back are all O(1), the exact three moves an LRU cache needs.

Module 06 of 84, phase 01

Plates and lines

Stacks and queues

A stack hands back the last thing you put in, a queue hands back the first. Undo buttons and function calls are stacks; print jobs, request buffers and message brokers are queues.

In detail

Stacks power recursion, expression parsing and depth first search. Queues power breadth first search, task scheduling and every buffer between a fast producer and a slow consumer. A deque supports both ends. A circular buffer is a fixed size queue that overwrites the oldest entry, which is how logs and metrics rings keep memory bounded. Message queues such as SQS, RabbitMQ and Kafka are queues made durable and shared across machines.

First in, first out: enqueue at the back, dequeue from the front
enqueue
  1. job 1
  2. job 2
  3. job 3
  4. job 4
dequeue

Cost of each operation

  • Push or enqueueO(1)
  • Pop or dequeueO(1)
  • PeekO(1)
  • SearchO(n)
buffer.ts
TypeScript
undo.push("bold");console.log(undo.pop()); // "bold"requests.push(`req-${i}`);const handle = requests.shift();
Why it matters Why it matters Capping the queue at a fixed size and dropping the oldest entry makes it a bounded buffer: a bounded queue is the simplest form of back pressure.

Module 07 of 84, phase 01

Family trees for data

Trees, BSTs and B-trees

A tree organises data in parent and child levels. A balanced search tree keeps keys sorted so lookups, inserts and range scans all take O(log n).

In detail

A binary search tree keeps smaller keys left and larger keys right; it must stay balanced (AVL, red black) or it degrades into a list. Databases use B-trees and B+ trees instead: each node holds hundreds of keys so the tree is only three or four levels deep, which means three or four disk reads to find any row among millions. Range queries walk the linked leaves in order. Traversals (in order, pre order, post order, level order) are the vocabulary of every tree problem.

Search for 60: right, then leftServiceEdgeData
50Service30Edge70Edge20Data40Data60Data80Data

Cost of each operation

  • Search, balancedO(log n)
  • Insert, balancedO(log n)
  • Range scan of kO(log n + k)
  • Unbalanced worstO(n)
bst.ts
TypeScript
export function search(node: TreeNode | null, key: number): TreeNode | null {	while (node && node.key !== key) {		node = key < node.key ? node.left : node.right;	}	return node;}
Why it matters In order traversal returns sorted keys. A B+ tree index does the same along its leaf level to answer BETWEEN queries.

Module 08 of 84, phase 01

Always know who is first

Heaps and priority queues

A heap keeps the smallest (or largest) item on top, so you can always grab the most urgent thing in O(1) and add or remove items in O(log n).

In detail

A binary heap is stored as a plain array where the children of index i sit at 2i plus 1 and 2i plus 2. Priority queues schedule jobs, run Dijkstra's shortest path, merge sorted streams and keep the top K items of an endless stream with a fixed size heap. Leaderboards, trending topics, timers in event loops and the next expiring key in a cache all rely on this structure.

Min heap: every parent is smaller than its childrenServiceCacheData
2Service5Cache3Cache9Data7Data8Data

Cost of each operation

  • Peek minO(1)
  • PushO(log n)
  • Pop minO(log n)
  • Build from listO(n)
top-k.ts
TypeScript
heap.push([count, word]);if (heap.size > k) {	heap.pop(); // drop the smallest of the current top k}
Why it matters A size k heap uses O(k) memory no matter how long the stream is, which is why it scales to trending topics.

Module 09 of 84, phase 01

Maps of who connects to whom

Graphs, BFS and DFS

A graph is a set of nodes joined by edges. Social networks, road maps, service dependencies and the web itself are graphs, and two simple walks explore them: breadth first and depth first.

In detail

Store graphs as adjacency lists for sparse data. BFS explores level by level with a queue and finds shortest paths in unweighted graphs, which is how you compute friends of friends. DFS dives deep with a stack or recursion and finds cycles and connected components. Dijkstra adds weights for routing, topological sort orders build steps or task dependencies, and PageRank scores web pages by the links between them. A web crawler is BFS over the internet.

BFS spreads out one hop at a timeServiceEdgeData
1 hop1 hop2 hops2 hopsAshaServiceBenEdgeChenEdgeDevDataEliData

Cost of each operation

  • BFS or DFSO(V + E)
  • Dijkstra with heapO(E log V)
  • Adjacency check, listO(degree)
bfs.ts
TypeScript
const queue: [string, number][] = [[start, 0]];const seen = new Set([start]);for (let head = 0; head < queue.length; head++) {const [node, dist] = queue[head];for (const nxt of graph[node]) {
Why it matters The seen set prevents loops; a crawler needs the same thing, only stored in a distributed set or bloom filter.

Module 10 of 84, phase 01

Words that share a path

Tries and prefix search

A trie stores strings letter by letter so words with the same prefix share a path. Finding every word that starts with a prefix takes time proportional to the prefix, not the dictionary.

In detail

Each node holds children keyed by the next character and a marker for word endings. Search autocomplete keeps a trie of popular queries and caches the top results at each node, so a keystroke is answered by walking a few nodes. Routers use a compressed variant (radix tree) to match URL paths and IP prefixes. Tries trade memory for speed, so large systems shard them by first letter or precompute results into a key value store.

Words starting with ne share one pathServiceEdgeData
net...new...nes...nbarootServicenEdgeeEdgebEdgetDatawDatasDataaData

Cost of each operation

  • Insert word of length mO(m)
  • Find prefixO(m)
  • List matchesO(m + results)
trie.ts
TypeScript
let node: TrieNode | undefined = this.root;for (const ch of prefix) {	node = node.children.get(ch);	if (!node) return [];
Why it matters Production typeahead caches the top k at every node so suggest() never walks the whole subtree.

Module 11 of 84, phase 01

Putting things in order

Sorting and binary search

Sorting arranges data so it can be searched by halving. Good general sorts take O(n log n), and binary search then finds anything in O(log n).

In detail

Merge sort splits, sorts halves and merges; it is stable and the basis of external sorting when data does not fit in memory, which is how databases and MapReduce sort terabytes. Quicksort is usually fastest in memory. Counting and radix sorts beat n log n for small integer ranges. Binary search is far more general than finding a number: you can binary search a version history for the first bad deploy or a capacity setting for the largest safe value.

Bars rise into sorted order, then a binary search halves the range each step
  1. 5
  2. 2
  3. 8
  4. 3
  5. 9
  6. 1
  7. 6
  8. 4

Cost of each operation

  • Merge sortO(n log n)
  • Quicksort averageO(n log n)
  • Binary searchO(log n)
  • Bubble sortO(n^2)
binary-search.ts
TypeScript
let lo = 1;let hi = n;while (lo < hi) {const mid = Math.floor((lo + hi) / 2);
Why it matters git bisect is first_bad_version over commits. Knowing the pattern makes debugging production regressions fast.

Phase 02, modules 12 to 20

Algorithms that run the internet

Hashing, limiting, caching and IDs

The small, clever algorithms hiding inside load balancers, caches, databases and URL shorteners.

Module 12 of 84, phase 02

A ring that barely moves

Consistent hashing

Consistent hashing places servers and keys on a ring. Each key belongs to the next server clockwise, so adding or removing a server only moves the keys next to it.

In detail

With plain modulo hashing, changing the number of servers from 4 to 5 moves almost every key, which would flush a cache cluster. On a ring only about 1 over n of the keys move. Virtual nodes give each server many points on the ring so load spreads evenly and a big machine can take more points. DynamoDB, Cassandra, Discord, Akamai and most distributed caches use this idea for partitioning and replication.

Each key belongs to the next node clockwise; adding a node only steals keys from its neighbour
BCDAk1→Dk2→Bk3→Ck4→Ck5→C

Cost of each operation

  • Find nodeO(log v)
  • Add nodeO(v log v)
  • Keys moved on changeabout K/n
hash-ring.ts
TypeScript
const idx = bisectRight(this.keys, h(key)) % this.keys.length; // next point clockwisereturn this.ring.get(this.keys[idx]) as string;
Why it matters Adding a fourth node moves roughly a quarter of the keys, exactly the share the new node should own.

Module 13 of 84, phase 02

Turnstiles for traffic

Rate limiting algorithms

Rate limiters decide how many requests a client may make in a period. Token bucket, leaky bucket, fixed window and sliding window are the four algorithms you will be asked to compare.

In detail

Token bucket refills tokens at a steady rate and lets clients burst up to the bucket size; it is the most common choice (AWS, Stripe). Leaky bucket drains a queue at a fixed rate, smoothing output. Fixed window counters are simple but allow double bursts at window edges. Sliding window log is exact but stores every timestamp; sliding window counter blends two fixed windows for accuracy with tiny memory. In a cluster the counters live in Redis with atomic operations.

Rate limiting algorithms compared
AlgorithmBurstsAccuracyMemory per keyUse it when
Token bucketAllowed up to bucket sizeGood2 numbersPublic APIs that tolerate short bursts
Leaky bucketSmoothed outGoodA queueSteady outflow to a fragile backend
Fixed windowDouble at window edgeRough1 counterSimple, coarse limits
Sliding logExactExactEvery timestampLow volume, strict limits
Sliding window counterSmoothedClose to exact2 countersThe usual production choice

Cost of each operation

  • Check a requestO(1)
  • Memory per client, bucketO(1)
  • Memory, sliding logO(requests)
token-bucket.ts
TypeScript
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.rate); // refillif (this.tokens >= 1) {	this.tokens -= 1;	return true;}
Why it matters Token bucket stores two numbers per client, so a million clients fit comfortably in Redis.

Module 14 of 84, phase 02

Forget the least loved

LRU and LFU caches

An LRU cache evicts the item used longest ago when it is full. A hash map finds items in O(1) and a doubly linked list keeps them in usage order.

In detail

On every get, move the item to the front; on every put beyond capacity, drop the item at the back. LFU evicts the least frequently used item instead, which keeps steady favourites but adapts slowly to new trends. Redis offers approximated LRU and LFU policies by sampling keys rather than keeping a perfect list. Choosing the eviction policy is a real design decision: LRU suits recency driven data like sessions, LFU suits stable popular content.

Cost of each operation

  • GetO(1)
  • PutO(1)
  • EvictO(1)
lru.ts
TypeScript
this.data.delete(key); // mark as most recently usedthis.data.set(key, value);if (this.data.size > this.capacity) {const oldest = this.data.keys().next().value as string;this.data.delete(oldest); // evict least recently used
Why it matters Hit ratio is the number to watch in production: a cache at 50% hits halves database load, at 99% it removes it.

Module 15 of 84, phase 02

Definitely not, probably yes

Bloom filters and probabilistic structures

A bloom filter answers is this item in the set with either definitely not or probably yes, using a tiny fraction of the memory a real set would need.

In detail

It sets k bits for each item using k hash functions; a lookup checks those bits. False positives happen, false negatives never do. Databases like Cassandra and RocksDB check a bloom filter before reading an SSTable from disk, crawlers use one to skip seen URLs, and CDNs use one to avoid caching one hit wonders. Its cousins answer other questions cheaply: HyperLogLog counts unique visitors in 12 KB, count min sketch estimates frequencies in a stream.

Cost of each operation

  • AddO(k)
  • CheckO(k)
  • Memory per itemabout 10 bits
bloom.ts
TypeScript
for (let i = 0; i < this.k; i++) {	this.bits[this.hash(item, i)] = 1;
Why it matters About 1.2 MB tracks a million URLs at 1% error; storing the URLs themselves would take around 100 MB.

Module 16 of 84, phase 02

Fingerprints of fingerprints

Merkle trees

A Merkle tree hashes data blocks, then hashes the hashes up to a single root. If two roots match, the data matches; if not, you can find the differing block in O(log n) comparisons.

In detail

Replicas in Cassandra and DynamoDB compare Merkle trees during anti entropy repair, exchanging only the branches that differ instead of whole datasets. Git commits, Bitcoin blocks, IPFS and certificate transparency logs all use hash trees. The idea is perfect whenever two machines must check large data is identical over a slow link.

Follow only the branch whose hash differsServiceEdgeDataCache
differsfoundroot hashServicehash 1-2Edgehash 3-4Edgeblock 1Datablock 2Datablock 3Cacheblock 4Data

Cost of each operation

  • BuildO(n)
  • Compare rootsO(1)
  • Locate one differenceO(log n)
merkle.ts
TypeScript
while (level.length > 1) {for (let i = 0; i < level.length; i += 2) {parents.push(h(Buffer.concat([level[i], level[i + 1]])));
Why it matters Only two hash comparisons per level are needed, so a billion rows differ out in about 30 steps.

Module 17 of 84, phase 02

Folding the map into boxes

Geohash, quadtrees and spatial indexes

Geohash turns a latitude and longitude into a short string where nearby places share a prefix. Quadtrees split the map into four boxes again and again until each box holds a manageable number of places.

In detail

Finding restaurants near me cannot scan every restaurant. With geohash you compute the user's cell and its eight neighbours, then query places whose geohash starts with those prefixes. Quadtrees adapt to density: Manhattan splits into tiny cells, the desert stays one big cell. Uber built H3, a hexagonal grid, because hexagons have equal distance to every neighbour. Databases expose these as geospatial indexes (PostGIS, Redis GEO, Elasticsearch geo_point).

Cost of each operation

  • Encode a pointO(precision)
  • Prefix lookupO(log n)
  • Quadtree queryO(log n + k)
geohash.ts
TypeScript
const rng = even ? lonRng : latRng; // interleave lon and lat bitsconst value = even ? lon : lat;const mid = (rng[0] + rng[1]) / 2;if (value >= mid) {	ch = (ch << 1) | 1;
Why it matters Prefix equals proximity, so a plain B-tree index on the geohash column answers nearby queries.

Module 18 of 84, phase 02

What is everyone talking about

Top K and heavy hitters

Top K finds the most frequent items in a stream, such as trending hashtags or most played songs, without keeping a counter for everything ever seen.

In detail

For exact results on bounded data, count with a hash map and keep a size K heap. For endless streams, approximate: a count min sketch estimates counts in fixed memory, and the heap keeps candidates. Systems compute top K per time window (last five minutes) on many machines, then merge partial results. The pattern appears in trending topics, top searched queries for autocomplete and top viewed videos for recommendations.

Cost of each operation

  • Add to sketchO(depth)
  • EstimateO(depth)
  • Keep top kO(log k)
count-min.ts
TypeScript
for (let row = 0; row < this.depth; row++) {	this.table[row][this.idx(item, row)] += n;}
Why it matters Memory stays at width times depth counters regardless of how many distinct tags appear.

Module 19 of 84, phase 02

Names that never clash

Unique ID generation

Distributed systems need IDs that are unique without asking a central database every time. UUIDs, Snowflake IDs and ticket servers are the usual answers.

In detail

Auto increment only works on one database. UUID v4 is random and needs no coordination but is 128 bits and sorts randomly, which fragments indexes; UUID v7 fixes sorting by putting time first. Twitter's Snowflake packs a timestamp, a machine id and a per millisecond sequence into 64 bits, so IDs are unique, compact and roughly time ordered. A ticket server hands out ranges of numbers to each app server to cut round trips.

Back of the envelope

  • Bits64fits a BIGINT column
  • IDs per ms per machine4,09612 bit sequence
  • Machines1,02410 bit id
  • Lifetimeabout 69 years41 bit timestamp
snowflake.ts
TypeScript
return (BigInt(ms - EPOCH_MS) << 22n) | (this.machine << 12n) | BigInt(this.sequence);
Why it matters Refusing to issue IDs when the clock goes backwards is what keeps Snowflake IDs unique after NTP corrections.

Module 20 of 84, phase 02

Big numbers, short names

Base62 encoding and short codes

Base62 writes a number using 0 to 9, a to z and A to Z, so a huge database id becomes a short, URL safe code. Seven characters cover 3.5 trillion links.

In detail

A URL shortener can turn each new row id into base62 (unique by construction, no collision checks) or hash the long URL and take the first characters (needs collision handling). Sequential ids make codes guessable; mixing in a Feistel shuffle or a random offset hides the pattern. The same encoding shows up in YouTube video ids and invite codes.

Back of the envelope

  • Alphabet62 symbols0-9 a-z A-Z
  • 6 characters56.8 billioncodes
  • 7 characters3.5 trillioncodes
  • 8 characters218 trillioncodes
base62.ts
TypeScript
while (n > 0) {	out.push(ALPHABET[n % 62]);	n = Math.floor(n / 62);
Why it matters Encoding an id needs no lookup and can never collide, which is why most shorteners prefer it to hashing.

Phase 03, modules 21 to 30

Thinking like an architect

System design fundamentals

Requirements, estimates, scale, availability and the trade-offs every design is really about.

Module 21 of 84, phase 03

Drawing the city before building it

What system design is, and the interview framework

System design is deciding which parts a product needs, how they talk, where data lives and how it all keeps working as users grow. Interviews test whether you can reason through those choices out loud.

In detail

A strong answer follows a rhythm: clarify requirements, estimate scale, sketch a high level design, define the API and data model, then deep dive into the hardest parts and discuss trade-offs and failure modes. There is rarely one right answer; there are justified choices. Interviewers listen for numbers, bottlenecks and what you would monitor, not for memorised diagrams.

  1. 01Requirements, 5 minFeatures, users, what is out of scope.
  2. 02Estimates, 5 minQPS, storage, bandwidth, read to write ratio.
  3. 03High level design, 10 minBoxes and arrows that satisfy the core flow.
  4. 04API and data, 5 minEndpoints, schemas, keys and indexes.
  5. 05Deep dives, 10 minThe hardest parts for this product.
  6. 06Wrap up, 5 minBottlenecks, failures, monitoring, next steps.
framework.md
Notes
1. Requirements  2. Estimates  3. High level design4. API + data model  5. Deep dives  6. Bottlenecks and trade-offs
Why it matters Saying the plan aloud in the first minute shows structure and lets the interviewer steer you.

Module 22 of 84, phase 03

Know what you are building

Functional and non functional requirements

Functional requirements are what the system does: shorten a URL, send a message. Non functional requirements are how well it does it: how fast, how available, how consistent, at what scale.

In detail

Write both lists before drawing anything. Non functional requirements drive the architecture far more than features do: 100 ms p99 latency forces caching, 99.99% availability forces redundancy across zones, strong consistency rules out some databases. Also state what is out of scope so the interview stays focused. Read heavy versus write heavy is the single most useful classification.

requirements.md
Notes
Functional: create short link, redirect, custom alias, expiryNon functional: 100M links/month, p99 redirect < 50 ms, 99.99% available
Why it matters The 100 to 1 read ratio alone tells you to cache aggressively and keep redirects off the write path.

Module 23 of 84, phase 03

Napkin maths that decides designs

Back of the envelope estimation

Rough numbers for users, requests per second, storage and bandwidth tell you whether one database will do or whether you need a fleet. Round aggressively and show your working.

In detail

Start from daily active users and actions per user, divide by 86,400 seconds (call it 100,000) for average QPS, multiply by 2 to 5 for peak. Storage is items per day times size times retention. Bandwidth is QPS times payload size. Then compare with what one machine handles: a well tuned PostgreSQL does thousands of simple writes per second, Redis around 100,000 operations per second, a single server tens of thousands of HTTP requests.

Powers of two and the sizes they mean
PowerExact valueApproximateBytesFeels like
2101,0241 thousand1 KBA short text message thread
2201,048,5761 million1 MBA photo
2301,073,741,8241 billion1 GBA film
240about 1.1 trillion1 trillion1 TBA laptop disk
250about 1.1 quadrillion1 quadrillion1 PBA large company's data

Back of the envelope

  • Seconds per day86,400round to 100k
  • 1 million per dayabout 12 per secondaverage
  • Peak factor2 to 5 timesof average
  • 1 KB times 1 billion1 TBstorage
estimate.ts
TypeScript
const writeQps = writesDay / DAY;const storage = writesDay * bytesPerItem * 365 * years;
Why it matters Seven hundred thousand reads per second at peak immediately says cache and fan out, not a single database.

Module 24 of 84, phase 03

How long things really take

Latency numbers and performance

Reading memory takes nanoseconds, an SSD takes microseconds, a network round trip across a data centre takes half a millisecond, and crossing an ocean takes over a hundred. Designs follow these gaps.

In detail

Latency is time for one request; throughput is requests per second. Measure percentiles, not averages: p99 is what your unluckiest regular users feel, and with fan out the slowest of many calls decides the total. Keep hot data in memory, avoid sequential network calls on the request path, and move far away users closer with CDNs and regional replicas.

Latency numbers, log scaleeach step to the right is about ten times slower

  1. L1 cache reference0.5 ns
  2. Main memory reference100 ns
  3. Compress 1 KB2 µs
  4. Read 1 MB from memory3 µs
  5. SSD random read16 µs
  6. Read 1 MB from SSD49 µs
  7. Round trip in same datacenter500 µs
  8. Disk seek2 ms
  9. Round trip India to US150 ms
percentiles.ts
TypeScript
const pct = (p: number): number =>	latencies[Math.min(latencies.length - 1, Math.floor(latencies.length * p))];
Why it matters A 1% tail becomes a 39% problem once a page fans out to 50 calls, which is why p99 matters more than the mean.

Module 25 of 84, phase 03

Bigger machine or more machines

Vertical and horizontal scaling

Vertical scaling buys a bigger server; horizontal scaling adds more servers behind a load balancer. The first is simple but capped, the second is unlimited but needs stateless services and partitioned data.

In detail

Scale up first: it is cheaper in engineering time than you think. When you scale out, keep application servers stateless (sessions in Redis, files in object storage) so any server can handle any request. Data is the hard part: read replicas scale reads, caching removes reads, sharding scales writes. Every extra machine adds coordination, so scale the part that is actually the bottleneck.

Vertical, scale up

64 cores, 512 GB

Buy a bigger machine. Simple, no code changes, but there is a ceiling and it is still one point of failure.

Horizontal, scale out

Add more ordinary machines behind a load balancer. Near unlimited, survives failures, but needs stateless services and sharded data.

stateless.ts
TypeScript
// Bad: session in process memory, breaks with 2+ servers// SESSIONS.set(token, userId);// Good: stored outside the process, so any app server behind the load balancer can read itawait r.setex(`session:${token}`, 3600, JSON.stringify({ userId } satisfies Session));
Why it matters Once state lives in shared stores, adding a server is just starting another identical process.

Module 26 of 84, phase 03

Counting the nines

Availability, reliability and SLOs

Availability is the share of time a system works. 99.9% allows about 43 minutes of downtime a month, 99.99% about 4 minutes. Every extra nine costs roughly ten times more.

In detail

Components in series multiply availability (two 99.9% services in a chain give 99.8%); redundant components in parallel improve it. Remove single points of failure with replicas across availability zones, health checks and automatic failover. Define service level indicators (what you measure), objectives (the target) and agreements (the promise with penalties). An error budget, the allowed unreliability, lets teams trade speed against stability with data.

What each extra nine of availability allows
AvailabilityDown per yearDown per monthDown per weekTypical for
99%3.65 days7.3 hours1.7 hoursInternal tools
99.9%8.8 hours43.8 minutes10.1 minutesMost web apps
99.95%4.4 hours21.9 minutes5 minutesBusiness critical APIs
99.99%52.6 minutes4.4 minutes1 minutePayments, core platforms
99.999%5.3 minutes26 seconds6 secondsTelecom, cloud control planes
nines.ts
TypeScript
for (const p of parts) total *= p;return 1 - (1 - part) ** copies;console.log(`chain: ${pct(serial(0.999, 0.999, 0.999), 2)}`); // 99.70%console.log(`redundant: ${pct(serial(...tiers), 4)}`); // 99.9997%
Why it matters Redundancy at every tier turns three 99.9% parts into a system near five nines, as long as failover actually works.

Module 27 of 84, phase 03

Expect everything to break

Reliability and fault tolerance

Fault tolerance means the system keeps serving when parts fail. Disks die, networks split and deploys go wrong, so good designs assume failure and plan the response.

In detail

Use redundancy (more than one of everything), isolation (a failure in one feature should not take down another), graceful degradation (show cached results when the recommender is down), and fast detection with automatic recovery. Chaos engineering, popularised by Netflix's Chaos Monkey, breaks things on purpose in production to prove the recovery works. Replication protects against machine loss; backups protect against mistakes such as a bad migration.

degrade.ts
TypeScript
try {	page.recommended = await recommender.forUser(userId, { timeoutMs: 200 });} catch (err) {	page.recommended = (await cache.get("popular:today")) ?? [];
Why it matters Split features into essential and optional; optional ones get tight timeouts and a fallback.

Module 28 of 84, phase 03

Pick two when the network splits

CAP and PACELC

When a network partition cuts replicas apart, a distributed system must choose: keep answering with possibly stale data (availability) or refuse until it can be sure (consistency).

In detail

Partitions are not optional in real networks, so the practical choice is CP or AP during a partition. PACELC adds the everyday trade-off: else, when there is no partition, you trade latency against consistency. Banks and inventory counters lean CP; social feeds, shopping carts and DNS lean AP. Many databases let you choose per query with tunable quorum reads and writes.

C A P CA: single nodeCP: etcd, Spanner, HBaseAP: Cassandra, DynamoDB, CouchDB

Networks always partition eventually, so in practice the choice is between C and A while the partition lasts. PACELC adds: else, choose between latency and consistency.

quorum.ts
TypeScript
const overlap = r + w > n; // read and write sets must overlap
Why it matters Tuning R and W is CAP made concrete: overlap buys consistency, small quorums buy availability and latency.

Module 29 of 84, phase 03

How fresh is fresh

Consistency models

A consistency model promises what a reader may see after a write. Strong consistency shows the latest value everywhere at once; eventual consistency promises replicas will agree soon.

In detail

Between the extremes sit useful middle grounds: read your own writes (you always see your own post), monotonic reads (you never go back in time), causal consistency (replies appear after the message they answer). Linearizability is the strongest single object guarantee and needs consensus or a single leader. Pick the weakest model the product tolerates, because each step up costs latency and availability.

read-your-writes.ts
TypeScript
if (Date.now() - (lastWriteAt.get(userId) ?? 0) < LAG_BUDGET_MS) {	return primary.read(`profile:${userId}`);}return replica.read(`profile:${userId}`);
Why it matters A small routing rule gives users the consistency they notice without paying for strong consistency everywhere.

Module 30 of 84, phase 03

Every choice has a price

Core trade-offs

System design is mostly trade-offs: latency against consistency, cost against availability, simplicity against flexibility. Naming the trade-off out loud is half of a good answer.

In detail

Common pairs: SQL versus NoSQL, push versus pull, cache everything versus always fresh, monolith versus microservices, synchronous versus asynchronous, normalised versus denormalised, strong versus eventual consistency. For each choice say what you gain, what you give up, and under which numbers you would switch. The table below is a quick reference you can return to before any interview.

Common trade-offs and when to pick each side
DecisionOption AOption BPick A whenPick B when
Data storeSQLNoSQLRelations, transactions, ad hoc queriesHuge scale, simple access patterns, flexible schema
ConsistencyStrongEventualMoney, inventory, permissionsFeeds, counts, analytics
DeliveryPushPullFew recipients, real time mattersMany recipients, read time merge is cheap
CallsSynchronousAsynchronousCaller needs the answer nowWork can wait, spikes must be absorbed
SchemaNormalisedDenormalisedWrite heavy, integrity firstRead heavy, latency first
ArchitectureMonolithMicroservicesSmall team, evolving domainMany teams, independent scaling
CachingCache everythingAlways freshRead heavy, staleness tolerableCorrectness critical reads

Phase 04, modules 31 to 37

The roads between machines

Networking, load balancing and APIs

How requests find a server, get spread across many, and talk in REST, gRPC or a live socket.

Module 31 of 84, phase 04

The internet's phone book

DNS

DNS turns a name like netflix.com into an IP address. It is hierarchical, heavily cached and also a simple traffic tool: it can send users to the nearest or healthiest region.

In detail

A resolver asks the root servers, then the .com servers, then the domain's authoritative servers, caching each answer for its time to live. Low TTLs make changes fast but increase lookups. GeoDNS and latency based routing answer differently per user location, and health checked records drop a failed region. Because DNS caching is outside your control, it is a slow failover tool; anycast and load balancers react faster.

A cold lookup walks three levels, then cachesClientCacheExternalService
netflix.com?123: 52.x.x.xBrowserClientResolverCacheRootExternal.comExternalnetflix.com NSService
lookup.sh
Shell
dig +short netflix.comdig netflix.com +trace
Why it matters The TTL column tells you how long a change could take to reach everyone.

Module 32 of 84, phase 04

Asking and answering

HTTP, TCP, UDP and QUIC

Clients send requests and servers send responses. HTTP rides on TCP for reliable, ordered delivery; UDP drops those guarantees for speed; QUIC brings reliability back on top of UDP for HTTP/3.

In detail

A TCP connection needs a handshake, and TLS adds more round trips, so reusing connections (keep alive, pooling) matters. HTTP/2 multiplexes many requests over one connection; HTTP/3 over QUIC removes head of line blocking and survives network changes on phones. Use UDP when late data is useless: video calls, games, DNS. Know status code families: 2xx success, 3xx redirect, 4xx client error, 5xx server error.

Transport and HTTP versions
ProtocolBuilt onStrengthWeaknessTypical use
TCPIPReliable, orderedHandshake cost, head of line blockingAlmost everything
UDPIPNo setup, low overheadNo delivery guaranteeVideo calls, games, DNS
HTTP/1.1TCPSimple, universalOne request at a time per connectionLegacy APIs
HTTP/2TCP + TLSMultiplexed streams, header compressionTCP head of line blockingModern web, gRPC
HTTP/3QUIC over UDPNo head of line blocking, fast setupNewer, some networks block UDPMobile, lossy networks
http.sh
Shell
curl -sI https://example.com | head -5curl --http3 -sI https://cloudflare.com
Why it matters The timing line shows how much of a request is setup; that is the cost connection reuse saves.

Module 33 of 84, phase 04

Copies close to everyone

Content delivery networks

A CDN keeps copies of static files, and sometimes whole pages, on servers near users. Images, video and scripts load from a nearby city instead of crossing an ocean.

In detail

Pull CDNs fetch from your origin on the first miss and cache by TTL; push CDNs receive content ahead of time, which suits large known files such as video. Cache keys, Cache-Control headers and versioned filenames decide what stays fresh. Netflix built Open Connect, appliances inside internet providers that serve most of its traffic from within the user's own ISP. CDNs also absorb DDoS attacks and terminate TLS close to users.

Hits stay at the edge, misses go to origin onceClientCacheService
10 ms8 msmiss onlymiss onlyUser DelhiClientUser LondonClientEdge MumbaiCacheEdge LondonCacheOriginService
headers.txt
Notes
Cache-Control: public, max-age=31536000, immutable   # app.3f9a2c.jsCache-Control: no-cache                              # index.html
Why it matters Versioned filenames plus long TTLs give near 100% CDN hit rates without ever serving outdated code.

Module 34 of 84, phase 04

A host who seats every guest

Load balancers

A load balancer spreads requests across healthy servers, hides failures and lets you add capacity without clients noticing. It works at layer 4 (TCP) or layer 7 (HTTP).

In detail

Layer 4 balancers route by IP and port and are extremely fast; layer 7 balancers read HTTP, so they can route by path, header or cookie and terminate TLS. Algorithms include round robin, weighted round robin, least connections and consistent hashing for stickiness. Health checks remove broken instances. Run balancers in pairs or as a managed service so the balancer itself is not a single point of failure.

Requests spread across healthy instancesClientEdgeService
HTTPSClientsClientLoad balancerEdgeApp 1ServiceApp 2ServiceApp 3Service
Load balancing algorithms
AlgorithmHow it picksGood forWatch out
Round robinNext server in orderEqual servers, short requestsIgnores current load
Weighted round robinIn order, by weightMixed machine sizesWeights need tuning
Least connectionsFewest open connectionsLong or uneven requestsNeeds connection tracking
Least response timeFastest recent responsesLatency sensitive APIsCan herd onto one fast node
IP or consistent hashHash of client or keySticky sessions, cache localityUneven if keys are skewed
nginx.conf
nginx
upstream api { least_conn; server app1:8080; server app2:8080; }location / { proxy_pass http://api; }
Why it matters max_fails and proxy_next_upstream give passive health checking: a dying server stops getting traffic within seconds.

Module 35 of 84, phase 04

One front door for many services

Reverse proxies and API gateways

A reverse proxy sits in front of servers; an API gateway is a reverse proxy that also handles authentication, rate limits, routing to microservices, request shaping and analytics.

In detail

Clients see one host while the gateway routes /orders to the order service and /users to the user service. Centralising cross cutting concerns keeps services simple, but the gateway becomes critical infrastructure that must scale and stay thin. The backend for frontend pattern gives mobile and web their own gateway tuned to their needs. Examples include Kong, Envoy, NGINX, AWS API Gateway and Apigee.

gateway.yaml
YAML
routes:  - path: /orders/*   -> orders-svc  - path: /users/*    -> users-svcplugins: [jwt-auth, rate-limit: 100/min]
Why it matters Auth, limits and tracing ids are configured once at the edge instead of in every service.

Module 36 of 84, phase 04

Agreeing how to talk

REST, GraphQL and gRPC

REST exposes resources over HTTP verbs, GraphQL lets clients ask for exactly the fields they need, and gRPC sends compact binary messages over HTTP/2 for fast service to service calls.

In detail

REST is simple, cacheable and universal, so it suits public APIs. GraphQL removes over fetching and many round trips for rich clients but makes caching and rate limiting harder. gRPC gives typed contracts from Protocol Buffers, streaming and low latency between internal services. Whatever the style, design idempotent writes, cursor pagination and versioning from day one.

REST, GraphQL and gRPC
StyleShapeStrengthWeaknessBest for
RESTResources and HTTP verbs, JSONSimple, cacheable, universalOver and under fetchingPublic APIs
GraphQLOne endpoint, client written queriesExactly the fields neededCaching and cost control are harderRich frontends, many clients
gRPCProtobuf over HTTP/2Fast, typed, streamingNot browser nativeService to service
WebSocketBidirectional messagesReal time both waysStateful connections to scaleChat, live updates
api.http
Notes
GET  /v1/links?cursor=abc&limit=20POST /v1/links   Idempotency-Key: 7f3c...
Why it matters Idempotency keys and cursors are the two API habits that save the most production incidents.

Module 37 of 84, phase 04

Keeping the line open

Polling, long polling, SSE and WebSockets

To push updates to clients you can poll, long poll, stream with server sent events, or hold a two way WebSocket. Chat, live scores and collaborative editing all depend on this choice.

In detail

Short polling is simple but wasteful. Long polling holds the request until there is news. SSE streams one way from server to client over plain HTTP with automatic reconnects, ideal for feeds and notifications. WebSockets give full duplex messaging for chat and games. Persistent connections are stateful, so you need a connection registry and a pub/sub layer to route a message to whichever server holds the recipient's socket.

Getting updates to clients
TechniqueDirectionLatencyServer costUse it for
Short pollingClient asks every few secondsUp to the intervalMany empty requestsRare updates, simplest option
Long pollingClient asks, server holds until newsLowHeld connectionsFallback for WebSocket
Server sent eventsServer to client streamLowLight, plain HTTPFeeds, notifications, AI token streams
WebSocketBoth ways, full duplexLowestStateful, sticky routingChat, games, collaboration
WebRTCPeer to peer media and dataLowest, directSignalling and TURN serversVideo and voice calls
sse.js
JavaScript
const events = new EventSource("/v1/scores/stream");events.onmessage = (e) => render(JSON.parse(e.data));
Why it matters clientId lets the server deduplicate a message the client resent after a dropped connection.

Phase 05, modules 38 to 45

Where data lives

Databases and storage

SQL and NoSQL, indexes, transactions, replication, sharding, blobs and search.

Module 38 of 84, phase 05

Tables or flexible documents

SQL versus NoSQL

Relational databases store tables with strict schemas, joins and transactions. NoSQL databases trade some of that for flexible models and easier horizontal scale.

In detail

Choose SQL (PostgreSQL, MySQL) by default when data is relational and correctness matters: payments, inventory, bookings. Choose a key value store (Redis, DynamoDB) for simple lookups at huge scale, a document store (MongoDB) for nested records read together, a wide column store (Cassandra) for massive write throughput and time ordered data, and a graph database (Neo4j) for deep relationship queries. Many systems use several, each for what it does best.

Picking a database
KindExamplesGreat atWeak at
RelationalPostgreSQL, MySQLTransactions, joins, constraintsHorizontal write scaling
Key valueRedis, DynamoDBFast lookups by key, huge scaleQueries on other fields
DocumentMongoDB, FirestoreFlexible nested recordsCross document transactions and joins
Wide columnCassandra, ScyllaDB, HBaseMassive write throughput, time seriesAd hoc queries
GraphNeo4j, NeptuneRelationship traversalBulk analytics on everything
SearchElasticsearch, OpenSearchFull text and faceted searchBeing the source of truth
Time seriesPrometheus, InfluxDB, TimescaleDBMetrics, compression, rollupsGeneral purpose data
schema.sql
SQL
CREATE TABLE links (  code TEXT PRIMARY KEY, long_url TEXT NOT NULL,  owner_id BIGINT, created_at TIMESTAMPTZ DEFAULT now());
Why it matters The redirect path only ever looks up by code, which is why a key value store is a strong fit for that one table.

Module 39 of 84, phase 05

The back of the book

Indexes, B-trees and LSM trees

An index is a sorted structure that points to rows, turning a full table scan into a few page reads. B-trees favour reads; LSM trees favour heavy writes.

In detail

Index the columns you filter, join and sort by, in the order your queries use them (composite index on owner_id then created_at serves where owner_id = ? order by created_at). Every index slows writes and uses space. LSM trees (Cassandra, RocksDB, ScyllaDB) write sequentially to a memtable and flush sorted files, compacting them later; bloom filters avoid checking files that cannot contain a key. EXPLAIN shows whether a query actually uses the index.

B-tree and LSM tree storage engines
AspectB-treeLSM tree
WritesUpdate pages in placeAppend to memtable, flush sorted files
ReadsOne path down the treeMay check several files, Bloom filters help
Write throughputModerateVery high
Space and compactionSome fragmentationBackground compaction merges files
Used byPostgreSQL, MySQL InnoDBCassandra, RocksDB, LevelDB, ScyllaDB

Cost of each operation

  • B-tree lookupO(log n)
  • Full scanO(n)
  • LSM writeO(1) append
  • LSM readO(log n) per level
explain.sql
SQL
CREATE INDEX links_owner_created ON links (owner_id, created_at DESC);EXPLAIN ANALYZE SELECT * FROM links WHERE owner_id = 42 ORDER BY created_at DESC LIMIT 20;
Why it matters CONCURRENTLY builds the index without locking writes, which matters on a live production table.

Module 40 of 84, phase 05

All or nothing

Transactions, ACID and isolation

A transaction groups operations so they all succeed or all fail. ACID promises atomicity, consistency, isolation and durability, and isolation levels decide how concurrent transactions see each other.

In detail

Read committed (PostgreSQL's default) prevents dirty reads; repeatable read gives each transaction a stable snapshot; serializable makes concurrent transactions behave as if run one by one, at the cost of retries. Lost updates and double booking come from read then write races: fix them with SELECT FOR UPDATE, atomic updates, unique constraints or optimistic version checks. Keep transactions short; long ones hold locks and bloat storage.

Isolation levels and the anomalies they allow
LevelDirty readNon repeatable readPhantom readLost update
Read uncommittedPossiblePossiblePossiblePossible
Read committedPreventedPossiblePossiblePossible
Repeatable readPreventedPreventedDepends on engineDepends on engine
SnapshotPreventedPreventedPreventedWrite skew possible
SerializablePreventedPreventedPreventedPrevented
book_seat.sql
SQL
UPDATE seats SET status = 'held', held_by = $1WHERE id = $2 AND status = 'free';   -- 0 rows = someone else got it
Why it matters The conditional UPDATE is a compare and set in SQL: the database decides the race, not your application code.

Module 41 of 84, phase 05

Copies that keep each other honest

Replication

Replication keeps copies of data on several machines for availability, durability and read scale. Leader follower, multi leader and leaderless are the three shapes.

In detail

With a single leader, writes go to the leader and stream to followers; reads can go to followers but may lag. Synchronous replication waits for a follower so no write is lost on failover; asynchronous is faster but can lose recent writes. Multi leader setups accept writes in several regions and must resolve conflicts. Leaderless systems (Dynamo, Cassandra) write to several nodes with quorums and repair differences with Merkle trees and read repair.

Writes go to the leader, reads can fan outServiceDataCache
writesWAL streamWAL streamreadsAppServiceLeaderDataFollower 1CacheFollower 2Cache
lag.sql
SQL
SELECT client_addr, state, replay_lag FROM pg_stat_replication;
Why it matters Alert on replay_lag: a lagging follower silently serves stale reads long before it causes an outage.

Module 42 of 84, phase 05

Splitting the library

Sharding and partitioning

Sharding splits one big dataset across many databases by a shard key, so writes and storage scale beyond a single machine.

In detail

Range sharding keeps nearby keys together (good for range scans, risky for hot spots); hash sharding spreads load evenly; directory based sharding keeps a lookup table for full control. The shard key is the hardest decision: it must spread load and keep related data together, because cross shard joins and transactions are slow. Plan for hot keys (a celebrity's account), resharding with consistent hashing, and per shard replication.

The shard key decides where each row livesEdgeData
user NehaRouterEdgeShard 0: A-FDataShard 1: G-MDataShard 2: N-SDataShard 3: T-ZData
Sharding strategies
StrategyHow keys mapStrengthWeakness
RangeKey ranges per shard, A to F, G to MRange scans stay localHot spots on recent or popular ranges
Hashhash(key) mod shardsEven spreadRange queries hit every shard
Consistent hashHash ring with virtual nodesLittle data moves on resizeMore moving parts
DirectoryLookup table key to shardFully flexible movesThe directory is critical infrastructure
GeographicBy region or tenantData near users, complianceUneven region sizes
router.ts
TypeScript
return SHARDS[digest.readUInt32BE(0) % SHARDS.length];const db = connect(shardFor(userId));await db.query("INSERT INTO posts (user_id, text) VALUES ($1, $2)", [userId, text]);
Why it matters Modulo by shard count makes adding a shard move most rows; production routers use consistent hashing or a directory.

Module 43 of 84, phase 05

Four shapes of NoSQL

Key value, document, wide column and graph stores

NoSQL is four families: key value stores for lookups by key, document stores for nested records, wide column stores for huge write heavy tables, and graph stores for relationships.

In detail

Model NoSQL data around access patterns, not entities: list the queries first, then design keys so each query reads one partition. Cassandra wants a partition key plus clustering columns (messages by conversation, ordered by time). DynamoDB single table design stores several entity types under shared keys. Graph databases shine for recommendations and fraud rings where queries hop many relationships.

messages.cql
SQL
PRIMARY KEY ((conversation_id), sent_at, message_id)) WITH CLUSTERING ORDER BY (sent_at DESC);
Why it matters In NoSQL one query maps to one table; duplication is the price of single partition reads.

Module 44 of 84, phase 05

An endless warehouse for files

Blob and object storage

Object storage such as Amazon S3 keeps files of any size behind a key, with eleven nines of durability and no servers to manage. Videos, images, backups and logs live here, not in databases.

In detail

Databases store metadata (owner, size, key) while bytes go to object storage. Clients upload directly with presigned URLs so files never pass through app servers. Large files use multipart uploads, which also enables resume. Lifecycle rules move old data to cheaper tiers. Object storage plus a CDN in front is the standard way to serve user generated media.

presign.ts
TypeScript
const command = new PutObjectCommand({ Bucket: BUCKET, Key: key, ContentType: contentType });const url = await getSignedUrl(s3, command, { expiresIn: 300 });
Why it matters Presigned uploads remove the biggest bandwidth cost from your app servers entirely.

Module 45 of 84, phase 05

Finding words in billions of documents

Search and inverted indexes

Full text search uses an inverted index: a map from each word to the documents that contain it. Elasticsearch, OpenSearch and Lucene power search bars, logs and product catalogs.

In detail

Text is tokenised, lower cased and stemmed, then each term points to a posting list of document ids. Queries intersect posting lists and rank results with BM25, boosts and freshness. The index is a secondary, eventually consistent copy fed from the main database through change data capture or events, and it is sharded and replicated like any data store.

inverted-index.ts
TypeScript
for (const [docId, text] of docs) {	for (const term of tokenize(text)) {		index.get(term)!.add(docId);
Why it matters Posting list intersection is the core of every search engine; ranking and sharding are layered on top.

Phase 06, modules 46 to 52

Remember and relay

Caching and messaging

Keeping hot data close, and letting services talk without waiting on each other.

Module 46 of 84, phase 06

Keep the answer close

Where caches live

A cache stores a copy of data somewhere faster than its source. Caches sit at every layer: the browser, the CDN, the API server's memory, a shared Redis cluster and the database's own buffer pool.

In detail

Each layer trades freshness for speed. Browser and CDN caches remove whole requests; in process caches avoid a network hop but are per instance; a shared cache such as Redis or Memcached is consistent across instances and survives deploys. Measure hit ratio: at 95% hits, the database only sees one request in twenty. Hot keys, cold starts after a flush and memory limits are the classic problems.

Each layer only forwards its missesClientEdgeServiceCacheData
missmiss12BrowserClientCDNEdgeApp + local cacheServiceRedisCacheDatabaseData

Back of the envelope

  • L1 hitabout 1 nsCPU cache
  • Local memoryabout 100 nsin process
  • Redis round tripabout 0.5 mssame zone
  • Database read1 to 10 msindexed
layers.ts
TypeScript
if (hit && hit.expiresAt > Date.now()) return hit.value; // ~100 nslet value = await this.redis.get(key); // ~0.5 msvalue = await this.db.load(key); // ~5 ms
Why it matters A short local TTL absorbs hot keys without letting instances drift apart for long.

Module 47 of 84, phase 06

Who fills the shelf, and when

Cache strategies and invalidation

Cache aside, read through, write through, write back and write around decide who loads and updates the cache. Invalidation and expiry decide how stale data may get.

In detail

Cache aside is the default: the app reads the cache, falls back to the database and fills the cache. Write through updates cache and database together; write back writes to the cache and flushes later, fast but risky. Expire with TTLs plus jitter, delete keys on writes, and protect against stampedes with request coalescing or a short lock. Phil Karlton's joke stands: cache invalidation is one of the two hard things.

Cache read and write strategies
StrategyReadsWritesStrengthRisk
Cache asideApp checks cache, loads on missApp writes DB, deletes keySimple, resilientFirst read is slow
Read throughCache loads on miss itselfSeparateApp code stays cleanCache must know the DB
Write throughFrom cacheCache and DB togetherCache always freshSlower writes
Write backFrom cacheCache now, DB laterVery fast writesData loss if cache dies
Write aroundCache asideDB onlyNo cache churn on writesRecent writes miss the cache
cache-aside.ts
TypeScript
const cached = await cache.get(key);const value = await db.fetchUser(userId);const ttl = 300 + Math.floor(Math.random() * 61); // jitterawait cache.set(key, JSON.stringify(value), "EX", ttl);
Why it matters Deleting instead of updating the cached value avoids a race where an older write overwrites a newer one.

Module 48 of 84, phase 06

Drop it in the tray

Message queues

A message queue lets a producer hand off work and move on while consumers process it at their own pace. It absorbs spikes, retries failures and decouples services.

In detail

Producers publish messages; the broker stores them durably; consumers pull, process and acknowledge. Unacknowledged messages are redelivered, so consumers must be idempotent. Messages that keep failing go to a dead letter queue. Delivery is usually at least once; exactly once is approximated with idempotency keys. RabbitMQ, Amazon SQS and Kafka are common choices, Kafka being a log rather than a classic queue.

Work waits safely until a worker is freeServiceQueueExternal
publishpullpullafter 5 failsProducerServiceQueueQueueWorker 1ServiceWorker 2ServiceDead lettersExternal
worker.ts
TypeScript
const batch = await queue.receive({ maxMessages: 10, waitSeconds: 20 }); // long pollingfor (const msg of batch) {		await handle(msg.body); // must be idempotent		await msg.ack();
Why it matters Acknowledging only after success gives at least once delivery; the dead letter queue stops one bad message from blocking the rest.

Module 49 of 84, phase 06

Shout once, many hear

Publish subscribe and logs

In publish subscribe, a message goes to every interested subscriber, not just one worker. A log based system such as Kafka keeps messages in an ordered, replayable log split into partitions.

In detail

Topics fan a message out to many independent consumer groups: billing, email and analytics each get every order event. Kafka partitions a topic by key, keeps order within a partition, stores messages for days and lets each consumer group track its own offset, so a new service can replay history. Throughput comes from partitions; ordering only holds within one.

One event, every subscriber gets a copyServiceQueue
publishOrder serviceServiceorders topicQueueBillingServiceEmailServiceAnalyticsService
kafka-like.ts
TypeScript
const p = hash(key) % this.parts.length; // same key, same partition, orderedthis.parts[p].push(event);
Why it matters Each consumer group keeps its own offset, which is why adding a new subscriber never disturbs the existing ones.

Module 50 of 84, phase 06

React to what happened

Event driven architecture, CQRS and event sourcing

In an event driven system, services announce facts such as OrderPlaced and others react. Event sourcing stores those facts as the source of truth; CQRS keeps separate models for writing and reading.

In detail

Events decouple teams: the order service does not know who listens. The outbox pattern writes the event in the same database transaction as the state change, then a relay publishes it, so you never lose or invent events. Event sourcing rebuilds state by replaying events, giving a full audit trail at the cost of complexity. Choreography lets services react on their own; orchestration has one coordinator drive the flow.

outbox.sql
SQL
BEGIN; UPDATE orders SET status='paid' WHERE id=42;INSERT INTO outbox(topic, payload) VALUES ('order.paid', '{"id":42}'); COMMIT;
Why it matters Publishing straight to Kafka inside a request can lose the event if the process dies after the commit; the outbox cannot.

Module 51 of 84, phase 06

Buckets or a running tap

Batch and stream processing

Batch processing crunches a large, bounded dataset on a schedule. Stream processing handles an unbounded flow of events continuously, producing results in seconds.

In detail

MapReduce and Spark split a batch job across many machines: map transforms records, shuffle groups by key, reduce aggregates. Stream processors such as Flink and Kafka Streams keep state per key, group events into windows (tumbling, sliding, session) and deal with late events using watermarks. Many companies run both: streams for live dashboards, batch nightly to correct and backfill.

Batch and stream processing
AspectBatchStream
DataBounded, a day of logsUnbounded, events as they happen
LatencyMinutes to hoursMilliseconds to seconds
ToolsSpark, Hadoop MapReduce, dbtFlink, Kafka Streams, Spark Structured Streaming
CorrectnessEasy to recomputeLate and out of order events
Typical useReports, model training, backfillsFraud alerts, live dashboards, trending
wordcount.ts
TypeScript
const sorted = [...pairs].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); // the shufflefor (const [k, v] of sorted) {	out[k] = (out[k] ?? 0) + v;
Why it matters The shuffle, grouping every value for one key onto one machine, is what makes MapReduce scale and what makes it slow.

Module 52 of 84, phase 06

Pressing the lift button twice

Idempotency and delivery guarantees

An idempotent operation has the same effect whether it runs once or ten times. It is what makes retries safe in a world where networks drop replies.

In detail

Delivery is at most once (may lose), at least once (may duplicate) or effectively exactly once (at least once plus deduplication). Clients send an idempotency key; the server stores the first result for that key and returns it on repeats. Natural keys, upserts and conditional writes (version numbers) give idempotency without extra tables. Payments, orders and anything charged must be idempotent.

idempotent-api.ts
TypeScript
const saved = await store.get(idemKey);return saved.response; // replay the first answerconst response = await payments.charge(request.card, request.amount);await store.update(idemKey, { status: "done", request, response }, { ttl: 86_400 });return response;
Why it matters Checking that a reused key carries the same body catches client bugs that would otherwise charge the wrong amount silently.

Phase 07, modules 53 to 60

Many machines, one truth

Distributed systems

Time, agreement, transactions across services, scaling out, regions and the patterns that stop one failure becoming many.

Module 53 of 84, phase 07

Whose watch is right

Time, clocks and ordering

Machines' clocks drift, so wall clock time cannot reliably order events across servers. Logical clocks order events by cause instead of by time.

In detail

NTP keeps clocks within milliseconds, but leap seconds, pauses and drift break assumptions. Lamport clocks give a total order consistent with causality; vector clocks detect concurrent updates, which Dynamo style stores use to keep conflicting versions. Google Spanner uses TrueTime, clocks with a known error bound, and waits out the uncertainty to give global ordering.

lamport.ts
TypeScript
tick(): number {	this.time += 1;	return this.time;}receive(remoteTime: number): number {	this.time = Math.max(this.time, remoteTime) + 1;
Why it matters If event x caused event y, x always has a smaller Lamport time; the reverse is not guaranteed, which is what vector clocks add.

Module 54 of 84, phase 07

Agreeing when some may not answer

Consensus, leader election and quorums

Consensus lets a group of machines agree on one value or one leader even when some fail. Raft and Paxos power etcd, ZooKeeper, Consul and the metadata of most distributed databases.

In detail

Raft elects a leader by majority vote; the leader appends commands to a replicated log and commits an entry once a majority has stored it. With five nodes, two can fail. Quorums generalise this: with N replicas, writing to W and reading from R where W + R > N guarantees a read sees the latest write. Split brain is prevented because only a majority can act.

Committed once 3 of 5 have the entryServiceData
appendappendappendappendLeaderServiceFollowerDataFollowerDataFollowerDataFollowerData
quorum.ts
TypeScript
function quorumOk(n: number, w: number, r: number): boolean {	return w + r > n; // every read overlaps the latest write}console.log(quorumOk(3, 2, 2)); // true: strong reads
Why it matters Odd cluster sizes are standard because a fourth node adds cost without surviving any more failures than three.

Module 55 of 84, phase 07

All of us or none of us

Distributed transactions and sagas

When one business action spans several services or databases, you need a way to keep them consistent. Two phase commit locks everyone until all agree; sagas use a chain of local transactions with compensating undo steps.

In detail

Two phase commit has a coordinator ask every participant to prepare, then commit; it is strongly consistent but blocks if the coordinator dies. Sagas suit microservices: book flight, then hotel, then car, and if the car fails, cancel the hotel and flight. Sagas are eventually consistent and every step needs an idempotent compensation. Orchestrated sagas have a central coordinator; choreographed ones react to events.

saga.ts
TypeScript
for (const step of steps) {await step.do(ctx);} catch (error) {for (const finished of done.reverse()) {await finished.undo(ctx); // must be idempotent and retriedthrow new SagaFailed(step.name, { cause: error });
Why it matters Compensations are business actions, not database rollbacks: a cancelled hotel may still email the guest, so design them deliberately.

Module 56 of 84, phase 07

Finding a service that keeps moving

Service discovery and configuration

In a cluster, instances start, die and move constantly. Service discovery keeps a live registry of healthy addresses so callers find each other without hard coded IPs.

In detail

Client side discovery has callers query a registry such as Consul or etcd and pick an instance; server side discovery puts a load balancer in front. Kubernetes Services and DNS do this for you. Health checks remove dead instances, and a service mesh such as Istio or Linkerd adds retries, timeouts and mutual TLS as sidecars. The same consensus backed stores also hold feature flags and dynamic config.

registry.ts
TypeScript
reg.heartbeat("orders", "10.0.3.17:8080");const addrs = reg.lookup("orders");console.log(addrs[Math.floor(Math.random() * addrs.length)]);
Why it matters TTL based registration means a crashed instance removes itself; nobody has to remember to deregister it.

Module 57 of 84, phase 07

More hands when it gets busy

Autoscaling and capacity planning

Autoscaling adds instances when load rises and removes them when it falls, so you pay for what you use and survive spikes. Capacity planning decides the floor, ceiling and headroom.

In detail

Scale on a signal that predicts saturation: CPU, request rate, queue depth or p99 latency. Stateless services scale horizontally easily; stateful ones need sharding. New instances take time to boot, so keep headroom (often 30 to 50 percent), pre warm before known events, and use cooldowns to avoid flapping. Queue length is the best signal for workers.

Back of the envelope

  • Boot time30 s to 3 minplan headroom for it
  • Headroom30 to 50%above expected peak
  • Cooldown3 to 5 minavoids flapping
  • Scale signalqueue depthfor workers
scaler.ts
TypeScript
const want = Math.ceil(current * (metric / target));
Why it matters Little's law turns a traffic forecast into a worker count before anything is built.

Module 58 of 84, phase 07

One region is a single point of failure

Multi region and disaster recovery

Running in several regions cuts latency for distant users and survives a whole region failing. It is also where consistency, cost and complexity peak.

In detail

Active passive keeps a warm standby region and fails over by DNS; active active serves traffic everywhere and must handle concurrent writes, often with per region ownership of data or conflict free replicated data types. Recovery point objective (how much data you may lose) and recovery time objective (how long you may be down) drive the design. Rehearse failovers; untested ones fail.

Traffic follows health; data follows replicationClientEdgeService
nearestfailoverreplicateUsersClientGeo DNSEdgeRegion AServiceRegion BService
failover.yaml
YAML
primary: ap-south-1   secondary: eu-west-1rpo: 1m   rto: 15m   dns_ttl: 60
Why it matters Writing the RPO and RTO down turns vague hopes into numbers you can test and budget for.

Module 59 of 84, phase 07

One house or a street of houses

Monoliths, microservices and modular design

A monolith is one deployable unit; microservices split a system into independently deployed services that own their data. Neither is better by default; team size and change rate decide.

In detail

Monoliths are simpler to build, test and run, and a modular monolith with clear boundaries gets most of the benefit. Microservices let teams deploy independently and scale parts separately, but add network failures, distributed data, versioning and heavy operations. Split along business capabilities, not technical layers, and avoid a distributed monolith where every change touches five services.

Monolith, modular monolith and microservices
AspectMonolithModular monolithMicroservices
DeployAll at onceAll at onceEach service alone
DataOne databaseOne database, owned schemasDatabase per service
CallsIn processIn process through interfacesOver the network
OperationsSimpleSimpleHeavy: tracing, discovery, CI per service
Team sizeOne teamA few teamsMany independent teams
boundaries.md
Notes
Split by capability: Orders | Payments | Catalog | ShippingEach owns its database; talk via APIs and events
Why it matters Shared databases between services are the clearest sign of a distributed monolith.

Module 60 of 84, phase 07

Fail small, not big

Timeouts, retries, circuit breakers and bulkheads

Resilience patterns stop one slow or broken dependency from dragging the whole system down: timeouts bound waiting, retries with backoff ride out blips, circuit breakers stop hammering a dead service, bulkheads isolate resources.

In detail

Every network call needs a timeout. Retry only idempotent calls, with exponential backoff and jitter, and cap total retries to avoid retry storms. A circuit breaker opens after repeated failures, fails fast for a while, then lets a trial request through. Bulkheads give each dependency its own pool of threads or connections. Graceful degradation serves a simpler response, such as cached recommendations, rather than an error.

breaker.ts
TypeScript
if (this.openedAt !== null && Date.now() - this.openedAt < this.cooldownMs) {	return fallback(); // open: fail fast}try {	const result = await fn(); // closed, or half open trial} catch {	this.fails += 1;
Why it matters Full jitter spreads retries randomly so thousands of clients do not all retry at the same instant.

Phase 08, modules 61 to 63

Keep it running

Observability, security and delivery

Seeing inside a live system, protecting it, and shipping changes without outages.

Module 61 of 84, phase 08

Seeing inside a running system

Logs, metrics, traces and SLOs

Observability is being able to ask new questions about a live system. Metrics show trends, logs explain single events, and traces follow one request across many services.

In detail

Track the four golden signals per service: latency, traffic, errors and saturation. Use RED (rate, errors, duration) for services and USE (utilisation, saturation, errors) for resources. Distributed tracing with OpenTelemetry propagates a trace id through every hop. Define service level objectives, such as 99.9% of requests under 300 ms, and alert on error budget burn rather than on every spike.

  • LatencyHow long requests take, at p50, p95 and p99.
  • TrafficRequests per second, messages per second.
  • ErrorsRate of failed requests, including wrong answers.
  • SaturationHow full the system is: CPU, memory, queue depth.
slo.ts
TypeScript
return (1 - slo) * totalRequests;const allowed = (1 - slo) * requestsInWindow;return allowed ? errorsInWindow / allowed : Number.POSITIVE_INFINITY;
Why it matters Alerting on burn rate pages for real user pain and ignores harmless blips, which keeps on call humane.

Module 62 of 84, phase 08

Locks on every door

Security: authentication, authorization and encryption

Security design covers who you are (authentication), what you may do (authorization), keeping data secret in transit and at rest (encryption), and limiting damage when something leaks.

In detail

Use TLS everywhere, including between services, and encrypt data at rest with keys in a key management service. OAuth 2.0 and OpenID Connect handle login; short lived JWTs or opaque tokens carry identity; every service checks authorization per request. Apply least privilege, rotate secrets, validate input, rate limit public endpoints and log security events. Assume breach: segment networks and keep blast radius small.

authz.ts
TypeScript
const expected = createHmac("sha256", secret).update(tokenBody).digest("hex");return a.length === b.length && timingSafeEqual(a, b); // constant timeif (!(claims.scopes ?? []).includes(action)) {if (resource.tenant_id !== claims.tenant_id) {
Why it matters Checking the tenant on every object read prevents the most common API breach: guessing another customer's id.

Module 63 of 84, phase 08

Changing the engine mid flight

Deployments: blue green, canary and feature flags

Safe delivery means small changes, released gradually, with a fast way back. Blue green swaps whole environments; canaries send a small slice of traffic to the new version first; feature flags decouple deploying code from releasing features.

In detail

Watch the canary's error rate and latency against the baseline and roll back automatically if they regress. Database changes use expand and contract: add new columns, write to both, backfill, switch reads, then remove the old ones, so every step is backward compatible. Most outages start with a change, so deploy frequency and rollback speed are reliability features.

canary.ts
TypeScript
for (const pct of STAGES) {	await lb.setWeight(version, pct);	await sleep(soakMinutes * 60_000);	if (canary.errorRate > base.errorRate * 1.5 || canary.p99 > base.p99 * 1.2) {		await lb.setWeight(version, 0); // instant rollback		throw new RolloutAborted(`regressed at ${pct}%`);
Why it matters Comparing the canary to the live baseline, not to a fixed number, avoids false alarms during normal daily traffic swings.

Phase 09, modules 64 to 70

Classic designs, part one

Bitly, rate limiter, notifications, feeds, chat, search and crawling

Seven interview favourites, each worked from requirements and numbers to a diagram, a data model and the hard part.

Module 64 of 84, phase 09

Long links, short codes

Design a URL shortener like Bitly

Turn a long URL into a seven character code and redirect anyone who opens it, billions of times a month, in a few milliseconds.

In detail

It is read heavy, about 100 redirects per link created. Generate codes from a unique 64 bit id encoded in base62 (no collisions, no lookups) or by hashing with collision checks. Store code to URL in a key value store or a sharded SQL table keyed by code. Redirects hit a cache first; most traffic goes to a small set of popular links. Use 301 for cacheable permanent links or 302 when you need every click counted, and push click events to a queue for analytics rather than writing them on the redirect path.

Redirects read the cache first; clicks go asyncClientEdgeServiceCacheDataExternalQueue
GET /aB3x9Q1 lookup2 on misson createclick eventClientClientLoad balancerEdgeLink serviceServiceRedis cacheCacheLink storeDataID generatorExternalClick queueQueue

Back of the envelope

  • New links100 M per monthabout 40 per second
  • Redirects10 B per monthabout 4,000 per second, 20k peak
  • Storageabout 500 bytes per link6 TB over 10 years
  • Code length7 base62 chars3.5 trillion codes
shortener.ts
TypeScript
const code = alias ?? base62(this.ids.next()); // snowflake style idif (!(await this.store.putIfAbsent(code, longUrl))) throw new Error("alias taken");return `https://sho.rt/${code}`;
Why it matters Counter based ids encoded in base62 guarantee uniqueness, so creation never needs a read to check for collisions.

Module 65 of 84, phase 09

Not everyone through at once

Design a distributed rate limiter

Limit how many requests each user, key or IP can make per window, consistently across every server, adding under a millisecond to each request.

In detail

Run the limiter in the API gateway or as middleware backed by Redis, so all instances share counters. Token bucket allows short bursts; sliding window counter is accurate and cheap. Make each check atomic with a Lua script or INCR plus EXPIRE. Return 429 with Retry-After and rate limit headers. Decide to fail open (allow traffic if Redis is down) or closed (block) per endpoint. Keep local in memory pre checks for very hot keys.

Every gateway node shares one counterClientEdgeCacheService
requestcheckallowedClientsClientAPI gateway + limiterEdgeRedis countersCacheServicesService

Back of the envelope

  • Check latencyunder 1 msone Redis round trip
  • Keys1 per user per windowexpire automatically
  • Memoryabout 100 bytes per key10 M users is 1 GB
  • Response429 + Retry-Afterwhen over limit
limiter.lua
Lua
local n = redis.call("INCR", KEYS[1])if n == 1 then redis.call("EXPIRE", KEYS[1], ARGV[1]) endreturn n
Why it matters Running the read and the increment in one script removes the race where two servers both see 99 and both allow request 100.

Module 66 of 84, phase 09

The right message, once, on time

Design a notification system

Send push, SMS and email notifications for many products at millions per minute, respecting user preferences, rate limits and quiet hours, without duplicates.

In detail

Producers call one notification API with a user, template and data. The service checks preferences and limits, renders per channel, and enqueues to separate queues per channel so a slow SMS provider never delays push. Channel workers call APNs, FCM, an SMS gateway or an email provider with retries, and record delivery status. An idempotency key per notification stops duplicates on retry. Scheduled and digest notifications go through a delay queue.

One queue per channel keeps providers isolatedServiceDataQueueExternal
checkProduct servicesServiceNotification serviceServicePreferencesDataPush queueQueueSMS queueQueueEmail queueQueueAPNs / FCMExternalSMS gatewayExternalEmail providerExternal

Back of the envelope

  • Notifications10 M per daybursts of 1 M per minute
  • Push payloadunder 4 KBAPNs limit
  • Dedup window7 daysby notification id
  • Retryexponential, 24 h maxthen dead letter
notify.ts
TypeScript
if (!(await prefs.allows(event.userId, channel, event.template))) continue;if (!(await limiter.ok(event.userId, channel))) continue;const message = templates.render(event.template, channel, event.data);await queues[channel].send({ id: event.id, userId: event.userId, message });
Why it matters One queue per channel isolates providers, so an email outage cannot back up push notifications.

Module 67 of 84, phase 09

Everyone's timeline, instantly

Design a news feed like Twitter or Instagram

Show each user a ranked feed of posts from people they follow, in under 200 milliseconds, for hundreds of millions of users, including celebrities with millions of followers.

In detail

Fan out on write pushes each new post id into every follower's cached feed list, making reads instant but costly for celebrities. Fan out on read pulls from followees at read time, cheap to write but slow to read. The hybrid: fan out on write for normal users, merge celebrity posts at read time. Feeds hold only post ids in Redis lists; posts and media are fetched in bulk and cached separately; a ranking service orders candidates.

Push for most, pull for celebritiesClientServiceDataQueueCache
savenew postpush idsreadhydrateUser postsClientPost serviceServicePosts DBDataFan out queueQueueFan out workersServiceFeed cacheCacheFeed serviceService

Back of the envelope

  • Daily users300 Meach opens the feed 5 times
  • Feed readsabout 17k per second50k peak
  • New postsabout 6k per secondaverage
  • Feed cache800 ids per userabout 2.4 TB in Redis
fanout.ts
TypeScript
for await (const follower of graph.followers(post.author)) {	await feeds.lpush(`feed:${follower}`, post.id);	await feeds.ltrim(`feed:${follower}`, 0, FEED_LEN - 1);}
Why it matters The hybrid avoids writing one celebrity post into 50 million lists while keeping reads fast for everyone else.

Module 68 of 84, phase 09

Messages that arrive, in order, once

Design a chat app like WhatsApp or Slack

Deliver one to one and group messages in real time, keep them in order, show delivery and read receipts and presence, and sync history across devices.

In detail

Clients hold a WebSocket to a chat gateway; a session service maps users to gateway servers. A message is stored first, then routed to the recipient's gateway or, if offline, a push notification. Per conversation sequence numbers keep order. Messages are stored in a wide column store such as Cassandra partitioned by conversation and sorted by time. Large groups use pub sub fan out. Presence uses heartbeats with a short TTL. End to end encryption keeps servers from reading content.

Store first, then deliver or pushClientEdgeServiceCacheDataExternal
WebSocketsendstorewhere is Bobdeliverif offlineAliceClientBobClientGateway 1EdgeGateway 2EdgeChat serviceServiceSessionsCacheMessagesDataPushExternal

Back of the envelope

  • Daily users500 M40 messages each
  • Messages20 B per dayabout 230k per second
  • Storageabout 100 bytes each2 TB per day
  • Connections50 M concurrentabout 50k per gateway box
schema.cql
SQL
PRIMARY KEY ((conversation_id, bucket), seq)) WITH CLUSTERING ORDER BY (seq DESC);
Why it matters Bucketing the partition by time stops a ten year old busy group chat from becoming one enormous, slow partition.

Module 69 of 84, phase 09

Finishing your sentence

Design search autocomplete

Suggest the top completions as someone types, within about 100 milliseconds per keystroke, ranked by popularity and freshness.

In detail

Precompute: a trie where each node stores its top k completions, built offline from query logs (batch) and refreshed with recent trends (stream). Serve the trie from memory, sharded by prefix range, with a CDN or browser cache for the most common short prefixes. Clients debounce keystrokes and cancel stale requests. Filter offensive suggestions at build time.

Built offline, served from memoryClientEdgeServiceCacheData
prefixmisshourlypublishBrowserClientCDN cacheEdgeSuggest serviceServiceTrie shardsCacheQuery logsDataTrie builderService

Back of the envelope

  • Queries10 B per dayabout 5 keystrokes each
  • Suggest QPSabout 500kper second at peak
  • Latency budgetunder 100 msper keystroke
  • Trie sizetens of GBsharded by prefix
trie-topk.ts
TypeScript
let node: TrieNode | undefined = root;for (const ch of prefix.toLowerCase()) {node = node.children.get(ch);return node.top.map(([, p]) => p); // O(length of prefix)
Why it matters Storing the answer at every node turns each keystroke into a walk of a few pointers instead of a search.

Module 70 of 84, phase 09

Reading the whole web politely

Design a web crawler

Download billions of pages, follow their links, avoid duplicates and traps, and never overload any single website.

In detail

A URL frontier holds what to fetch next, prioritised by importance and freshness and split into per host queues so politeness (one request per host every few seconds, robots.txt) is enforced. Fetchers download pages; parsers extract links and content; a seen URL set, often a Bloom filter, drops duplicates; content hashes catch mirrored pages. DNS results are cached. The system is a giant pipeline of queues and workers that can run for months.

A loop of queues and workersClientQueueServiceCacheData
politehtmlsavenew links?add newSeed URLsClientURL frontierQueueFetchersServiceParsersServiceSeen set (Bloom)CachePage storeData

Back of the envelope

  • Pages1 B per monthabout 400 per second
  • Page sizeabout 500 KB200 TB per month raw
  • Politeness1 request per host per 2 sper host queue
  • Seen URLs10 B entriesBloom filter about 12 GB
crawler.ts
TypeScript
const url = frontier.nextPolite();const html = await fetchPage(url); // respects robots.txtfor (const link of extract(html, url)) {	if (!seenUrls.mightContain(link)) {		seenUrls.add(link);		frontier.add(link);
Why it matters Per host queues are the heart of a crawler: they make politeness automatic while thousands of hosts are fetched in parallel.

Phase 10, modules 71 to 77

Classic designs, part two

Netflix, Instagram, Dropbox, Uber, Yelp, leaderboards and tickets

Media at planet scale, location, real time ranking and selling the last seat exactly once.

Module 71 of 84, phase 10

Press play for 200 million people

Design video streaming like Netflix or YouTube

Upload, transcode and stream video to millions of devices at once, starting playback in under two seconds and adapting quality to every connection.

In detail

Uploads land in object storage, then a pipeline splits each video into chunks and transcodes them in parallel into many resolutions and codecs. Players use adaptive bitrate streaming (HLS or DASH): a manifest lists segments at each quality and the player switches every few seconds based on bandwidth. Nearly all bytes come from a CDN; Netflix places its own Open Connect boxes inside ISPs and pre fills them overnight with what each region will watch. Metadata, search, recommendations and watch history are separate services.

Transcode once, serve from the edge foreverClientDataQueueServiceEdge
new videochunksrenditionspre fillABR streambrowseCreator uploadClientRaw storageDataTranscode queueQueueTranscodersServiceSegments storeDataCDN / Open ConnectEdgeViewersClientCatalog + recsService

Back of the envelope

  • Concurrent viewers50 M at peakevening prime time
  • Average bitrateabout 5 MbpsHD
  • Peak egressabout 250 Tbpswhy CDN is everything
  • Renditions10 to 30 per titlecodecs times resolutions
master.m3u8
Notes
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360360p/index.m3u8
Why it matters Because every rendition shares the same segment boundaries, the player can change quality mid stream without a stutter.

Module 72 of 84, phase 10

A billion photos, each in a blink

Design photo sharing like Instagram

Let users upload photos, follow others, and scroll a feed of images that load instantly worldwide, with likes and comments.

In detail

Clients upload directly to object storage with a pre signed URL, then a worker creates thumbnails and sizes. Metadata (post id, owner, caption, image keys) lives in a sharded database keyed by user id. Images are served through a CDN with long cache lifetimes, since a published photo never changes. The feed reuses the news feed design. Likes and counts use sharded counters and are eventually consistent.

Bytes go around the API, not through itClientServiceDataEdge
create postPUT imageeventliveoriginimagesMobile appClientPost APIServiceObject storageDataResize workersServiceMetadata DBDataCDNEdge

Back of the envelope

  • Uploads100 M photos per dayabout 1,200 per second
  • Stored per photoabout 3 MBall sizes
  • New storageabout 300 TB per dayobject storage
  • Read to writeabout 100 to 1CDN absorbs most
upload.ts
TypeScript
const uploadUrl = await storage.presignPut(key, { expiresIn: 300, maxBytes: 20_000_000 });return { postId, uploadUrl }; // the client uploads straight to storage
Why it matters Uploading straight to storage keeps multi megabyte image bytes off the API servers entirely.

Module 73 of 84, phase 10

The same folder on every device

Design file sync like Dropbox or Google Drive

Keep a folder identical across a user's devices and collaborators, uploading only what changed, working offline, and resolving conflicts sensibly.

In detail

Split files into content addressed chunks (around 4 MB, named by their hash). Uploading a file means sending only chunks the server does not already have, which gives deduplication and resumable uploads for free. A metadata service records each file version as a list of chunk hashes; a notification service tells other devices to pull changes. Conflicts produce a conflicted copy rather than silent loss.

Chunks move once; versions are just lists of hashesClientServiceData
new chunkscommit versionchangedpullfetch chunksLaptopClientPhoneClientMetadata serviceServiceBlock storeDataVersions DBDataChange notifierService

Back of the envelope

  • Users500 M100 M daily
  • Chunk size4 MBcontent addressed
  • Dedup savingsoften 30% or moresame files across users
  • Sync latencysecondsvia long poll or push
chunks.ts
TypeScript
const blocks: AsyncIterable<Buffer> = createReadStream(path, { highWaterMark: CHUNK });hashes.push(createHash("sha256").update(block).digest("hex"));for (const h of await server.missingChunks(hashes)) {	await blobs.put(h, await readChunk(path, hashes.indexOf(h)));
Why it matters Editing one paragraph of a large file re uploads one 4 MB chunk, not the whole file.

Module 74 of 84, phase 10

The nearest driver, right now

Design ride hailing like Uber

Match riders with nearby drivers within seconds, track millions of moving cars live, price trips and handle the trip lifecycle safely.

In detail

Drivers send GPS every few seconds to a location service that keeps the latest position in memory, indexed by geohash or H3 cells. A rider request queries nearby cells, ranks candidate drivers by ETA, and offers the trip to one driver at a time with a short timeout. Trip state lives in a strongly consistent store because double assignment is unacceptable. Surge pricing compares demand and supply per cell. Payments and receipts are asynchronous.

Index by cell, match by ETA, assign atomicallyClientServiceCacheData
GPS every 4 supdate cellrequestnearbyassignofferDriver appsClientRider appClientLocation serviceServiceGeo index (H3)CacheMatchingServiceTrip storeData

Back of the envelope

  • Active drivers5 Mupdates every 4 s
  • Location writesabout 1.25 M per secondkept in memory
  • Cell sizeabout 1 kmgeohash 6 or H3 res 8
  • Match timeunder 5 soffer timeout 10 s
match.ts
TypeScript
const nearby = [cell, ...neighbours(cell)].flatMap((c) => [...(index.get(c) ?? [])]);const free = nearby.filter((d) => drivers.get(d)?.status === "free");for (const driver of free.sort((a, b) => eta(a, rider) - eta(b, rider)).slice(0, 5)) {
Why it matters Why it matters compareAndSet on the driver's status guarantees one driver is never assigned to two riders, even if two matches race.

Module 75 of 84, phase 10

What is near me

Design a proximity service like Yelp or Google Maps places

Find businesses within a radius of a user, filtered and ranked, at very high read rates, where the data itself changes slowly.

In detail

Places rarely move, so precompute a spatial index: geohash prefixes, a quadtree or S2 cells. A search looks up the user's cell plus neighbours, filters by exact distance and category, then ranks by rating and distance. Read replicas and caches by cell handle the load; writes (new businesses, edits) go to the primary and are indexed within minutes. Reviews and photos are separate services.

Back of the envelope

  • Places200 Mchange slowly
  • Search QPSabout 5kread only path
  • Index sizea few GBfits in memory
  • Radius0.5 to 20 kmpick cell precision to match
nearby.sql
SQL
SELECT id, name FROM placesWHERE geohash6 IN (:cell, :n1, :n2, ...) AND ST_DWithin(geo, :point, 2000);
Why it matters The coarse cell narrows millions of rows to hundreds; only then does the precise distance calculation run.

Module 76 of 84, phase 10

Who is on top, live

Design a real time leaderboard

Rank millions of players by score, update instantly as games finish, and answer top 100 and what is my rank in milliseconds.

In detail

A Redis sorted set does exactly this: ZINCRBY updates a score in O(log n), ZREVRANGE returns the top k, ZREVRANK returns a player's rank. Keep one sorted set per leaderboard and season. For hundreds of millions of players, shard by score range or keep exact ranks for the top and approximate ranks elsewhere. Persist scores in a database as the source of truth and rebuild the set if Redis is lost.

Cost of each operation

  • Update scoreO(log n)
  • Top kO(log n + k)
  • Rank of playerO(log n)
  • Players around meO(log n + k)

Back of the envelope

  • Players50 Mone sorted set
  • Memoryabout 100 bytes eachabout 5 GB
  • Updates50k per secondsingle Redis handles it
  • Read latencyunder 1 mstop 100
leaderboard.ts
TypeScript
return this.r.zincrby(this.key, points, player); // O(log n)return this.r.zrevrange(this.key, 0, k - 1, "WITHSCORES");
Why it matters A sorted set is a skip list plus a hash map, so updates, rank lookups and range reads are all logarithmic.

Module 77 of 84, phase 10

Selling the last seat exactly once

Design ticket booking like BookMyShow or Ticketmaster

Let thousands of fans compete for the same seats at the moment sales open, without ever selling one seat twice, and keep the site up under the stampede.

In detail

Hold seats briefly: selecting a seat creates a reservation with a short expiry (five to ten minutes) using a conditional update or row lock, and payment confirms it. A virtual waiting room admits users at a controlled rate so the booking system sees a steady flow. Inventory for a show fits on one shard, which keeps transactions local. Seat maps are cached and refreshed often; the final check is always against the database.

Queue the crowd, hold, then confirmClientEdgeServiceDataExternal
spikesteady ratehold seatchargeconfirmFansClientWaiting roomEdgeBooking serviceServiceSeats DBDataPaymentsExternal

Back of the envelope

  • Fans at on sale2 Min the first minute
  • Seats60kone stadium show
  • Admit rateabout 2k per secondwaiting room
  • Hold time8 minutesthen released
hold.sql
SQL
UPDATE seats SET status='held', held_by=:user, hold_until=now()+interval '8 minutes'WHERE show_id=:show AND seat_no=:seat AND (status='free' OR hold_until < now());
Why it matters Putting the whole check inside one conditional UPDATE lets the database settle every race; no application lock is needed.

Phase 11, modules 78 to 82

Hard mode

Payments, key value stores, collaboration, monitoring and scheduling

Designs where correctness, consistency or sheer volume make every shortcut dangerous.

Module 78 of 84, phase 11

Never lose a rupee, never charge twice

Design a payment system

Accept payments through external processors, move money between accounts correctly, and reconcile everything, where correctness matters more than speed.

In detail

Every request carries an idempotency key. A payment service records intent, calls the payment service provider, and handles asynchronous webhooks for the final status. Money movements are recorded in a double entry ledger: every transaction writes balanced debit and credit rows, append only, never updated. Nightly reconciliation compares the ledger with the provider's settlement files. Retries, timeouts and unknown states are designed explicitly; an unknown is resolved by querying the provider, never by guessing.

Intent, provider, webhook, ledger, reconcileClientServiceExternalData
pay + idem keyauthorisestatusrecordintentnightly checkCheckoutClientPayment serviceServicePSP (Stripe, Razorpay)ExternalLedgerDataWebhooksServiceReconciliationService

Back of the envelope

  • Payments10 M per dayabout 115 per second
  • Peak10x averagesales days
  • Ledger rows2 to 4 per paymentappend only
  • Availability99.99%but correctness first
ledger.sql
SQL
INSERT INTO ledger(txn_id, account, amount) VALUES (:t, 'customer:42', -1999), (:t, 'merchant:7', 1999);   -- sums to zero
Why it matters Integers in minor units and a zero sum invariant make whole classes of rounding and lost money bugs impossible.

Module 79 of 84, phase 11

A giant, unbreakable dictionary

Design a distributed key value store like Dynamo

Store and retrieve values by key across hundreds of machines, staying available when nodes and networks fail, with tunable consistency.

In detail

Partition keys with consistent hashing and virtual nodes; replicate each key to N successors on the ring. Reads and writes use quorums (R and W) chosen per request. Conflicting versions are detected with vector clocks and resolved by the client or last writer wins. Hinted handoff covers temporarily down nodes, read repair and Merkle tree anti entropy heal replicas, and gossip spreads membership. Each node stores data in an LSM tree.

Each key belongs to the next node clockwise; adding a node only steals keys from its neighbour
BCDEAk1→Dk2→Bk3→Ck4→Ck5→Ck6→C

Back of the envelope

  • Nodeshundredsvirtual nodes 256 each
  • ReplicationN = 3across zones
  • Typical quorumW = 2, R = 2strong reads
  • Latencysingle digit msp99 under 10 ms
kv.ts
TypeScript
for (const node of this.ring.successors(key, this.n)) {	try {		node.write(key, value, clock);		acks += 1;
Why it matters Hinted handoff keeps writes available during an outage, and read repair quietly fixes stale replicas afterwards.

Module 80 of 84, phase 11

Many cursors, one document

Design collaborative editing like Google Docs

Let several people edit the same document at once, seeing each other's changes in real time, with no lost edits and the same final result for everyone.

In detail

Each keystroke becomes an operation (insert or delete at a position). Operational transformation adjusts concurrent operations against each other through a central server that orders them; CRDTs give every character a unique, ordered id so replicas merge without a central authority and work offline. Clients connect via WebSocket to a document session server, operations are appended to a log, and periodic snapshots make loading fast. Cursors and presence are ephemeral and not stored.

Back of the envelope

  • Editors per docup to about 100live
  • Ops per editora few per secondtyping
  • Snapshotevery few hundred opsfast load
  • Latencyunder 100 msto see others' typing
ot.ts
TypeScript
function transform(a: Insert, b: Insert): Insert {if (a.pos < b.pos || (a.pos === b.pos && a.site < b.site)) return a;return { ...a, pos: a.pos + b.text.length };
Why it matters Both orders converge to the same text; that convergence property is what every OT or CRDT design must prove.

Module 81 of 84, phase 11

Watching ten million time series

Design a metrics and monitoring system

Collect metrics from thousands of servers every few seconds, store them efficiently, query dashboards in milliseconds and fire alerts reliably.

In detail

Agents scrape or push metrics (name, labels, timestamp, value). A time series database such as Prometheus, VictoriaMetrics or M3 compresses points with delta of delta timestamps and XOR floats, achieving about 1 to 2 bytes per point. Data is downsampled with age: raw for days, 5 minute rollups for months. Label cardinality is the main cost driver. The alerting pipeline must be more reliable than the systems it watches, so it is often run separately.

Ingest, store compressed, query, alertClientEdgeQueueDataService
every 10 swriterulesqueryServers + agentsClientCollectorsEdgeIngest queueQueueTime series DBDataAlert managerServiceDashboardsClient

Back of the envelope

  • Series10 M activelabel cardinality
  • Samples1 M per second10 s scrape
  • Compressedabout 1.4 bytes per sampleGorilla encoding
  • Raw retention15 daysthen rollups
downsample.ts
TypeScript
const start = ts - (ts % bucketSeconds);buckets.set(start, [...(buckets.get(start) ?? []), value]);v.reduce((sum, x) => sum + x, 0) / v.length,
Why it matters Keeping min and max in rollups preserves spikes that a plain average would hide.

Module 82 of 84, phase 11

Run this, at that time, exactly once

Design a distributed job scheduler

Run millions of scheduled and delayed jobs (send this email in an hour, rebuild that report at midnight) on time, once, even when workers crash.

In detail

Store jobs with their next run time in a database indexed by that time. Scheduler nodes poll for due jobs in small batches using SELECT FOR UPDATE SKIP LOCKED or a leased claim, push them onto a queue, and workers execute them. A lease with a timeout returns stuck jobs; idempotent job handlers make the inevitable duplicate harmless. Time buckets or a delay queue (Redis sorted set by timestamp) suit very large volumes. Recurring jobs compute the next run after each execution.

Back of the envelope

  • Jobs100 M per dayabout 1,200 per second
  • Poll interval1 sbatches of 100
  • Lease5 minutesthen retried
  • Indexpartial on pendingstays small
claim.sql
SQL
SELECT id FROM jobs WHERE run_at <= now() AND status='pending'ORDER BY run_at LIMIT 100 FOR UPDATE SKIP LOCKED;
Why it matters SKIP LOCKED lets many schedulers poll the same table in parallel without ever handing the same job to two of them.

Phase 12, modules 83 to 84

Mastery

The interview playbook and the capstone

A repeatable way to run any design conversation, and one final system that uses every phase.

Module 83 of 84, phase 12

Forty five minutes, one clear story

The system design interview playbook

A repeatable script for any design question: clarify, estimate, sketch, detail, deep dive and wrap up, while thinking out loud and naming trade-offs.

In detail

Spend the first five minutes on requirements and the next five on numbers; they decide everything after. Draw the simplest design that works, then evolve it under load. Pick one or two deep dives that matter for this product (hot keys for a feed, double booking for tickets). Mention failure modes and what you would monitor. Common mistakes: jumping to Kafka and microservices before stating requirements, no numbers, and going silent while thinking.

45 minutes in total; the first ten decide whether the rest goes well.

script.md
Notes
"Before I design, can I confirm the core features and the scale?""Reads outnumber writes 100 to 1, so I'll cache redirects."
Why it matters Interviewers grade the reasoning they can hear, so narrating choices matters as much as the diagram.

Module 84 of 84, phase 12

Design a whole streaming platform

Capstone: an end to end system

Put every phase together: design a short video platform with uploads, a feed, chat, notifications, payments for creators and global delivery, then defend it.

Questions people ask

Short answers to the questions that come up most while preparing for system design, each linked to the module that covers it in depth.

Do I need to know data structures before system design?

Yes, at least the basics. Hash tables, trees, heaps, queues and graphs are the parts every database, cache and queue is built from, and interviewers expect you to reason about their costs.

Module 01: Why data structures matter for system design
How many designs should I practise?

Around fifteen classic ones done properly beats fifty skimmed. The URL shortener, news feed, chat, video streaming, ride sharing and payments cover most patterns.

Module 64: Design a URL shortener like Bitly
SQL or NoSQL in an interview?

Start with SQL unless the numbers or access patterns rule it out. Say why: transactions and joins versus massive scale with simple key based access.

Module 38: SQL versus NoSQL
When should I bring up Kafka or microservices?

Only after requirements and estimates show a need, such as fan out to many consumers or independent team scaling. Reaching for them first is a common red flag.

Module 30: Core trade-offs
What does CAP actually mean in practice?

During a network partition you choose between rejecting requests to stay consistent or serving possibly stale data to stay available. Most of the time you trade latency against consistency instead.

Module 28: CAP and PACELC
How precise do estimates need to be?

Within an order of magnitude. Round aggressively, state assumptions, and use the result to decide things such as whether data fits on one machine.

Module 23: Back of the envelope estimation
How do I handle a celebrity with millions of followers?

Use a hybrid feed: fan out on write for normal users, and merge celebrity posts at read time so one post never writes millions of rows.

Module 67: Design a news feed like Twitter or Instagram
How do systems avoid charging twice?

Idempotency keys on every payment request, an append only ledger, and resolving unknown states by asking the provider rather than retrying blindly.

Module 78: Design a payment system
Why does Netflix need its own CDN?

Video is most of the internet's traffic. Placing servers inside ISPs and pre filling them overnight cuts cost and keeps playback smooth at peak.

Module 71: Design video streaming like Netflix or YouTube
What is the difference between a queue and pub sub?

A queue gives each message to one worker; pub sub gives each message to every subscriber group. Kafka can do both through consumer groups.

Module 49: Publish subscribe and logs
How do I talk about failures without sounding pessimistic?

Name the single points of failure, how you would detect each one, and what degrades gracefully. Interviewers reward this.

Module 27: Reliability and fault tolerance
How is progress saved on this page?

In this browser only, in local storage. Nothing is sent anywhere.

Module 83: The system design interview playbook
What keyboard shortcuts does this page support?

T switches theme, F toggles full screen, M opens the menu, slash searches, J and K move between modules, and Esc closes the menu or leaves focus mode.

Module 00: The system design dictionary

Official documentation and sources

Every source linked from the modules above, grouped by the phase that uses it and then by where it lives. 220 links in total, all opening in a new tab.

The system design dictionary

Phase 00, Before you start

3

Complexity and data structures

Phase 01, Counting the cost

25

Hashing, limiting, caching and IDs

Phase 02, Algorithms that run the internet

22

System design fundamentals

Phase 03, Thinking like an architect

19

Networking, load balancing and APIs

Phase 04, The roads between machines

19

Databases and storage

Phase 05, Where data lives

23

Caching and messaging

Phase 06, Remember and relay

21

Distributed systems

Phase 07, Many machines, one truth

24

Observability, security and delivery

Phase 08, Keep it running

9

Bitly, rate limiter, notifications, feeds, chat, search and crawling

Phase 09, Classic designs, part one

18

Netflix, Instagram, Dropbox, Uber, Yelp, leaderboards and tickets

Phase 10, Classic designs, part two

19

Payments, key value stores, collaboration, monitoring and scheduling

Phase 11, Hard mode

15

The interview playbook and the capstone

Phase 12, Mastery

3

Credits

The technologies this roadmap teaches and the tools used to build the page. The people behind it are listed in the footer.

Keyboard shortcuts

Modules

←→
Previous, next module in focus mode
JK
Next, previous module
O
Focus on the current module
I
Show or hide the details
C
Collapse or expand the module
D
Mark the module as done

Page

M
Module menu
/
Search modules
T
Light or dark theme
F
Full screen
G
Back to the top

Help

?
Open this list
Esc
Close a dialog, the menu or focus mode

Shortcuts pause while you type in a search box.