Lesson 7: Transactions

Lesson 0007 — 55 min read

Transactions are one of the most misunderstood ideas in distributed systems and databases. People say "use ACID" as if that settles the matter, but Chapter 7 shows the exact opposite: transaction guarantees are subtle, inconsistently implemented, often oversold, and deeply tied to concurrency control.

This chapter matters because it teaches you how to reason about partial failure, concurrent access, race conditions, and what kinds of anomalies your database may still allow even when it claims to support transactions. It is one of the most interview-relevant and production-relevant chapters in DDIA.

Mission tie-in: transactions are an abstraction that lets the application pretend many ugly failure and concurrency cases do not exist. But that abstraction is only as good as the isolation and durability guarantees your database actually provides.

Why Transactions Exist

Real data systems live in an environment where many things can go wrong:

A transaction groups several reads and writes into one logical unit. Either the whole thing commits, or the whole thing aborts. That reduces a large class of messy edge cases into one simple application-level event: retry or fail the whole unit.

The deeper idea is not merely "all or nothing writes." The deeper idea is programming model simplification. Transactions exist so the application does not have to personally reason through every partial-failure and interleaving case.


The Slippery Reputation of Transactions

In the NoSQL era, transactions were often treated as the enemy of scalability. At the same time, traditional database marketing often treated transactions as a badge of seriousness. DDIA rejects both extremes.

Transactions are neither universally necessary nor fundamentally incompatible with scale. They are a tradeoff: they simplify correctness reasoning, but they also impose implementation and performance costs.

The right question is not "Are transactions good?" but:

  1. What guarantees do I actually need?
  2. What anomalies would break my application?
  3. What isolation level does my database really implement?
  4. What performance and availability costs am I paying for those guarantees?

ACID Is Useful but Also Misleading

Transactions are commonly summarized with the acronym ACID: Atomicity, Consistency, Isolation, Durability. The problem is that these words are overloaded, underspecified, and implemented differently across systems.

The chapter is blunt about this: in modern usage, ACID is often partly a technical concept and partly a marketing term.

Letter Core idea Important warning
A Abort all partial writes if transaction fails Atomicity is about failure handling, not concurrency
C Application invariants remain valid This is mainly the application's job, not the database's
I Concurrent transactions should not interfere badly Isolation is the most ambiguous and most important letter in practice
D Committed data should survive crashes Durability is never absolute; it is risk reduction, not immortality

Atomicity: Really About Abortability

In ACID, atomicity means that if a transaction cannot complete, the database discards or undoes every write it made. If a failure occurs halfway through, the application should not be left wondering which subset of writes took effect.

That is why DDIA notes that abortability might have been a better name. Atomicity is not mainly about two threads observing a half-finished memory update. It is about not leaving the database in a half-finished transaction state after failure.

Without atomicity, retries are dangerous. The application might retry and accidentally duplicate some of the earlier work. With atomicity, abort means: assume nothing from that transaction took effect, then safely retry if appropriate.


Consistency: The Most Overloaded Word Here

ACID consistency does not mean replica consistency, eventual consistency, CAP consistency, or consistent hashing. In this chapter, it means that the application's invariants remain true.

Example invariant: in an accounting system, total credits and debits should balance. If a transaction starts in a valid state and preserves that invariant, the database remains consistent in the ACID sense.

But that invariant is defined by the application, not the database. The database can help with things like uniqueness or foreign keys, but in general it cannot know the full semantic meaning of valid versus invalid data.

Important correction: atomicity, isolation, and durability are database properties. ACID consistency is mostly an application property that may rely on the database's guarantees.


Isolation: The Heart of the Chapter

Isolation means that concurrent transactions should not step on each other's toes. In the ideal case, each transaction behaves as if it were running alone.

The strongest and cleanest interpretation is serializability: even if transactions run concurrently, the final result must be the same as some serial order.

In practice, many databases do not use full serializable isolation by default because it costs throughput, latency, or implementation complexity. That is why weak isolation levels exist, and why so many bugs survive despite an application using a so-called ACID database.


Durability: A Promise With Limits

Durability means that once a transaction commits, its data should not be forgotten even if the database crashes. On a single node, that usually means writing to nonvolatile storage plus recovery logs. In a replicated system, it may mean waiting until enough replicas have persisted the write.

But DDIA is very careful here: durability is never absolute. Hardware lies. SSD firmware has bugs. Filesystems corrupt data. Async replication can lose acknowledged writes. Correlated failures can take out many replicas at once.

