Mid-level 4 min · March 17, 2026

REPEATABLE READ Phantom Inventory — DBMS Transactions

REPEATABLE READ does not prevent phantom reads — new inserts appear in range scans, causing $50k stock discrepancy.

N
Naren Founder & Principal Engineer

20+ years shipping production systems from the metal up. Everything here is grounded in real deployments.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • ACID defines the contract: Atomicity, Consistency, Isolation, Durability — violations cause silent data corruption
  • Isolation levels trade safety for throughput: READ COMMITTED is the production default for most web apps
  • MVCC keeps reads from blocking writes, but phantom reads still sneak through in REPEATABLE READ
  • Deadlocks are an application design failure, not a database bug — always lock resources in a consistent order
  • The biggest mistake: assuming your app's isolation level matches the database default (they often don't)
✦ Definition~90s read
What is Transactions in DBMS?

REPEATABLE READ is an SQL isolation level that guarantees that if you read the same row twice within a transaction, you'll get the same data — no dirty reads, no non-repeatable reads. But it does NOT prevent phantom reads: new rows inserted by other transactions that match your query's WHERE clause can appear between your reads.

Think of a transaction like a bank transfer.

This is the 'phantom inventory' problem — a classic gotcha in DBMS transactions where your inventory count changes mid-transaction because REPEATABLE READ locks existing rows but not the gap where new rows can be inserted. PostgreSQL's REPEATABLE READ actually prevents phantoms via snapshot isolation (MVCC), but MySQL's InnoDB under REPEATABLE READ uses next-key locking to block them — so the behavior varies by database.

The practical consequence: if you're building an inventory system and rely on REPEATABLE READ to prevent phantom inserts, you'll get burned on MySQL unless you use SERIALIZABLE or explicit gap locks, while PostgreSQL handles it transparently. This is why understanding your specific DBMS's implementation of isolation levels — not just the SQL standard — is critical for transaction correctness in production systems handling concurrent writes.

Plain-English First

Think of a transaction like a bank transfer. You want to send $100 from your account to a friend's. The database must either do both steps (deduct and add) or none. If the server dies after deducting but before adding, your money shouldn't vanish. That's ACID in action.

Why REPEATABLE READ Still Lets Phantom Inventory Slip Through

REPEATABLE READ is a transaction isolation level that guarantees that any row read during a transaction will appear unchanged on subsequent reads within the same transaction. The core mechanic is that it locks every row the transaction touches (via shared or exclusive locks) and holds those locks until commit. This prevents dirty reads and non-repeatable reads — but it does not prevent phantom reads. A phantom read occurs when a new row inserted by another transaction matches the WHERE clause of a query you already executed. Because REPEATABLE READ locks only existing rows, not the gap where a new row could appear, phantoms are possible. In practice, this means that if you run SELECT ... WHERE quantity > 0 twice in the same transaction, the second query may return rows that weren't there the first time. This is a direct consequence of the isolation level's lock granularity: row locks, not predicate locks. Most databases implement REPEATABLE READ with snapshot isolation (e.g., PostgreSQL, Oracle) or with lock-based semantics (e.g., MySQL InnoDB). Under snapshot isolation, you get a consistent view of the database as of the transaction start — which actually prevents phantoms for reads, but not for write skew or write conflicts. Under lock-based REPEATABLE READ, phantoms are real and can corrupt inventory counts, reservation systems, or any logic that assumes a stable result set. Use REPEATABLE READ when you need to read the same rows multiple times and ensure they haven't changed, but you can tolerate new rows appearing. For inventory systems where phantom inserts would break business logic (e.g., overselling stock), you must escalate to SERIALIZABLE or use explicit gap locks (e.g., SELECT ... FOR UPDATE with a range condition).

