Post 03 · DDIA Ch. 5–7

Distribution

One database on many machines pretending to be one. Every trick to keep them in sync leaks — and every leak is a bug someone paged you about.

Copy the data (replication). Split the data (partitioning). Then keep concurrent users from stepping on each other (transactions). Three chapters, one problem: the fiction of a single database, defended.

Part I · DDIA Ch. 5

Replication

You updated your profile, refreshed, and it was gone. Then it came back. That's lag — the gap between a leader accepting a write and a follower catching up. Every consistency anomaly in this section is a symptom.

A follower is always some milliseconds behind the leader — sometimes microseconds under load, sometimes minutes if it's been offline. Reads from the leader see fresh data; reads from a follower may see stale data. Load-balancing reads across followers is how you scale reads, and also how you accidentally show users the wrong thing.

The lag

Same client, three replicas. One gets there late.

U you L leader F1 follower · fast F2 follower · slow name: "Idrees" stale read replica caught up

Three anomalies lag creates read-your-writes your own write vanishes monotonic reads time appears to move backward consistent prefix you see effects before causes

Synchronous replication: the leader waits for at least one follower to acknowledge before returning success. Zero data loss on failover, but every write pays the network round-trip and one slow follower stalls everyone. Asynchronous: the leader returns immediately and streams the write to followers in the background. Fast, but a leader crash before the flush loses committed writes. Most systems run a hybrid: one sync follower for safety, the rest async for speed.

Sync or async

Same write, plotted on time. Ack when?

sync async 0 time → sync commit async commit Δt lag durable · 2/2 ack in 1 hop leader crash → write lost

Takeaway

Eventual consistency: your replicas will agree, just not right now.

Single-leader: one node accepts writes, everyone else follows. Simple, but the leader is a single point of failure and a bottleneck. Multi-leader: writes accepted on several nodes, replicated between them. Great for multi-datacenter, but concurrent writes to the same row need conflict resolution and no strategy is fully satisfying. Leaderless (Dynamo-style): every replica accepts writes; clients read from and write to a quorum. Uses vector clocks and read-repair to converge — powerful, but the mental model is hard.

Who holds the pen?

01

Single-leader

One node accepts writes. Followers copy it. Reads scatter.

WRITE PATH all writes → one leader.
CONFLICT no cross-replica conflicts (leader orders everything).
FAILS WHEN leader dies → failover, possible data loss.
EXAMPLES Postgres primary, MySQL primary, most OLTP databases.
02

Multi-leader

Two writers, same key, at once. Conflict is now your problem.

WRITE PATH any leader accepts · async replicate to peers.
CONFLICT must resolve · LWW, CRDT, or app logic.
USE FOR multi-datacenter · offline clients · collab editing.
WATCH OUT hidden conflicts silently pick a winner.
03

Leaderless

Write to w of n. Read from r of n. Quorum decides truth.

WRITE PATH client writes to w of n replicas.
READ PATH client reads from r replicas · newest wins.
QUORUM w + r > n · guarantees overlap.
EXAMPLES Dynamo, Cassandra, Riak.

Three topologies · three answers to who accepts writes. Same client, same three replicas, different arrows.

Part II · DDIA Ch. 6

Partitioning

Replication copies the data. Partitioning splits it. And the moment you split, one customer is 30% of your traffic and one shard is on fire.

Real workloads are almost never uniform. One tenant is 30% of the traffic, one hashtag is trending, one product goes viral — and the shard holding that key gets buried while the rest sit idle. Even the best partitioning scheme can't spread a single hot key; you have to break the key itself.

The fire

Even by design. Skewed by workload.

C clients P0 node-A P1 node-A P2 node-B P3 node-B P4 node-C P5 node-C req/s celebrity key · 1 partition · fan-out amplification

Range partitioning keeps keys sorted, so range scans stay cheap — but sequential keys (timestamps, auto-increment IDs) create the classic hot-shard-at-the-end pattern. Hash partitioning spreads writes uniformly across shards — but destroys range scans, since neighbours in key-space land on different nodes. Salting is what you do for a single unavoidable hot key: prepend a small random prefix so the load spreads, and accept that reads now hit multiple shards.

Takeaway

Hash spreads load. Range keeps scans cheap. Salting patches hot keys. Every scheme is a trade.

The decision is driven by your query pattern, not your data. If reads are point lookups by a well-distributed key, hash. If reads are range scans over a naturally-ordered key, range. If one key concentrates traffic no matter what you do, salt it and pay the fan-out cost on reads.

Pick your partitioning scheme.

01

Range

sorted keys, scan-friendly.

PICK WHEN range queries dominate · time-series with rolling windows.
STRUGGLES AT sequential keys (timestamps) → all writes on one partition.
EXAMPLES HBase, BigTable.
02

Hash

even load, no scans.

PICK WHEN point lookups dominate · load must spread evenly.
STRUGGLES AT range queries → scatter/gather across every partition.
EXAMPLES Cassandra hash, DynamoDB.
03

Fixed-N partitions

even load · cheap rebalancing.

PICK WHEN nodes come and go · you want to add capacity without downtime.
STRUGGLES AT range queries (same as hash) · one hot key still needs salting.
EXAMPLES Riak, Elasticsearch, Couchbase.

Rebalancing is orthogonal: fixed-N is the default sane choice; dynamic (HBase, Mongo) splits as data grows; proportional-to-nodes (Cassandra) scales bins with the cluster.

Part III · DDIA Ch. 7

Transactions

Two people booked the same seat. Two on-call doctors both went off duty. A transaction is a promise — all-or-nothing, isolated from the neighbors — and every weakening of that promise is a race waiting to happen.

