Sign in
TodayMapLearnPracticeReview
Library
14 MINexpertDatabases & Data SystemsNot started

Isolation Levels

Isolation levels control how concurrent transactions see each other's effects. The ANSI SQL standard defines four levels — Read Uncommitted, Read Committed, Repeatable Read, and Serializable — each preventing specific anomalies (dirty reads, non-repeatable reads, phantom reads, serialization anomalies). Higher isolation means more correctness but less concurrency. Most databases default to Read Committed, which is weaker than people assume.

Why this matters

Almost every subtle concurrency bug in database-backed systems comes down to an isolation assumption: 'I read the balance, checked it was sufficient, then wrote.' At Read Committed isolation, that check can be invalidated between the read and the write — overdrafts, double-spending, race conditions. Knowing which level your database actually uses by default, what anomalies it allows, and when to upgrade per-transaction is the difference between correct and 'mostly correct' systems.

Prerequisites
  • Transactions & ACID
Related
  • MVCC
  • Transactions & ACID
  • Write-Ahead Log (WAL)
Used in
  • Design Hotel Reservation
  • MVCC
Lesson

How it works

Isolation is the 'I' in ACID — the promise that concurrent transactions don't interfere. Perfect isolation (Serializability) means transactions produce results indistinguishable from running them one at a time in some order. Perfect isolation is expensive; real databases offer a spectrum of isolation levels, each preventing some anomalies but allowing others.

The ANSI SQL standard defines four levels, from weakest to strongest:

  1. Read Uncommitted — a transaction can read uncommitted changes from other transactions ('dirty reads'). Almost no database uses this in practice.
  2. Read Committed — a transaction only sees data that other transactions have committed. No dirty reads. But re-reading a row within one transaction can return different values ('non-repeatable reads'), and the set of rows matching a predicate can change ('phantom reads'). This is the PostgreSQL, SQL Server, and Oracle default.
  3. Repeatable Read — once a transaction reads a row, re-reading it returns the same value. Non-repeatable reads are prevented. Phantom reads may still occur (new rows matching a WHERE can appear). This is the MySQL/InnoDB default.
  4. Serializable — transactions are equivalent to running them sequentially. No anomalies. Most expensive.

The choice is a per-transaction or per-session setting: SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;.

Dirty Read — transaction T2 reads a value that T1 wrote but hasn't committed. If T1 then rolls back, T2 has read data that 'never existed.' Almost no database allows this; Read Uncommitted isolation exists in the standard but is rarely used.

Non-Repeatable Read — within a single transaction, T2 reads balance = 100, then T1 commits balance = 50, and T2 reads balance = 50. The same query returns different values within one transaction. Read Committed allows this; Repeatable Read prevents it.

Phantom Read — within a single transaction, T2 runs SELECT * FROM orders WHERE total > 100 and gets 5 rows. T1 inserts a new row with total = 200 and commits. T2 runs the same query and gets 6 rows. The row set changed.

Serialization Anomaly — the most subtle. Two transactions each read a count (5), each increment it (6), each write 6. The final count is 6, not 7. Individually each transaction looks fine; the anomaly is only visible in their interaction. This is what Repeatable Read allows and Serializable prevents.

The classic example of why this matters: a post_view_count table where two simultaneous viewers each read the count (5), each increment, each write 6. You've lost a view. At Serializable, one transaction would have aborted and retried, giving the correct count of 7.

What's the default in your database?

PostgreSQL: Read Committed. MySQL/InnoDB: Repeatable Read (with next-key locking that also prevents phantoms). SQL Server: Read Committed. Oracle: Read Committed (called 'Read Committed' but with a snapshot variant). MongoDB: Snapshot isolation per session. Most production apps run at Read Committed or Repeatable Read and are surprised to learn Serializable exists. Always check your database's default — it's the most important thing to know about your isolation story.

Serializable is the gold standard but expensive. There are two implementation strategies:

  1. Lock-based (two-phase locking, 2PL) — the classic approach. Acquire shared locks on reads, exclusive locks on writes; never release a lock until the transaction ends. Guarantees serializability but suffers from deadlocks and low concurrency. MySQL's Repeatable Read uses next-key locking, a variant of 2PL.

  2. SSI (Serializable Snapshot Isolation) — used by PostgreSQL 9.1+. Uses MVCC for reads (no read locks) and tracks 'rw-dependencies' between transactions. If a transaction's reads might have been invalidated by another's writes, the database aborts it. Higher concurrency than 2PL but with abort/retry overhead.

When to use Serializable:

  • Financial invariants where races are unacceptable.
  • Constraints that span multiple rows (e.g., 'at most 5 active reservations per user').
  • Operations where retrying on abort is safe and acceptable.

When NOT to use Serializable:

  • High-throughput workloads where the abort rate kills performance.
  • Read-heavy workloads where Read Committed suffices.
  • Long-running transactions (they'll abort more often).

In practice, many applications run at Read Committed and use SELECT … FOR UPDATE (an explicit row lock) for the specific read-modify-write sequences that need stronger isolation. This is cheaper than upgrading the whole transaction to Serializable.

SELECT … FOR UPDATE is the surgical alternative to bumping the isolation level. It acquires a row-level write lock on the rows returned by the SELECT, holding them until the transaction commits. Other transactions trying to read (with FOR UPDATE) or write those rows block.

Pattern for safe read-modify-write:

sql
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- locks row
-- application checks balance >= 100
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

This gives the correctness of Serializable for the specific rows involved, without paying the cost for the rest of the transaction.

Trade-off: row locks can cause contention and deadlocks. If two transactions try to lock the same set of rows in opposite orders, you get a deadlock — Postgres will abort one and you must retry.

Rules of thumb:

  • Default to Read Committed.
  • Use SELECT … FOR UPDATE for specific read-modify-write sequences.
  • Upgrade to Serializable only when correctness requires it and the workload tolerates aborts.
  • Never hold locks across external calls (HTTP, etc.) — that's how you get deadlocks at scale.
Check yourself
core

At Read Committed isolation, transaction T1 reads `balance = 100`, then transaction T2 commits `balance = 50`, then T1 reads balance again. What does T1 see on the second read?

Pick one answer.

Check yourself
interview

Two transactions concurrently increment a counter: both read count=5, both write count=6, both commit. The final count is 6 instead of 7. Which isolation level would have prevented this, and how?

Pick one answer.

Check yourself
interview

Your PostgreSQL app uses Read Committed (the default). You have a read-modify-write sequence on a balance field that's race-prone. What's the most surgical fix?

Pick one answer.

Check yourself
interview

Why does MySQL/InnoDB's Repeatable Read prevent phantom reads while PostgreSQL's does not (entirely)?

Pick one answer.

Engineering mental model

Mental model. Think of Isolation Levels as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does Isolation Levels mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”

Design lens

Before choosing Isolation Levels, name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.

Original NO CAP systems visual for Isolation Levels.
Image unavailable. Original NO CAP systems visual for Isolation Levels.
Isolation Levels: a compact system-thinking visual.— Original NO CAP visual.
// Pseudocode
request = receive()
result = isolation_levels(request)
return result

// Production questions:
// 1. What happens on timeout?
// 2. Can this operation be retried safely?
// 3. What is the bottleneck?
A minimal engineering sketch for reasoning about Isolation Levels.

Back-of-the-envelope reasoning

Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.

Interactive sandboxdeterministic

Interactive thought experiment: Isolation Levels

Change the variables below and predict what breaks first in Isolation Levels. The production lab can later reuse these same inputs.

System pressure6%
Try this

Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.

Hint

If you are stuck on Isolation Levels, start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.

Check yourself
solid

You increase traffic by 10× in a system using Isolation Levels. What should you inspect first?

Pick one answer.

Check yourself
interview

Which statement is the safest engineering habit when using Isolation Levels?

Pick one answer.

Try this
interview

You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.

Production scenario: your system uses Isolation Levels, traffic suddenly spikes, and p99 latency doubles. What is your first move?

Interview drill

Answer this without notes: When would you choose Isolation Levels, and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.

Engineering lens

For Isolation Levels, start with access patterns rather than brand names. Identify the dominant reads/writes, data relationships, consistency requirements, partition key, hot keys and failure behavior before choosing a storage strategy.

Numerical sanity check

A rough capacity check: required write throughput ≈ peak writes/s × average record size. At 5,000 writes/s and 2 KB average payloads, the raw incoming data stream is about 10 MB/s before indexes, replication and overhead.

Check yourself
interview

Do not optimize for a memorized definition. Reason from the workload and failure mode.

A team proposes Isolation Levels because it 'scales'. What workload characteristic would make that choice a poor fit?

Pick one answer.

Trade-offs

What you gain, what you pay

Pros
  • +Higher isolation = stronger correctness guarantees — fewer anomalies to reason about.
  • +Serializable gives you 'transactions behave as if sequential' — the cleanest mental model.
  • +Per-transaction choice — bump isolation only where needed.
  • +SSI (PostgreSQL) makes Serializable practical for many workloads via optimistic concurrency.
  • +`SELECT … FOR UPDATE` provides surgical strong isolation for specific rows.
Cons
  • −Higher isolation = lower concurrency — more locking, more aborts.
  • −Serializable transactions can abort and require retry logic in the application.
  • −Lock-based serializability (2PL) causes deadlocks under contention.
  • −Each level has subtle, database-specific behavior — the standard is ambiguous.
  • −Defaults vary by database — assumptions break across migrations.
Failure modes

How this breaks in production

  • Lost updates — two concurrent read-modify-write sequences overwrite each other.
  • Phantom reads — aggregate queries return different totals within one transaction.
  • Write skew — two transactions read overlapping data and write disjoint updates that together violate a constraint.
  • Deadlocks — transactions holding locks in opposite orders abort each other.
  • Long-running transactions at Serializable abort frequently under contention.
  • Assuming the database default is stronger than it is — Read Committed is weaker than people think.
Common mistakes

Don't fall into these traps

  • •Assuming 'ACID' means 'Serializable' — most databases default to weaker levels.
  • •Not knowing the default isolation level of the database you're using.
  • •Using Serializable for everything — performance collapse.
  • •Using Read Committed for read-modify-write sequences without SELECT … FOR UPDATE.
  • •Holding locks across external calls — deadlocks and timeouts.
  • •Not implementing retry logic when using Serializable (you will get aborts).
  • •Treating 'PostgreSQL Repeatable Read' and 'MySQL Repeatable Read' as identical — they aren't.
Where you see it

Real systems using this

Every SQL database — isolation level is a per-session setting.Banking, payments, inventory — anywhere read-modify-write matters.Reporting queries — Repeatable Read gives consistent snapshots for analytics.Migration code — explicit isolation levels to avoid surprises.
Teardowns

How real systems implement this

  • PostgreSQL — Defaults to Read Committed; offers Serializable via SSI (Serializable Snapshot Isolation). MVCC underlies both — see the MVCC concept. SSI tracks rw-dependencies and aborts transactions whose reads might have been invalidated.
  • MySQL / InnoDB — Defaults to Repeatable Read, implemented with next-key locking (a variant of 2PL). Prevents phantoms at Repeatable Read — stricter than the SQL standard requires. Offers Serializable as a stricter mode.
  • Oracle — Defaults to Read Committed. Offers Serializable (table-level locking, rarely used in practice) and Read Only (for reports). Uses undo segments to reconstruct older versions of rows for snapshot reads.
  • Microsoft SQL Server — Defaults to Read Committed; offers all four ANSI levels plus SNAPSHOT isolation (similar to PostgreSQL Repeatable Read with MVCC). Read Committed Snapshot Isolation (RCSI) is a popular option that uses MVCC for Read Committed, avoiding read locks.
Interview prompts

Practice saying it out loud

  • Q1What are the four ANSI SQL isolation levels, and which anomalies does each prevent?
  • Q2Two concurrent transactions increment a counter and both read 5. The final count is 6 instead of 7. What happened and how do you prevent it?
  • Q3What isolation level does your database default to? What does that allow?
  • Q4What is `SELECT … FOR UPDATE` and when should you use it?
  • Q5How does PostgreSQL implement Serializable isolation? Why is it different from MySQL's approach?
Research

Further reading & references

System Design Primer
Open source
ByteByteGo — Scale from zero to millions
ByteByteGo
System Design Tutorial
GeeksforGeeks
System Design Roadmap
roadmap.sh
Databases & Data Systems reference
Reference
Databases & Data Systems reference
Reference
Databases & Data Systems reference
Reference

Core explanations are original NO CAP material. External references are provided for deeper study and standards.

What next?

Mark as understood once the mental model clicks.

Next recommended

MVCC