ACID Transactions

The four guarantees that database transactions provide: Atomicity, Consistency, Isolation, and Durability—and how they enable reliable data operations

TL;DR

ACID is a set of guarantees that database transactions provide: Atomicity (all-or-nothing), Consistency (constraints preserved), Isolation (concurrent transactions don’t interfere), and Durability (committed = permanent). These guarantees simplify application development by letting you reason about operations as atomic units.

Visual Overview

ACID Properties
THE ACID GUARANTEES
┌─────────────────────────────────────────────────┐
│                                                 │
│  A - ATOMICITY: All or Nothing                  │
│  ┌────────────────────────────────────────┐     │
│  │ BEGIN                                  │     │
│  │   Debit Account A: -$100               │     │
│  │   Credit Account B: +$100    ← CRASH   │     │
│  │ COMMIT                                 │     │
│  └────────────────────────────────────────┘     │
│  If crash before COMMIT → Both ops rolled back  │
│  Money doesn't disappear                        │
│                                                 │
│  C - CONSISTENCY: Constraints Preserved         │
│  ┌────────────────────────────────────────┐     │
│  │ CHECK balance >= 0                     │     │
│  │ FOREIGN KEY user_id → users.id         │     │
│  └────────────────────────────────────────┘     │
│  Transaction can't leave DB in invalid state    │
│                                                 │
│  I - ISOLATION: No Interference                 │
│  ┌────────┐   ┌────────┐                        │
│  │  Tx A  │   │  Tx B  │  Running concurrently  │
│  │ Read X │   │ Read X │                        │
│  │ X = 10 │   │ X = 10 │  Both see consistent   │
│  └────────┘   └────────┘  snapshot              │
│                                                 │
│  D - DURABILITY: Committed = Permanent          │
│  ┌────────────────────────────────────────┐     │
│  │ COMMIT returns → Data on disk (WAL)    │     │
│  │ Server crash → Data survives restart   │     │
│  └────────────────────────────────────────┘     │
│                                                 │
└─────────────────────────────────────────────────┘

BANK TRANSFER EXAMPLE
┌─────────────────────────────────────────────────┐
│                                                 │
│  Without ACID:                                  │
│  1. Debit A: balance = 900    ✓                 │
│  2. ─── CRASH ───                               │
│  3. Credit B: never happens   ✗                 │
│  Result: $100 vanished!                         │
│                                                 │
│  With ACID:                                     │
│  BEGIN TRANSACTION                              │
│  1. Debit A: balance = 900    (in memory)       │
│  2. Credit B: balance = 1100  (in memory)       │
│  3. Write to WAL                                │
│  4. ─── CRASH ───                               │
│  On restart: Check WAL, rollback uncommitted    │
│  Result: Both accounts unchanged!               │
│                                                 │
└─────────────────────────────────────────────────┘

The Four Properties

Atomicity: All or Nothing

A transaction is an indivisible unit—either all operations succeed, or all fail.

Example: Bank Transfer

BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;

If the system crashes after the first UPDATE but before COMMIT:

  • With atomicity: Both updates rolled back, money intact
  • Without atomicity: Account A debited, Account B not credited

Implementation: Write-Ahead Log (WAL) records changes before applying them. On crash, replay or undo based on WAL state.

Consistency: Constraints Preserved

Transactions transition the database from one valid state to another. All constraints (foreign keys, unique, check) are enforced.

Example: Referential Integrity

-- orders.user_id must reference existing user
INSERT INTO orders (user_id, amount) VALUES (999, 50.00);
-- Fails if user 999 doesn't exist

Note: Application-level consistency (business rules) is your responsibility. The database only enforces declared constraints.

Isolation: Concurrent Transactions Don’t Interfere

Even when transactions run concurrently, the result is as if they ran serially.

Example: Preventing Double-Booking

-- Transaction A                    -- Transaction B
BEGIN;                              BEGIN;
SELECT * FROM rooms                 SELECT * FROM rooms
  WHERE available = true;             WHERE available = true;
-- Both see Room 1 available        -- Both see Room 1 available

UPDATE rooms SET available = false  UPDATE rooms SET available = false
  WHERE id = 1;                       WHERE id = 1;
COMMIT;                             COMMIT; -- One must fail!

Isolation Levels (trade-off: correctness vs performance):

LevelDirty ReadNon-Repeatable ReadPhantom
Read UncommittedPossiblePossiblePossible
Read CommittedPreventedPossiblePossible
Repeatable ReadPreventedPreventedPossible
SerializablePreventedPreventedPrevented

Durability: Committed = Permanent

Once a transaction commits, its changes survive any subsequent failure.

Implementation:

  1. Write to WAL (Write-Ahead Log) before responding
  2. fsync() to force data to disk
  3. On crash recovery, replay committed transactions from WAL

Trade-off: fsync() is slow (~10ms). Options:

  • Synchronous: Wait for disk, guaranteed durability (only after fsync() returns for a committed transaction)
  • Asynchronous: Risk losing recent commits on crash

ACID in Practice

PostgreSQL Transaction

BEGIN;

-- All operations in one atomic unit
INSERT INTO orders (user_id, total) VALUES (1, 99.99);
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 123;
INSERT INTO payments (order_id, amount) VALUES (currval('orders_id_seq'), 99.99);

COMMIT;  -- All succeed, or all fail

Application Code Pattern

def transfer_money(from_account, to_account, amount):
    with db.transaction():  # BEGIN
        from_balance = db.query(
            "SELECT balance FROM accounts WHERE id = ?", from_account
        )
        if from_balance < amount:
            raise InsufficientFunds()

        db.execute(
            "UPDATE accounts SET balance = balance - ? WHERE id = ?",
            amount, from_account
        )
        db.execute(
            "UPDATE accounts SET balance = balance + ? WHERE id = ?",
            amount, to_account
        )
    # COMMIT happens automatically at end of 'with' block
    # ROLLBACK if exception raised

When ACID Isn’t Enough

Distributed Transactions

ACID within a single database is well-solved. Across databases? Much harder.

ApproachTrade-off
Two-Phase Commit (2PC)Blocking, single point of failure
Saga PatternEventual consistency, compensation logic
Event SourcingAppend-only, rebuild state from events

Performance at Scale

Strict ACID limits throughput:

  • Locks reduce concurrency
  • fsync() adds latency
  • Cross-partition transactions coordinate

Solutions:

  • Lower isolation levels where safe
  • Partition data to avoid distributed transactions
  • Use eventual consistency where appropriate (BASE)

ACID vs BASE

For distributed systems that prioritize availability:

PropertyACIDBASE
ConsistencyStrong (immediate)Eventual
AvailabilityMay sacrificeHighly available
StateAlways validSoft state (may change)
Use CaseBanking, paymentsSocial media, analytics

Related Concepts:

Used In Systems:

  • PostgreSQL, MySQL, SQL Server (single-node ACID)
  • Google Spanner, CockroachDB (distributed ACID)
  • Kafka (transactional messaging)

Explained In Detail:

  • Database Internals Deep Dive - How WAL, isolation, and durability work under the hood

Next Recommended: Write-Ahead Log - Learn how databases implement durability

Production signal

Why this concept matters

Interview 80% of database-related interviews
Production Every transactional database
Performance Isolation level trade-offs
Scale Single-node vs distributed transactions