ACID is a set of four properties that define what a reliable database transaction must guarantee: Atomicity, Consistency, Isolation, and Durability. The acronym is decades old, but its implications are still misunderstood — especially as distributed databases and ORMs abstract away the details. Getting ACID wrong is how you get double-charges, lost writes, and phantom inventory counts in production.
What changed in 2026
- Serializable isolation became practical. PostgreSQL's Serializable Snapshot Isolation (SSI) has been production-grade for years, and CockroachDB uses it as the default. More teams are now running serializable where they previously accepted weaker levels.
- Distributed ACID went mainstream. CockroachDB, Spanner, YugabyteDB, and TiDB all offer full ACID across nodes with multi-region support — no longer a research achievement.
- ORMs expose isolation mismatches. Prisma and SQLAlchemy 2.x now make isolation level configuration explicit in their API, reducing accidental weak-isolation deployments.
- LLM-assisted SQL review flags missing transactions. Code review tools using Claude/GPT-class models flag multi-step database operations not wrapped in a transaction.
The four properties
Atomicity
All operations in a transaction succeed together or all fail together. There is no partial success.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- debit
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- credit
COMMIT;
-- If the second UPDATE fails, the first is rolled back automatically
Without atomicity, a crash between the debit and credit would leave money missing.
Consistency
A transaction brings the database from one valid state to another. Constraints (NOT NULL, FOREIGN KEY, CHECK) are enforced at commit time.
Consistency is partially a database responsibility (enforcing constraints) and partially an application responsibility (writing correct business logic).
Isolation
Concurrent transactions should not interfere with each other. The degree of interference permitted is controlled by the isolation level.
Durability
Once a transaction commits, the data survives crashes, power loss, and restarts. PostgreSQL achieves this via the write-ahead log (WAL) — the change is written to disk before the commit acknowledgment.
Isolation levels compared
| Level |
Dirty Read |
Non-repeatable Read |
Phantom Read |
Lost Update |
| Read Uncommitted |
Possible |
Possible |
Possible |
Possible |
| Read Committed (PG default) |
Prevented |
Possible |
Possible |
Possible |
| Repeatable Read |
Prevented |
Prevented |
Possible in SQL; prevented in PG |
Prevented in PG |
| Serializable |
Prevented |
Prevented |
Prevented |
Prevented |
PostgreSQL's implementation of Repeatable Read and Serializable is stronger than the SQL standard requires — Postgres uses snapshot isolation and SSI.
-- Set isolation level for a transaction
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 1; -- snapshot is taken here
-- another transaction cannot change this row and commit before we do
COMMIT;
How to pick the right isolation level
- Read Committed — safe default for most OLTP. Simple reads and writes with no multi-step logic.
- Repeatable Read — use when a transaction reads the same row multiple times and needs consistency (e.g., computing a running total).
- Serializable — use when correctness requires that transactions appear to run one-at-a-time: inventory reservation, seat booking, financial double-entry.
# SQLAlchemy 2.x — serializable transaction
from sqlalchemy import text
with engine.begin() as conn:
conn.execute(text("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"))
balance = conn.scalar(text("SELECT balance FROM accounts WHERE id = :id"), {"id": 1})
if balance >= 100:
conn.execute(text("UPDATE accounts SET balance = balance - 100 WHERE id = :id"), {"id": 1})
Common mistakes
Using Read Committed for financial operations. Two concurrent transactions both reading a balance of $200, both deciding to spend $150, and both succeeding — because neither sees the other's uncommitted write. This is a lost update.
Long-running transactions. A transaction open for minutes holds row locks (or snapshot overhead) that block other writers. Keep transactions short and commit early.
Not handling serialization failures. Serializable transactions can fail with ERROR: could not serialize access due to concurrent update. Your application must catch 40001 error codes and retry.
import psycopg2
from psycopg2 import OperationalError
for attempt in range(3):
try:
with conn: # auto-commit/rollback
# ... transaction logic
break
except OperationalError as e:
if e.pgcode == "40001": # serialization failure
continue
raise
Assuming ORMs wrap individual queries in transactions. By default, Prisma auto-commits each operation. Multi-step operations must be explicitly wrapped in prisma.$transaction([...]).
What to skip
- Distributed sagas for simple operations — a single-node ACID transaction is simpler and safer than a saga pattern when all your data lives in one database.
- Compensating transactions as a first resort — prefer serializable ACID; add sagas only when the transaction must span external systems.
- Disabling fsync for "performance" — this breaks Durability entirely and risks data corruption on crash.
FAQ
Does PostgreSQL support full ACID?
Yes. PostgreSQL provides full ACID with Serializable Snapshot Isolation as the strongest isolation level, and WAL-based durability.
Is MongoDB ACID?
MongoDB 4.0+ supports multi-document ACID transactions within a replica set. Cross-shard transactions are supported but slower. Single-document operations have always been atomic.
What is the difference between a transaction and a savepoint?
A savepoint marks a point within a transaction you can roll back to without aborting the entire transaction. Useful for nested operations where partial rollback is acceptable.
How do distributed databases implement ACID across nodes?
Using protocols like Two-Phase Commit (2PC) or variants like Percolator (used by TiDB/CockroachDB). These add network round-trips to the commit path, increasing latency compared to single-node commits.
Where to go next