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.
REPEATABLE READ is often mistaken as a silver bullet for consistency, but it leaves a gaping hole for phantom inserts that can corrupt inventory counts in production. If you're running range scans on MySQL InnoDB without gap locks, you'll see phantom rows appear mid-transaction — a $50k stock discrepancy waiting to happen. PostgreSQL's snapshot isolation handles this transparently, but relying on the SQL standard alone will burn you. Know your DBMS's implementation, or escalate to SERIALIZABLE when phantom prevention is non-negotiable.
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.
thecodeforge.io
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 ExampleBEGIN;
-- Step 1: Deduct from sourceUPDATE io_thecodeforge.accounts
SET balance = balance - 500, updated_at = NOW()
WHERE account_id = 'ACC-001'AND balance >= 500;
-- Step 2: Add to destinationUPDATE 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.SETSESSIONCHARACTERISTICSASTRANSACTIONISOLATIONLEVELREADCOMMITTED;
-- 2. REPEATABLE READ (Default for MySQL InnoDB)-- Uses MVCC (Multi-Version Concurrency Control) to ensure you see a snapshot.SETSESSIONCHARACTERISTICSASTRANSACTIONISOLATIONLEVELREPEATABLEREAD;
-- 3. SERIALIZABLE (Highest Safety)-- Prevents Phantoms by placing range locks or using predicate locking.SETSESSIONCHARACTERISTICSASTRANSACTIONISOLATIONLEVELSERIALIZABLE;
-- 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'FORUPDATE;
-- 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
thecodeforge.io
Transactions Dbms
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 lockingBEGIN;
-- By ordering our IDs, all transactions wait in the same 'queue'SELECT * FROM io_thecodeforge.accounts
WHERE account_id IN ('ACC-001', 'ACC-002')
ORDERBY account_id ASCFORUPDATE;
UPDATE io_thecodeforge.accounts SET balance = balance - 100WHERE account_id = 'ACC-001';
UPDATE io_thecodeforge.accounts SET balance = balance + 100WHERE account_id = 'ACC-002';
COMMIT;
Output
Rows locked in ascending order. No deadlock cycle possible.
Mental Model
Deadlock as a Traffic Circle
A deadlock is like four cars entering a roundabout at the same time, each waiting for the other to yield.
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 transactionBEGINISOLATIONLEVELREPEATABLEREAD;
SELECT balance FROM io_thecodeforge.accounts WHERE account_id = 'ACC-001';
-- Returns 1000-- Session 2 (concurrent): Update and commitBEGIN;
UPDATE io_thecodeforge.accounts SET balance = 800WHERE 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 1000COMMIT;
-- 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
@ServicepublicclassTransferService {
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 5)
publicvoidtransfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepo.findByIdForUpdate(fromId);
if (from.getBalance().compareTo(amount) < 0) {
thrownewInsufficientBalanceException();
}
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)BEGINISOLATIONLEVELREPEATABLEREAD;
SELECT balance FROM accounts WHERE account_id = 123;
-- sees: 1000-- Session 2 (T2) — starts while T1 is openBEGINISOLATIONLEVELREPEATABLEREAD;
SELECT balance FROM accounts WHERE account_id = 123;
-- also sees: 1000 (snapshot from T2 start)-- T1 deducts $200UPDATE accounts SET balance = balance - 200WHERE account_id = 123;
COMMIT; -- balance becomes 800-- T2 continues with stale snapshotUPDATE accounts SET balance = balance - 200WHERE account_id = 123;
COMMIT; -- balance becomes 800 (expects 600, but T1 already set to 800)-- Query after both commitsSELECT * 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 Transactionimport redis
import psycopg2
deftransfer_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:
raiseValueError("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}")
exceptExceptionas 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
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
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).
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 Level
Dirty Read
Non-repeatable Read
Phantom Read
Standard SQL
READ UNCOMMITTED
Possible
Possible
Possible
Yes
READ COMMITTED
Prevented
Possible
Possible
Yes
REPEATABLE READ
Prevented
Prevented
Possible (InnoDB prevents)
Yes
SERIALIZABLE
Prevented
Prevented
Prevented
Yes
⚙ Quick Reference
2 commands from this guide
File
Command / Code
Purpose
write_skew.sql
BEGIN ISOLATION LEVEL REPEATABLE READ;
The Forgotten Enemy
cache_bust.py
def transfer_funds(account_id: int, amount: int):
When Your Cache Lies
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.
Q02 of 05SENIOR
You are seeing high 'Lock Wait Timeout' errors in a Spring Boot application. Walk me through your debugging process to identify the offending transaction.
ANSWER
First, check the database to find blocked transactions: in MySQL run SHOW ENGINE INNODB STATUS and look for lock waits; in PostgreSQL query pg_locks with pg_stat_activity. Identify the blocking PID and the query it's running. Then review the application code: which endpoints or background jobs hold transactions for long periods? Use a database profiler or distributed tracing to correlate the connection. Common fixes: reduce the transaction scope, add indexes to speed up queries, or decrease isolation level if safe.
Q03 of 05SENIOR
What is the difference between Optimistic and Pessimistic locking? When would you use a version column (@Version in JPA) over a 'SELECT FOR UPDATE'?
ANSWER
Pessimistic locking assumes conflicts are frequent and acquires locks upfront (e.g., SELECT FOR UPDATE). Optimistic locking assumes conflicts are rare and checks at commit time using version columns. Use optimistic locking when contention is low (e.g., read-heavy applications) and you can't afford to hold locks for long. Use pessimistic locking when you need guaranteed immediate consistency, like financial transfers where a retry could cause a duplicate.
Q04 of 05SENIOR
LeetCode SQL Context: Write a query to find and resolve duplicate records within a single transaction without locking the entire table.
ANSWER
Use a CTE with window functions to identify duplicates, then delete within a transaction with appropriate locking. For example, to keep the lowest ID among duplicates: WITH duplicates AS (SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users) DELETE FROM users WHERE id IN (SELECT id FROM duplicates WHERE rn > 1); To avoid table locks, ensure there is an index on the partition column and use a limit or batch delete. Consider using SELECT FOR UPDATE SKIP LOCKED if deleting in batches.
Q05 of 05SENIOR
How does MVCC (Multi-Version Concurrency Control) allow a database to provide Isolation without excessive locking?
ANSWER
MVCC keeps multiple physical versions of each row. A writer creates a new version; readers see the version that existed at the start of their transaction (or statement). No shared locks are needed for reads, so readers never block writers and writers never block readers. This allows high concurrency. PostgreSQL and MySQL InnoDB both use MVCC, but with different implementations for version pruning and cleanup (vacuum vs undo logs).
01
Explain the 'Phantom Read' anomaly. In which isolation level is it resolved in standard SQL, and how does MySQL's InnoDB handle it differently?
SENIOR
02
You are seeing high 'Lock Wait Timeout' errors in a Spring Boot application. Walk me through your debugging process to identify the offending transaction.
SENIOR
03
What is the difference between Optimistic and Pessimistic locking? When would you use a version column (@Version in JPA) over a 'SELECT FOR UPDATE'?
SENIOR
04
LeetCode SQL Context: Write a query to find and resolve duplicate records within a single transaction without locking the entire table.
SENIOR
05
How does MVCC (Multi-Version Concurrency Control) allow a database to provide Isolation without excessive locking?
SENIOR
FAQ · 3 QUESTIONS
Frequently Asked Questions
01
What exactly is a 'Dirty Read' and why is it dangerous?
A dirty read happens when Transaction A reads data that Transaction B has modified but hasn't committed yet. If Transaction B fails and performs a ROLLBACK, Transaction A is now working with 'ghost' data that technically never existed. This can lead to incorrect financial balances or corrupted state logic. Most production databases (Oracle, Postgres, SQL Server) prevent this by default using READ COMMITTED.
Was this helpful?
02
What is the difference between a non-repeatable read and a phantom read?
A non-repeatable read concerns a single row: you read a row, someone else updates/deletes it, and your second read of that same row yields different data. A phantom read concerns a range of rows: you query all 'Gold Members' and find 5; someone else inserts a new 'Gold Member'; your second query finds 6. REPEATABLE READ stops the first; SERIALIZABLE is usually required to stop the second.
Was this helpful?
03
How does the 'Durability' in ACID handle a literal power out?
Databases use a Write-Ahead Log (WAL) or Redo Log. Before any data is changed in the actual tables, the intention is written to a sequential log file on disk. If the power cuts, the database restarts, reads the WAL, and 'replays' any committed transactions that hadn't made it to the main data files yet.