Preface#
System design interviews are filled with jargon. Interviewers throw around terms like "CAP theorem," "eventual consistency," "idempotent," and "horizontal scaling" expecting you to not just know what they mean, but to reason about their tradeoffs.
Weak answer
"I'd use a database that's consistent."
Strong answer
"We need to decide what consistency level we need. For user balances, we need strong consistency — linearizable reads. For the activity feed, eventual consistency is fine — users can tolerate seeing posts a few seconds late. I'd use synchronous replication for the balance table and async replication for the feed, accepting the CAP tradeoff that during a partition, the balance service might become unavailable while the feed remains available with stale data."
This document is your glossary and theory guide — every term you'll encounter in system design, explained with examples and interview context.
The trade-offs (CAP during a partition, PACELC otherwise, ACID vs BASE), scaling (up vs out, sharding, consistent hashing that moves about 1/N of keys, quorum replication where R + W > N), data and messaging (cache stampedes, queue vs stream, at-least-once delivery with idempotent consumers, sagas with compensations) and reliability (N+1 redundancy, circuit breakers and a 99.9% SLO's 43.2-minute monthly error budget).
Part I: Fundamental Theorems and Trade-offs#
Chapter 1: CAP Theorem#
1.1 The Three Properties#
CAP Theorem (Brewer's Theorem) states that in a distributed system, you can only guarantee TWO of three properties:
C - CONSISTENCY Every read receives the most recent write or an error. All nodes see the same data at the same time.
A - AVAILABILITY Every request receives a response (not an error). The system is always operational.
P - PARTITION TOLERANCE The system continues to operate despite network partitions. Messages between nodes can be lost or delayed.
The Reality: You Don't "Choose 2 of 3"
In distributed systems, network partitions WILL happen. P is not optional — it's a given.
So the real choice is: During a partition, do you want:
- CP: Consistency (refuse requests until partition heals)
- AP: Availability (serve requests but may be stale/inconsistent)
A partition: answer or refuse?
A network partition cuts node B off from node A, and node A has just accepted a write. A client asks node B for data. Node B can refuse until it hears from A, or answer with what it has. Which should it do for a bank balance, and which for a user's profile bio?
One strong answer
- Bank balance: refuse (CP). An error or timeout is better than a wrong balance that leads to money moving twice.
- Profile bio: answer (AP). A bio that's a few seconds old does no harm, and an error page does.
- Partitions happen whether you plan for them or not. The real choice is what each kind of data does during one, and different services in one system can choose differently.
1.2 CAP in Practice#
Partition Scenario:
%% title: CAP partition scenario flowchart TD CW["Client writes<br/>value = 100"]:::client --> A["Node A"]:::core CR["Client reads<br/>value = ???"]:::client --> B["Node B"]:::core A x--x|"Network Partition"| B
CP System (Consistency + Partition Tolerance): Node B refuses to answer until it can confirm with Node A. Client gets an error or timeout. "Sorry, I can't guarantee this data is current."
Examples: ZooKeeper, etcd, HBase (when configured for consistency), Traditional RDBMS with synchronous replication
AP System (Availability + Partition Tolerance): Node B returns its last known value (might be stale). Client gets a response (possibly outdated). "Here's what I have, but it might not be the latest."
Examples: Cassandra (tunable, default AP), DynamoDB (tunable), CouchDB, DNS
A network partition cuts Node A off from Node B, and a client writes value = 100 to A. A read at B then either gets an error or timeout (CP: consistent, not available, like ZooKeeper or etcd) or B's last known, stale value (AP: available, not consistent, like Cassandra, DynamoDB or DNS).
CA System (Consistency + Availability): Only possible without partitions — meaning single node! Not relevant for distributed systems.
Examples: Single-node PostgreSQL, Single-node MySQL
1.3 Interview Explanation#
Strong answer:
"CAP theorem states that in a distributed system experiencing a network partition, you must choose between consistency and availability.
For our user profile service, I'd choose AP — availability over consistency. Here's why:
- User experience: Showing slightly stale profile data is better than showing an error.
- Nature of data: Profile updates are infrequent. If a user updates their bio and another user sees the old bio for a few seconds, it's acceptable.
- Conflict resolution: For concurrent updates, we can use last-write-wins with timestamps, or version vectors for more complex merging.
However, for the payment service, I'd choose CP — we cannot allow inconsistent account balances. Users would rather see an error than have their money disappear or duplicate.
The key is: different parts of the system can make different CAP trade-offs based on their requirements."
Chapter 2: PACELC Theorem#
Estimate it: a write that waits for every replica
There's no partition. A primary replicates every write synchronously to three followers, which confirm in 2 ms, 3 ms and 40 ms (assumed: the last one is far away). How long does each write wait for replication? What do you trade if the primary stops waiting?
One strong answer
- A synchronous write waits for all confirmations, so it's as slow as the slowest follower: ~40 ms every time.
- If the primary stops waiting, writes return almost at once, but a read from a follower may miss the latest write.
- That's a trade with no partition involved: latency against consistency, the "else" half of PACELC.
2.1 Extending CAP#
PACELC: What Happens When There's NO Partition?
CAP only addresses partition scenarios. PACELC extends it:
if (Partition) {
choose: Availability OR Consistency // Same as CAP
} else {
choose: Latency OR Consistency // New insight!
}| Letter | Stands for |
|---|---|
| P | Partition |
| A | Availability (partition) |
| C | Consistency (partition) |
| E | Else (no partition) |
| L | Latency (else) |
| C | Consistency (else) |
- Strong consistency requires coordination (adds latency)
- Low latency requires relaxed consistency
Example: Synchronous replication
- Write to primary
- Wait for ALL replicas to confirm
- Then return success to client
Consistent? Yes. Fast? No — you wait for the slowest replica.
2.2 PACELC Classifications#
| System | Partition (PA/PC) | Else (EL/EC) |
|---|---|---|
| DynamoDB | PA (Available) | EL (Low latency) |
| Cassandra | PA (Available) | EL (Low latency) |
| MongoDB | PA (Abadi's classification; majority write concern behaves PC) | EC (Consistent) |
| HBase | PC (Consistent) | EC (Consistent) |
| PNUTS (Yahoo) | PC (Consistent) | EL (Low latency) |
PA/EL Systems (DynamoDB, Cassandra):
- Always try to be fast and available
- Accept eventual consistency
- Great for: user sessions, product catalogs, social feeds
PC/EC Systems (HBase; MongoDB with majority reads/writes):
- Always try to be consistent
- Accept higher latency and potential unavailability
- Great for: financial data, inventory counts, user accounts
Chapter 3: ACID Properties#
3.1 The Four Properties#
ACID: Guarantees for Database Transactions
A - ATOMICITY All or nothing. Transaction either fully completes or fully rolls back. No partial updates.
Example: Transfer $100 from Account A to Account B
- Debit A: $100
- Credit B: $100 Both happen, or neither happens. Never just one.
C - CONSISTENCY Database moves from one valid state to another. All constraints, triggers, and rules are satisfied.
Example: Account balance cannot be negative
- If transfer would make balance negative, transaction fails
- Database never contains invalid data
I - ISOLATION Concurrent transactions don't interfere with each other. Each transaction sees a consistent snapshot.
Example: Two transfers happening simultaneously
- Transfer 1: A → B
- Transfer 2: A → C Each sees correct balance, neither corrupts the other.
D - DURABILITY Once committed, data survives crashes. Written to non-volatile storage.
Example: After "COMMIT" returns success
- Server can crash
- Power can fail
- Data is still there after restart
3.2 Isolation Levels#
Isolation Levels (Weakest to Strongest)
1. READ UNCOMMITTED Can read data that hasn't been committed yet.
Problem: Dirty Reads
- Transaction A updates row (not committed)
- Transaction B reads updated value
- Transaction A rolls back
- Transaction B has garbage data!
Use case: Almost never. Maybe analytics where accuracy doesn't matter.
2. READ COMMITTED Can only read committed data. But same query can return different results within transaction.
Problem: Non-Repeatable Reads
- Transaction A reads row (value = 100)
- Transaction B updates row to 200, commits
- Transaction A reads same row again (value = 200)
- Different values in same transaction!
Use case: Default in PostgreSQL, Oracle. Good for most applications.
3. REPEATABLE READ Same query always returns same result within transaction. But new rows can appear.
Problem: Phantom Reads
- Transaction A: SELECT * WHERE price < 100 (returns 10 rows)
- Transaction B: INSERT row with price = 50, commits
- Transaction A: SELECT * WHERE price < 100 (returns 11 rows!)
- New row appeared!
Use case: Default in MySQL. Financial reports within a transaction.
4. SERIALIZABLE Transactions execute as if they were serial (one after another). No anomalies possible.
Implementation: Locking or optimistic concurrency
Use case: Critical financial operations. Highest consistency, lowest throughput.
Comparison Table:
| Level | Dirty Read | Non-Repeatable | Phantom |
|---|---|---|---|
| Read Uncommitted | Possible | Possible | Possible |
| Read Committed | No | Possible | Possible |
| Repeatable Read | No | No | Possible |
| Serializable | No | No | No |
(These are the ANSI SQL definitions; PostgreSQL's and MySQL InnoDB's snapshot-based Repeatable Read also hide phantoms from plain reads.)
3.3 ACID Implementation#
How Databases Achieve ACID
ATOMICITY: Write-Ahead Logging (WAL)
- Write intention to log FIRST
- Then modify actual data
- On crash: replay log to recover
- Write
"BEGIN TX-123"to log - Write
"UPDATE accounts SET balance=900 WHERE id=A"to log - Write
"UPDATE accounts SET balance=1100 WHERE id=B"to log - Write
"COMMIT TX-123"to log - Apply changes to data files
Crash before COMMIT? Transaction never happened.
Crash after COMMIT? Replay log on recovery.
ISOLATION: Locking or MVCC
Locking (Pessimistic):
- Acquire locks before reading/writing
- Other transactions wait
- Simple but can cause contention
MVCC - Multi-Version Concurrency Control (snapshots instead of read locks):
- Keep multiple versions of each row
- Each transaction sees a snapshot taken at its start (per statement under Read Committed)
- Writers don't block readers
- Used by: PostgreSQL, MySQL InnoDB, Oracle
DURABILITY: fsync to disk
After COMMIT:
- Flush WAL to disk
- fsync() ensures data hits persistent storage
- Only then return success to client
Trade-off: fsync is slow (roughly 10-20ms on a 7,200 RPM HDD)
- Batch commits (group commit)
- Use fast storage (NVMe SSD)
Chapter 4: BASE Properties#
4.1 BASE vs ACID#
BASE: The Alternative to ACID
For distributed systems where ACID is too restrictive:
BA - BASICALLY AVAILABLE System guarantees availability (in CAP sense). Might return stale data, but always responds.
S - SOFT STATE State can change over time, even without input. Due to eventual consistency propagation.
E - EVENTUAL CONSISTENCY If no new updates, all replicas will eventually converge. No guarantee of when, but eventually consistent.
ACID vs BASE Comparison:
| ACID | BASE |
|---|---|
| Strong consistency | Eventual consistency |
| Pessimistic (lock early) | Optimistic (resolve later) |
| Complex transactions | Simple operations |
| Vertical scaling | Horizontal scaling |
| Traditional RDBMS | NoSQL databases |
When to Use Each:
ACID (Traditional databases):
- Financial transactions
- Inventory management
- User account management
- Any place where data integrity is critical
BASE (Distributed databases):
- Social media feeds
- Product catalogs
- Session stores
- Analytics data
- Any place where eventual consistency is acceptable
4.2 Eventual Consistency in Practice#
Example: Social Media Post
User posts status update in US data center:
- t=0ms — User clicks "Post"
- t=10ms — US data center saves post
- t=10ms — User sees their post (read-your-writes)
- t=50ms — Replication to EU data center starts
- t=150ms — EU data center receives post
During t=10ms to t=150ms: US users see the post; EU users don't see the post yet; system is inconsistent but available.
After t=150ms: All users see the post; system is eventually consistent.
Consistency Guarantees (Strongest to Weakest):
-
STRONG CONSISTENCY - After write completes, all reads see new value.
-
READ-YOUR-WRITES - You always see your own writes. Others might not see them yet.
-
MONOTONIC READS - Once you see a value, you never see older values. Time doesn't go backward for you.
-
MONOTONIC WRITES - Your writes are applied in order. Write A before Write B → A applied before B.
-
EVENTUAL CONSISTENCY - Eventually, all replicas converge. No timing guarantee.
Implementing Read-Your-Writes:
Option 1: Sticky sessions - Route user to same replica that handled their write.
Option 2: Read from primary - For user's own data, always read from primary.
Option 3: Version tracking - Client tracks version of last write. Only accept reads with version >= last write.
Chapter 5: Consistency Models#
5.1 Linearizability#
Linearizability (a.k.a. atomic consistency; not the same as strict serializability, which adds multi-object transactions)
The strongest consistency model. Operations appear to happen instantaneously at some point between invocation and response.
Every operation has a "linearization point" in real time.
| Time → | ||||
|---|---|---|---|---|
| Client A | write(x=1) |
|||
| Client B | read(x) |
|||
| Client C | write(x=2) |
|||
| Client D | read(x) |
Linearizable execution
write(x=1)completesread(x)returns 1 (must see the write)write(x=2)completesread(x)returns 2
If read(x) started after write(x=1) completed, it MUST return 1 or later value. Never 0.
Where It's Needed:
- Distributed locks (must see latest lock state)
- Leader election (must agree on leader)
- Unique ID generation (no duplicates)
Cost:
- Requires coordination
- Higher latency
- Lower throughput
5.2 Sequential Consistency#
Sequential Consistency
Weaker than linearizability. All operations appear in SOME sequential order. Order is consistent with program order for each process. But doesn't have to match real-time order.
- Client A:
write(x=1),write(x=2) - Client B:
read(x),read(x)
| Ordering | Valid? |
|---|---|
write(x=1) → write(x=2) → read(x)=2 → read(x)=2 |
ok |
write(x=1) → read(x)=1 → write(x=2) → read(x)=2 |
ok |
read(x)=0 → write(x=1) → write(x=2) → read(x)=2 |
ok |
write(x=1) → write(x=2) → read(x)=2 → read(x)=1 |
no — No legal order: A wrote 1 before 2, so B can't read 2 and then 1 |
write(x=2) → write(x=1) → ... |
no — Violates program order: A wrote 1 before 2 |
Difference from Linearizability:
- Linearizable: real-time ordering matters
- Sequential: only program order matters per process
5.3 Causal Consistency#
Causal Consistency
Operations that are causally related must be seen in order. Concurrent operations can be seen in any order.
Causality: A "happens before" B if:
- A and B are in same process, A before B
- A is a write, B is a read that sees A
- Transitive: A→B and B→C means A→C
Example: Social Media Comments
| Who | Action | Causality |
|---|---|---|
| Alice | posts "Hello!" | (A) |
| Bob | sees Alice's post | (causally after A) |
| Bob | comments "Hi Alice!" | (B, causally after A) |
| Carol | reads the thread |
Carol MUST see: Alice's post before Bob's comment (because B is causally dependent on A).
But two independent comments?
- Dave: comments "Nice!"
- Eve: comments "Great!"
Carol might see Dave before Eve, or Eve before Dave. These are concurrent, no causal relationship.
Implementation: Vector Clocks
Each node maintains vector of logical clocks: [Node1_time, Node2_time, Node3_time]
When sending message:
- Increment own clock
- Attach vector to message
When receiving:
- Update vector: max(local[i], received[i]) for each i
- Increment own clock
Compare vectors to determine causality:
- V1 < V2 if all V1[i] <= V2[i] and at least one V1[i] < V2[i]
- If neither V1 < V2 nor V2 < V1, they're concurrent
CAP, PACELC, ACID and consistency
Q1/4 A network partition cuts node B off from node A. A client asks B for an account balance. What does a CP design do?
Answer & explanation
Answer: C
CP gives up availability during a partition: B refuses rather than risk returning a wrong balance. Returning the last known value is the AP choice, fine for a profile bio but not for money.
Q2/4 There is no partition, but every write waits for all replicas to confirm, so p99 write latency is high. What does switching to async replication buy, and what does it cost?
Answer & explanation
Answer: B
This is PACELC's "else" branch: with no partition you still trade latency against consistency. Partition tolerance is not something you give up by tuning replication; partitions happen regardless.
Q3/4 A report transaction reads a row twice and gets 100, then 200, because another transaction committed in between. What is the weakest isolation level that prevents this?
Answer & explanation
Answer: A
That is a non-repeatable read, which Read Committed allows. Repeatable Read guarantees the same row returns the same value within a transaction. Read Committed only prevents reading uncommitted (dirty) data.
Q4/4 A user edits their bio, refreshes, and sees the old bio because the read hit a lagging replica. Which guarantee is missing, and what is one fix?
Answer & explanation
Answer: C
Read-your-writes means you always see your own updates, even if others briefly do not. Reading your own data from the primary (or tracking versions) provides it. Making every read linearizable would work, but it pays a global latency cost for a per-user problem.
Part II: Scaling Concepts#
Chapter 6: Scaling Strategies#
6.1 Vertical vs Horizontal Scaling#
VERTICAL SCALING (Scale Up)
Add more power to existing machine:
- More CPU cores
- More RAM
- Faster disks
- Better network
Before: 4 cores, 16GB RAM, 500GB HDD After: 64 cores, 512GB RAM, 4TB NVMe
Pros:
- Simple — no code changes
- No distributed system complexity
- ACID transactions easy
- Lower latency (no network hops)
Cons:
- Hardware limits (can't buy infinite CPU)
- Single point of failure
- Expensive (high-end hardware costs exponentially more)
- Downtime for upgrades
Use when:
- Starting out (don't prematurely distribute)
- Database with complex transactions
- Workload fits on one machine
HORIZONTAL SCALING (Scale Out)
Add more machines:
Before
Server handles all traffic
After
Load Balancer → S1, S2, S3
Pros:
- Near-infinite scaling
- Fault tolerance (one fails, others continue)
- Cost-effective (commodity hardware)
- No downtime for scaling
Cons:
- Distributed system complexity
- Data consistency challenges
- Network latency between nodes
- More operational overhead
Use when:
- Need high availability
- Workload exceeds single machine
- Traffic is unpredictable (auto-scale)
6.2 Stateless vs Stateful Services#
STATELESS SERVICES
Server holds no client-specific state between requests. All state is external (database, cache, client).
%% title: Stateless services
flowchart TD
R1["Request 1"]:::client --> SA@{ icon: "sd:server", label: "Server A" }
R2["Request 2"]:::client -->|"any server can handle"| SB@{ icon: "sd:server", label: "Server B" }
R3["Request 3"]:::client --> SC@{ icon: "sd:server", label: "Server C" }
SA & SB & SC --> DB@{ icon: "sd:database", label: "Database" }
class SA,SB,SC core
class DB storeCharacteristics:
- Any server can handle any request
- Easy to scale (add more servers)
- Easy to recover (restart server, no data lost)
- Load balancer can use round-robin
Examples: REST API servers, Web servers (serving static content), Serverless functions
STATEFUL SERVICES
Server holds state that's needed for subsequent requests.
%% title: Stateful services
flowchart TD
CA@{ icon: "sd:user", label: "Client A" } ==>|"must go to Server 1!"| S1@{ icon: "sd:server", label: "Server 1<br/>(has A's session)" }
CB@{ icon: "sd:user", label: "Client B" } --> S2@{ icon: "sd:server", label: "Server 2<br/>(has B's session)" }
class CA,CB client
class S1,S2 coreCharacteristics:
- Requests must route to specific server
- Harder to scale (session affinity needed)
- Failover is complex (state must be replicated)
Examples: WebSocket servers, Game servers, Database servers
Making Stateful Services Scalable:
Option 1: Externalize state - Store session in Redis/Memcached. Now servers are stateless.
Option 2: Sticky sessions - Load balancer routes same client to same server. Still have failover problems.
Option 3: State replication - Replicate state across servers. Complex but handles failover.
Option 4: Partition state - Each server owns subset of state. Route requests by partition key.
Chapter 7: Load Balancing#
7.1 Load Balancing Algorithms#
1. ROUND ROBIN Requests distributed in order: 1→A, 2→B, 3→C, 4→A, 5→B...
Pros: Simple, even distribution Cons: Ignores server capacity and current load Use: Homogeneous servers, similar request costs
2. WEIGHTED ROUND ROBIN Servers have weights: A(3), B(2), C(1) Distribution: A,A,A,B,B,C,A,A,A,B,B,C...
Pros: Accounts for different server capacities Cons: Static weights, doesn't adapt Use: Mixed hardware environments
3. LEAST CONNECTIONS Send to server with fewest active connections.
Pros: Adapts to actual load Cons: Doesn't account for request complexity Use: Long-lived connections, varying request times
4. WEIGHTED LEAST CONNECTIONS Least connections, but weighted by server capacity. Score = connections / weight Send to lowest score.
Use: Mixed hardware with varying connection times
5. IP HASH Hash client IP to determine server. Same client always goes to same server.
Pros: Session affinity without cookies Cons: Uneven if clients have different request rates Use: Stateful services, caching benefits
6. LEAST RESPONSE TIME Send to server with fastest recent response times.
Pros: Accounts for actual performance Cons: Needs continuous measurement Use: When response time is critical
7. RANDOM Randomly select a server.
Pros: Simple, stateless Cons: Can be uneven short-term Use: Large number of servers (law of large numbers)
7.2 Layer 4 vs Layer 7 Load Balancing#
LAYER 4 (Transport Layer) LOAD BALANCING
Works at TCP/UDP level. Sees: IP addresses, ports Doesn't see: HTTP headers, URLs, cookies
How it works:
- Client connects to load balancer IP
- LB picks backend server
- Forwards TCP connection (NAT or DSR)
- All packets in connection go to same server
Pros:
- Very fast (minimal processing)
- Protocol agnostic (works with any TCP/UDP)
- Low latency
Cons:
- Can't route based on content
- No SSL termination (typically; AWS NLB can terminate TLS)
- Limited health checks
Examples: AWS NLB, HAProxy (TCP mode), LVS
LAYER 7 (Application Layer) LOAD BALANCING
Works at HTTP/HTTPS level. Sees: URLs, headers, cookies, body
How it works:
- Client connects to load balancer
- LB terminates HTTP connection
- LB inspects request (URL, headers)
- LB opens new connection to chosen backend
- Forwards request, returns response
Pros:
- Content-based routing (URL, header)
- SSL termination (offload from backends)
- Request modification (add headers)
- Better health checks (check HTTP response)
- Caching, compression, rate limiting
Cons:
- More CPU intensive
- Higher latency
- Must understand protocol
Examples: AWS ALB, nginx, HAProxy (HTTP mode), Envoy
Choosing Between Them:
Use L4 when:
- Need raw performance
- Non-HTTP protocols (databases, custom protocols)
- SSL passthrough required
Use L7 when:
- Need content-based routing
- Want SSL termination
- Need request inspection/modification
- HTTP-specific features (websocket upgrade, gRPC)
Chapter 8: Sharding and Partitioning#
8.1 Partitioning Strategies#
HORIZONTAL PARTITIONING (Sharding)
Split rows across multiple databases. Each shard has same schema, different data.
Example: Users table with 100M rows
| Shard | Users |
|---|---|
| Shard 1 | Users 1 - 25M |
| Shard 2 | Users 25M - 50M |
| Shard 3 | Users 50M - 75M |
| Shard 4 | Users 75M - 100M |
1. RANGE PARTITIONING
Partition by value ranges.
| user_id range | Shard |
|---|---|
user_id 1-1M |
→ Shard A |
user_id 1M-2M |
→ Shard B |
user_id 2M-3M |
→ Shard C |
Pros:
- Range queries efficient (users 100-200 on same shard)
- Easy to understand
Cons:
- Hot spots (new users always hit latest shard)
- Uneven distribution if ranges are unequal
2. HASH PARTITIONING
Partition by hash of key.
- hash(1) % 4 = 1 → Shard B
- hash(2) % 4 = 2 → Shard C
- hash(3) % 4 = 3 → Shard D
- hash(4) % 4 = 0 → Shard A
Pros:
- Even distribution (if hash is good)
- No hot spots
Cons:
- Range queries hit all shards
- Adding shards requires rehashing (use consistent hashing!)
3. DIRECTORY-BASED PARTITIONING
Lookup table maps keys to shards.
| Key | Shard |
|---|---|
user_1 |
→ Shard A |
user_2 |
→ Shard B |
user_3 |
→ Shard A |
| ... |
Pros:
- Flexible placement
- Can move data easily
Cons:
- Lookup service is bottleneck/SPOF
- Extra hop for every query
4. GEOGRAPHIC PARTITIONING
Partition by location for data locality.
| Users | Data center |
|---|---|
| US users | → US data center |
| EU users | → EU data center |
| Asia users | → Asia data center |
Pros:
- Low latency for users
- Data sovereignty compliance
Cons:
- Complex for global data
- Cross-region queries expensive
What breaks when a cache cluster grows from 4 to 5 nodes?
A cache cluster places keys with hash(key) % N. Traffic grows, so you add a fifth node to the four. Roughly what share of keys now map to a different node? What does the database behind the cache see in the next few minutes?
One strong answer
- A key stays put only if
hash % 4equalshash % 5, which holds for just 4 of every 20 hash values. So ~80% of keys move. - Every moved key is a cache miss on its new node, so most reads fall through to the database at once, right when you were adding capacity.
- With a hash ring, the new node takes over only the keys between it and its neighbour: about 1/5 move, and the rest stay cached.
8.2 Consistent Hashing#
The Problem with Simple Hashing
Simple: shard = hash(key) % N
Add a server (N=4 → N=5):
- hash(key) % 4 = 2 → Shard C
- hash(key) % 5 = 3 → Shard D (different!)
Almost ALL keys move to different shards! Massive data migration, cache invalidation.
Consistent Hashing
Imagine a ring (0 to 2^32):
0
/|\
/ | \
/ | \
270 | 90
\ | /
\ | /
\|/
180Place servers on ring (by hashing server name):
- hash("server-a") = 45 → position 45
- hash("server-b") = 120 → position 120
- hash("server-c") = 200 → position 200
- hash("server-d") = 300 → position 300
Place keys on ring, assign to next server clockwise:
- hash("user-123") = 100 → server-b (next after 100 is 120)
- hash("user-456") = 250 → server-d (next after 250 is 300)
Adding a Server
Add server-e at position 150:
Before: Keys 120-200 → server-c After: Keys 120-150 → server-e Keys 150-200 → server-c
Only keys between server-b and server-e move! About 1/N of keys move instead of all keys.
Virtual Nodes
Problem: Servers might be unevenly distributed on ring.
Solution: Each physical server gets multiple positions.
| Server | Ring positions |
|---|---|
server-a |
→ positions 45, 160, 280 (3 virtual nodes) |
server-b |
→ positions 90, 200, 320 (3 virtual nodes) |
More even distribution, and bigger servers can have more virtual nodes.
Used By:
- Amazon Dynamo (the 2007 paper behind DynamoDB)
- Apache Cassandra
- Memcached clients
- Content Delivery Networks
Chapter 9: Replication#
9.1 Replication Strategies#
SINGLE-LEADER REPLICATION
One primary (leader), multiple secondaries (followers). Writes go to leader, replicate to followers. Reads can go to any node.
%% title: Single-leader replication
flowchart TD
C@{ icon: "sd:user", label: "Client" } -->|"writes"| L
subgraph RG["reads can go anywhere"]
L["Leader"]:::core
F1@{ icon: "sd:database", label: "Follower 1" }
F2@{ icon: "sd:database", label: "Follower 2" }
F3@{ icon: "sd:database", label: "Follower 3" }
end
L --> F1
L --> F2
L --> F3
class C client
class F1,F2,F3 serviceSYNCHRONOUS REPLICATION
Leader waits for followers to confirm before returning success.
Client → Leader → Followers (wait for ACK) → Success to Client
Pros:
- Strong consistency
- No data loss on leader failure
Cons:
- High latency (wait for slowest follower)
- Availability impact (follower down = writes blocked)
ASYNCHRONOUS REPLICATION
Leader returns success immediately, replicates in background.
%% title: Asynchronous replication flowchart TD C["Client"]:::client --> L["Leader"]:::core L -->|"Success to Client"| C L -.->|"async"| F["Followers"]:::service
Pros:
- Low latency
- High availability
Cons:
- Potential data loss (leader fails before replication)
- Followers may be behind (replication lag)
SEMI-SYNCHRONOUS REPLICATION
Wait for at least one follower to confirm. Leader confirms after 1+ followers ACK. Other followers replicate async.
Balance between consistency and performance. Used by: MySQL semi-sync replication
9.2 Multi-Leader Replication#
MULTI-LEADER (Multi-Master) REPLICATION
Multiple nodes accept writes. Changes replicate between all leaders.
%% title: Multi-leader replication flowchart LR LA["Leader A<br/>Reads + writes"]:::core <--> LB["Leader B<br/>Reads + writes"]:::core <--> LC["Leader C<br/>Reads + writes"]:::core
Use cases:
- Multi-datacenter (leader per datacenter)
- Offline-capable applications (each device is a leader)
- Collaborative editing
Conflict Resolution
What if two leaders modify same record simultaneously?
| Leader | Write |
|---|---|
| Leader A | UPDATE user SET name = 'Alice' |
| Leader B | UPDATE user SET name = 'Bob' |
Strategies:
1. LAST-WRITE-WINS (LWW) Timestamp each write, highest timestamp wins.
Simple but can lose data. Time sync issues (clock skew).
2. VERSION VECTORS Track version per node. Detect conflicts, let application resolve.
| User | Version vector | Write |
|---|---|---|
| User A | [A:1, B:0] |
name = 'Alice' |
| User B | [A:0, B:1] |
name = 'Bob' |
3. MERGE Combine both values somehow.
For shopping cart: union of items For counters: sum of increments For text: operational transformation
4. CUSTOM RESOLUTION Application-specific logic.
"Most recent login location wins" "Merge address fields, flag for review"
9.3 Leaderless Replication#
LEADERLESS REPLICATION
No single leader. Client writes to multiple nodes. Reads from multiple nodes, use quorum.
Client writes to N nodes; client reads from N nodes. R + W > N ensures overlap (quorum).
- N = 3 (total replicas)
- W = 2 (write to 2 nodes)
- R = 2 (read from 2 nodes)
- W + R = 4 > 3 → guaranteed to read latest write
Example: Write x=5, then read
- Write x=5 to nodes A and B (W=2)
- Node A: x=5
- Node B: x=5
- Node C: x=1 (stale, missed write)
- Read from nodes B and C (R=2)
- Get x=5 from B, x=1 from C
- Return x=5 (highest version)
Quorum Configuration
- R=1, W=N: Fast reads, slow writes; any one node down blocks writes
- R=N, W=1: Slow reads, fast writes; any one node down blocks reads, and a write is lost if its only node dies before propagating
- R=W=N/2+1: Balanced, typical configuration
N=3, R=2, W=2: Good balance N=5, R=3, W=3: More fault tolerant
Used By:
- Amazon Dynamo (DynamoDB's ancestor; DynamoDB itself uses a Multi-Paxos leader per partition)
- Apache Cassandra
- Riak
Sloppy Quorum and Hinted Handoff
If designated node is down, write to another node temporarily. "Hint" stored that data belongs elsewhere. When original node recovers, data transferred back.
Improves availability but weakens consistency guarantee.
Scaling, sharding and replication
Q1/4 Users are range-partitioned by an auto-increment
user_id. Which problem should you expect as the product grows?Answer & explanation
Answer: B
Range partitioning on a growing key sends all new writes to the latest range: a hot spot. Range queries hitting every shard is the downside of hash partitioning, not range.
Q2/4 You place keys with
hash(key) % Nand grow from 4 to 5 nodes. Why is consistent hashing better here?Answer & explanation
Answer: C
With modulo hashing, changing N changes the result for most keys, causing massive migration and cache misses. On a ring, only keys between the new node and its predecessor move. Some data still moves, and even spread needs virtual nodes.
Q3/4 Leaderless store with N = 3 replicas. Which setting guarantees that a read overlaps the latest successful write?
Answer & explanation
Answer: A
The read and write sets must overlap: R + W > N. Here 2 + 2 = 4 > 3. W = 2, R = 1 gives exactly 3, which is not greater than N, so the single node you read may be the one that missed the write.
Q4/4 With async replication, the leader acknowledges a write and then crashes before any follower receives it. A follower is promoted. What happens to that write?
Answer & explanation
Answer: C
Async replication returns success before followers have the data, so a leader crash in that window loses acknowledged writes. Avoiding this is the point of sync or semi-sync replication: wait for at least one follower first.
Part III: Data Concepts#
Chapter 10: Data Storage Patterns#
10.1 SQL vs NoSQL#
SQL (Relational) DATABASES
Structured data with fixed schema. Tables, rows, columns. SQL query language. ACID transactions.
Examples: PostgreSQL, MySQL, Oracle, SQL Server
Best for:
- Complex queries with JOINs
- Transactions across multiple tables
- Strong consistency requirements
- Well-defined schema
NoSQL DATABASES
Various data models, flexible schema. Optimized for specific access patterns.
Types:
1. KEY-VALUE STORES Simple: key → value Examples: Redis, DynamoDB, Memcached Use: Caching, session storage, simple lookups
2. DOCUMENT STORES Key → JSON/BSON document Examples: MongoDB, CouchDB, Firestore Use: Content management, catalogs, user profiles
3. COLUMN-FAMILY (Wide Column) Rows with dynamic columns, column families Examples: Cassandra, HBase, ScyllaDB Use: Time series, IoT, large-scale analytics
4. GRAPH DATABASES Nodes and relationships Examples: Neo4j, Amazon Neptune, JanusGraph Use: Social networks, recommendation, fraud detection
Comparison:
| Aspect | SQL | NoSQL |
|---|---|---|
| Schema | Fixed, defined | Flexible, dynamic |
| Scaling | Vertical (mainly) | Horizontal (designed for) |
| Consistency | Strong (ACID) | Eventual (BASE) typically |
| Query Language | SQL | Varies by database |
| JOINs | Native support | Usually application-side |
| Transactions | Multi-table | Usually single-document |
10.2 Indexing#
INDEX TYPES
1. B-TREE INDEX (Most Common)
Balanced tree structure. Good for: equality, range queries, sorting
Query: WHERE age > 25 AND age < 50
Without index: Scan all rows (O(n)) With B-tree: Navigate tree (O(log n), plus O(k) to scan the k matching rows)
2. HASH INDEX
Hash table structure. Good for: exact equality only
Query: WHERE id = 12345
Hash id → find bucket → get value (O(1))
Cannot do: range queries, sorting
3. INVERTED INDEX (Full-Text Search)
Maps words to documents containing them.
| Word | Documents |
|---|---|
| "hello" | → [doc1, doc3, doc7] |
| "world" | → [doc1, doc5] |
Query: "hello world" → Intersection: [doc1]
Used by: Elasticsearch, PostgreSQL full-text
4. BITMAP INDEX
Bit array for each value. Good for: low-cardinality columns
| Value | Bit array |
|---|---|
gender='M' |
1,0,1,1,0,1,0,0... |
gender='F' |
0,1,0,0,1,0,1,1... |
Fast for: COUNT, AND/OR operations
5. GEOSPATIAL INDEX (R-Tree, Quad-tree)
Indexes 2D/3D coordinates.
Query: Find all restaurants within 5km
Used by: PostGIS (R-tree via GiST); MongoDB and Redis use geohash-style cell indexes
COMPOSITE INDEX
Index on multiple columns. Order matters!
Efficient for
WHERE country = 'US'WHERE country = 'US' AND city = 'NYC'WHERE country = 'US' AND city = 'NYC' AND street = 'Broadway'
NOT efficient for
WHERE city = 'NYC'(leftmost column not used)WHERE country = 'US' AND street = 'Broadway'(gap in columns: only thecountryprefix is used,streetis filtered)
COVERING INDEX
Index contains all columns needed for query. No need to access table data.
INDEX (user_id, name, email)- Query:
SELECT name, email WHERE user_id = 123 - Index has all data → no table lookup → faster!
10.3 Write-Ahead Log and LSM Trees#
WRITE-AHEAD LOG (WAL)
Before modifying data, write to append-only log. Used for durability and crash recovery.
Write path:
- Append operation to WAL (sequential write, fast)
- fsync WAL to disk
- Apply to in-memory data structures
- Periodically flush to data files (checkpoint)
Recovery:
- Load last checkpoint
- Replay WAL from checkpoint
- System is consistent
LSM TREE (Log-Structured Merge Tree)
Optimized for write-heavy workloads. Used by: LevelDB, RocksDB, Cassandra, HBase
Structure:
%% title: LSM tree structure
flowchart TD
MT["MemTable<br/>(in-memory, sorted)"]:::cache -->|"flush when full"| L0[("Level 0 SSTables<br/>(Sorted String Tables on disk)")]:::store
L0 -->|"compaction merges files"| L1[("Level 1 SSTables")]:::store
L1 --> LN[("Level N SSTables")]:::storeWrite path:
- Write to MemTable (memory, fast)
- When MemTable full, flush to Level 0 SSTable
- Background compaction merges and sorts levels
Read path:
- Check MemTable
- Check each level (newest to oldest)
- Use bloom filters to skip files without key
Pros:
- Excellent write performance (sequential I/O)
- Compression friendly
Cons:
- Read amplification (check multiple levels)
- Write amplification (compaction rewrites data)
- Space amplification (data exists in multiple levels)
Chapter 11: Caching Concepts#
Page-view counters: which cache write pattern?
You count page views. Writes are extremely frequent, and losing a few increments in a crash is acceptable. Would you write each view to the database before replying, or let the cache take it and save it later? Would you make the same choice for account balances?
One strong answer
- Page views: write-behind. The write lands in the cache and returns immediately. A background process saves it to the database later, so the database never sees every single increment.
- The risk: anything not yet saved is lost if the cache fails. That's fine for view counts.
- Account balances: no. Use a pattern that writes to the database before replying (write-through, or write to the database and invalidate the cache). Losing a payment isn't acceptable.
11.1 Cache Patterns#
CACHING STRATEGIES
1. CACHE-ASIDE (Lazy Loading)
Application manages cache explicitly.
Read:
data = cache.get(key)
if data is None:
data = db.query(key)
cache.set(key, data)
return dataWrite:
db.update(key, data)
cache.delete(key) # InvalidatePros: Only requested data is cached Cons: Cache miss penalty, potential stale data
2. READ-THROUGH
Cache sits in front of database. Cache handles misses automatically.
Read:
data = cache.get(key) # Cache fetches from DB on missPros: Simpler application code Cons: Cache library must know how to query DB
3. WRITE-THROUGH
Writes go to cache, cache writes to database. Synchronous — returns after both succeed.
Write:
cache.set(key, data) # Also writes to DBPros: Cache always consistent with DB Cons: Higher write latency
4. WRITE-BEHIND (Write-Back)
Write to cache, asynchronously write to database.
Write:
cache.set(key, data) # Returns immediately
# Background process writes to DB laterPros: Very fast writes Cons: Data loss risk if cache fails before flush
5. REFRESH-AHEAD
Proactively refresh cache before expiration.
If item accessed and TTL < threshold: Async refresh in background
Pros: Reduces cache miss latency Cons: May refresh unused data
11.2 Cache Eviction Policies#
EVICTION POLICIES
When cache is full, which item to remove?
1. LRU (Least Recently Used)
Remove item that hasn't been accessed longest.
Access order: A, B, C, D, A, B Evict: C (least recently used)
Pros: Good general-purpose policy Cons: One-time scans pollute cache
Used by: Many caches (Memcached); Redis defaults to noeviction unless set to e.g. allkeys-lru
2. LFU (Least Frequently Used)
Remove item with fewest accesses.
Access counts: A(10), B(2), C(5), D(1) Evict: D (least frequent)
Pros: Keeps popular items Cons: Old popular items never evicted
Variant: LFU with aging (decay counts over time)
3. FIFO (First In, First Out)
Remove oldest item.
Insert order: A, B, C, D Evict: A (first inserted)
Pros: Simple to implement Cons: Doesn't consider access patterns
4. TTL (Time To Live)
Items expire after fixed time.
cache.set(key, value, ttl=300) # Expires in 5 minutesPros: Bounds staleness Cons: Popular items may expire unnecessarily
5. RANDOM
Randomly select item to evict.
Pros: Simple, no metadata overhead Cons: May evict important items
Real-World Combinations:
- Redis: Multiple policies (LRU, LFU, TTL, random)
- Memcached: LRU with slab allocation
- CPU caches: Pseudo-LRU (approximate for speed)
11.3 Cache Problems#
CACHE PROBLEMS AND SOLUTIONS
1. CACHE STAMPEDE (Thundering Herd)
Problem:
- Popular key expires
- 100 requests arrive simultaneously
- All miss cache, all query database
- Database overwhelmed
Solutions:
a) Locking: Only one request fetches, others wait
if not cache.get(key):
if cache.acquire_lock(key):
data = db.query()
cache.set(key, data)
cache.release_lock(key)
else:
wait_and_retry()b) Probabilistic early refresh: Refresh before expiration with some probability. Spreads refresh load over time.
2. CACHE PENETRATION
Problem:
- Query for non-existent key (e.g., user ID 999999999)
- Cache always misses
- Database queried every time
- Attacker can DoS with fake keys
Solutions:
a) Cache null values:
if db.query(key) is None:
cache.set(key, NULL, ttl=60)b) Bloom filter: Check bloom filter before database. If not in bloom filter, definitely doesn't exist.
3. CACHE BREAKDOWN
Problem:
- Single hot key expires
- Massive traffic hits database
Solutions: a) Never expire hot keys b) Background refresh before expiration c) Mutex lock (only one request refreshes)
4. HOT KEY
Problem:
- One key gets disproportionate traffic
- Single cache node overwhelmed
Solutions:
a) Local caching: Cache hot keys in application memory
b) Key replication: Store same key on multiple nodes key_1, key_2, key_3 all store same data Hash to different nodes
c) Read replicas: Multiple cache replicas for reads
Indexes, storage engines and caching
Q1/4 A table has
INDEX (country, city, street). Which query cannot use it efficiently?Answer & explanation
Answer: B
Composite indexes are ordered by their leftmost column first. Skipping
countrymeans the index cannot narrow the search. Any query that starts from the leftmost column, likecountryalone, can use it.Q2/4 Bots request millions of random, non-existent user IDs. Every one misses the cache and hits the database. What fixes this?
Answer & explanation
Answer: A
This is cache penetration: keys that do not exist are never cached, so every request reaches the DB. Caching nulls or a Bloom filter stops them early. TTL and eviction only affect keys that are already cached; these never were.
Q3/4 A very popular cache key expires and 100 requests miss at the same moment, all querying the database. What is the standard defense?
Answer & explanation
Answer: C
This is a cache stampede. A lock or probabilistic early refresh means one query rebuilds the value instead of 100. A shorter TTL makes the expiry, and so the stampede, happen more often.
Q4/4 Why do LSM-tree stores like Cassandra write fast but sometimes check several places on a read?
Answer & explanation
Answer: A
Buffering in memory and flushing sorted files makes writes sequential. The cost is read amplification across levels, which Bloom filters reduce. LSM stores still use a write-ahead log for durability; it is replayed on recovery, not on reads.
Part IV: Messaging Concepts#
Chapter 12: Message Queues and Streams#
12.1 Queue vs Stream#
MESSAGE QUEUE (e.g., RabbitMQ, SQS)
Message consumed by ONE consumer, then deleted.
%% title: Message queue flowchart LR P["Producer"]:::service --> Q[["Queue"]]:::queue --> C["Consumer"]:::core
- Messages: A, B, C, D
- Consumer 1 takes: A, C
- Consumer 2 takes: B, D
- After consumption, messages are gone.
Characteristics:
- Competing consumers (work distribution)
- At-least-once delivery (with acknowledgment)
- No replay (message deleted after consume)
- Good for: Task queues, work distribution
MESSAGE STREAM (e.g., Kafka, Kinesis)
Messages retained, multiple consumers can read.
%% title: Message stream flowchart LR P["Producer"]:::service --> S[["Stream/Log"]]:::queue S --> GA["Consumer Group A"]:::core S --> GB["Consumer Group B"]:::core
- Messages: A, B, C, D (stored with offsets)
- Consumer Group A: reads A, B, C, D at their pace
- Consumer Group B: reads A, B, C, D independently
- Messages retained for configured period.
Characteristics:
- Multiple consumer groups (fan-out)
- Replay capability (rewind to past offset)
- Ordered within partition
- Good for: Event sourcing, real-time analytics, audit logs
Comparison:
| Feature | Queue | Stream |
|---|---|---|
| Consumption | Destructive | Non-destructive |
| Consumer groups | Competing | Independent |
| Replay | No | Yes |
| Ordering | Per-queue | Per-partition |
| Retention | Until consumed | Time/size based |
| Use case | Task distribution | Event streaming |
12.2 Delivery Guarantees#
DELIVERY SEMANTICS
1. AT-MOST-ONCE
Message may be lost, never delivered twice.
Producer sends → No acknowledgment If lost, gone forever.
Use case: Metrics, logs where loss is acceptable Trade-off: Fastest, least reliable
2. AT-LEAST-ONCE
Message will be delivered, possibly multiple times.
Producer sends → Wait for ACK If no ACK, retry May deliver twice if ACK lost but message received.
Use case: Most applications (with idempotent consumers) Trade-off: Reliable but needs deduplication
3. EXACTLY-ONCE
Message delivered exactly once.
Very hard to achieve in distributed systems!
Approaches: a) Idempotent producer + idempotent consumer Producer retries with same ID Consumer tracks processed IDs, ignores duplicates
b) Transactional messaging Kafka transactions: atomic produce + consume
Use case: Financial transactions, inventory Trade-off: Most complex, some performance cost
Implementing Exactly-Once (Practical):
Producer side (idempotent):
message_id = generate_unique_id()
send(message_id, payload) # Retry with same IDConsumer side (idempotent):
if message_id in processed_set:
return # Already processed
process(message)
processed_set.add(message_id) # ideally in the same transaction as process()With transactional outbox:
BEGIN TRANSACTION
INSERT INTO outbox (message)
UPDATE business_table
COMMIT
-- Separate process reads outbox, publishes to queueChapter 13: Event-Driven Architecture#
13.1 Event Sourcing#
EVENT SOURCING
Store events, not current state. Current state = replay all events.
Traditional (State-based):
id=123, balance=1000, last_updated=2024-01-15
Problems:
- Lost history of how we got here
- Can't answer "what was balance on Jan 1?"
Event Sourcing:
AccountCreated(id=123, initial=0)Deposited(id=123, amount=500)Deposited(id=123, amount=700)Withdrawn(id=123, amount=200)
- Current balance: 0 + 500 + 700 - 200 = 1000
- Balance on Jan 1: Replay events up to Jan 1
Benefits:
- Complete audit trail
- Time travel (state at any point)
- Debugging (replay events to reproduce bugs)
- Event replay (rebuild read models, fix bugs)
Challenges:
- Event schema evolution
- Rebuild time for long event streams
- Snapshots needed for performance
CQRS (Command Query Responsibility Segregation)
Separate write model (events) from read model (projections).
%% title: CQRS
flowchart TD
CMD["Commands"]:::client --> WM[("Write Model<br/>(Event Store)")]:::store
WM -->|"project events"| RM[("Read Model<br/>(Optimized for queries)")]:::cache
QRY["Queries"]:::client --> RMWrite: Append event to event store Read: Query from denormalized read model
Read model can be:
- Relational database with joins pre-computed
- Search index (Elasticsearch)
- Cache (Redis)
- Multiple different views of same data
13.2 Saga Pattern#
SAGA PATTERN
Manage distributed transactions across services. Instead of ACID, use sequence of local transactions with compensations.
Example: Order Placement
- Order Service: Create order (PENDING)
- Payment Service: Charge customer
- Inventory Service: Reserve items
- Shipping Service: Schedule delivery
- Order Service: Mark order (CONFIRMED)
If step 3 fails (out of stock): Compensate step 2: Refund payment · Compensate step 1: Cancel order
Saga Execution Styles
1. CHOREOGRAPHY
Each service listens for events, decides what to do.
%% title: Saga choreography flowchart TD E1[["Order Created"]]:::queue -->|"Payment listens"| E2[["Payment Charged event"]]:::queue E2 -->|"Inventory listens"| E3[["Items Reserved event"]]:::queue E3 -->|"Shipping listens"| E4[["Delivery Scheduled event"]]:::ok
Pros: Decoupled, simple for few steps Cons: Hard to track, distributed logic
2. ORCHESTRATION
Central orchestrator controls the flow.
- Tell Order: Create order
- Tell Payment: Charge
- Tell Inventory: Reserve
- Tell Shipping: Schedule
- Tell Order: Confirm
If failure: Execute compensations in reverse order
Pros: Clear flow, easy to track/debug Cons: Orchestrator is potential bottleneck
Saga Compensations
Each action needs a compensation:
| Action | Compensation |
|---|---|
| Create order | Cancel order |
| Charge payment | Refund payment |
| Reserve inventory | Release inventory |
| Schedule shipping | Cancel shipping |
Compensations must be idempotent! May be called multiple times.
Queues, streams, delivery and sagas
Q1/3 A new fraud-detection service must process every order event from the last 30 days, while the existing billing consumer keeps going. Which fits?
Answer & explanation
Answer: B
Streams keep messages for a retention period, and each consumer group has its own offset, so the new service can replay history. A queue deletes messages once consumed, and competing consumers would split events between billing and fraud.
Q2/3 Your queue gives at-least-once delivery and a consumer charges credit cards. What must the consumer do?
Answer & explanation
Answer: B
At-least-once means a lost ACK causes redelivery, so the same message can arrive twice. Deduplicating by message ID prevents a double charge. At-most-once avoids duplicates by allowing loss, which is worse for payments.
Q3/3 In an order saga, payment succeeds, then inventory reservation fails because the item is out of stock. What happens next?
Answer & explanation
Answer: C
Sagas replace one ACID transaction with local steps plus compensating actions, run in reverse order on failure. There is no cross-service rollback; that is exactly the gap sagas fill. Compensations must also be idempotent, since they may run more than once.
Part V: Reliability Concepts#
Chapter 14: Fault Tolerance#
14.1 Failure Types#
FAILURE MODES
1. CRASH FAILURE
Node stops and doesn't respond.
Detection: Heartbeat timeout Recovery: Failover to replica
Example: Process killed, server power off
2. OMISSION FAILURE
Node fails to send or receive messages.
Send omission: Node doesn't send response Receive omission: Node doesn't receive request
Often indistinguishable from crash.
3. TIMING FAILURE
Response outside acceptable time window.
Example: Response takes 30 seconds instead of 100ms
For caller: Looks like failure Challenge: Did request succeed or not?
4. BYZANTINE FAILURE
Node behaves arbitrarily (including maliciously).
Example: Node sends different data to different nodes
Hardest to handle. Requires 3f+1 nodes to tolerate f failures.
Relevant for: Blockchain, hostile environments
Detection Methods:
Heartbeat:
- Node periodically sends "I'm alive" message.
- Missing heartbeats → suspected failure.
- Challenge: Network partition vs actual failure
Timeout:
- Request times out → suspected failure.
- Challenge: Slow vs failed
Lease:
- Node holds lease (timed permission).
- Must renew before expiration.
- No renewal → other nodes can take over.
- Used by: Distributed locks, leader election
14.2 Redundancy Patterns#
REDUNDANCY PATTERNS
1. ACTIVE-PASSIVE (Hot Standby)
Primary handles all traffic. Standby replicates data, takes over on failure.
%% title: Active-passive (hot standby) flowchart TD T["traffic"]:::client --> P["Primary<br/>(active)"]:::ok P <==>|"replication"| S["Standby<br/>(passive)"]:::service
Failover: Promote standby to primary
Pros: Simple, standby has consistent data Cons: Standby resources underutilized
2. ACTIVE-ACTIVE
Multiple nodes handle traffic simultaneously.
%% title: Active-active flowchart TD TA["traffic"]:::client --> A["Node A<br/>(active)"]:::ok TB["traffic"]:::client --> B["Node B<br/>(active)"]:::ok A <==>|"sync/async"| B
Pros: Better resource utilization, no failover needed Cons: Conflict resolution, data consistency
3. N+1 REDUNDANCY
N nodes handle normal load. +1 spare for failures.
Example: 3 nodes needed for traffic, deploy 4. One can fail without impact.
4. N+2 REDUNDANCY
One failure + one for maintenance.
Example: 3+2 = 5 nodes. Handles one failure while another is being updated.
5. 2N REDUNDANCY
Double everything.
Example: Need 3 nodes, deploy 6. Half can fail without impact.
Used for: Critical systems, disaster recovery
What breaks when a dependency hangs?
Your service calls a recommendations service, which starts hanging: every call waits the full 30-second timeout and then fails. Your service keeps calling it on every request. What happens to your service? What should it do instead?
One strong answer
- Each request ties up a thread (or connection) for 30 seconds. Soon all of them are waiting on the dead dependency, and your service stops answering even requests that don't need recommendations. The failure has spread upstream.
- Fail fast instead: after a threshold of failures, stop calling it and return an error or fallback straight away. Every so often, let one test request through to see if it has recovered.
- That's a circuit breaker. Giving each dependency its own pool (a bulkhead) also keeps one slow dependency from using up every thread.
14.3 Circuit Breaker Pattern#
CIRCUIT BREAKER
Prevent cascading failures by failing fast.
States:
- CLOSED: Normal operation, requests pass through
- OPEN: Failure threshold exceeded, requests fail immediately
- HALF-OPEN: Test if downstream recovered
State Machine:
%% title: Circuit breaker state machine stateDiagram-v2 direction LR HALF_OPEN: HALF-OPEN CLOSED --> OPEN: failures >= threshold OPEN --> HALF_OPEN: timeout expires HALF_OPEN --> CLOSED: test request succeeds HALF_OPEN --> OPEN: test request fails class CLOSED ok class OPEN bad class HALF_OPEN queue
Implementation:
class CircuitBreaker:
def __init__(self, threshold=5, timeout=30):
self.failure_count = 0
self.threshold = threshold
self.timeout = timeout
self.state = CLOSED
self.last_failure_time = None
def call(self, func):
if self.state == OPEN:
if time.monotonic() - self.last_failure_time > self.timeout:
self.state = HALF_OPEN
else:
raise CircuitOpenError()
try:
result = func()
self.on_success()
return result
except Exception as e:
self.on_failure()
raise
def on_success(self):
self.failure_count = 0
self.state = CLOSED
def on_failure(self):
self.failure_count += 1
self.last_failure_time = time.monotonic()
if self.failure_count >= self.threshold:
self.state = OPENBULKHEAD PATTERN
Isolate failures to prevent spreading. Like ship bulkheads that contain flooding.
%% title: Bulkhead pattern flowchart TD A["Service A"]:::core --> P1["Pool 1"]:::service --> X["Dependency X"]:::external A --> P2["Pool 2"]:::service --> Y["Dependency Y"]:::external
Pool 1 exhausted (X is slow)? Pool 2 still works (Y unaffected).
Implementation:
- Separate thread pools per dependency
- Separate connection pools
- Separate rate limits
Chapter 15: SLIs, SLOs, and SLAs#
15.1 Definitions#
SERVICE LEVEL TERMINOLOGY
SLI (Service Level Indicator) A metric that measures service behavior.
Examples:
- Request latency (p99 < 100ms)
- Error rate (errors / total requests)
- Availability (uptime / total time)
- Throughput (requests per second)
SLO (Service Level Objective) Target value for an SLI. Internal goal.
Examples:
- "99.9% of requests complete in < 100ms"
- "Error rate < 0.1%"
- "99.95% availability"
SLA (Service Level Agreement) Contract with consequences. External commitment.
Examples:
- "99.9% uptime or customer gets credit"
- "Response within 4 hours or penalty"
SLA should be less strict than SLO! (Buffer for unexpected issues)
Relationship:
%% title: SLI, SLO and SLA flowchart TD SLI["SLI<br/>#quot;Our p99 latency is 85ms#quot;"]:::service -->|"measures"| SLO["SLO<br/>#quot;We aim for p99 < 100ms#quot;"]:::core SLO -->|"commits"| SLA["SLA<br/>#quot;We guarantee p99 < 200ms#quot;<br/>(with penalties)"]:::hot
15.2 Error Budgets#
ERROR BUDGET
If SLO is 99.9% availability: Error budget = 0.1% downtime allowed
Per month (30 days): 0.1% × 30 × 24 × 60 = 43.2 minutes of downtime allowed
Error Budget Policy:
- Budget remaining → Ship fast, take risks
- Budget exhausted → Focus on reliability, freeze features
| Budget at | Mode |
|---|---|
| 80% | Normal development |
| 50% | Increased caution |
| 20% | Reliability focus |
| 0% | Feature freeze, fix reliability |
Calculating Error Budget:
Monthly error budget for 99.9% availability:
- Total minutes: 30 × 24 × 60 = 43,200 minutes
- Allowed downtime: 43,200 × 0.001 = 43.2 minutes
- Incidents this month:
- Deploy failure: 15 minutes
- Database issue: 10 minutes
- Budget used: 25 minutes
- Budget remaining: 18.2 minutes (42% left)
Monitoring Error Budget:
Track:
- Current error budget consumption rate
- Projected budget at end of period
- Alert when consumption rate too high
If burning budget faster than expected:
- Investigate
- Maybe pause risky deployments
- Focus on reliability
Fault tolerance and SLOs
Q1/3 Service A calls dependencies X and Y through one shared thread pool. X becomes very slow, and soon calls to Y fail too. What pattern prevents this?
Answer & explanation
Answer: A
Slow calls to X hold every thread in the shared pool, starving Y. Separate pools contain the damage to X's pool. Load balancing and redundancy spread or replace capacity, but the shared pool is still exhausted.
Q2/3 Your SLO is 99.9% monthly availability (43.2 minutes of downtime). You have used 25 minutes, and a 20-minute outage follows. What does the error-budget policy say?
Answer & explanation
Answer: B
25 + 20 = 45 minutes, more than the 43.2-minute budget. At 0% budget the policy is a feature freeze. 18.2 minutes was the balance before the second outage.
Q3/3 You need 3 nodes to carry peak load, and you patch nodes one at a time. You still want to survive a node failure during patching. How many nodes do you deploy?
Answer & explanation
Answer: B
N+2 covers one node down for maintenance plus one unexpected failure while keeping 3 serving. N+1 only covers one of those at a time; 2N works but costs more than this requirement needs.
Summary#
Key Concepts at a Glance#
THEOREMS AND TRADE-OFFS:
CAP Theorem:
- Consistency, Availability, Partition tolerance
- During partition: choose C or A
- CP: refuse requests if can't guarantee consistency
- AP: serve requests with possibly stale data
PACELC:
- Extends CAP: what about when no partition?
- EL: prioritize low latency
- EC: prioritize consistency
ACID (Databases):
- Atomicity: all or nothing
- Consistency: valid state to valid state
- Isolation: transactions don't interfere
- Durability: committed = permanent
BASE (Distributed):
- Basically Available
- Soft state
- Eventually consistent
CONSISTENCY MODELS (Strong to Weak):
Linearizability → Sequential → Causal → Eventual
SCALING:
- Vertical: bigger machine (simple, limited)
- Horizontal: more machines (complex, unlimited)
- Stateless: easy to scale, any server works
- Stateful: needs session affinity or externalized state
SHARDING:
- Range: predictable, hot spots possible
- Hash: even distribution, no range queries
- Consistent hashing: minimal redistribution on changes
REPLICATION:
- Single-leader: writes to one, reads from many
- Multi-leader: writes anywhere, conflict resolution needed
- Leaderless: quorum reads/writes (R + W > N)
- Sync: consistent but slow
- Async: fast but possible data loss
CACHING:
- Patterns: cache-aside, read-through, write-through, write-behind
- Eviction: LRU, LFU, TTL
- Problems: stampede, penetration, breakdown, hot keys
MESSAGING:
- Queue: one consumer, message deleted
- Stream: multiple consumers, replay possible
- Delivery: at-most-once, at-least-once, exactly-once
- Saga: distributed transactions with compensations
RELIABILITY:
- Circuit breaker: fail fast when downstream is down
- Bulkhead: isolate failures
- Redundancy: N+1, N+2, 2N
- SLI: metric (latency, error rate)
- SLO: target (99.9% availability)
- SLA: contract (penalties for breach)
- Error budget: allowed failure within SLO
Further Reading#
Books#
- Book
Designing Data-Intensive Applications by Martin Kleppmann
- The definitive guide to distributed systems concepts
- Book
Database Internals by Alex Petrov
- Deep dive into storage engines and distributed databases
- Book
Site Reliability Engineering by Google
- SLIs, SLOs, error budgets, and operational practices
Papers#
- Book
CAP Twelve Years Later by Eric Brewer
- Book
Harvest, Yield, and Scalable Tolerant Systems by Fox & Brewer
- Read
Dynamo: Amazon's Highly Available Key-value Store
- Read
Spanner: Google's Globally-Distributed Database
Online Resources#
- Book
Distributed Systems for Fun and Profit (free book)
- Read
jepsen.io - Distributed systems correctness testing
- Blog
Martin Kleppmann's Blog - https://martin.kleppmann.com/
- Blog
High Scalability Blog - http://highscalability.com/
Videos#
- Talk
MIT 6.824: Distributed Systems (free course)
- Talk
Martin Kleppmann's Conference Talks
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
Go deeper · reading trail
all reading →Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.
CAP Twelve Years Later: How the "Rules" Have Changed
Eric Brewer · 2012- What
- The CAP theorem's author revisits it twelve years on.
- Key idea
- 'Pick two of three' misleads: partitions are rare, so design for detecting them, operating in a degraded mode and recovering consistency afterwards.
- Why here
- The authoritative reading of the CAP chapter here, and the bridge to PACELC: the real trade-off is what you give up while partitioned.
Consistency Tradeoffs in Modern Distributed Database System Design
Daniel J. Abadi · 2012- What
- The paper that introduced PACELC as a refinement of CAP.
- Key idea
- If there is a Partition choose Availability or Consistency, Else choose Latency or Consistency; most systems pay the latency trade-off all the time.
- Why here
- The original source for the PACELC chapter, with Dynamo, Cassandra, PNUTS and others classified.
Please stop calling databases CP or AP
Martin Kleppmann · 2015- What
- A critique of labelling real databases as CP or AP.
- Key idea
- CAP's definitions are narrow (linearizability, total availability), so most systems are neither; talk about specific consistency models and latency instead.
- Why here
- Sharpens the CAP and consistency-model chapters: in interviews, name the guarantee (linearizable, causal, read-your-writes), not the letter pair.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


