ACID Properties in DBMS: The Foundations of Database Reliability
- Atomicity: all-or-nothing transactions implemented via WAL and undo logs. Without explicit BEGIN/COMMIT boundaries, auto-commit mode makes each statement its own transaction and a crash between statements leaves unrecoverable partial state.
- Consistency: the database moves from one valid state to another by enforcing all constraints. Application-level validation should catch business rule violations before they reach the database; constraints are the safety net, not the primary check.
- Isolation: a speed-versus-safety dial from READ COMMITTED (fast, allows non-repeatable reads) to SERIALIZABLE (safe, 20-40% slower). Default to READ COMMITTED and increase isolation only for specific transactions with demonstrated anomalies.
- ACID guarantees transactional reliability: Atomicity (all-or-nothing), Consistency (valid state transitions only), Isolation (concurrent transactions do not interfere), Durability (committed data survives crashes)
- Atomicity is implemented via Write-Ahead Log and undo segments — partial failures roll back completely without leaving orphaned state
- Isolation uses MVCC or locking to prevent dirty reads, lost updates, and phantom reads — the level you choose is a speed-versus-safety dial
- Durability flushes WAL to disk via fsync before COMMIT returns — this is the most expensive operation in any database write path
- Setting synchronous_commit=off in PostgreSQL boosts throughput 5-10x but risks losing up to 600ms of committed data on crash — never acceptable for financial data
- The biggest production mistake: assuming higher isolation levels are always safer — SERIALIZABLE can cut throughput by 40% without preventing anything that READ COMMITTED with proper application logic would not handle
Transaction stuck in 'idle in transaction' state — holding locks, blocking other queries
SELECT pid, now() - xact_start AS duration, state, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY duration DESC;SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - xact_start > interval '5 minutes';Deadlock detected in application logs — transactions are mutually blocking each other
SELECT
blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_catalog.pg_locks bl
JOIN pg_catalog.pg_stat_activity blocked
ON blocked.pid = bl.pid
JOIN pg_catalog.pg_locks bbl
ON bbl.locktype = bl.locktype
AND bbl.relation = bl.relation
AND bbl.pid != bl.pid
JOIN pg_catalog.pg_stat_activity blocking
ON blocking.pid = bbl.pid
WHERE NOT bl.granted;SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;WAL disk usage growing uncontrollably — disk space alerts firing on the WAL partition
SELECT slot_name, active,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
) AS retained_wal
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;SELECT pg_size_pretty(sum(size)) AS total_wal_size
FROM pg_ls_waldir();Production Incident
Production Debug GuideDiagnosing transactional failures in production PostgreSQL and MySQL systems. These symptoms appear silently and compound over time.
pg_current_wal_lsn() and compare against the last flushed position.now() - pg_last_xact_replay_timestamp() AS replication_lag. If lag is the cause, either route critical reads to the primary or wait for the replica to catch up before serving the response. If using MVCC on the primary, check whether VACUUM is running — excessive dead tuples from un-vacuumed tables can cause reads to see outdated row versions. Run SELECT relname, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10 to identify bloated tables.ACID is the set of properties that makes a database trustworthy. Without ACID, a power outage at the wrong millisecond could leave your data in a corrupted, half-updated state — a nightmare for financial records, medical systems, or any application where partial state is not just wrong but dangerous. Imagine a bank transfer: if the system fails after debiting your account but before crediting the recipient, that money vanishes. Not temporarily missing, not in a reconciliation queue — gone. ACID prevents this.
These four properties work in tandem, implemented through sophisticated internal mechanisms like transaction logs, locking protocols, and multi-version concurrency control. As a developer, understanding ACID is not about passing interviews. It is about understanding the performance trade-offs you make every time you choose an isolation level, decide between a traditional RDBMS and a relaxed NoSQL system, or configure how aggressively your database flushes data to disk.
A common misconception is that ACID is binary — either a database has it or it does not. In reality, every property has configurable levels that trade safety for throughput. PostgreSQL lets you trade durability for speed with synchronous_commit. MySQL lets you choose between InnoDB's row-level MVCC and MyISAM's table-level locking. The properties exist on a spectrum, and production tuning means knowing which dial to turn, how far to turn it, and what breaks if you go too far.
Atomicity — The All-or-Nothing Guarantee
Atomicity ensures that a transaction is treated as a single, indivisible unit. If any part of the transaction fails — a constraint violation, a timeout, a crash, an OOM kill — the entire operation is rolled back, leaving the database in the exact state it was in before the transaction started. There is no partial commit. There is no state where half the work is done and the other half is not.
The primary implementation mechanism is the Write-Ahead Log (WAL). Before any data pages are modified on disk, the intended changes are appended sequentially to the WAL. If a crash occurs mid-transaction, the database reads the WAL during recovery and identifies any transaction that started but never committed. Those transactions are rolled back — their changes are undone as if they never happened.
In MySQL/InnoDB, atomicity uses the undo log in addition to the redo log. When a transaction modifies a row, the original value is copied to the undo log before the change is applied to the data page. If ROLLBACK is issued — or if the server crashes before COMMIT — the database replays the undo log to restore every modified row to its pre-transaction state.
The critical production insight is this: atomicity only exists within explicit transaction boundaries. If you run 10,000 SQL statements in auto-commit mode, each statement is its own transaction. A crash at statement 4,700 means 4,699 statements are permanently committed and 5,301 never happened — with no mechanism to identify or recover the boundary. The $47,000 incident described above is exactly this failure mode.
SAVEPOINT extends atomicity within a transaction. It creates a named checkpoint inside a running transaction. If a subsequent operation fails, you can ROLLBACK TO SAVEPOINT to undo only the work after the savepoint, while preserving everything before it. This is essential for long-running batches where reprocessing everything from scratch is not acceptable.
-- ============================================================ -- Atomic E-commerce Order Flow -- All three operations succeed together or none of them persist. -- A crash between statement 2 and statement 3 rolls back everything. -- ============================================================ BEGIN; -- Step 1: Create the order header INSERT INTO io_thecodeforge.orders (customer_id, status, total_amount) VALUES (1024, 'PENDING_PAYMENT', 599.00); -- Step 2: Reserve inventory — the heavy, contention-prone operation -- The WHERE clause prevents negative stock without a CHECK constraint race UPDATE io_thecodeforge.inventory SET stock_count = stock_count - 1 WHERE product_id = 'SKU-99' AND stock_count > 0; -- Verify the UPDATE actually affected a row (stock was available) -- If 0 rows affected, the product is out of stock — abort the transaction -- Application code checks the affected row count here -- Step 3: Record the payment intent INSERT INTO io_thecodeforge.payments (order_id, provider, amount, status) VALUES (currval('orders_order_id_seq'), 'STRIPE', 599.00, 'INITIATED'); -- If ANY constraint violation occurs (FK, CHECK, UNIQUE), -- the entire transaction is rolled back — no orphaned order, -- no phantom inventory decrement, no unmatched payment record. COMMIT; -- ============================================================ -- SAVEPOINT example for batch processing -- Enables partial rollback without losing the entire batch -- ============================================================ BEGIN; -- Process first 100 records SAVEPOINT batch_checkpoint_100; -- ... 100 INSERT/UPDATE statements ... -- Process records 101-200 SAVEPOINT batch_checkpoint_200; -- ... 100 INSERT/UPDATE statements ... -- Record 157 fails with a constraint violation -- Roll back ONLY to the last checkpoint — records 1-100 survive ROLLBACK TO SAVEPOINT batch_checkpoint_100; -- Resume processing from record 101 with corrected data -- ... retry logic here ... COMMIT; -- Everything that was not rolled back is now durable
UPDATE 1
INSERT 0 1
COMMIT
SAVEPOINT batch_checkpoint_100
SAVEPOINT batch_checkpoint_200
ROLLBACK TO SAVEPOINT
COMMIT
- WAL records the intent before the change is applied to data pages — crash recovery replays or undoes based on COMMIT status.
- Undo log (InnoDB) or rollback segments store the original row values — ROLLBACK restores them atomically.
- SAVEPOINT creates named checkpoints inside a transaction — partial rollback without losing all prior work.
- Without explicit BEGIN/COMMIT boundaries, auto-commit mode means each statement is its own transaction — a crash between statements leaves an unrecoverable partial state.
- OOM kills, SIGKILL, and hardware failures do not trigger application-level rollback logic. Only database-level transaction boundaries provide crash safety.
Consistency — The Valid State Machine
Consistency ensures that a transaction brings the database from one valid state to another valid state, respecting every defined rule — constraints, triggers, cascades, and domain invariants. If a transaction would violate any constraint (foreign key, unique, check, not null), the database rejects the entire transaction. The database never persists an invalid state, even if the application code attempts to create one.
Consistency is the property that ties the other three together. Atomicity ensures no partial transaction leaks invalid intermediate state. Isolation ensures concurrent transactions do not create invalid combinations of values that no single transaction would produce. Durability ensures the valid state, once committed, persists through crashes. Without any one of the other three, consistency cannot be guaranteed.
There are two kinds of consistency that matter in practice. Schema-level consistency is enforced by database constraints: foreign keys prevent orphaned references, unique constraints prevent duplicates, check constraints prevent domain violations like negative stock counts. Application-level consistency is enforced by business logic: a transfer must debit and credit the same amount, an order must have at least one line item, a subscription cancellation must trigger a prorated refund. The database cannot enforce application-level invariants automatically — those are the application developer's responsibility, backed by database constraints as a safety net.
In production, constraint violations surface as SQL errors that applications must handle gracefully. A common anti-pattern is catching constraint violations and retrying blindly without diagnosing why the violation occurred. A unique constraint violation on an idempotency key is normal and expected — the correct response is to return the existing record, not to retry with a new key. A foreign key violation is a bug — it means the application is referencing data that does not exist, and retrying will produce the same failure.
-- ============================================================ -- Consistency Enforcement — Constraints as Safety Nets -- The database rejects any transaction that would create invalid state. -- ============================================================ -- 1. Foreign Key: an order cannot reference a non-existent customer -- This prevents orphaned records that break JOIN integrity. INSERT INTO io_thecodeforge.orders (customer_id, status, total_amount) VALUES (99999, 'PENDING', 100.00); -- ERROR: insert or update on table "orders" violates foreign key constraint -- "orders_customer_id_fkey" -- DETAIL: Key (customer_id)=(99999) is not present in table "customers". -- 2. Check Constraint: stock cannot go negative -- This catches the bug where two concurrent decrements both pass -- the application-level check but the net result goes below zero. ALTER TABLE io_thecodeforge.inventory ADD CONSTRAINT chk_stock_non_negative CHECK (stock_count >= 0); UPDATE io_thecodeforge.inventory SET stock_count = stock_count - 50 WHERE product_id = 'SKU-99' AND stock_count = 3; -- ERROR: new row for relation "inventory" violates check constraint -- "chk_stock_non_negative" -- DETAIL: Failing row contains (SKU-99, -47). -- 3. Unique Constraint with idempotency key: prevent duplicate payments -- This is how you make payment retries safe — the database enforces -- that each logical payment attempt can only succeed once. CREATE UNIQUE INDEX IF NOT EXISTS idx_payments_idempotency ON io_thecodeforge.payments (idempotency_key); -- First attempt — succeeds INSERT INTO io_thecodeforge.payments (idempotency_key, order_id, provider, amount, status) VALUES ('pay_abc123', 1024, 'STRIPE', 599.00, 'COMPLETED'); -- Retry with same idempotency key — safely ignored INSERT INTO io_thecodeforge.payments (idempotency_key, order_id, provider, amount, status) VALUES ('pay_abc123', 1024, 'STRIPE', 599.00, 'COMPLETED') ON CONFLICT (idempotency_key) DO NOTHING; -- INSERT 0 0 — no duplicate created, no error raised -- To return the existing record on conflict: INSERT INTO io_thecodeforge.payments (idempotency_key, order_id, provider, amount, status) VALUES ('pay_abc123', 1024, 'STRIPE', 599.00, 'COMPLETED') ON CONFLICT (idempotency_key) DO UPDATE SET status = io_thecodeforge.payments.status RETURNING *;
ERROR: CHECK violation stock_count=-47
INSERT 0 1 (first payment)
INSERT 0 0 (idempotent retry — no duplicate)
Isolation — Handling the Chaos of Concurrency
Isolation defines how and when the changes made by one transaction become visible to other concurrent transactions. Without isolation, the lost update anomaly occurs: two transactions read the same row, both compute a new value independently, and the second commit silently overwrites the first commit's changes. Neither transaction is aware that the other existed.
Databases solve concurrency problems using two fundamentally different approaches. Pessimistic locking (SELECT ... FOR UPDATE) acquires an exclusive lock on the row at read time — any other transaction that tries to read or write the same row blocks until the lock is released. This guarantees no lost updates but reduces concurrency. Optimistic concurrency (MVCC plus version columns) allows concurrent reads without blocking and detects conflicts at write time — if the row changed since you read it, your update fails and the application retries.
The SQL standard defines four isolation levels, from weakest to strongest:
READ UNCOMMITTED allows dirty reads — you can see uncommitted changes from other transactions. PostgreSQL silently promotes this to READ COMMITTED because dirty reads are almost never desirable.
READ COMMITTED is the PostgreSQL default. Each SQL statement within a transaction sees a fresh snapshot of committed data at the moment that statement begins. This prevents dirty reads but allows non-repeatable reads: if you read a row twice within the same transaction, you might get different values if another transaction committed a change between your two reads.
REPEATABLE READ takes a snapshot when the transaction starts and uses that snapshot for every statement within the transaction. This prevents both dirty reads and non-repeatable reads. In PostgreSQL, REPEATABLE READ also prevents phantom reads because MVCC snapshots are transaction-scoped. In MySQL/InnoDB, REPEATABLE READ uses gap locks to prevent phantom inserts, which makes it stricter than the SQL standard requires.
SERIALIZABLE provides full isolation — transactions execute as if they ran one at a time, sequentially. PostgreSQL implements this with Serializable Snapshot Isolation (SSI), which detects read-write dependencies and aborts transactions that would violate serializability. This is the safest level but reduces throughput by 20-40% under contention because conflicting transactions are aborted and must be retried.
-- ============================================================ -- Preventing the Lost Update Anomaly -- Two concurrent transactions try to update the same row. -- Without isolation controls, the second commit silently -- overwrites the first commit's changes. -- ============================================================ -- Approach 1: Pessimistic Locking — SELECT FOR UPDATE -- Acquires an exclusive row lock at read time. -- Any other transaction that tries to read this row FOR UPDATE -- will block until this transaction commits or rolls back. -- Transaction A: BEGIN; SELECT stock_count FROM io_thecodeforge.inventory WHERE product_id = 'SKU-99' FOR UPDATE; -- exclusive lock acquired on this row -- Returns stock_count = 10 UPDATE io_thecodeforge.inventory SET stock_count = stock_count - 1, updated_at = now() WHERE product_id = 'SKU-99'; -- stock_count is now 9 COMMIT; -- lock released, Transaction B can proceed -- Transaction B (concurrent): BEGIN; SELECT stock_count FROM io_thecodeforge.inventory WHERE product_id = 'SKU-99' FOR UPDATE; -- BLOCKS here until Transaction A commits -- After A commits, returns stock_count = 9 (the updated value) UPDATE io_thecodeforge.inventory SET stock_count = stock_count - 1 WHERE product_id = 'SKU-99'; -- stock_count is now 8 — CORRECT COMMIT; -- ============================================================ -- Approach 2: Optimistic Locking — Version Column -- No locks held during read. Conflict detected at write time. -- If someone else changed the row, the UPDATE affects 0 rows -- and the application retries. -- ============================================================ -- Add a version column to the table ALTER TABLE io_thecodeforge.inventory ADD COLUMN IF NOT EXISTS version INTEGER DEFAULT 1; -- Transaction A reads the row (no lock) SELECT stock_count, version FROM io_thecodeforge.inventory WHERE product_id = 'SKU-99'; -- Returns: stock_count=10, version=1 -- Transaction A writes with version check UPDATE io_thecodeforge.inventory SET stock_count = stock_count - 1, version = version + 1 WHERE product_id = 'SKU-99' AND version = 1; -- only succeeds if no one else changed it -- UPDATE 1 — success, version is now 2 -- Transaction B (concurrent) tries the same UPDATE io_thecodeforge.inventory SET stock_count = stock_count - 1, version = version + 1 WHERE product_id = 'SKU-99' AND version = 1; -- FAILS: version is now 2, not 1 -- UPDATE 0 — zero rows affected, application detects and retries -- ============================================================ -- Check current isolation level -- ============================================================ SHOW transaction_isolation; -- Default in PostgreSQL: read committed -- Set isolation level for a specific transaction BEGIN ISOLATION LEVEL REPEATABLE READ; -- ... your queries here ... COMMIT;
UPDATE 1
COMMIT
-- Transaction B blocks, then:
SELECT: stock_count=9
UPDATE 1
COMMIT
-- Optimistic locking:
UPDATE 1 (Transaction A succeeds)
UPDATE 0 (Transaction B detects conflict — application retries)
transaction_isolation: read committed
- READ COMMITTED: each statement sees a fresh snapshot of committed data. Fast. Allows non-repeatable reads within a transaction.
- REPEATABLE READ: the entire transaction sees one consistent snapshot from when it started. Prevents most anomalies in PostgreSQL. ~5-10% slower under contention.
- SERIALIZABLE: transactions execute as if they ran sequentially. Safest. 20-40% slower under contention because conflicting transactions are aborted and retried.
- MVCC (PostgreSQL): readers never block writers and writers never block readers — both work on different row versions simultaneously.
- Pessimistic locking (SELECT FOR UPDATE): blocks other transactions explicitly. Use when write conflicts are frequent and the cost of retrying a failed optimistic write is higher than the cost of blocking.
Durability — Survival After the Crash
Durability guarantees that once a transaction is committed, it remains committed — even if the server loses power one millisecond later, even if the kernel panics, even if the disk controller lies about having flushed its cache. This is the property that lets you show a user 'Payment Confirmed' and know that the confirmation is permanent.
The mechanism is straightforward in principle and expensive in practice. Before the database returns COMMIT to the client, it ensures the WAL record for that transaction has been physically written to non-volatile storage — not just to the OS page cache (which is volatile RAM), but all the way down to the disk platters or flash cells. This operation is called fsync, and it is the single most expensive operation in any database write path.
Each fsync takes 1-5 milliseconds on enterprise SSDs and 5-15 milliseconds on spinning disks. With synchronous_commit=on (the PostgreSQL default), every COMMIT waits for fsync to complete before returning to the client. This limits throughput to roughly 200-1,000 commits per second per disk on SSD, because each commit must wait for the physical write to complete.
Setting synchronous_commit=off changes the bargain dramatically. The database writes the WAL record to the OS page cache and returns COMMIT immediately, without waiting for fsync. The wal_writer background process flushes the page cache to disk every wal_writer_delay milliseconds (default 200ms). This batches multiple transactions' WAL records into a single fsync, boosting throughput to 5,000-10,000 commits per second — a 5-10x improvement.
The cost of that improvement is a durability window. If the server crashes within the wal_writer_delay window after a commit, any transactions that committed during that window but were not yet flushed to disk are lost. The data was in volatile RAM (the OS page cache), and volatile RAM does not survive power loss. In practice, you can lose up to roughly 3x the wal_writer_delay value because the wal_writer may not have completed even one flush cycle before the crash.
On restart, the database replays the WAL from the last confirmed flush point. Any committed transaction whose WAL record was flushed is recovered. Any committed transaction whose WAL record was only in the page cache is gone — permanently.
-- ============================================================ -- Durability Configuration — PostgreSQL -- synchronous_commit controls whether COMMIT waits for WAL fsync -- ============================================================ -- Check current durability setting SHOW synchronous_commit; -- Default: on (full durability — COMMIT waits for fsync) -- Check current WAL position SELECT pg_current_wal_lsn() AS current_wal_position; -- Check WAL file currently being written SELECT pg_walfile_name(pg_current_wal_lsn()) AS current_wal_file; -- ============================================================ -- Performance vs Safety Trade-off -- ============================================================ -- OPTION 1: Full durability (default) — for financial data -- Every COMMIT waits for fsync. Throughput: ~200-1000 TPS on SSD. SET synchronous_commit = on; -- Use this for: payments, account balances, audit trails, medical records -- Rule: if the user sees a success confirmation, the data must be durable. -- OPTION 2: Relaxed durability — for analytics and session data -- COMMIT returns before fsync. Throughput: ~5000-10000 TPS on SSD. -- Risk: lose up to wal_writer_delay (default 200ms) of committed data on crash. SET synchronous_commit = off; -- Use this for: analytics events, session caches, non-critical logs -- NEVER use for: payments, balances, anything a user has seen confirmed -- OPTION 3: Per-transaction durability — the production sweet spot -- Default to 'on', relax for specific non-critical writes BEGIN; SET LOCAL synchronous_commit = off; -- only affects THIS transaction INSERT INTO io_thecodeforge.analytics_events (event_type, payload) VALUES ('page_view', '{"url": "/product/42"}'); COMMIT; -- returns immediately, WAL flushed asynchronously BEGIN; -- synchronous_commit is back to 'on' for this transaction INSERT INTO io_thecodeforge.payments (order_id, amount, status) VALUES (1024, 599.00, 'COMPLETED'); COMMIT; -- waits for fsync — data is durable before returning -- ============================================================ -- Monitor WAL performance in production -- ============================================================ SELECT stats_reset, wal_records, wal_bytes, wal_write, wal_sync, wal_write_time, wal_sync_time FROM pg_stat_wal; -- Monitor COMMIT latency (requires pg_stat_statements extension) SELECT query, calls, round(mean_exec_time::numeric, 2) AS avg_ms, round(total_exec_time::numeric, 2) AS total_ms FROM pg_stat_statements WHERE query ILIKE '%COMMIT%' ORDER BY total_exec_time DESC LIMIT 5;
current_wal_position: 0/16B3F60
current_wal_file: 000000010000000000000001
SET LOCAL synchronous_commit = off
INSERT 0 1
COMMIT (immediate — no fsync wait)
INSERT 0 1
COMMIT (waited 2.3ms for fsync)
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Implementation Mechanism | Throughput Impact |
|---|---|---|---|---|---|
| READ UNCOMMITTED | Possible in theory — PostgreSQL promotes to READ COMMITTED | Possible | Possible | MVCC snapshot per statement (identical to READ COMMITTED in PostgreSQL) | Baseline — fastest possible |
| READ COMMITTED (PostgreSQL default) | Prevented — each statement sees only committed data | Possible — two reads of the same row in one transaction may return different values | Possible — new rows inserted by other transactions become visible between statements | MVCC snapshot taken at the start of each statement, not the transaction | Minimal overhead — the sweet spot for 90% of workloads |
| REPEATABLE READ | Prevented | Prevented — the entire transaction sees one consistent snapshot | Prevented in PostgreSQL (MVCC snapshot is transaction-scoped). MySQL/InnoDB uses gap locks. | MVCC snapshot taken at the start of the transaction and held for its entire duration | ~5-10% slower under contention due to increased serialization failures that require retry |
| SERIALIZABLE | Prevented | Prevented | Prevented | Serializable Snapshot Isolation (SSI) with predicate locking — detects read-write dependencies | ~20-40% slower under contention — conflicting transactions are aborted and must be retried by the application |
🎯 Key Takeaways
- Atomicity: all-or-nothing transactions implemented via WAL and undo logs. Without explicit BEGIN/COMMIT boundaries, auto-commit mode makes each statement its own transaction and a crash between statements leaves unrecoverable partial state.
- Consistency: the database moves from one valid state to another by enforcing all constraints. Application-level validation should catch business rule violations before they reach the database; constraints are the safety net, not the primary check.
- Isolation: a speed-versus-safety dial from READ COMMITTED (fast, allows non-repeatable reads) to SERIALIZABLE (safe, 20-40% slower). Default to READ COMMITTED and increase isolation only for specific transactions with demonstrated anomalies.
- Durability: committed data survives crashes because WAL is flushed to disk via fsync before COMMIT returns. synchronous_commit=off trades durability for throughput — acceptable for analytics, never for financial data.
- Every ACID property has a configurable dial. Turning durability down (synchronous_commit=off) or isolation down (READ COMMITTED) is a legitimate production optimization — but only when the business requirement explicitly permits it and the decision is documented in code.
- CAP theorem governs distributed systems: during a network partition, choose between consistency and availability. ACID focuses on local integrity within a single database. BASE trades consistency for availability in distributed systems. The choice depends on the business requirement, not on technology preference.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QExplain the Lost Update anomaly. How would you solve it in a Spring Boot application using @Version (Optimistic Locking) versus SELECT FOR UPDATE (Pessimistic Locking)?Mid-levelReveal
- QHow does the database ensure Consistency when a transaction violates a Foreign Key constraint? Does it roll back the entire transaction or just the offending statement?Mid-levelReveal
- QDescribe a scenario where READ COMMITTED isolation would cause an application bug that REPEATABLE READ would prevent.Mid-levelReveal
- QWhat is the performance impact of setting synchronous_commit=off in PostgreSQL? What specific ACID property are you compromising, and when is this trade-off acceptable?SeniorReveal
- QWhy do many NoSQL databases adopt BASE instead of ACID? When would you choose BASE over ACID, and what specific guarantees are you giving up?SeniorReveal
Frequently Asked Questions
How does PostgreSQL implement MVCC (Multi-Version Concurrency Control)?
PostgreSQL implements MVCC by keeping multiple physical versions of the same logical row simultaneously. Every row has two hidden system columns: xmin (the transaction ID that created this version) and xmax (the transaction ID that deleted or replaced this version, or zero if the row is still current). When you run a query, PostgreSQL compares your transaction's snapshot against these xmin and xmax values to determine which version of each row was visible at your snapshot point. This is why readers never block writers in PostgreSQL — the reader sees the old version of the row while the writer creates a new version. Both operate on different physical tuples without any locking conflict. The old versions (dead tuples) are eventually cleaned up by the VACUUM process, which reclaims the space they occupied.
What is the difference between ACID and the CAP theorem?
ACID and CAP address different scopes and different problems. ACID describes the internal properties of transactions within a single logical database — it is about integrity guarantees for individual operations. CAP describes the fundamental trade-offs in distributed systems — it is about how data behaves when it is replicated across multiple nodes connected by a network that can partition. You can have a fully ACID-compliant database running on a single node with no CAP concerns. The moment you shard or replicate that database across a network, CAP dictates that during a network partition, you must choose between Consistency (all nodes see the same data simultaneously) and Availability (every request receives a response even if some nodes are unreachable). ACID and CAP are not alternatives — they operate at different architectural layers.
What is a Write-Ahead Log (WAL) and why is it faster than writing directly to data files?
A WAL is a sequential, append-only log file where the database records the intent of every data modification before applying it to the actual data files. Writing to data files is random I/O — the database must seek to different physical locations on disk to update specific rows in different tables and pages. WAL is sequential I/O — it simply appends each new record to the end of a single file. Sequential writes are dramatically faster than random writes on both SSDs and spinning disks because they avoid seek latency and can fully utilize the storage device's write bandwidth. By writing the intent to the WAL first and fsyncing it, the database achieves durability quickly. The actual data files (heap pages, index pages) are updated later in the background by the checkpoint process, which can batch and optimize the random writes without any time pressure.
Can I use synchronous_commit=off on a read replica?
synchronous_commit controls whether the primary server waits for its own WAL to be flushed to its own disk before returning COMMIT to the client. This setting is irrelevant on read replicas because replicas do not accept commits — they replay WAL records received from the primary. What matters on replicas is different: max_standby_streaming_delay controls how far behind the replica can fall before it starts cancelling long-running queries that conflict with incoming WAL replay. hot_standby_feedback tells the primary that the replica still needs certain row versions, preventing the primary's VACUUM from removing rows the replica's queries are reading. Monitor replication lag with: SELECT now() - pg_last_xact_replay_timestamp() AS lag. If lag exceeds your application's staleness tolerance, route critical reads to the primary.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.