Database Transactions & ACID: What Every Engineer Must Know
Deep dive into ACID properties, isolation levels, deadlocks, and how to write safe transactional code in production systems.
Database transactions are the backbone of data integrity. Yet many engineers use them without truly understanding what guarantees they provide — or don’t provide.
What Is a Transaction?
A transaction is a unit of work that is either fully committed or fully rolled back. It’s the database’s promise: “either all of this happens, or none of it.”
If the server crashes after line 2 but before COMMIT, neither update persists.
ACID Properties Explained
Atomicity — All or Nothing
Every statement in a transaction succeeds together or fails together. No partial updates.
Consistency — Rules Always Hold
After a transaction completes, all database constraints (foreign keys, NOT NULL, CHECK) must be satisfied. The database moves from one valid state to another.
Isolation — Concurrent Transactions Don’t Interfere
This is the most nuanced property. Isolation levels define how much one transaction can “see” of another in-progress transaction.
Durability — Committed Data Survives Failures
Once COMMIT returns, the data is written to disk (via WAL — Write-Ahead Log). A server crash won’t lose it.
Isolation Levels — The Real Complexity
PostgreSQL’s four isolation levels and the anomalies they prevent:
| Level | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | Possible | Possible | Possible |
| Read Committed (default) | Prevented | Possible | Possible |
| Repeatable Read | Prevented | Prevented | Possible |
| Serializable | Prevented | Prevented | Prevented |
Read Committed (Default) — The Practical Choice
Transaction A sees different values for the same row in the same transaction. This is fine for most reads, problematic for calculations.
Repeatable Read — Snapshot Consistency
PostgreSQL uses MVCC (Multi-Version Concurrency Control) to serve the snapshot without blocking writers.
Serializable — The Safest, Slowest
Use serializable for financial calculations, inventory deductions, or any place where phantom reads cause incorrect results.
Deadlocks — When Transactions Block Each Other
Prevention strategy: always acquire locks in a consistent order.
Transactional Code in Go
Key patterns:
defer tx.Rollback()— defensive cleanup even on panicFOR UPDATE— explicit row-level lockingBeginTxwith explicit isolation level
Savepoints — Nested Rollbacks
Savepoints let you roll back part of a transaction without losing everything.
Common Mistakes
- Opening transactions too wide — long-running transactions hold locks and block other writers
- Ignoring
Rollbackerrors — always check and log them - Using Read Committed for financial math — use Repeatable Read or Serializable
- Not handling serialization failures — Serializable transactions can fail; retry them
Transactions are your first line of defense against data corruption. Understand the isolation level you need, acquire locks in a consistent order, and keep transactions short.