system-design · beginner

ACID Transactions — All-or-Nothing Database Work

The Central Question

Consider an enterprise financial ledger platform running on the TxLab platform (txlab.com) processing 100,000,000 transactions per day:

1. UPDATE accounts SET balance = balance - 500 WHERE account_id = 'acc_alice';
2. UPDATE accounts SET balance = balance + 500 WHERE account_id = 'acc_bob';

If the database server suffers an unexpected power loss immediately after executing statement 1, $\$500$ is deducted from Alice, but Bob never receives the money. $\$500$ has completely vanished from the system.

Furthermore, if a second user attempts to read Alice's account balance while statement 1 has executed but statement 2 is still pending, the second user sees an inconsistent, transient state.

To prevent partial updates and concurrent data corruption, databases group operations into protected execution units.

A Database Transaction is a sequence of SQL operations executed as a single, indivisible unit of work governed by ACID (Atomicity, Consistency, Isolation, Durability) properties.

This lesson answers one central question: How do the four ACID guarantees (Atomicity, Consistency, Isolation, Durability) prevent data corruption during concurrent operations and system crashes, and how do Write-Ahead Logging (WAL) and Multi-Version Concurrency Control (MVCC) implement transaction safety in relational database engines?


Deconstructing ACID: The Four Guarantees

The ACID acronym defines four distinct operational promises made by a relational database engine:

flowchart TD
  ACID[ACID Transaction Properties] --> A[Atomicity: All or Nothing]
  ACID --> C[Consistency: Valid Schema States]
  ACID --> I[Isolation: Concurrency Safety]
  ACID --> D[Durability: Crash Survival]
  
  A --> ADesc["Prevents partial executions. If any step fails, all steps roll back."]
  C --> CDesc["Enforces schema constraints (foreign keys, CHECK, UNIQUE rules)."]
  I --> IDesc["Prevents concurrent transactions from seeing partial dirty work."]
  D --> DDesc["Guarantees committed transactions survive hardware power loss."]

Figure 1: Taxonomy of the four foundational ACID database guarantees.

1. Atomicity ("All or Nothing")

Atomicity guarantees that all SQL statements contained within a transaction boundary (`BEGIN ... COMMIT`) execute to completion successfully, or the database **rolls back** every statement to its pre-transaction state. There is no partial execution state.

2. Consistency (Schema Rules)

In ACID, "Consistency" means maintaining database **Integrity Constraints**. A transaction can only transition the database from one valid schema state to another, enforcing primary keys, foreign keys, `CHECK` constraints, and `NOT NULL` rules. If an operation violates a constraint, the entire transaction aborts.
[!NOTE] ACID Consistency vs. CAP Consistency: ACID Consistency refers to schema constraint integrity within a single database node. CAP Consistency (Linearizability) refers to read freshness across multiple replicated nodes in a distributed system.

3. Isolation (Concurrent Non-Interference)

Isolation guarantees that concurrently executing transactions cannot inspect or corrupt each other's un-committed intermediate states. The database provides the illusion that transactions run sequentially one after another.

4. Durability (Committed Data Persists)

Durability guarantees that once a transaction receives a successful `COMMIT` acknowledgment, its changes are permanently recorded in non-volatile storage and will survive any subsequent server crash or power outage.

Durability Internals: Write-Ahead Logging (WAL)

To achieve high write throughput while preserving Durability, databases do not write modified data pages directly to disk data files on every COMMIT. Disk data files are large and scattered across un-contiguous sectors, making synchronous disk writes extremely slow.

Instead, relational databases utilize Write-Ahead Logging (WAL):

sequenceDiagram
    autonumber
    actor App as Application
    participant RAM as DB Shared Buffer (RAM)
    participant WAL as Write-Ahead Log (Disk)
    participant DataFile as Main Data File (Disk)
    
    App->>RAM: 1. UPDATE balance = balance - 500
    Note over RAM: In-memory page marked Dirty
    App->>RAM: 2. COMMIT
    RAM->>WAL: 3. Append Transaction Record to WAL File (Sequential Disk I/O)
    WAL-->>App: 4. COMMIT Acknowledged (HTTP 200)
    
    Note over RAM,DataFile: Asynchronous Background Checkpoint
    RAM->>DataFile: 5. Flushes Dirty Buffer Pages to Main Data File on Disk

Figure 2: Write-Ahead Logging sequence appending commits sequentially before background disk checkpoints.


SQL Isolation Levels and Concurrency Anomalies

The ANSI SQL standard defines four transaction isolation levels based on which concurrent read anomalies they permit or prevent:

