Transactions & ACID
A transaction is a sequence of database operations treated as a single, indivisible unit of work. ACID — Atomicity, Consistency, Isolation, Durability — is the set of guarantees that make transactions safe. Single-database transactions are well understood; the moment a transaction spans multiple databases or services, ACID breaks down and you must adopt alternatives like sagas or two-phase commit.
How it works
A transaction is a unit of work — a sequence of operations that the database commits or aborts as a whole. Either all of them happen, or none of them do. The classic example is a bank transfer: debit account A by $100, credit account B by $100, log the transfer. If the system crashes after the debit but before the credit, you've destroyed $100.
The four guarantees a transactional database provides are called ACID:
- Atomicity — all-or-nothing. Either every operation in the transaction commits, or none do. If anything fails, the entire transaction rolls back. ('A' is for Atomic, not 'all-or-nothing' — but the meaning is the same.)
- Consistency — the database moves from one valid state to another. Constraints (foreign keys, uniqueness, checks) are never violated. Note: this 'C' is application-defined — the database enforces the constraints you've declared.
- Isolation — concurrent transactions behave as if they ran sequentially. Their effects don't interleave. (In practice, perfect isolation is expensive; databases offer a spectrum of isolation levels — see the Isolation Levels concept.)
- Durability — once committed, the transaction's effects survive crashes, power loss, and restarts. Typically implemented via the write-ahead log (WAL).
The transfer example is impossible without ACID. Without atomicity, a mid-transfer crash leaves the books unbalanced. Without durability, a crash after commit loses the customer's money. Without isolation, two simultaneous transfers between the same accounts produce wrong balances.
Atomicity is implemented with an undo log: the database records enough information to reverse each operation if the transaction aborts. A crash mid-transaction triggers rollback on restart — every uncommitted transaction is undone before the database accepts new connections.
Durability is implemented with a redo log — the write-ahead log (WAL). Before any change is applied to the data files, the database writes the change to the WAL (an append-only file) and fsyncs it. On crash, the data files may be inconsistent, but the WAL has every committed change; the database replays the WAL on startup to bring the data files up to date.
This is why the WAL is the linchpin of durability — losing the WAL means losing committed transactions even if the data files are intact. Most databases replicate the WAL to a standby for high availability (see the WAL and Replication concepts).
The key insight: atomicity and durability are about surviving crashes. The database promises that even if you pull the power cord mid-transaction or mid-commit, the result is consistent: committed transactions survive, uncommitted ones don't.
The 'C' in ACID is different from the 'C' in CAP. ACID consistency is about application-defined invariants (foreign keys, uniqueness, CHECK constraints) — the database enforces what you've declared. CAP consistency is about replicas agreeing — every read sees the latest write. The two are related but distinct: a database can have ACID consistency (constraints enforced within a transaction) while being eventually consistent across replicas (CAP).
Isolation is the hardest guarantee to provide efficiently. True isolation (serializability) means concurrent transactions produce the same result as if they had run one at a time. Achieving this naively requires locking every row a transaction touches for its full duration — which kills concurrency.
Real databases offer a spectrum of isolation levels: Read Uncommitted (almost no isolation), Read Committed (PostgreSQL default), Repeatable Read, and Serializable. Lower levels allow more concurrency but expose more anomalies (dirty reads, non-repeatable reads, phantom reads). The choice is a per-transaction trade-off between correctness and performance.
See the Isolation Levels concept for the full hierarchy. For this lesson, the key takeaway is that 'ACID' doesn't specify which isolation level — a database is still ACID with Read Committed isolation, but that's weaker than what most people assume 'ACID' guarantees. Always ask 'which isolation level?' when someone says 'ACID'.
ACID transactions are straightforward within a single database — the database engine coordinates them internally. The hard problem is transactions across multiple databases or services: deduct from the Billing DB, write to the Inventory DB, send to the Notification service. Each has its own transaction log and crash model.
Two-phase commit (2PC) is the classical solution: a coordinator asks every participant to 'prepare' the transaction (Phase 1), and if all agree, tells them to 'commit' (Phase 2). The guarantees are strong — atomic across databases — but the costs are severe: blocking (if the coordinator dies, participants hold locks indefinitely), slow (multiple network round trips), and fragile (any participant failure aborts the whole transaction). 2PC is used in sharded SQL databases (PostgreSQL-XL, MySQL NDB) but rarely across services in modern architectures.
Sagas are the modern alternative for distributed transactions. A saga is a sequence of local transactions, each with a compensating action that undoes its effect on failure. If step 3 fails, the saga runs compensations for steps 1 and 2. Sagas don't provide isolation across services (intermediate states are visible), but they're resilient, non-blocking, and align with how microservices actually work.
The rule of thumb: stay within one database whenever possible. Cross-database transactions are 10× harder. If you must, prefer sagas over 2PC unless you have a strict need for the latter (and even then, measure the latency cost).
See the Saga Pattern and Two-Phase Commit concepts for deep dives.
Use transactions when:
- Money or count invariants — payments, balances, inventory. A half-completed transfer is a P0 incident.
- Multi-row updates that must be atomic — order with line items, user with preferences. Partial writes corrupt application state.
- Read-modify-write sequences — read balance, check sufficient funds, deduct. Without isolation, two concurrent withdrawals can both see sufficient funds and overdraft the account.
- Cross-table invariants — deleting a user should atomically delete their sessions, posts, etc. (or use ON DELETE CASCADE within the same transaction).
Don't use transactions for:
- Single-row inserts/updates — already atomic.
- Long-running operations — transactions hold locks; long ones block other work. Move long work out of the transaction (background job) and use a small transaction to record the result.
- Operations that can be eventually consistent — updating a search index, sending a notification, recomputing a counter. These are fine as eventual — and using a transaction unnecessarily slows the primary path.
- Cross-service coordination — use sagas instead.
The common anti-pattern is the 'big transaction' — wrapping an entire request handler in one transaction so that an external API call to a payment processor happens inside the DB transaction, holding locks for the duration. Move the external call out: open the transaction, write the intent, commit, call the external service, then update the result. Smaller transactions are faster, hold fewer locks, and fail more gracefully.
A banking app does the following to transfer money: read account A's balance, check it's ≥ $100, write the debit, write the credit. Two concurrent transfers from account A both succeed despite insufficient funds. Which ACID guarantee was violated, and what's the fix?
Pick one answer.
A microservices checkout flow charges payment, decrements inventory, creates an order, and sends a confirmation email — each in a different service's database. Why is 2PC rarely the right choice, and what's the modern alternative?
Pick one answer.
What does 'Durability' in ACID actually guarantee, and how is it implemented?
Pick one answer.
Engineering mental model
Mental model. Think of Transactions & ACID 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 Transactions & ACID mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”
Before choosing Transactions & ACID, 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.
SELECT id, created_at
FROM records
WHERE tenant_id = ?
ORDER BY created_at DESC
LIMIT 50;
-- Ask: which index makes this query predictable at scale?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 thought experiment: Transactions & ACID
Change the variables below and predict what breaks first in Transactions & ACID. The production lab can later reuse these same inputs.
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.
If you are stuck on Transactions & ACID, 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.
You increase traffic by 10× in a system using Transactions & ACID. What should you inspect first?
Pick one answer.
Which statement is the safest engineering habit when using Transactions & ACID?
Pick one answer.
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 Transactions & ACID, traffic suddenly spikes, and p99 latency doubles. What is your first move?
Interview drill
Answer this without notes: When would you choose Transactions & ACID, 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.
For Transactions & ACID, 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.
Do not optimize for a memorized definition. Reason from the workload and failure mode.
A team proposes Transactions & ACID because it 'scales'. What workload characteristic would make that choice a poor fit?
Pick one answer.
What you gain, what you pay
- +Atomic multi-row updates — partial failures roll back cleanly.
- +Application-level invariants enforced (foreign keys, constraints).
- +Concurrent transactions appear sequential (at proper isolation levels).
- +Committed data survives crashes via the WAL.
- +Mature, well-understood semantics across SQL databases.
- −Higher isolation levels reduce concurrency (more locking).
- −Long transactions hold locks and block other work.
- −Cross-database ACID requires 2PC — slow, blocking, fragile.
- −Transactions aren't free — even short ones add latency from BEGIN/COMMIT and WAL fsync.
- −Naive use of transactions can mask architectural problems (e.g., wrapping slow external calls).
How this breaks in production
- Read-modify-write races without proper isolation — overdrafts, double-spending, oversold inventory.
- Long-running transactions holding locks — causing throughput collapse and timeouts.
- Cross-database transactions via 2PC blocking on coordinator failure.
- Committing too early — splitting a logical unit of work into multiple transactions.
- Committing too late — wrapping external calls in a transaction, holding locks for seconds.
- Foreign key constraints not enforced — orphaned rows, referential integrity lost.
Don't fall into these traps
- •Assuming 'ACID' means 'serializable isolation' — Read Committed is the default in PostgreSQL and is weaker than people assume.
- •Wrapping entire request handlers in one transaction — including external API calls and slow work.
- •Using cross-database transactions instead of sagas — getting 2PC's blocking cost without realizing it.
- •Forgetting to use SELECT … FOR UPDATE on read-modify-write sequences — causing race conditions.
- •Treating 'eventually consistent' as 'always consistent' for data that actually requires ACID.
- •Not testing rollback paths — applications assume commit always succeeds and crash on partial failure.
Real systems using this
How real systems implement this
- Stripe — Payments require strict ACID transactions across charges, transfers, and ledger entries — every double-charge or lost-charge is a P0. Stripe runs PostgreSQL for transactional integrity with carefully designed transaction boundaries that keep external API calls outside the transaction.
- Uber — Originally used PostgreSQL with distributed transactions across sharded instances; moved to Saga-style coordination when sharding complexity and 2PC overhead became untenable. The Schemaless storage system provides per-shard ACID but no cross-shard transactions, with sagas for cross-shard operations.
- Amazon DynamoDB — Originally offered only single-item atomicity; later added DynamoDB Transactions (up to 100 items) for cases like multi-row financial updates — a recognition that some workloads need ACID even in NoSQL.
- Google Spanner — Provides externally consistent distributed transactions via TrueTime (atomic clocks) and Paxos groups. One of the few production systems offering true serializable ACID across datacenters — at significant latency cost.
Practice saying it out loud
- Q1What are the ACID guarantees, and which is hardest to provide at scale?
- Q2Explain how a database survives a crash mid-transaction. What's the role of the WAL?
- Q3Two concurrent withdrawals from the same account both succeed despite insufficient funds. What went wrong and how do you fix it?
- Q4Why is two-phase commit rarely used in microservices? What's the alternative?
- Q5When should you NOT use a transaction?
Further reading & references
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
Isolation Levels