Back to the map

Module 07 · Databases · Lesson 3 of 13

ACID transactions

What a transaction promises: atomicity, consistency, isolation and durability, each with the failure it prevents. Isolation levels and their anomalies, how locks and MVCC implement them, and where transactions stop being free.

CoreMust-knowMid 12 minVideo Updated Nov 2026
Video walkthrough · 2:33 · narrated, with captions

Why it matters

Transfer money from account A to account B: debit A, credit B. Crash between the two and money vanishes. Run two transfers at once without care and both read the same balance and one update is lost. Transactions are the database's answer to both problems, and they are the reason relational databases remain the default for anything involving money, inventory or state that must stay internally consistent. Knowing what they guarantee, and at which isolation level, tells you when you can rely on the database and when you must do the work yourself.

The idea

A transaction groups several reads and writes into one unit that either fully happens or does not happen at all, and that behaves as if it ran alone. ACID names the four promises.

PromiseMeansFailure it preventsHow it is implemented
AtomicityAll of the writes commit, or none doA crash after the debit and before the creditWrite-ahead log: changes are logged before applied; on recovery, incomplete transactions are rolled back
ConsistencyThe transaction moves the database from one valid state to another; constraints holdA negative balance, an order with no customerConstraints, foreign keys, checks, triggers; the application supplies the rest
IsolationConcurrent transactions do not see each other's intermediate stateLost updates, reading half-applied changesLocks or multi-version concurrency control (MVCC), per isolation level
DurabilityOnce committed, the change survives crashesAcknowledged writes disappearing on power lossFlushing the log to disk before acknowledging (fsync), plus replication

Isolation levels and the anomalies they permit

Full isolation is expensive, so SQL defines levels that allow specific anomalies in exchange for throughput.

LevelDirty readNon-repeatable readPhantomWrite skewNotes
Read uncommittedYesYesYesYesRarely used
Read committedNoYesYesYesDefault in Postgres and Oracle; each statement sees committed data as of its start
Repeatable read / snapshotNoNoPostgres: no; MySQL: mostly noYesWhole transaction sees one snapshot; default in MySQL InnoDB
SerializableNoNoNoNoEquivalent to some serial order; Postgres uses SSI, others use strict two-phase locking
Dirty read: seeing uncommitted data. Non-repeatable: the same row changes within your transaction. Phantom: a re-run query returns new rows. Write skew: two transactions each read, then write based on a stale premise.
Txn A (Alice)Txn B (Bob)on_callAlice, Bobread: 2 on call → leaveread: 2 on call → leaveboth commit: 0 on call
Write skew under snapshot isolation: both doctors see two on call, both go off call, the invariant 'at least one on call' is broken. Only serializable isolation or an explicit lock prevents it.

How isolation is implemented

Locking (two-phase locking) makes readers and writers block each other: simple, serializable when strict, and prone to contention and deadlocks. MVCC keeps multiple versions of each row stamped with transaction ids; readers see the version visible at their snapshot and never block writers, writers never block readers. Nearly every modern database (Postgres, MySQL InnoDB, Oracle, SQL Server in snapshot mode) uses MVCC, which is why "readers do not block writers" is something you can usually rely on. The cost is version cleanup (vacuum, purge) and the subtle anomalies like write skew that snapshots permit.

Practical patterns

Keep transactions short: hold them across network calls or user think time and you hold locks and versions for everyone. Use SELECT ... FOR UPDATE or an explicit constraint when a read-then-write decision must be safe under snapshot isolation. Retry on serialization failures, which serializable databases raise instead of allowing the anomaly. Use unique constraints to enforce invariants the application cannot reliably check (one active subscription per user). Across services, there is no shared transaction: that is the Distributed transactions module.

Trade-offs

Stronger isolation means fewer anomalies and more aborts or blocking; most applications run at read committed or snapshot and add targeted locks or constraints where it matters. Durability's fsync costs latency per commit, which is why batched or group commits exist, and why "durable" must be read with the replication setting in mind: a commit acknowledged by one node is only as durable as that node until replicas have it.

In an interview

Name the level and the exception: "Postgres at read committed for throughput; the inventory decrement uses SELECT FOR UPDATE so two checkouts cannot both see stock and both decrement; the one-active-subscription rule is a unique partial index, not application logic." If someone says "ACID solves it", add: "within one database; across services we need sagas or an outbox."

Check yourself

Two checkouts read stock = 1 for the last item under snapshot isolation, both see it available, and both decrement. Stock is now -1. Which ACID property's level was insufficient, and what fixes it?

ACID transactions · Backplane