Transactions and Isolation

A transaction is a promise that a group of operations happens all-or-nothing and doesn't get corrupted by everyone else doing the same thing at once. Most developers know the word ACID; far fewer know that the "I" — isolation — is a dial with several settings, and that the default setting in most databases allows anomalies they've never heard of.

So far the series has covered how a database stores and finds data. This post moves to correctness under concurrency: how a database lets many clients read and write simultaneously without producing garbage. The mechanism is the transaction, and the subtle, consequential part is isolation — how much concurrent transactions are allowed to see of each other’s in-progress work. Getting this wrong causes bugs that are intermittent, data-dependent, and nearly impossible to reproduce.

ACID: what a transaction guarantees

A transaction groups operations into a single logical unit with four guarantees, abbreviated ACID:

Atomicity and durability come largely from the WAL. Consistency is enforced by constraints. Isolation is the hard, tunable one, because perfect isolation is expensive, so databases offer levels that trade correctness for concurrency.

Why isolation is a dial, not a switch

Ideal isolation — serializability — means the outcome of running transactions concurrently is identical to some serial (one-at-a-time) order. That’s the strongest, most intuitive guarantee: you can reason about each transaction as if it had the database to itself. But enforcing it requires heavy coordination (locking or validation) that limits how much can run in parallel, hurting throughput.

So the SQL standard defines weaker isolation levels that permit specific anomalies in exchange for more concurrency. The engineering reality: most databases do not default to serializable — they default to a weaker level (often Read Committed, sometimes Repeatable Read), meaning your application is, by default, exposed to certain anomalies. Knowing which anomalies your level allows is not academic; it’s the difference between correct and subtly-broken code.

The anomalies

Each anomaly is a specific way concurrent transactions can interfere. They form a ladder — weaker levels permit more of them:

These aren’t exotic — lost updates and write skew in particular cause real money bugs (double-spends, oversold inventory, negative balances) in applications that assumed the database “just handles concurrency.”

The isolation levels

The SQL standard defines four levels, each preventing more anomalies:

Level              Dirty read  Non-repeatable  Phantom
Read Uncommitted   allowed     allowed         allowed
Read Committed     prevented   allowed         allowed
Repeatable Read    prevented   prevented       allowed*
Serializable       prevented   prevented       prevented

The critical, practical caveat: isolation levels are not implemented identically across databases. PostgreSQL’s Repeatable Read (snapshot-based) behaves differently from the standard’s minimum; “Serializable” in one database may use locking and in another use optimistic validation with different performance and failure modes. You must know what your specific database does at your chosen level, not just the standard’s table.

Choosing a level, and defending against anomalies

Because higher isolation costs concurrency, the choice is a real trade-off — but it’s one you must make deliberately, not by accepting a default you don’t understand:

The overarching lesson: transactions give strong guarantees, but isolation is a spectrum you configure, and the common defaults trade safety for speed in ways that bite applications assuming the database serialized everything. The next post explains the mechanism most modern databases use to provide isolation efficiently — MVCC — which is why your reads usually don’t block even at higher isolation levels.

Key takeaways

Further reading

Sources & References