Under the Hood of Database Transactions: A Deep Dive into ACID and Isolation
A deep technical breakdown of database transaction guarantees. Learn how modern database engines handle concurrency, hardware crashes, and state corruption using Write-Ahead Logging (WAL), Multi-Version Concurrency Control (MVCC), and isolation levels from Read Uncommitted to Serializable.
Contents
- 1. Atomicity: The Power to Abort Safely
- How Databases Implement Atomicity
- 2. Consistency: Preserving System Invariants
- The Boundary of Database Consistency
- 3. Durability: The Contract with the Disk
- The Lifecycle of a Durable Write
- 4. Isolation: Concurrency Control Under Contention
- Deep Dive: The Four Isolation Levels in Practice
- 5. Architectural Comparison: The Isolation Spectrum
- 6. Real-World Architecture: Local ACID vs. Distributed Realities
- The Outbox Pattern
- The Saga Pattern
- 7. Practical Engineering Checklist
- Summary
When writing software that interacts with a database, you operate under a pleasant delusion: that your query executes in clean isolation, that the hardware underneath never blinks, and that the data on disk represents absolute truth.
In reality, physical storage is unreliable, operating systems crash mid-write, threads interleave unpredictably across CPU cores, and network packets drop without warning.
A database transaction is an abstraction layer constructed to tame this chaos. It groups a sequence of distinct read and write operations into a single logical unit. To evaluate the reliability of that abstraction, we rely on the ACID framework—a set of guarantees formulated across decades of database research:
-
Atomicity
-
Consistency
-
Isolation
-
Durability
These four letters are often treated as a simple checklist, but their underlying mechanisms are nuanced. The trade-offs between performance and safety—particularly around isolation levels—directly dictate how your architecture handles race conditions, throughput bottlenecks, and data corruption.
1. Atomicity: The Power to Abort Safely
Atomicity is frequently defined as "all operations succeed, or none do." While true, this definition obscures the real technical challenge: abortability.
Consider a basic balance transfer between two bank accounts:
SQLBEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;If the database crashes after the first statement executes, or if account B fails an integrity check, the money cannot simply vanish into thin air. The database must guarantee that if the process terminates at any point before the final commit confirmation, all partial changes are completely undone.

How Databases Implement Atomicity
Databases achieve this guarantee through two primary mechanisms:
-
Write-Ahead Logging (WAL) / Undo Logs: Before any data block is altered in-memory (in the buffer pool) or written to table files, the engine writes an entry into an append-only log on disk. In engines like MySQL’s InnoDB, Undo Logs preserve the "before image" of the row. If the transaction aborts, the engine walks backward through the undo chain to reconstruct the original state.
-
Shadow Paging: Used by engines such as SQLite (in rollback journal mode) or CouchDB. Modifications are written to duplicate pages on disk. If the transaction succeeds, a pointer is atomically swung to reference the new pages; if it fails, the new pages are discarded, leaving the original data untouched.
Atomicity has nothing to do with concurrent access (which is the domain of Isolation). It is strictly a contract that prevents the persistence of partial operations when errors occur.
2. Consistency: Preserving System Invariants
Consistency is the most overloaded term in software engineering.
In the CAP theorem, Consistency means linearizability—every read receives the most recent write or an error. In distributed systems / replication, it refers to replica convergence (such as eventual consistency).
In the context of ACID, Consistency means data validity: a transaction must transition the database from one valid state to another valid state, where "valid" is defined by a set of explicit schema rules and predicates:

These rules include:
-
Relational constraints: Primary keys, Foreign keys, NOT NULL, and UNIQUE.
-
Explicit check constraints: CHECK (balance >= 0).
-
Triggers: Custom programmatic validators executed before a write commits.
CREATE TABLE accounts (
id UUID PRIMARY KEY,
balance NUMERIC(12, 2) NOT NULL,
CONSTRAINT balance_non_negative CHECK (balance >= 0)
);If an operation forces balance to -0.01, the engine blocks the write and triggers a rollback.
The Boundary of Database Consistency
The relational engine only understands the invariants you explicitly teach it. It cannot validate implicit business logic:
# The database cannot know this represents a logic bug
with db.transaction():
user.reward_points += 5000 # Intended to be 50, but a typo added two zerosFrom the perspective of ACID, the transaction above is completely "consistent" as long as reward_points is an integer and within allowed numeric ranges. The responsibility for systemic correctness is always divided: the database enforces structural and predicate invariants, while your domain models enforce functional logic.
3. Durability: The Contract with the Disk
Durability guarantees that once a transaction receives a successful commit acknowledgment, its state changes survive indefinitely—even if the machine immediately loses power, kernel-panics, or restarts.
Writing directly to final table files on disk on every commit is too slow; random I/O quickly degrades throughput. To solve this, database engines rely on a combination of in-memory caching and sequential logging.
The Lifecycle of a Durable Write
1. Client issues COMMIT
2. Changes applied to In-Memory Buffer Pool (Dirty Pages)
3. WAL / Redo Log appended to Disk via fsync()
4. Engine returns "SUCCESS" to Client
5. Background process asynchronously flushes Buffer Pool to final Data Files (Checkpointer)-
Dirty Pages in the Buffer Pool: The row update happens first in fast, volatile RAM.
-
Sequential Append to WAL: The engine writes the changes to a sequential, append-only Write-Ahead Log (WAL) or Redo Log. Because sequential writes bypass the seek penalties of random access, this log operation completes quickly.
-
Flushing with fsync(): Operating systems do not immediately write file writes to physical platters or flash cells; they buffer them in OS page caches. To guarantee durability, the database must issue an fsync() system call, forcing the disk controller to flush physical write caches to non-volatile storage.
-
Crash Recovery: If the system reboots unexpectedly, the engine inspects the WAL. Any committed transaction present in the log that had not yet been flushed from RAM to the main data files is systematically replayed (Redo). Any uncommitted transaction is reversed (Undo).
Durability exists on a spectrum. If you run PostgreSQL with synchronous_commit = off or MySQL with innodb_flush_log_at_trx_commit = 2, you trade strict durability for raw throughput, accepting a window of a few hundred milliseconds of data loss during a hard crash.
4. Isolation: Concurrency Control Under Contention
If every transaction executed sequentially, maintaining correctness would be trivial. However, modern systems require high throughput, running hundreds or thousands of transactions concurrently across distributed worker threads.
Isolation determines how and when concurrent modifications become visible to other operations.
Without isolation, transactions interleave in ways that introduce serious anomalies:
| Anomaly | Description |
| Dirty Read | Transaction A reads uncommitted modifications made by Transaction B. If B aborts, A acted on "phantom" state. |
| Non-Repeatable (Fuzzy) Read | Transaction A reads a row. Transaction B modifies or deletes that row and commits. Transaction A re-reads the row and observes different values. |
| Phantom Read | Transaction A reads a range of rows matching a condition. Transaction B inserts new rows matching that condition and commits. Transaction A re-runs the range query and sees new rows. |
| Write Skew | Two concurrent transactions read overlapping data, make disjoint updates based on that data, and commit. Together, their updates violate an invariant that neither violated individually. |
To manage these anomalies, the ANSI/ISO SQL-92 standard established four isolation tiers. Modern databases implement them using either Two-Phase Locking (2PL) (pessimistic locking) or Multi-Version Concurrency Control (MVCC) (optimistic/snapshot models).

