Chapter 7 is about one of the most important abstractions in databases: the transaction. A transaction groups several reads and writes into one logical unit that either entirely succeeds (commit) or entirely fails (abort/rollback). This lets the application pretend that concurrency problems and partial failures don't exist — a massive simplification. But transactions are not a law of nature; they're a trade-off, and many distributed datastores weakened or dropped them for performance. This chapter examines what guarantees transactions actually provide, the surprising ways "isolation" can leak, and what it really takes to be fully serializable.

⚡ Quick Takeaways
  • ACID is marketing as much as definition — Atomicity (all-or-nothing), Consistency (an app concern, the odd one out), Isolation (concurrent txns don't interfere), Durability (committed data survives).
  • Isolation levels are a hierarchy of compromises — read committed → snapshot isolation → serializable, each preventing more anomalies at higher cost.
  • Read committed stops dirty reads and dirty writes; snapshot isolation (MVCC) gives each transaction a consistent point-in-time view so readers and writers never block each other.
  • Lost updates, write skew, and phantoms are the classic race conditions — and snapshot isolation does not prevent write skew.
  • Serializability is the only guarantee that rules out all races, achieved three ways: actual serial execution, two-phase locking (2PL), or serializable snapshot isolation (SSI).
  • SSI is the modern win — optimistic concurrency that lets transactions run on a snapshot and aborts only those that actually conflict at commit.
tldr

A transaction makes a bundle of operations atomic and isolated so the application doesn't have to reason about partial failure or interleaving. "Isolation" comes in levels: weaker ones (read committed, snapshot isolation) are fast but allow subtle anomalies like lost updates and write skew; serializability forbids them all. Databases reach serializability via literal serial execution, pessimistic locking (2PL), or optimistic conflict detection (SSI) — the last being the best general-purpose approach.

The Meaning of ACID

The safety guarantees of transactions are usually summarized by the acronym ACID — but Kleppmann stresses that the term has been diluted into a marketing slogan, with each database meaning something slightly different. Taken precisely:

Single-Object and Multi-Object Operations

Atomicity and isolation are easy to provide for a single object (most datastores do). The hard, valuable case is multi-object transactions, where several rows/documents must change together — e.g. transferring money between accounts, or keeping a denormalized counter in sync with the rows it counts. Many distributed stores abandoned multi-object transactions because they're hard to implement across partitions; the chapter argues they're more valuable than the NoSQL movement assumed. When an operation does fail and aborts, retrying is the right recovery — but with caveats (retrying a transaction that actually succeeded but whose acknowledgement was lost can double-apply side effects).

Weak Isolation Levels

Serializable isolation has a performance cost, so databases historically offered weaker levels that prevent some anomalies but not others. These weak levels are the source of countless subtle bugs, precisely because they look fine in testing and only break under concurrency.

Read Committed

The most basic level. It makes two guarantees: no dirty reads (you only see data that has been committed — never another transaction's uncommitted writes) and no dirty writes (you only overwrite data that has been committed — preventing two transactions' writes from interleaving badly). It's typically implemented by holding a write lock per row, while reads return the last committed value (remembering the old value while a write is in progress) so reads never block.

Snapshot Isolation and Repeatable Read

Read committed still allows read skew (a nonrepeatable read): if you read two rows at moments straddling another transaction's commit, you can see an inconsistent combination — like a bank balance that appears to have lost money mid-transfer. Snapshot isolation fixes this: each transaction reads from a consistent snapshot of the database as of the moment it started, so it sees all-or-nothing of every other transaction. The key implementation idea is multi-version concurrency control (MVCC): the database keeps several committed versions of each object, and each transaction sees the versions that were committed when it began. The big payoff: readers never block writers and writers never block readers. Snapshot isolation is invaluable for long-running reads like backups and analytics. (Confusingly, many databases call this "repeatable read.")

Preventing Lost Updates

The lost update problem is a read-modify-write race: two transactions read a value, each modifies it, and one write clobbers the other.

lost update — a read-modify-write race
# two clients incrementing the same counter (starts at 42)
T1: read(counter) → 42
T2: read(counter) → 42         # both saw 42
T1: write(counter = 43)
T2: write(counter = 43)         # should be 44! one increment lost

Solutions, roughly in order of preference:

Write Skew and Phantoms

Write skew is a generalization of the lost update: two transactions read the same set of objects, then each updates different objects based on what it read — and together they violate an invariant that each alone would have preserved. The canonical example: a hospital requires at least one doctor on call. Two doctors, both currently on call, simultaneously request to go off call; each transaction checks "is someone else still on call? yes" and proceeds — leaving nobody on call. This is a phantom: a write in one transaction changes the result of a search query in another. Snapshot isolation does not prevent write skew, because the two transactions read and wrote different rows and never directly conflicted. Defenses include explicit locks on the queried rows, or materializing conflicts by introducing rows to lock on.

interview-grade point

The most common trap is assuming snapshot isolation (a.k.a. "repeatable read") is safe for everything. It prevents dirty/nonrepeatable reads and read skew, but not write skew or phantoms. If your correctness depends on a check-then-act over a set of rows ("only if no one else has booked this slot"), weak isolation will eventually bite you — you need serializability or an explicit lock.

AnomalyRead CommittedSnapshot IsolationSerializable
Dirty readPreventedPreventedPrevented
Dirty writePreventedPreventedPrevented
Read skew (nonrepeatable)PossiblePreventedPrevented
Lost updatePossibleSometimes detectedPrevented
Write skew / phantomPossiblePossiblePrevented

Serializability

Serializable isolation is the strongest: it guarantees that even though transactions may run concurrently, the end result is the same as if they had run one at a time, serially, in some order. It rules out all the race conditions above. There are three ways to implement it.

Actual Serial Execution

The simplest idea: literally execute one transaction at a time, on a single thread. This was impractical for decades, but two changes made it viable: RAM got cheap enough to hold many datasets entirely in memory, and OLTP transactions are usually short and few. Systems like VoltDB and Redis do this. To make it work you must avoid interactive multi-statement transactions (network round-trips would stall the single thread), so transactions are submitted as stored procedures that run to completion. Throughput is limited to a single CPU core, so to scale you partition the data — but cross-partition transactions then need coordination and are much slower.

Two-Phase Locking (2PL)

For ~30 years the standard algorithm for serializability. The rule: readers and writers block each other. To read an object you take a shared lock; to write it you take an exclusive lock; a write must wait for all readers' shared locks to release, and vice versa (this is the difference from snapshot isolation, where readers and writers never block). Locks are held until the transaction ends. To prevent phantoms, 2PL uses predicate locks or, more practically, index-range locks that cover a range of possible matches. 2PL is correct but has real downsides: lots of locking overhead, frequent deadlocks (transactions waiting on each other, requiring detection and abort), and highly variable, often poor, latency.

Serializable Snapshot Isolation (SSI)

A newer algorithm (2008) that delivers full serializability with much better performance, used by PostgreSQL's serializable level and FoundationDB. It's optimistic: rather than blocking on locks, transactions proceed on a consistent snapshot (as in snapshot isolation), and the database tracks the reads and writes. At commit time it checks whether the transaction's reads are still valid — i.e. whether a concurrent transaction wrote something that would have changed this transaction's decisions. If a dangerous read-write dependency is detected, the database aborts the offending transaction and lets it retry. SSI performs well when contention is low (few aborts) and avoids the lock contention of 2PL, while still ruling out write skew and phantoms.

takeaway

Transactions trade a little throughput for an enormous reduction in application complexity. The practical wisdom: don't reinvent isolation in application code, know exactly which isolation level your database gives you by default (often read committed or snapshot isolation, not serializable), and reach for serializable — ideally SSI — whenever correctness depends on concurrent decisions over shared data.

🎯 interview hot-takes

What does the C in ACID really mean? Application-level invariants — it's the app's responsibility, not really a database guarantee, unlike A, I, and D.
Dirty read vs nonrepeatable read? Dirty read = seeing uncommitted data (stopped by read committed). Nonrepeatable read/read skew = seeing inconsistent values across a transaction (stopped by snapshot isolation).
Does snapshot isolation prevent write skew? No. Two transactions reading the same set and writing different rows can violate an invariant; you need serializability or explicit locks.
2PL vs SSI? 2PL is pessimistic (readers/writers block, deadlocks, slow); SSI is optimistic (run on a snapshot, detect conflicts at commit, abort losers) and usually faster under low contention.

← previous
Partitioning