flowchart TD
  Levels[SQL Isolation Levels] --> RU[1. Read Uncommitted]
  Levels --> RC[2. Read Committed]
  Levels --> RR[3. Repeatable Read]
  Levels --> Ser[4. Serializable]
  
  RU --> DirtyRead["Permits Dirty Reads.<br/>Transaction A reads un-committed draft changes of Transaction B."]
  RC --> NonRepRead["Prevents Dirty Reads.<br/>Transaction A re-reads same row and sees updated values (Non-Repeatable Read)."]
  RR --> Phantom["Prevents Non-Repeatable Reads.<br/>Transaction A re-runs range query and sees new rows inserted (Phantom Read)."]
  Ser --> Strict["Strict Serializability.<br/>Zero anomalies. Transactions execute in total sequential order."]

Figure 3: Taxonomy of SQL isolation levels and allowed concurrency anomalies.

Anomaly Matrix

Isolation LevelDirty ReadNon-Repeatable ReadPhantom ReadSerialization Anomaly
Read UncommittedAllowedAllowedAllowedAllowed
Read CommittedPreventedAllowedAllowedAllowed
Repeatable ReadPreventedPreventedPrevented (in Postgres)Allowed (Write Skew)
SerializablePreventedPreventedPreventedPrevented

Two-Phase Locking (2PL) vs Snapshot Isolation (SI)

Database engines achieve isolation using one of two primary strategies:

Nested Transactions and Savepoints

Within a long-running transaction, applications can issue `SAVEPOINT savepoint_name` commands. If a subsequent SQL statement fails, the application can issue `ROLLBACK TO SAVEPOINT savepoint_name` to undo partial operations back to the savepoint marker without aborting the entire parent transaction, preserving earlier committed work.

WAL Archiving & Point-In-Time Recovery (PITR)

In addition to crash recovery, databases continuously archive completed WAL log segments to remote object storage (such as Amazon S3). By combining a base nightly database snapshot with continuous WAL log replay, operating teams can execute **Point-In-Time Recovery (PITR)**—restoring the database to the exact millisecond before a disastrous human error or corruption incident occurred.

Auto-Vacuum Tuning for High-Update Workloads

To prevent MVCC tuple bloat from degrading performance, operating teams auto-tune PostgreSQL autovacuum daemons (`autovacuum_vacuum_scale_factor = 0.05`), forcing aggressive background dead-tuple cleanup whenever $5\%$ of table rows undergo mutations.

Multi-Version Concurrency Control (MVCC)

Modern databases (such as PostgreSQL and MySQL InnoDB) avoid restrictive read-write locks by implementing Multi-Version Concurrency Control (MVCC):

flowchart LR
  subgraph Data Row Tuple Version Stack
    V1["Tuple V1 (xmin: 100, xmax: 105)<br/>balance = 1000"]
    V2["Tuple V2 (xmin: 105, xmax: 0)<br/>balance = 500"]
  end

Tx102[Tx 102: SELECT balance] -->|Reads Snapshot| V1
Tx108[Tx 108: SELECT balance] -->|Reads Snapshot| V2

Figure 4: MVCC maintaining multiple tuple versions to enable non-blocking concurrent reads.

Golden Rule of MVCC

"Readers never block writers, and writers never block readers."

When a transaction modifies a row under MVCC, the database does not overwrite the existing disk row. Instead, it creates a new tuple version stamped with xmin (creating transaction ID) and xmax (deleting transaction ID). Concurrent transactions read historical tuple versions matching their snapshot timestamp without taking locks!


Complete Worked Example: Production Go Transaction Isolation Engine

Let's inspect a complete Go implementation of a Transaction Isolation Engine for the TxLab platform (txlab.com).

package main

import (
"context"
"database/sql"
"fmt"
"time"
)

type AccountTxManager struct {
db *sql.DB
}

func NewAccountTxManager(db sql.DB) AccountTxManager {
return &AccountTxManager{db: db}
}

func (m *AccountTxManager) TransferMoneySerializable(ctx context.Context, fromAcc, toAcc string, amount float64) error {
// Enforce Serializable Isolation Level for Financial Transfers
txOpts := &sql.TxOptions{
Isolation: sql.LevelSerializable,
ReadOnly: false,
}

tx, err := m.db.BeginTx(ctx, txOpts)
if err != nil {
return fmt.Errorf("failed to begin serializable transaction: %w", err)
}
defer tx.Rollback()

// 1. Check Sender Balance
var balance float64
err = tx.QueryRowContext(ctx, "SELECT balance FROM accounts WHERE account_id = $1 FOR UPDATE", fromAcc).Scan(&balance)
if err != nil {
return fmt.Errorf("failed to query sender balance: %w", err)
}

if balance < amount {
return fmt.Errorf("insufficient funds: balance %.2f < amount %.2f", balance, amount)
}

// 2. Deduct from Sender
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE account_id = $2", amount, fromAcc)
if err != nil {
return fmt.Errorf("failed to update sender balance: %w", err)
}

// 3. Add to Recipient
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE account_id = $2", amount, toAcc)
if err != nil {
return fmt.Errorf("failed to update recipient balance: %w", err)
}

// 4. Commit Transaction Atomically
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit failed (serializable collision): %w", err)
}