Deep Dive: The Four Isolation Levels in Practice
Level 1: READ UNCOMMITTED (Dirty Reads Permitted)
At this level, queries execute with zero read-lock contention and completely ignore row versioning boundaries. A transaction can read changes written by other transactions that have not yet been committed.
Transaction 1: Transaction 2:
---------------- ----------------
BEGIN;
UPDATE video_views
SET count = count + 1
WHERE id = 9876;
BEGIN;
-- Dirty Read: Reads uncommitted count
SELECT count FROM video_views WHERE id = 9876;
ROLLBACK; -- Count was never real!-
Real-World Use Case: High-traffic aggregation metrics, such as real-time dashboard approximations or video view counters.
-
Why it fits: When you are aggregating ten million rows for an analytics dashboard, precision down to the single-digit increment is meaningless compared to the latency penalty of taking Shared (S) locks or traversing MVCC rollback undo chains.
-- Read-only analytics thread targeting performance over row correctness
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN TRANSACTION;
SELECT COUNT(*)
FROM video_views
WHERE video_id = 9876;
COMMIT;Level 2: READ COMMITTED (The Industry Workhorse)
This is the default isolation level for systems like PostgreSQL, Oracle, and Microsoft SQL Server. It guarantees that any data read was committed at the exact instant the query was issued.
-
Dirty reads are impossible.
-
However, a transaction executing two identical queries within the same transaction block may read different data if an external transaction commits between them (Non-Repeatable Reads).
Transaction 1: Transaction 2:
---------------- ----------------
BEGIN;
SELECT address FROM users
WHERE id = 42;
-- Returns "123 Main St"
BEGIN;
UPDATE users
SET address = '456 Elm St'
WHERE id = 42;
COMMIT;
SELECT address FROM users
WHERE id = 42;
-- Returns "456 Elm St" (Non-Repeatable Read)
COMMIT;-
Mechanism: In MVCC systems, a new, fresh Snapshot of the database is generated at the start of each individual statement, rather than at the start of the surrounding transaction.
-
Real-World Use Case: E-Commerce profile updates and checkout forms.
-
Why it fits: You never want to read uncommitted, dirty data. If a customer is editing a shipping address during checkout, the checkout system should only see finalized entries. However, if the user changes their billing phone number in another tab, the system seeing that newly committed value on a subsequent query within the same checkout session does not cause state corruption.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
-- Guarantees visibility into committed data only
SELECT shipping_address
FROM users
WHERE user_id = 42;
-- Long-running work happens here...
-- If another process updated shipping_address in the interim,
-- this query reads the new value.
SELECT shipping_address
FROM users
WHERE user_id = 42;
COMMIT;Level 3: REPEATABLE READ (Snapshot Isolation)
Under REPEATABLE READ, a transaction reads from a fixed point in time. In modern engines, this is implemented as Snapshot Isolation: when the transaction begins, the engine takes a point-in-time snapshot. Every subsequent query inside that transaction sees only:
-
Data that was already committed prior to the start of the transaction.
-
Changes made by the transaction itself.

