← All writing

September 5, 2026 · 5 min read

Isolation controls which concurrent histories may commit

Dirty reads, lost updates, read skew, and write skew — four overlapping histories, drawn the way DDIA draws time.

  • system-design
  • databases
  • transactions
Cover illustration for Isolation controls which concurrent histories may commit

These are my notes from chapter 7 of Martin Kleppmann’s Designing Data-Intensive Applications. Replication is about copies that disagree. Transaction isolation is about what overlapping work may observe, and which combinations are allowed to commit.

The diagrams in that chapter are time moving left to right, one track per person or row, and arrows for the queries in between. I wanted the same thing on this site: a playhead you can pause, not a static PNG. The figures below are generated from a small isolation simulator. They are not a database or a benchmark. They are the interleavings from the book, stepped slowly enough to follow.

Each operation has three visible parts: the request travels to the row, the database spends time processing it, and the response travels back. The thin purple bar is how long the client waits; the blue bar is work inside the database. Those intervals are deliberately relative rather than milliseconds—the point is to see that a result cannot arrive at the same instant the request does.

What “committed” is for

If Alice writes 600 and then aborts, that 600 should vanish. Under read uncommitted, Bob can still see it. That is a dirty read: a value that never became history.

Dirty readBob reads a write Alice later aborts.
Isolation level

Reads can see writes that later abort.

  • Account 1500

Dirty read. Logical time runs left to right across tracks for Alice, Bob, Account 1. Requests, database processing, client waiting, responses, and transaction end events appear as the playhead advances. The step history provides the same sequence as text.

  • request
  • database processing
  • client waiting
  • response
ClientsRecordsAliceBobAccount 1starts at 500logical timeAccount 1 = 500

Run an operation above and every step lands here — scrub, replay, or slow it down.

Speed

Bob reads a write Alice later aborts.

Read committed is the default isolation-level label in PostgreSQL, Oracle, and SQL Server. It prevents dirty reads and dirty writes: one transaction cannot overwrite another transaction’s still-uncommitted write. It does not stop the rest of this page. PostgreSQL maps READ UNCOMMITTED to READ COMMITTED, so the dirty-read run is a model of the SQL level, not behavior PostgreSQL exposes. SQL Server’s implementation of Read Committed also depends on whether READ_COMMITTED_SNAPSHOT is enabled.

Lost updates are two correct transactions

Alice and Bob both read 500 and each computes 600. Alice writes and commits first. Bob then sends the literal 600 he computed from his earlier read, overwriting Alice’s committed result. Each assignment looks reasonable alone. Together they lose a hundred dollars.

Lost updateAlice and Bob both add 100 to a balance they already read.
Isolation level

Reads only see committed data. Concurrent writes can still surprise you.

  • Account 1500

Lost update. Logical time runs left to right across tracks for Alice, Bob, Account 1. Requests, database processing, client waiting, responses, and transaction end events appear as the playhead advances. The step history provides the same sequence as text.

  • request
  • database processing
  • client waiting
  • response
ClientsRecordsAliceBobAccount 1starts at 500logical timeAccount 1 = 500

Run an operation above and every step lands here — scrub, replay, or slow it down.

Speed

Alice and Bob both add 100 to a balance they already read.

Read committed lets the stale assignment win. Canonical snapshot isolation detects that another transaction changed the same row since Bob’s snapshot and aborts his write; a retry would read 600 and write 700. At ordinary Read Committed isolation, one UPDATE accounts SET balance = balance + 100 avoids the stale client-side read-modify-write because the database locks the row and evaluates the expression against its current version. Snapshot-based levels may instead report a retryable write conflict.

Atomic updates, row locking with SELECT … FOR UPDATE, and compare-and-set can prevent or detect this race without requiring database-wide serializable isolation. A compare-and-set such as UPDATE … SET balance = 600 WHERE balance = 500 only works if the caller checks that one row changed; zero rows means retry or report a conflict.

Read skew is a total that never existed

Alice has 500 in each of two accounts. She transfers 100 from the first to the second in one transaction. Bob, in his own transaction, reads the first account before the transfer commits and the second account after. He adds 500 + 600 = 1100. There was never a moment when the committed totals were 1100.

Read skewAlice transfers 100 while Bob reads the two accounts.
Isolation level

Reads only see committed data. Concurrent writes can still surprise you.

  • Account 1500
  • Account 2500

Read skew. Logical time runs left to right across tracks for Alice, Bob, Account 1, Account 2. Requests, database processing, client waiting, responses, and transaction end events appear as the playhead advances. The step history provides the same sequence as text.

  • request
  • database processing
  • client waiting
  • response
ClientsRecordsAliceBobAccount 1starts at 500Account 2starts at 500logical timeAccount 1 = 500Account 2 = 500

Run an operation above and every step lands here — scrub, replay, or slow it down.

Speed

Alice transfers 100 while Bob reads the two accounts.

Snapshot isolation fixes this by pinning Bob to one consistent committed snapshot for the transaction—typically established at the first data-access statement, depending on the database. Both reads see 500. The transfer still happens; he just does not straddle it.

Write skew looks like two independent decisions

Two doctors are on call. The constraint is that at least one must remain. Each reads both rows, sees the other is still on, and goes off. Snapshot isolation allows both commits. Nobody wrote the same row. The invariant still dies.

Write skewEach doctor sees the other is on call, then both go off.
Isolation level

Canonical snapshot isolation gives each transaction one consistent snapshot and rejects overlapping writes to the same row.

  • Alice shifton call
  • Bob shifton call

Write skew. Logical time runs left to right across tracks for Alice, Bob, Alice shift, Bob shift. Requests, database processing, client waiting, responses, and transaction end events appear as the playhead advances. The step history provides the same sequence as text.

  • request
  • database processing
  • client waiting
  • response
ClientsRecordsAliceBobAlice shiftBob shiftlogical timeAlice shift = on callBob shift = on call

Run an operation above and every step lands here — scrub, replay, or slow it down.

Speed

Each doctor sees the other is on call, then both go off.

A serializable implementation must prevent both doctors from committing this history. This simulator uses simplified commit-time validation and rejects Bob; PostgreSQL’s SSI detects a dangerous dependency pattern and aborts one transaction, while a lock-based implementation may block or abort one earlier. Snapshot isolation does not prevent the history. In PostgreSQL, REPEATABLE READ provides snapshot isolation and can still permit serialization anomalies; SERIALIZABLE adds SSI conflict tracking. A row-local check constraint cannot enforce an invariant spanning two rows.

How I keep the levels straight

Model used hereDirty readLost updateRead skewWrite skew
Read uncommittedYesYesYesYes
Read committedNoYesYesYes
Snapshot isolation with write-conflict detectionNoOne writer abortsNoYes
SerializableNoPreventedNoPrevented

This is an anomaly table for the simplified model, not a portability chart. Product names and mechanisms do not match it one-for-one: PostgreSQL’s REPEATABLE READ behaves like snapshot isolation, while PostgreSQL SERIALIZABLE adds SSI; Oracle’s SERIALIZABLE provides a transaction-level snapshot and write-conflict checks but can still permit write skew. If you need a guarantee, look at the anomaly and the product documentation, not only the label.

Try the same interleaving at both levels in each figure. Follow one operation from request, through the processing delay, to the response. Read visibility explains dirty reads and read skew. Write-conflict handling prevents the lost update. Serializability additionally rules out dependency patterns such as write skew.