Sign in
TodayMapLearnPracticeReview
Library
15 MINadvancedDatabases & Data SystemsNot started

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.

Why this matters

Almost every correctness bug in data systems comes down to a missing or broken transaction. Double-charged payments, oversold inventory, corrupted account balances, half-completed profile updates — these are all transaction failures. Understanding ACID is understanding what guarantees your database actually provides, what trade-offs each guarantee imposes, and why distributed transactions are the hardest problem in data systems.

Prerequisites
  • SQL vs NoSQL
Related
  • Isolation Levels
  • MVCC
  • Write-Ahead Log (WAL)
  • Two-Phase Commit
  • Saga Pattern
Used in
  • Design File Storage System
  • Design Hotel Reservation
  • Distributed Transactions
  • Isolation Levels
  • Write-Ahead Log (WAL)
Lesson

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.

Why 'Consistency' in ACID is confusing

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:

  1. Money or count invariants — payments, balances, inventory. A half-completed transfer is a P0 incident.
  2. Multi-row updates that must be atomic — order with line items, user with preferences. Partial writes corrupt application state.
  3. Read-modify-write sequences — read balance, check sufficient funds, deduct. Without isolation, two concurrent withdrawals can both see sufficient funds and overdraft the account.
  4. 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:

  1. Single-row inserts/updates — already atomic.
  2. 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.
  3. 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.
  4. 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.

Check yourself
interview

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.

Check yourself
interview

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.

Check yourself
solid

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?”

Design lens

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.

Original NO CAP systems visual for Transactions & ACID.
Image unavailable. Original NO CAP systems visual for Transactions & ACID.
Transactions & ACID: a compact system-thinking visual.— Original NO CAP visual.
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?
A minimal engineering sketch for reasoning about Transactions & ACID.

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: Transactions & ACID

Change the variables below and predict what breaks first in Transactions & ACID. 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 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.

Check yourself
solid

You increase traffic by 10× in a system using Transactions & ACID. What should you inspect first?

Pick one answer.

Check yourself
interview

Which statement is the safest engineering habit when using Transactions & ACID?

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 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.

Engineering lens

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.

Check yourself
interview

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.

Trade-offs

What you gain, what you pay

Pros
  • +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.
Cons
  • −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).
Failure modes

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.
Common mistakes

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.
Where you see it

Real systems using this

Every SQL database — transactions are the default and the foundation.Banking, payments, accounting — money invariants require ACID.E-commerce checkout — atomic inventory deduction + order creation + payment.Booking systems — atomic seat reservation.Distributed systems use sagas or outbox pattern as the modern equivalent.
Teardowns

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.
Interview prompts

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?
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

Isolation Levels