ACID: atomic · consistent · isolated · durable. This part is the I.

Six anomalies, from mild to career-ending: dirty reads, dirty writes, read skew, lost updates, write skew, phantoms. Every one of them is a concrete way concurrent transactions produce a state that no serial ordering could have produced. Each isolation level is a promise to prevent some of them — and to leave the others as your problem.

Every anomaly is a promise the database didn't make. Isolation levels are the rungs of stronger promises.

The isolation ladder each row lists what that level newly prevents. protections are cumulative up the ladder. Read Committed dirty read · dirty write Snapshot Iso non-repeatable read · lost updates (concurrent write conflicts)* Serializable write skew · phantoms * Lost-update detection is implementation-dependent. PostgreSQL (40001), SQL Server (3960), and Oracle (ORA-08177) abort the later writer on a same-row conflict. MySQL/InnoDB's snapshot covers reads only, not writes, so read-modify-write silently loses updates unless you SELECT ... FOR UPDATE. † SI blocks phantom reads via snapshots. But write skew is a separate anomaly, and snapshots alone can't catch it (the doctors above). Vendor names for SI: PostgreSQL → REPEATABLE READ · SQL Server → SNAPSHOT · Oracle → SERIALIZABLE (SI-based; write skew still possible).

Give every transaction its own consistent view of the database as of the moment it started. Under the hood, the database keeps multiple versions of each row (MVCC) and shows each reader the versions that were committed when its snapshot began. Readers never block writers, writers never block readers, and most races just… don't happen. Write skew is the anomaly it can't catch — and that's the whole reason serializability exists.

Takeaway

Snapshot isolation blocks nearly every race. Write skew is the exception, and it is the whole reason the next section exists.

Three routes. Actual serial execution: run one transaction at a time on a single thread (Redis, VoltDB) — fast if transactions are short and data fits in RAM. Two-phase locking: take shared locks on reads, exclusive locks on writes, hold until commit — correct, but throughput dies under contention. Serializable Snapshot Isolation (SSI): run at SI speed, track read-write dependencies, abort the loser when a conflict would produce a non-serializable outcome. SSI is the modern answer; PostgreSQL and CockroachDB use it.

Three ways to actually be serializable

01

Serial execution

One thread per partition. Every txn is a stored procedure that runs to completion, no interactive round-trips.

WHEN TO PICK working set fits in RAM and every txn is fast. Redis, VoltDB.
WATCH OUT no cross-partition scaling. one slow txn stalls the queue.
02

Two-phase locking

Grab a shared lock on read, upgrade to exclusive on write. Hold until commit.

WHEN TO PICK you need strong guarantees on a mature engine. MySQL/InnoDB, SQL Server.
WATCH OUT deadlocks. Throughput collapses under contention.
03

Serializable Snapshot Iso

Run at SI. Track read-write dependencies. Abort losers at commit.

WHEN TO PICK reads far outnumber writes; conflicts are rare. PostgreSQL 9.1+.
WATCH OUT performance cliffs on hot rows. Aborts cost work.

PostgreSQL 9.1+ defaults to SSI. It is the modern answer.

── Reference ──

Six anomalies · three sources of confusion · five things to remember.

All the anomalies, one table
# Anomaly Definition Example First level that prevents it Mechanism
1Dirty readReading another txn's uncommitted write.A reader sees balance=600 mid-transfer; the writer aborts, leaving a ghost value.Read CommittedOnly committed row versions visible.
2Dirty writeOverwriting another txn's uncommitted write.Two txns update listing + invoice; interleave sells to Alice, invoices Bob.Read CommittedRow-level write locks held until commit.
3Read skewTwo reads in one txn see different committed states.Read acc1=$500 before transfer, acc2=$400 after: $900 sum that never existed.Snapshot IsoOne snapshot per txn, not per statement.
4Lost updateTwo RMW cycles on the same row; later write overwrites earlier.Both read counter=42, both write 43: one +1 lost.Snapshot Iso*First-committer-wins: abort with 40001 / 3960 / ORA-08177.
5Write skewTwo txns read a shared premise, write disjoint rows; combined result violates invariant.Both doctors see count=2, each removes themselves: 0 on call.SerializableSSI tracks read→write deps, aborts one.
6PhantomA row matching a predicate appears or disappears due to a concurrent write.Check "slot free" returns empty, insert; concurrent insert wins the slot.Read-side: SI. Write-decision: Serializable.SSI read-write dependency tracking.

* At each engine's SI level: PostgreSQL REPEATABLE READ (40001), SQL Server SNAPSHOT (3960), and Oracle SERIALIZABLE (ORA-08177) abort the conflicting writer. MySQL/InnoDB REPEATABLE READ silently loses updates; use SELECT ... FOR UPDATE or an atomic UPDATE.

How to tell lost update, write skew, and phantom-driven skew apart
Question Lost update Write skew Phantom-driven skew
Both txns write the same row?YesNo; disjointNo; one writes a row the other's query would have matched
What's shared?The row itselfA premise both readA predicate
Does the conflicting row exist at read time?YesYesNo. The defining feature.
Can SI catch it?Yes (same-row w-w)NoNo
Cheapest fixAtomic UPDATE or FOR UPDATESerializable, or materialize the conflictUNIQUE / exclusion constraint if expressible; else Serializable

Dirty read/write → touching uncommitted data.

Read skewmy reads disagree with each other.

Lost updateour writes collide on one row.

Write skewour writes don't collide, but our premises do.

Phantom → the collision is with a row that didn't exist yet.