fmt.Printf("[TX SUCCESS] Transferred $%.2f from %s to %s at %s\n", amount, fromAcc, toAcc, time.Now().Format(time.RFC3339))
return nil
}


Failure Modes and Engineering Mitigations

Failure ScenarioRoot CauseSystem SymptomDetection SignalMitigation Strategy
1. Write Skew Financial LossTwo doctors on call both check if count $> 1$, and both un-assign themselves under Repeatable Read.Hospital left with 0 doctors on call despite non-zero checks.Business logic violation alerts on application monitoring dashboards.Use Serializable Isolation or explicit row locking (SELECT FOR UPDATE).
2. Deadlock Cancellation CascadeTransaction A locks Row 1 then Row 2; Transaction B locks Row 2 then Row 1 simultaneously.Database engine aborts 50% of transactions with deadlock detected errors.High volume of SQL exception state 40P01 in database error logs.Enforce strict Deterministic Row Lock Ordering across application code.
3. MVCC Bloat Table SaturationLong-running reporting queries hold old snapshots, preventing PostgreSQL VACUUM from purging dead tuples.Database disk usage explodes by 300%; query performance drops by 80%.High dead tuple count metric (n_dead_tup) on PostgreSQL statistics views.Enforce Max Execution Time Limits (statement_timeout = 30s) on reporting queries.
4. WAL Sync Disk BottleneckApplication commits thousands of tiny transactions individually with synchronous_commit = on.Database QPS caps at 500 QPS due to disk NVMe write latency limits.High disk write wait percentage metrics on host operating system.Use Batch Transactions (BEGIN ... 100 INSERTS ... COMMIT) or Group Commit.

What You Should Remember

  1. ACID guarantees data integrity: Atomicity (All or Nothing), Consistency (Schema Constraints), Isolation (Concurrency Safety), and Durability (Crash Persistence).
  2. WAL provides crash survival with high throughput: Write-Ahead Logging appends commits sequentially to disk logs before flushing data pages asynchronously.
  3. MVCC enables non-blocking reads: Multi-Version Concurrency Control maintains tuple versions so readers never block writers and writers never block readers.
  4. Choose isolation levels deliberately: Use Read Committed for general QPS and Serializable for financial transfers to prevent Write Skew.
  5. Lock rows in deterministic order to prevent deadlocks: Always acquire locks in alphabetical or numerical key order (acc_A before acc_B).

Glossary of Terms

TermDefinition
ACIDThe four foundational properties of relational database transactions (Atomicity, Consistency, Isolation, Durability).
AtomicityThe guarantee that all operations in a transaction execute successfully or all roll back completely.
Write-Ahead Log (WAL)An append-only disk log file where transaction changes are recorded sequentially before data files are modified.
Multi-Version Concurrency Control (MVCC)A concurrency mechanism maintaining multiple row versions so reads do not take locks.
Dirty ReadA concurrency anomaly where Transaction A reads draft un-committed data modified by Transaction B.
Write SkewA concurrency anomaly where two transactions read overlapping data and update disjoint data, violating an invariant.

Practice Scenario and Self-Assessment

Architecture Scenario

You are designing the transaction layer for a high-frequency trading ledger platform (`ledger.txlab.com`): **Questions**:
  1. Formulate the exact SQL isolation level and locking strategy required to prevent Write Skew and overdrafts.
  2. Design the WAL logging configuration to achieve sub-millisecond commit latency without risking financial data loss.

Interactive Self-Assessment

It maintains multiple row versions so read queries read historical snapshots without taking locks, preventing readers and writers from blocking each other.

MVCC automatically converts relational database primary key indexes into un-indexed CSV files.

MVCC replaces public DNS nameservers with local hosts file entries.

MVCC doubles the physical hardware clock speed of primary database CPUs.

Repeatable Read permits Write Skew anomalies, allowing concurrent transactions to violate balance invariants and cause overdrafts.

Repeatable Read automatically formats persistent NVMe SSD disk drives on database servers.

Repeatable Read revokes edge HTTPS TLS encryption certificates on load balancers.

Repeatable Read reboots operating system hypervisors across all database nodes.


What to Learn Next

Track: Data, Storage and Messaging

Next: Banking Transaction Platform Design

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab