CodeOath
← All posts
Architecture & Patterns80 min total · 26 parts

ACID, SOLID, and Common Design Patterns: A Software Design Reference

Contents — Part 4 of 26: Isolation and the Concurrency Spectrum
Part 4 of 26 · ~3 min

Isolation and the Concurrency Spectrum

Isolation governs what one transaction can see of another transaction's uncommitted changes while both run concurrently. Without isolation, two transactions running at the same time can interfere with each other in several specific, named ways:

AnomalyWhat happens
Dirty readTransaction A reads a row that transaction B has changed but not yet committed. If B rolls back, A read data that never really existed.
Non-repeatable readTransaction A reads the same row twice, and gets different values, because B committed a change to that row in between A's two reads.
Phantom readTransaction A re-runs the same filtered query twice, and gets a different set of rows, because B inserted or deleted a row matching that filter in between.

Isolation isn't all-or-nothing — it's a spectrum of isolation levels, each one preventing more anomalies at the cost of more locking (and therefore less concurrency):

LevelPreventsStill allows
Read UncommittedNothingDirty reads, non-repeatable reads, phantom reads
Read CommittedDirty readsNon-repeatable reads, phantom reads
Repeatable ReadDirty reads, non-repeatable readsPhantom reads
SerializableAll of the aboveBehaves as if every transaction ran one at a time, in some order

Most databases default to Read Committed as the practical middle ground — dirty reads are almost never acceptable, but full serializability's locking overhead is more than most workloads need. Picking a stronger isolation level than a given piece of code actually requires is a common, quiet source of production slowdowns under load: every extra lock held is another point of contention for concurrent transactions to wait on.

-- A transfer that must see a consistent snapshot of both balances,
-- immune to a concurrent transaction changing them mid-flight.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;

SELECT balance FROM accounts WHERE id = 'A'; -- read once
-- ... application logic decides the transfer is valid ...
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';

COMMIT;

Two specific bugs worth knowing by name because they show up in interviews constantly:

  • Lost update — two transactions both read the same row, both compute a new value based on what they read, and both write it back; whichever commits second silently overwrites the first's change, and neither transaction ever errors. Preventing it requires either a stronger isolation level, an explicit row lock (SELECT ... FOR UPDATE), or optimistic concurrency (a version/rowversion column checked on write, so the second writer's UPDATE matches zero rows and the application knows to retry).
  • Deadlock — transaction A holds a lock on row 1 and waits for row 2; transaction B holds a lock on row 2 and waits for row 1. Neither can proceed. Databases detect this and forcibly roll back one of the two transactions (the "deadlock victim"), which is why application code that runs inside transactions should always be prepared to retry on a deadlock exception rather than treating it as a fatal error.