So durability is best understood as a collection of risk-reduction mechanisms:

None of them is perfect in isolation. Robust systems combine them.


Single-Object vs Multi-Object Operations

A key move in this chapter is distinguishing single-object atomicity/isolation from multi-object transactions. Many systems provide good safety for one object but not for coordinated updates across several objects.

Single-object safety is common

If you write a 20 KB JSON document and the process crashes halfway, you do not want the database storing a 10 KB garbage fragment. Likewise, readers should not see a half-overwritten object.

So databases usually provide atomicity and isolation for a single object on one node. That is necessary, but not sufficient, for many applications.

Why multi-object transactions matter

Real applications often need several pieces of data to move together:

If those writes do not commit together, data can become semantically inconsistent even if each individual object write was atomic.

This is one of the biggest conceptual lessons in the chapter: single-object correctness is not enough for multi-object invariants.


Why Some Systems Avoid Multi-Object Transactions

Distributed datastores often weakened or abandoned multi-object transactions because cross-partition coordination is hard and can reduce availability or performance. But DDIA does not accept the idea that transactions are impossible at scale. They are difficult, not forbidden.

The right takeaway is: if you remove multi-object transactions, the difficulty does not disappear. It moves upward into application logic, retries, reconciliation, and bug handling.


Aborts and Retries: Powerful but Not Free

ACID systems often deal with danger by aborting the transaction and asking the application to retry. This is elegant because many low-level issues collapse into one high-level response.

But retries are not always trivial:

This is why retries often need idempotency keys, deduplication, backoff, and careful separation of database work from external side effects.


Weak Isolation Levels Exist Because Serializability Is Expensive

Concurrency bugs are hard because they depend on unlucky timing. You usually cannot see them by reading one transaction in isolation. You have to reason about interleavings.

Databases hide some of this pain with isolation levels. But weak isolation levels protect against some races and still allow others. That is where many production bugs live.

Chapter 7 therefore teaches isolation by anomalies, not by vague labels.


Read Committed

Read committed is the most basic widely used isolation level. It gives two guarantees:

  1. No dirty reads.
  2. No dirty writes.

No dirty reads

A transaction cannot see another transaction's uncommitted writes. That prevents users and other transactions from observing partially updated state that may later be rolled back.

No dirty writes

A transaction cannot overwrite another transaction's uncommitted write. Usually this is enforced with write locks so the second writer waits.

Why this is useful but insufficient

Read committed prevents some obvious corruption, but it still allows many anomalies. In particular, it does not prevent a read-modify-write race like two concurrent counter increments.

How read committed is usually implemented

That last trick is already the beginning of multi-version thinking.


Read Skew and Nonrepeatable Reads

Under read committed, one transaction may see different committed values at different moments during its execution. This is called read skew or a nonrepeatable read.

The bank-account example is classic: you read one account before a transfer and another after the transfer, so it looks like money disappeared.

This is especially dangerous for:

If a backup sees part of the database before a transaction and part after it, the restored backup may permanently encode an inconsistent state.


Snapshot Isolation

Snapshot isolation solves read skew by making each transaction read from a consistent snapshot of the database at one point in time. Even if concurrent writes happen later, the transaction keeps seeing the same older picture.

This is one of the most practically important ideas in the chapter because it makes backups, analytics, and long read-only transactions much easier to reason about.

Mantra: under snapshot isolation, a transaction sees a stable past, not a moving present.

MVCC: how snapshot isolation works

Snapshot isolation is typically implemented with multiversion concurrency control (MVCC). Instead of overwriting data in place, the database keeps multiple versions.

A row is tagged with who created it and who deleted it. Updates become "create a new version and mark the old one deleted." Visibility rules then decide which versions a transaction is allowed to see.

Why MVCC is powerful

This is a major contrast with two-phase locking later in the chapter.

Important naming confusion

Several databases expose snapshot-isolation-like behavior under names such as repeatable read or even serializable. DDIA's warning is clear: do not trust the name of an isolation level alone. Different databases use the same label for different guarantees.


Lost Updates

Lost update is one of the most common write-write anomalies. It happens when two transactions read the same value, both modify it, and the later write overwrites the earlier one without incorporating its change.

Common cases include:

Ways to prevent lost updates

1. Atomic write operations

Best when possible. Example: UPDATE counters SET value = value + 1. This avoids application-level read-modify-write races.

2. Explicit locking