Phantom ≠ Non-Repeatable Read
Non-repeatable read is a row that changed; phantom read is a new row that appears. REPEATABLE READ prevents the first, not the second.
Production Insight
E-commerce inventory reservation: two concurrent transactions read available stock (both see 1 item), each decrements and commits — overselling by 1.
Symptom: order confirmation shows 'in stock' but fulfillment finds zero inventory.
Rule: if your WHERE clause is a range or predicate, REPEATABLE READ is not enough — use SERIALIZABLE or explicit gap locks.
Key Takeaway
REPEATABLE READ locks rows, not gaps — phantoms are possible.
Snapshot isolation (PostgreSQL, Oracle) hides phantoms from reads but not from write conflicts.
For inventory or reservation systems, escalate to SERIALIZABLE or use SELECT FOR UPDATE with gap locks.
REPEATABLE READ Phantom Inventory — DBMS Transactions THECODEFORGE.IO REPEATABLE READ Phantom Inventory — DBMS Transactions Isolation levels, MVCC, deadlocks, and write skew in practice REPEATABLE READ Prevents non-repeatable reads, allows phantoms MVCC Snapshot Reads consistent snapshot, ignores concurrent inserts Phantom Inventory New rows appear in range queries across transactions Write Skew Concurrent updates based on stale snapshot cause anomalies Deadlock Detection Cycle detection or timeout to resolve contention SERIALIZABLE True isolation via predicate locks or SSI ⚠ REPEATABLE READ does not prevent phantom rows Use SERIALIZABLE or explicit range locks for inventory consistency THECODEFORGE.IO
thecodeforge.io
REPEATABLE READ Phantom Inventory — DBMS Transactions
Transactions Dbms

ACID in Practice: Beyond the Acronym

While every textbook defines ACID, seeing it handle a failure in real-time clarifies why we pay the performance tax for a relational engine. Atomicity is the 'all-or-nothing' guarantee, but Durability is the 'promise'—ensuring that once a COMMIT is acknowledged, even a total power failure won't revert the change.

ExampleSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- io.thecodeforge: Atomic Bank Transfer Example
BEGIN;

-- Step 1: Deduct from source
UPDATE io_thecodeforge.accounts 
SET balance = balance - 500, updated_at = NOW() 
WHERE account_id = 'ACC-001' AND balance >= 500;

-- Step 2: Add to destination
UPDATE io_thecodeforge.accounts 
SET balance = balance + 500, updated_at = NOW() 
WHERE account_id = 'ACC-002';

-- If a network timeout or constraint violation occurs here,
-- the entire block is invalidated.
COMMIT;

-- PRO TIP: In Spring Boot, use @Transactional(propagation = Propagation.REQUIRED)
-- to wrap these calls in a single proxy-managed transaction.
Output
Query OK, 2 rows affected (0.01 sec)
Transaction committed successfully.
The Outbox Pattern Rescue
If you publish a message inside a transaction, and the transaction rolls back, the message is still sent. Use the transactional outbox pattern: insert the message into a separate table within the same transaction, then have a background process read and send it.
Production Insight
The most common ACID failure in production is forgetting that Atomicity applies only to database operations.
External side effects (HTTP calls, message queues) are not rolled back — you need the Outbox Pattern for that.
Rule: your transaction scope must include only database work, or use a compensating transaction mechanism.
Key Takeaway
ACID is not magic — it guarantees database consistency within boundaries you define.
Atomicity does not extend to side effects outside the database.
Always design for rollback by cleaning up partial work in the application layer.

Isolation Levels and Read Phenomena

Isolation levels are a slider between 'Perfect Safety' and 'Maximum Speed.' As you lower the level, you introduce specific anomalies: 1. Dirty Reads: Reading data that hasn't been committed yet. 2. Non-Repeatable Reads: A row changes its value while you're still looking at it. 3. Phantom Reads: New rows appear in a range query because another transaction inserted them.

ExampleSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- io.thecodeforge: Demonstrating Read Phenomena

-- 1. READ COMMITTED (Default for Postgres)
-- Prevents Dirty Reads but allows Non-Repeatable Reads.
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 2. REPEATABLE READ (Default for MySQL InnoDB)
-- Uses MVCC (Multi-Version Concurrency Control) to ensure you see a snapshot.
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 3. SERIALIZABLE (Highest Safety)
-- Prevents Phantoms by placing range locks or using predicate locking.
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Production Pattern: Use 'SELECT ... FOR UPDATE' to skip isolation logic
-- and force a row-level write lock immediately.
BEGIN;
SELECT balance FROM io_thecodeforge.accounts WHERE account_id = 'ACC-001' FOR UPDATE;
-- This row is now locked until we COMMIT.
Output
Transaction isolation level set to REPEATABLE READ.
Production Insight
Phantom reads are the silent killer of reporting accuracy.
Non-repeatable reads cause inconsistent lookups (e.g., fetching the same row twice in a transaction yields different values).
Rule: don't assume REPEATABLE READ is enough for range queries — test with concurrent inserts.
Key Takeaway
Isolation levels prevent specific anomalies — know which ones apply to your use case.
READ COMMITTED prevents dirty reads but allows non-repeatable and phantom reads.
SERIALIZABLE prevents all three but reduces concurrency — use it for reports, not every request.
Choosing an Isolation Level
IfRead-only, tolerance for stale data
UseUse READ UNCOMMITTED / READ COMMITTED
IfTransactional writes with moderate contention
UseUse READ COMMITTED (default for PostgreSQL)
IfNeed consistent view within a transaction, no phantoms
UseUse REPEATABLE READ + explicit range locks
IfAbsolutely no anomalies allowed, low concurrency expected
UseUse SERIALIZABLE