-
Real-World Use Case: End-of-day financial balance sheets, inventory valuation, and nightly audit reconciliation scripts.
-
Why it fits: An audit script iterates over accounts sequentially to verify ledger balance totals:
∑Assets = ∑Liabilities + ∑Equity
If the script reads Account A, and an external transfer moves $500 from Account A to Account B before the script reads Account B, reading the updated Account B would result in double-counting that $500. REPEATABLE READ prevents this by ensuring Account B is evaluated as it existed at the start of the transaction.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;
-- Snapshot generated at this exact point in time
SELECT balance FROM accounts WHERE account_id = 'A';
-- Concurrent transactions can modify accounts freely...
-- But Account A's read value remains completely stable here
SELECT balance FROM accounts WHERE account_id = 'A';
COMMIT;Engine Nuance: The ANSI-SQL standard warns that REPEATABLE READ allows Phantom Reads (newly inserted rows appearing in range queries). However, in PostgreSQL and MySQL (InnoDB), the standard REPEATABLE READ level also prevents phantoms—PostgreSQL does this using its MVCC snapshot engine, while InnoDB combines MVCC with Next-Key Locks (range locking on index gaps).
Level 4: SERIALIZABLE (Absolute Isolation)
SERIALIZABLE provides the strongest isolation guarantee. It ensures that the final state of concurrent transactions is identical to running those transactions sequentially, one after another.
This level eliminates the most subtle concurrency bugs, including Write Skew.
Imagine a hospital policy requiring at least one doctor to be on call. Doctors Alice and Bob are currently on call, and both try to step down simultaneously:
-- Alice runs:
BEGIN;
SELECT count(*) FROM shift WHERE on_call = TRUE; -- Returns 2
UPDATE shift SET on_call = FALSE WHERE doctor = 'Alice'; -- Allowed!
COMMIT;
-- Bob runs simultaneously under REPEATABLE READ:
BEGIN;
SELECT count(*) FROM shift WHERE on_call = TRUE; -- Returns 2 (due to snapshot)
UPDATE shift SET on_call = FALSE WHERE doctor = 'Bob'; -- Allowed!
COMMIT;Under REPEATABLE READ, both transactions execute concurrently, evaluate the count as 2, update disjoint rows, and successfully commit. Result: Zero doctors are on call, violating the system invariant.
SERIALIZABLE prevents this by detecting the overlapping read/write dependencies and forcibly aborting one of the transactions.
-
Real-World Use Case: High-demand concert seat reservations, stock-clearing houses, or high-stakes auctions.
-
Why it fits: When the final VIP front-row seat is sold, multiple checkout workers will read:
SQLWHERE seat_id = 1 AND status = 'AVAILABLE'Allowing two workers to resolve that check simultaneously leads to double-booking. SERIALIZABLE forces total ordering.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
-- Locks the rows and gaps in index space (or tracks read-locks via SSI)
SELECT status
FROM ticket_seats
WHERE seat_id = 1;
-- Guarantees absolute safety from double-booking anomalies
UPDATE ticket_seats
SET status = 'SOLD', user_id = 551
WHERE seat_id = 1;
COMMIT;Databases achieve serializability in one of two main ways:
-
Two-Phase Locking (2PL): Readers lock out writers, and writers lock out readers. This approach limits throughput and introduces deadlock risks.
-
Serializable Snapshot Isolation (SSI): Used by modern engines like PostgreSQL. Transactions do not block. Instead, the engine monitors the execution graph for cycles (SIREAD locks). If a dependency cycle is detected—indicating an anomaly like Write Skew—the engine immediately aborts the offending transaction with a 40001 serialization_failure error code, leaving the application layer to catch the error and retry.
5. Architectural Comparison: The Isolation Spectrum
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Write Skew | Throughput Cost | Mechanism |
| READ UNCOMMITTED | Possible | Possible | Possible | Possible | Very Low | Raw reads; locks ignored |
| READ COMMITTED | Prevented | Possible | Possible | Possible | Low | Per-statement snapshots |
| REPEATABLE READ | Prevented | Prevented | Engine Dependent | Possible | Moderate | Per-transaction snapshots (MVCC) |
| SERIALIZABLE | Prevented | Prevented | Prevented | Prevented | High | 2PL or SSI cycle aborts |
6. Real-World Architecture: Local ACID vs. Distributed Realities
A common architectural trap is assuming ACID guarantees extend across an entire application ecosystem.
ACID guarantees exist within a single database storage engine. The moment your transaction leaves that local boundary—spanning across network calls, microservices, Kafka topics, or third-party APIs (like Stripe)—traditional database transactions cannot protect you.
Attempting to enforce ACID across networks via protocols like Two-Phase Commit (2PC) creates fragile, tightly coupled systems. If one participant hangs or network partitions occur, locks remain open, holding system resources hostage and hurting availability.
In distributed architectures, engineers replace monolithic ACID with alternative patterns:
The Outbox Pattern
Ensures atomicity between a local database update and a message broker publish:
By storing the event directly inside the local transactional boundary, the state change and the event record commit or rollback together.
The Saga Pattern
Decomposes a distributed transaction into a sequence of smaller, local transactions managed by an orchestrator or event choreographies.
-
If Step 3 fails, the system triggers Compensating Transactions that walk backward, undoing the side effects of Steps 1 and 2.
7. Practical Engineering Checklist
When choosing isolation levels and managing transactional workloads in production, use this operational checklist:
-
Rely on Defaults Carefully: PostgreSQL and Oracle default to READ COMMITTED. MySQL (InnoDB) defaults to REPEATABLE READ. Know your engine's baseline before writing business logic.
-
Explicit Locking Beats High Isolation Tiers: If you need to stop race conditions on a few critical rows under READ COMMITTED, prefer explicit pessimistic locks instead of jumping to SERIALIZABLE:
SQLSELECT balance FROM accounts WHERE id = 'A' FOR UPDATE;This takes an exclusive row-level lock, preventing concurrent updates without the overhead or abort risks of full serializability.
-
Handle Serializable Failures: If you select SERIALIZABLE, you must write retry loops in your application layer. Your code must catch the engine's serialization errors and automatically re-run the failed transaction block:
PythonMAX_RETRIES = 3 for attempt in range(MAX_RETRIES): try: with db.transaction(isolation="SERIALIZABLE"): execute_trade() break except SerializationFailure: if attempt == MAX_RETRIES - 1: raise time.sleep(exponential_backoff(attempt)) -
Keep Transactions Short: Long-running transactions prevent the database engine from cleaning up old versions of rows via vacuuming/undo log purge threads. This causes disk bloat and degrades read performance across the entire system.
Summary
ACID is not a binary toggle—it is a modular framework designed to balance data integrity against system performance:
-
Atomicity gives you clean failure recovery via Undo Logs and the WAL.
-
Consistency ensures your relational models enforce declared domain rules.
-
Isolation lets you control the trade-off between concurrency and correctness.
-
Durability ensures your committed operations survive infrastructure failures.
Designing reliable systems requires matching these mechanisms to your operational needs—leaning on lower isolation tiers where performance is critical, applying snapshot isolation for point-in-time auditing, and reserving strict serializability for updates where even minor race conditions could corrupt system state.




Responses 0
No responses yet. Yours would be the first.