Use SELECT ... FOR UPDATE to lock rows before read-modify-write logic. This works, but it relies on application developers remembering exactly where the lock is needed.

3. Automatic lost-update detection

Some databases detect conflicting read-modify-write cycles under snapshot-style isolation and abort one transaction automatically. This is safer than relying on programmers to remember locks.

4. Compare-and-set

In weaker systems, a write may be allowed only if the value still equals the one you originally read. But this is only safe if the database evaluates it against current reality rather than an old snapshot.

Replication complication

In multi-leader or leaderless systems, there may not be one single up-to-date copy to lock against. That is why conflict resolution, siblings, CRDT-like merging, or commutative operations become relevant. Last-write-wins is particularly dangerous because it can silently drop updates.


Write Skew: The More Dangerous Relative of Lost Update

Write skew happens when two transactions read overlapping state, make decisions based on what they saw, and then write to different objects. Because they do not write the same row, simple lost-update protection often does not catch the problem.

The doctor-on-call example is the chapter's centerpiece:

  1. Both transactions read that two doctors are on call.
  2. Each concludes it is safe for one doctor to go off call.
  3. Each updates its own row.
  4. Both commit.
  5. Now zero doctors are on call.

No single row was doubly written. Yet the invariant was broken. That is what makes write skew so subtle.

Mental model: write skew means "the decision was individually reasonable on an old snapshot, but globally wrong once both decisions landed."

Why snapshot isolation does not save you

Snapshot isolation gives each transaction a stable picture of the past, but that does not mean the picture is still valid when the transaction commits. If your write depends on a condition across multiple rows, that condition may have changed concurrently.

Possible mitigations


Phantoms

Phantoms occur when one transaction reads rows matching a search condition and another transaction inserts, updates, or deletes rows that change the result of that search.

Example patterns:

The important point is that there may be no existing row to lock when checking for absence. If the query returns zero rows, SELECT FOR UPDATE has nothing concrete to attach locks to.

That is why phantoms are hard and why they so often lead to write skew.

Materializing conflicts

One workaround is to create explicit lock rows, such as a room-timeslot table, so transactions can lock something concrete. DDIA calls this materializing conflicts.

It works, but it leaks concurrency-control machinery into the data model. It is usually a last resort, not an elegant default.


Serializable Isolation

Serializable isolation is the gold standard: the outcome must be equivalent to some serial execution order. If transactions are correct one at a time, serializability guarantees they remain correct concurrently.

This is the only general answer that eliminates dirty reads, dirty writes, read skew, lost updates, write skew, and phantom-related anomalies together.

The rest of the chapter is basically: if serializability is so nice, why is it so difficult? The answer is implementation cost.


Approach 1: Actual Serial Execution

The simplest way to get serializability is to actually run only one transaction at a time on one thread. No concurrency means no concurrency anomalies.

Why this became viable again

Why stored procedures matter here

Interactive client/server transactions would waste the serial executor's time waiting on network round trips. So systems like VoltDB or Datomic-style approaches require the whole transaction logic to be submitted up front as a stored procedure.

That keeps the single thread busy doing useful work instead of waiting for application code to send the next statement.

Advantages

Constraints

This is a recurring DDIA theme: simplicity often returns if you constrain the execution model aggressively enough.


Approach 2: Two-Phase Locking (2PL)

For decades, two-phase locking was the standard practical route to serializability. It is important not to confuse 2PL with 2PC. They are entirely different.

What 2PL changes compared with weaker locking

In read committed, writers block writers but readers usually proceed using old committed versions. In 2PL, readers and writers block each other when necessary to preserve serial order.

Concretely:

That "hold until the end" rule is the heart of two-phase locking.

Why 2PL works

It prevents the dangerous interleavings by making one transaction wait while another still holds relevant locks. That blocks lost updates, write skew, and other anomalies that weaker levels permit.

Why 2PL hurts

This is why 2PL is correct but often operationally unpleasant.

Deadlocks

If transaction A waits on B while B waits on A, neither can proceed. The database detects this and aborts one transaction. The application must retry.

Correctness is preserved, but wasted work increases, especially under contention.


Predicate Locks and Index-Range Locks

Serializability must also handle phantoms, not just conflicts on existing rows. That is why 2PL conceptually needs predicate locks: locks on a search condition, not merely on a concrete row.

Example: "all bookings for room 123 between noon and 1 p.m." The lock must also protect rows that do not yet exist but could be inserted and match that predicate.

Predicate locks are expensive, so many real systems approximate them with index-range locks or next-key locks. These attach locks to relevant ranges in an index.

They are less precise than ideal predicate locks, but much cheaper and still safe.


Approach 3: Serializable Snapshot Isolation (SSI)

SSI is one of the most important modern ideas in the chapter because it tries to get the best of both worlds: snapshot isolation's nonblocking reads plus serializable correctness.

Pessimistic vs optimistic

2PL is pessimistic: if something could be dangerous, block first. SSI is optimistic: let transactions proceed, then check at commit whether the observed execution was serializable.

Core SSI idea

Under snapshot isolation, a transaction may read a premise that was true at the beginning but false by commit time. SSI tracks these dangerous dependencies and aborts transactions that would make the execution nonserializable.

Two major things SSI must detect

  1. Reading an old MVCC version while another transaction later commits a conflicting change.
  2. A later write that changes the result set of an earlier read.

Why SSI is attractive

Why SSI is not magic

Tracking dependencies costs memory and bookkeeping. High contention can cause many aborts. Long read-write transactions are especially likely to fail. So SSI is powerful, but it still depends heavily on workload shape.

Deep comparison: 2PL pays with waiting; SSI pays with retries. Both buy correctness, but they stress the system in different ways.


Three Serializable Strategies Compared

Approach How it gets serializability Main strength Main weakness
Actual serial execution Remove concurrency entirely Simple correctness model Single-thread bottleneck, hard on cross-partition transactions
Two-phase locking Block conflicting reads and writes until commit Strong and time-tested Blocking, deadlocks, unstable latency
SSI Allow execution on snapshots, abort dangerous conflict patterns Nonblocking reads with serializable semantics Abort/retry overhead, bookkeeping complexity

How to Think About Isolation Levels in Practice

Isolation labels are not enough. You need to reason about the actual anomalies your workload can tolerate.

Anomaly Read committed Snapshot isolation Serializable
Dirty reads Prevents Prevents Prevents
Dirty writes Prevents Prevents Prevents
Read skew / nonrepeatable read Allows Prevents Prevents
Lost update Can allow Depends on implementation Prevents
Write skew Can allow Can allow Prevents
Phantom-related write anomalies Can allow Can allow Prevents

The phrase "depends on implementation" matters a lot. Snapshot isolation is already more precise than many names used by real databases. Vendor documentation and real observed behavior both matter.


Interview Framing

Strong transaction answers in interviews do not stop at saying "use a transaction." They explain what kind of anomaly is being prevented.

Good examples:

Interview-quality sentence: "Transactions are not just about all-or-nothing writes; the real question is what isolation anomalies the application can tolerate. If a business invariant depends on a multi-row predicate, snapshot isolation may still be unsafe because of write skew, so I would either enforce a database constraint, use serializable isolation, or explicitly materialize the conflict."


What This Chapter Is Really Teaching

Chapter 7 is not merely a chapter about SQL transactions. It is a chapter about how systems defend correctness when failure and concurrency are normal.

The most important shift in mindset is this: correctness is not a yes/no property of a database brand. It depends on which anomalies are possible under the exact isolation level, workload, and replication model you are actually using.

Retrieval Quiz

1. What is the deepest purpose of transactions according to this chapter?

Simplifying error and concurrency reasoning for applications Making every database query faster Preventing unauthorized reads

2. In ACID, what does atomicity mainly protect against?

Partial failure leaving only some writes applied Disk wear-out over time Two threads reading simultaneously

3. Why is the C in ACID often misleading?

Because it mainly depends on application invariants Because it means checksum validation Because it guarantees synchronous replication everywhere

4. What anomaly does read committed still allow that snapshot isolation fixes?

Read skew / nonrepeatable reads Dirty reads Dirty writes

5. What is MVCC fundamentally buying you?

Consistent snapshots without reader-writer blocking A single physical copy of every row forever Cryptographic integrity checking

6. What is a lost update?

One concurrent modification overwrites another in a read-modify-write cycle A committed write disappearing after crash recovery only A query returning more rows than expected because of inserts

7. Why is write skew more subtle than a lost update?

Because transactions can break an invariant while writing different rows Because only one transaction writes anything Because databases always abort both transactions immediately

8. What is a phantom in this chapter’s sense?

A change to the result set of a search condition during concurrency A row that was never committed but remains in cache A WAL entry after power failure

9. What is the core downside of two-phase locking?

Blocking, deadlocks, and unstable latency under contention It requires too many replicas on disk It only works when all data is in RAM

10. What is the key intuition behind SSI?

Proceed optimistically, then abort dangerous nonserializable executions Run only one transaction in the whole database at a time Accept every concurrent write and keep only the newest timestamp

Notes

Transactions why :-

Group many reads/writes into one logical unit

Either commit all or abort all

Main value = simplify app reasoning under partial failure + concurrency

ACID reality :-

Useful vocabulary, but often vague / marketing-heavy

Need ask what exact guarantees DB actually gives

Atomicity :-

Better thought of as abortability

If failure happens in middle, discard / undo all writes from that txn

Lets app safely retry without guessing partial state

Consistency in ACID :-

Not replica consistency / not CAP consistency / not consistent hashing

Means application invariants remain valid

Mainly app responsibility ; DB only helps with some constraints

Isolation :-

Concurrent txns should not step on each other

Strongest form = serializability

Result should equal some serial order

Durability :-

Committed data should survive crash

But no durability is perfect

Need combine logs + disk + replication + backups

Single-object vs multi-object :-

Single-object atomicity common and necessary

But many real invariants span multiple rows/docs/index entries

That is where real transactions become valuable

Retries caveats :-

Retry good for transient aborts

But danger if commit ack lost, overload, permanent errors, or external side effects already happened

Read committed :-

Guarantees = no dirty reads + no dirty writes

Usually write locks for dirty-write prevention

Readers often see old committed version until writer commits

Still allows read skew and many other anomalies

Read skew / nonrepeatable read :-

Txn sees different committed values at different moments

Dangerous for backups, analytics, integrity checks

Snapshot isolation :-

Each txn reads from one consistent snapshot

Great for long reads

Typical implementation = MVCC

Readers never block writers ; writers never block readers

Names are confusing: some DBs call this repeatable read or even serializable

MVCC idea :-

Keep multiple versions of rows

Updates become new-version create + old-version mark deleted

Visibility rules decide which version each txn sees

Lost update :-

Two txns read same value, both modify, later one clobbers earlier one

Common in counters, balances, wiki edits, JSON rewrite

Fixes :- atomic ops / explicit locks / automatic lost-update detection / compare-and-set

Replication twist :-

In multi-leader / leaderless there may not be one current copy to lock against

Need sibling merge / commutative ops / conflict resolution

LWW can silently lose updates

Write skew :-

Txns read shared premise, then write different rows

Invariant breaks even though same row not double-written

Classic example = both on-call doctors go off call

Snapshot isolation can allow this

Need serializable isolation, explicit locking, or strong constraints

Phantoms :-

Another txn changes result set of your search condition

Examples :- room booking, username claim, double spending

Hard because sometimes query finds no row, so there is nothing concrete to lock

Materializing conflicts :-

Create explicit lock rows (e.g. room-timeslot rows)

Turns phantom into real lock conflict

Works, but ugly and error-prone

Serializable isolation :-

Gold standard

Concurrent execution must equal some serial order

Only general protection against all major anomalies here

Actual serial execution :-

Run one txn at a time on one thread

Simple correctness ; no lock overhead

Needs tiny fast txns and usually in-memory active dataset

Interactive txns bad here ; stored procedures help

Cross-partition txns expensive

2PL :-

Not same as 2PC

Shared locks for reads ; exclusive locks for writes

Hold locks until commit/abort

Correct, but creates blocking, deadlocks, bad tail latency

Predicate / index-range locks :-

Need protect search conditions too, not only existing rows

Predicate locks precise but expensive

Index-range locks = practical approximation for phantom protection

SSI :-

Optimistic serializable approach layered over snapshot isolation

Do not block first ; let txns run ; abort dangerous ones at commit

Tracks stale reads and writes affecting prior reads

2PL pays via waiting ; SSI pays via retries

Primary source: Kleppmann, M. (2017). Designing Data-Intensive Applications, Chapter 7: “Transactions.” O'Reilly Media.
Recommended supplements: PostgreSQL MVCC and SSI docs, InnoDB locking docs, FoundationDB transaction model docs, and practical writeups on transaction retries, idempotency keys, and isolation anomalies.

Ask follow-up questions if you want to drill into MVCC visibility rules, write skew intuition, why snapshot isolation is not serializable, or how to explain 2PL vs SSI in interviews.

← Lesson 6: Partitioning Lesson 8: The Trouble with Distributed Systems →