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.
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?
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?
Single-leader
One node accepts writes. Followers copy it. Reads scatter.
Multi-leader
Two writers, same key, at once. Conflict is now your problem.
Leaderless
Write to w of n. Read from r of n. Quorum decides truth.
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.
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.
Distributing the load
Same board, four moves. Every scheme is a trade.
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.
Range
sorted keys, scan-friendly.
Hash
even load, no scans.
Fixed-N partitions
even load · cheap rebalancing.
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.
Three ways transactions collide
Same data, two clients, wrong answer.
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.
Snapshot isolation
Reads the past reliably. Cannot stop two futures from colliding.
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
Serial execution
One thread per partition. Every txn is a stored procedure that runs to completion, no interactive round-trips.
Two-phase locking
Grab a shared lock on read, upgrade to exclusive on write. Hold until commit.
Serializable Snapshot Iso
Run at SI. Track read-write dependencies. Abort losers at commit.
PostgreSQL 9.1+ defaults to SSI. It is the modern answer.
── Reference ──
Six anomalies · three sources of confusion · five things to remember.
| # | Anomaly | Definition | Example | First level that prevents it | Mechanism |
|---|---|---|---|---|---|
| 1 | Dirty read | Reading another txn's uncommitted write. | A reader sees balance=600 mid-transfer; the writer aborts, leaving a ghost value. | Read Committed | Only committed row versions visible. |
| 2 | Dirty write | Overwriting another txn's uncommitted write. | Two txns update listing + invoice; interleave sells to Alice, invoices Bob. | Read Committed | Row-level write locks held until commit. |
| 3 | Read skew | Two reads in one txn see different committed states. | Read acc1=$500 before transfer, acc2=$400 after: $900 sum that never existed. | Snapshot Iso | One snapshot per txn, not per statement. |
| 4 | Lost update | Two 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. |
| 5 | Write skew | Two txns read a shared premise, write disjoint rows; combined result violates invariant. | Both doctors see count=2, each removes themselves: 0 on call. | Serializable | SSI tracks read→write deps, aborts one. |
| 6 | Phantom | A 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.
| Question | Lost update | Write skew | Phantom-driven skew |
|---|---|---|---|
| Both txns write the same row? | Yes | No; disjoint | No; one writes a row the other's query would have matched |
| What's shared? | The row itself | A premise both read | A predicate |
| Does the conflicting row exist at read time? | Yes | Yes | No. The defining feature. |
| Can SI catch it? | Yes (same-row w-w) | No | No |
| Cheapest fix | Atomic UPDATE or FOR UPDATE | Serializable, or materialize the conflict | UNIQUE / exclusion constraint if expressible; else Serializable |
Dirty read/write → touching uncommitted data.
Read skew → my reads disagree with each other.
Lost update → our writes collide on one row.
Write skew → our writes don't collide, but our premises do.
Phantom → the collision is with a row that didn't exist yet.