Deadlocks: Detection and Prevention

A deadlock occurs when Transaction A holds Lock 1 and wants Lock 2, while Transaction B holds Lock 2 and wants Lock 1. They will wait forever. Most modern RDBMS (Postgres, MySQL) run a background 'Deadlock Detector' thread that identifies these cycles and kills the 'cheapest' transaction to break the loop.

ExampleSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- io.thecodeforge: Deadlock Prevention via Deterministic Ordering

-- BAD: Locking in random order based on app logic
-- TX1: Update A, then B
-- TX2: Update B, then A  <-- High Deadlock Risk!

-- GOOD: Always sort IDs before locking
BEGIN;
-- By ordering our IDs, all transactions wait in the same 'queue'
SELECT * FROM io_thecodeforge.accounts 
WHERE account_id IN ('ACC-001', 'ACC-002') 
ORDER BY account_id ASC 
FOR UPDATE;

UPDATE io_thecodeforge.accounts SET balance = balance - 100 WHERE account_id = 'ACC-001';
UPDATE io_thecodeforge.accounts SET balance = balance + 100 WHERE account_id = 'ACC-002';
COMMIT;
Output
Rows locked in ascending order. No deadlock cycle possible.
Deadlock as a Traffic Circle
  • Each transaction (car) holds a resource (position in the roundabout).
  • Each wants another resource that is held by another car.
  • The database traffic cop (deadlock detector) picks one car to reverse out.
  • That transaction is rolled back and must retry.
  • You can avoid the gridlock by enforcing a consistent direction of travel (lock order).
Production Insight
Deadlocks are often an application design failure, not a database config problem.
The classic fix: order your lock acquisition (e.g., always process accounts by ID ascending).
Rule: if you see recurring deadlocks, fix the code — don't just increase lock timeout.
Key Takeaway
Deadlocks are inevitable in high-concurrency systems — your app must handle retries.
Standardize lock ordering across all code paths.
If you use serializable isolation, be prepared for serialization failures (40001) and retry.

MVCC: How Isolation Levels Actually Work

Multi-Version Concurrency Control (MVCC) allows each transaction to see a snapshot of the data at the start of the transaction (or at the statement level, depending on isolation). Instead of locking every row a reader might touch, the database keeps multiple versions of each row. When a transaction writes, it creates a new version; concurrent readers see the old version until the writer commits. This is why PostgreSQL can run thousands of concurrent SELECTs without blocking — readers never block writers, and writers never block readers (unless they conflict).

ExampleSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- io.thecodeforge: MVCC Snapshot Demonstration (PostgreSQL)

-- Session 1: Start a repeatable read transaction
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM io_thecodeforge.accounts WHERE account_id = 'ACC-001';
-- Returns 1000

-- Session 2 (concurrent): Update and commit
BEGIN;
UPDATE io_thecodeforge.accounts SET balance = 800 WHERE account_id = 'ACC-001';
COMMIT;

-- Session 1: Still sees 1000 (its snapshot is from the start)
SELECT balance FROM io_thecodeforge.accounts WHERE account_id = 'ACC-001';
-- Returns 1000

COMMIT;

-- After Session 1 commits, new queries see 800.
Output
REPEATABLE READ snapshot isolation works as expected.
Production Insight
MVCC works beautifully until you have long-running write transactions that accumulate many old row versions.
Vacuum is essential: in PostgreSQL, autovacuum must keep up with transaction ID wraparound and bloat.
Rule: monitor your transaction age — any transaction open for hours will prevent cleanup of dead tuples.
Key Takeaway
MVCC is the key reason modern databases handle high concurrency without locking everything.
Understanding MVCC snapshots explains why REPEATABLE READ sees a point-in-time view.
PostgreSQL's SERIALIZABLE uses Serializable Snapshot Isolation (SSI) — it detects conflicts and forces retries.

Transaction Management in Practice: Application-Level Patterns

Real-world apps rarely use raw SQL transactions. Frameworks like Spring Boot, Hibernate, and Entity Framework manage transaction boundaries declaratively. You need to understand: - Transaction propagation: REQUIRED, REQUIRES_NEW, NESTED — when does a new transaction start? - Isolation level override: @Transactional(isolation = Isolation.REPEATABLE_READ) can clash with database defaults. - Transactional outbox pattern for reliable messaging. - Handling rollback-only exceptions: sometimes a caught exception still marks the transaction for rollback. - Setting timeout and read-only hints to optimize connection usage.

ExampleJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge.service.TransferService
@Service
public class TransferService {

    @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 5)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findByIdForUpdate(fromId);
        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException();
        }
        Account to = accountRepo.findByIdForUpdate(toId);
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
        accountRepo.save(from);
        accountRepo.save(to);
        // If an exception is thrown here (e.g., optimistic lock failure),
        // the entire transaction rolls back automatically.
    }
}
Output
Transaction completes with no issues.
Rollback-Only Trap
If you catch an exception inside a @Transactional method and do not rethrow it, the transaction is still marked rollback-only. The commit will fail with Transaction rolled back because it has been marked as rollback-only. Always rethrow the exception or use TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() deliberately.
Production Insight
The #1 Spring Boot transaction mistake is using @Transactional on a private method — it's ignored because the proxy can't intercept it.
Another common failure: calling @Transactional method from within the same class bypasses the proxy.
Rule: always use public methods and call @Transactional from another bean to trigger the proxy.
Key Takeaway
Annotations like @Transactional are syntactic sugar — they still rely on database locks and isolation levels.
Read-only transactions can be optimized by the database — always set readOnly=true for SELECT-only operations.
Never assume a transaction that catches an exception commits — check for rollback-only state.

The Forgotten Enemy: Lost Updates and Write Skew

Atomicity, isolation, and the rest of ACID still leave a bastard child — write skew. Two transactions read overlapping data, then write conflicting results based on stale snapshots. The bank example: you check your balance ($1000), I check mine ($1000). We both transfer $200 from your account to mine. REPEATABLE READ says each sees its own snapshot. Neither sees the other's write until commit. Result: your balance goes to $800, mine goes to $1200. We just printed $200. That's a phantom surplus, and no isolation level under SI fixes it. Why this happens: MVCC snapshots freeze read sets but not write sets. Both transactions see the original $1000 balance — neither notices the other's deduction. Prevent it with SELECT FOR UPDATE or explicit predicate locks. In PostgreSQL: BEGIN; SELECT balance FROM accounts WHERE account_id = 123 FOR UPDATE; — that locks the row, forcing the second transfer to wait. Without it, you're printing money in production.

write_skew.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- io.thecodeforge Write Skew in Postgres
-- Two concurrent transactions that both deduct from account 123

-- Session 1 (T1)
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE account_id = 123;
-- sees: 1000

-- Session 2 (T2) — starts while T1 is open
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE account_id = 123;
-- also sees: 1000 (snapshot from T2 start)

-- T1 deducts $200
UPDATE accounts SET balance = balance - 200 WHERE account_id = 123;
COMMIT;  -- balance becomes 800

-- T2 continues with stale snapshot
UPDATE accounts SET balance = balance - 200 WHERE account_id = 123;
COMMIT;  -- balance becomes 800 (expects 600, but T1 already set to 800)

-- Query after both commits
SELECT * FROM accounts WHERE account_id = 123;
-- result: balance = 800 (wrong; should be 600)
Output
balance
---------
800
(1 row)
Production Trap:
REPEATABLE READ won't save you from write skew. Only explicit SELECT FOR UPDATE or SERIALIZABLE isolation detects these conflicts. If your app updates from stale snapshots, expect phantom money or missing inventory.
Key Takeaway
Write skew is the silent ACID killer. Use locks or serializable isolation, or the database will silently corrupt your data.

When Your Cache Lies: Transaction Boundaries for Read-Heavy Workloads

Every junior dev learns to cache. Few learn that distributed caches break transaction isolation. Picture this: your RDBMS holds account balances. The app caches 'balance = 1000' in Redis. T1 deducts $200 from the DB — no cache update. A second service reads Redis, sees $1000, and initiates another deduction. The database becomes consistent after T1 commits, but your cache serves stale data for minutes. The fix: cache-aside with a TTL no longer than your business tolerance for inconsistency. Better: use database-level caching (PgBouncer with statement caching) or implement a cache invalidation protocol that aligns with transaction commit timing. Do not cache aggregate values that change per transaction unless you are willing to accept eventual consistency. If your business logic depends on strict serializability, disable caching completely. Real production incident: a fintech startup lost $12k because a Redis cache returned a stale pre-tax balance after a bulk transaction update. The DB was consistent the whole time — but nobody asked the DB.

cache_bust.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# io.thecodeforge Cache Invalidation After Transaction
import redis
import psycopg2

def transfer_funds(account_id: int, amount: int):
    conn = psycopg2.connect("dbname=bank user=app")
    cursor = conn.cursor()
    cache = redis.Redis(host="redis-cluster", port=6379)

    try:
        # Read balance directly from DB — never trust cache
        cursor.execute(
            "SELECT balance FROM accounts WHERE account_id = %s FOR UPDATE",
            (account_id,)
        )
        balance = cursor.fetchone()[0]

        if balance + amount < 0:
            raise ValueError("Insufficient funds")

        cursor.execute(
            "UPDATE accounts SET balance = balance + %s WHERE account_id = %s",
            (amount, account_id)
        )

        # Invalidate cache AFTER commit — not before
        conn.commit()
        cache.delete(f"balance:{account_id}")

    except Exception as e:
        conn.rollback()
        raise e
    finally:
        cursor.close()
        conn.close()

# If you cache before commit, a crash leaves stale data forever
# This pattern ensures cache only updates after DB persistence
Output
Transaction commits → cache invalidated successfully.
Next read hits DB, repopulates cache with correct balance.
Production Trap:
Caches do not participate in database transaction commit. A crash after cache write but before DB commit leaves irreversible stale data. Always invalidate cache after the commit.
Key Takeaway
Never write to cache before the database transaction commits. Cache invalidation must be the last step in your transaction boundary.
● Production incidentPOST-MORTEMseverity: high

The Phantom Inventory: How REPEATABLE READ Caused a $50k Stock Discrepancy

Symptom
Daily reconciliation reports were inconsistent by hundreds of units, yet individual transaction logs showed no errors.
Assumption
The team assumed REPEATABLE READ prevented all read phenomena, so concurrency problems were ruled out.
Root cause
The reporting query ran under REPEATABLE READ. While it prevented non-repeatable reads, it did not prevent phantom reads. New inventory additions (inserts) from concurrent warehouse receipt transactions could appear in the report's second range scan, causing phantom rows.
Fix
Changed the reporting transaction to use SERIALIZABLE isolation level (PostgreSQL's SSI mode) and added explicit locking with SELECT ... FOR UPDATE on the product range.
Key lesson
  • REPEATABLE READ stops row updates from changing your view, but it does not stop new rows from appearing.
  • For any report that aggregates across a dynamic range of rows, use SERIALIZABLE or lock the expected key range.
  • Always verify which anomalies your isolation level actually prevents — don't assume it's all three.
Production debug guideSymptom → Action guide for common transaction failures4 entries
Symptom · 01
High lock wait timeout errors (ERROR: 1205 Lock wait timeout exceeded)
Fix
Run SHOW ENGINE INNODB STATUS (MySQL) or SELECT * FROM pg_locks WHERE NOT granted (PostgreSQL) to find the blocking transaction.
Symptom · 02
Deadlock detected (error 1213 or deadlock detected)
Fix
Check the deadlock details in database logs. Identify the two transactions and the resources involved. Ensure they lock resources in the same order.
Symptom · 03
Transaction log growing rapidly (WAL or redo log)
Fix
Look for long-running open transactions. Run SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction' or SHOW PROCESSLIST (MySQL). Kill idle transactions over a threshold.
Symptom · 04
Phantom rows appearing in reports
Fix
Set the reporting transaction isolation level to SERIALIZABLE or use range locks (FOR UPDATE with condition). Verify by testing with concurrent inserts.
★ Quick Transaction Debug CommandsCommands for diagnosing transaction issues in PostgreSQL and MySQL (InnoDB).
Deadlock error
Immediate action
Check the database log for deadlock details
Commands
PostgreSQL: tail -f /var/log/postgresql/postgresql-*.log | grep -i deadlock
MySQL: SHOW ENGINE INNODB STATUS\G
Fix now
Ensure all transactions lock tables/rows in the same order. Add retry logic in the application.
Long-running transaction+
Immediate action
Find the PID and duration
Commands
PostgreSQL: SELECT pid, state, query_start, now() - query_start AS duration FROM pg_stat_activity WHERE state = 'active';
MySQL: SELECT * FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
Fix now
Kill the transaction (SELECT pg_terminate_backend(pid) or KILL CONNECTION id). Add statement_timeout on the application connection.
Lock contention+
Immediate action
Identify blocked queries
Commands
PostgreSQL: SELECT blocked_locks.pid AS blocked_pid, blocking_locks.pid AS blocking_pid FROM pg_locks blocked_locks JOIN pg_locks blocking_locks ON...;
MySQL: SELECT * FROM sys.innodb_lock_waits;
Fix now
Optimize the slow query holding locks. Consider decreasing transaction isolation level or using index to reduce lock range.
Isolation Levels Quick Reference
Isolation LevelDirty ReadNon-repeatable ReadPhantom ReadStandard SQL
READ UNCOMMITTEDPossiblePossiblePossibleYes
READ COMMITTEDPreventedPossiblePossibleYes
REPEATABLE READPreventedPreventedPossible (InnoDB prevents)Yes
SERIALIZABLEPreventedPreventedPreventedYes

Key takeaways

1
ACID is the gold standard
Atomicity, Consistency, Isolation, and Durability ensure data integrity even during hardware failure.
2
Isolation levels are a trade-off
READ COMMITTED is usually the sweet spot for web apps, while REPEATABLE READ is safer for complex reporting.
3
MVCC (Multi-Version Concurrency Control) allows readers to not block writers in engines like Postgres and InnoDB.
4
Deadlocks are an architectural failure, not a database bug. Prevent them by implementing consistent lock ordering in your code.
5
PostgreSQL is 'Pessimistic' by default with row-locks, whereas some distributed DBs use 'Optimistic' concurrency (OCC) which fails at commit time if a conflict occurred.

Common mistakes to avoid

3 patterns
×

Using READ UNCOMMITTED without understanding the consequences

Symptom
Dirty reads cause application state inconsistencies. For example, a dashboard shows an account balance that was never committed, leading to incorrect decisions.
Fix
Never use READ UNCOMMITTED in production unless you fully accept dirty reads. Use READ COMMITTED as the minimum default. If you need better performance, optimize query or index, not isolation.
×

Not handling serialization failures when using SERIALIZABLE isolation

Symptom
Random 'could not serialize access' errors (PostgreSQL error 40001) cause transactions to fail intermittently. Without retry logic, the user sees error pages or semantic failures.
Fix
Always implement retry logic for serialization failures. In Spring, consider using a retry advice on @Transactional methods. Typically retry 3 times with exponential backoff.
×

Assuming REPEATABLE READ prevents all read phenomena

Symptom
Reports become inconsistent because phantom rows appear. Inventory systems show more stock than exists.
Fix
Add explicit range locks (SELECT ... WHERE ... FOR UPDATE) to the reading transaction, or switch to SERIALIZABLE for range-reporting transactions.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the 'Phantom Read' anomaly. In which isolation level is it resol...
Q02SENIOR
You are seeing high 'Lock Wait Timeout' errors in a Spring Boot applicat...
Q03SENIOR
What is the difference between Optimistic and Pessimistic locking? When ...
Q04SENIOR
LeetCode SQL Context: Write a query to find and resolve duplicate record...
Q05SENIOR
How does MVCC (Multi-Version Concurrency Control) allow a database to pr...
Q01 of 05SENIOR

Explain the 'Phantom Read' anomaly. In which isolation level is it resolved in standard SQL, and how does MySQL's InnoDB handle it differently?

ANSWER
A phantom read occurs when a transaction reads a set of rows twice and sees different rows (newly inserted) in the second read. Standard SQL resolves it at SERIALIZABLE isolation. InnoDB also prevents phantom reads in REPEATABLE READ by using next-key locking (line of record locks + gap locks) on the index. So InnoDB's REPEATABLE READ actually prevents phantoms, which is stronger than the SQL standard requires.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What exactly is a 'Dirty Read' and why is it dangerous?
02
What is the difference between a non-repeatable read and a phantom read?
03
How does the 'Durability' in ACID handle a literal power out?
N
Naren Founder & Principal Engineer

20+ years shipping production systems from the metal up. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's DBMS. Mark it forged?

4 min read · try the examples if you haven't

Previous
Indexing in DBMS
7 / 11 · DBMS
Next
Deadlock in DBMS