AfriBa Labs Logo
AfriBa LabsProduct Studio
Databases & Architecture•
9 min read

Understanding Database Transactions and Isolation Through a Real Banking Example

ACID in practice: Dirty reads, non-repeatable reads, phantom reads, and choosing isolation levels.

AL

AfriBa Labs Engineering

Systems & Backend Engineering

Published 2026-09-27T10:00:00Z

Key Takeaways

  • ACID's 'Isolation' is not binary; SQL defines four standard isolation levels with specific concurrency trade-offs.
  • Most relational databases default to Read Committed, which does NOT prevent lost updates or non-repeatable reads.
  • Pessimistic locking (`SELECT FOR UPDATE`) prevents lost updates in Read Committed without the abort-penalty of Serializable.
  • Understanding your database's concurrency behavior is crucial when designing high-throughput ledgers or inventory systems.

1. The Account Transfer Scenario

Consider a classic banking example: transferring $100 from Alice's account (Balance: $500) to Bob's account (Balance: $200).

In an ideal world, the operation consists of two updates enclosed in a transaction:

sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 'alice';
UPDATE accounts SET balance = balance + 100 WHERE id = 'bob';
COMMIT;
A basic SQL funds transfer transaction

If the server experiences a power loss after the first UPDATE but before the second, the database rolls back the transaction upon recovery. The money does not vanish. This is Atomicity.

However, what happens when Charlie simultaneously attempts to withdraw $450 from Alice's account while this transfer is executing? This is where Transaction Isolation comes into play.

2. The Four Standard ANSI SQL Isolation Levels

The SQL-92 standard defines four isolation levels based on three concurrency phenomena:

1. Dirty Read: Transaction B reads uncommitted modifications made by Transaction A.

2. Non-Repeatable Read: Transaction A re-reads a row and finds that values changed because Transaction B committed an update.

3. Phantom Read: Transaction A executes a range query (e.g., `WHERE balance > 100`), and Transaction B inserts a new matching row before Transaction A finishes.

text
+------------------+-------------+---------------------+--------------+
| Isolation Level  | Dirty Read  | Non-Repeatable Read | Phantom Read |
+------------------+-------------+---------------------+--------------+
| Read Uncommitted | Allowed     | Allowed             | Allowed      |
| Read Committed   | Prevented   | Allowed             | Allowed      |
| Repeatable Read  | Prevented   | Prevented           | Allowed*     |
| Serializable     | Prevented   | Prevented           | Prevented    |
+------------------+-------------+---------------------+--------------+
* Note: Modern PostgreSQL prevents phantom reads even at Repeatable Read via MVCC.
Standard ANSI SQL transaction isolation matrix

3. The Danger of Lost Updates in Default 'Read Committed'

Most developers assume that wrapping statements in `BEGIN ... COMMIT` guarantees safe concurrent execution. In PostgreSQL and Oracle, the default level is 'Read Committed'. Under this level, each statement sees a snapshot taken at the start of that statement, not at the start of the transaction.

This causes the infamous Lost Update bug when applications read a balance, compute the new balance in application memory, and write it back:

sql
-- Thread 1 (Web Request A)        -- Thread 2 (Web Request B)
BEGIN;                             BEGIN;
SELECT balance FROM accounts;      SELECT balance FROM accounts;
-- Both see $500!                  -- Both see $500!

-- App computes 500 - 100 = 400    -- App computes 500 - 50 = 450
UPDATE accounts SET balance = 400; 
COMMIT;

                                   UPDATE accounts SET balance = 450;
                                   COMMIT;
-- Thread 2 overwrote Thread 1's deduction! Alice only lost $50 instead of $150.
How concurrent Read Committed transactions produce lost updates

How to Prevent Lost Updates

Use either atomic SQL expressions (`SET balance = balance - 100`), pessimistic row-locking (`SELECT ... FOR UPDATE`), or elevate to `REPEATABLE READ` / `SERIALIZABLE` isolation with retry logic on serialization errors.

4. The Architecture Choice for Real Products

For high-volume financial or inventory systems, relying solely on highest-level Serializable isolation can cause high abort rates due to lock contention. Conversely, leaving everything at default Read Committed without row-locking leads to data inconsistencies.

Careful engineering means choosing the right tool for the job: atomic SQL queries and row-level locks for hot accounts, and strict transaction isolation for critical multi-table balance audits.

Tags:#SQL#PostgreSQL#Transactions#Concurrency#ACID#Databases

Related Technical Guides

Backend & APIs
7 min read•Sep 18, 2026

Why Idempotency Matters When a User Clicks 'Pay' Twice

When a client sends a payment request and experiences a network drop, retrying blindly can result in duplicate transactions. Here is how idempotency keys and atomic locking solve this permanently.

APIsDistributed SystemsIdempotency
By AfriBa Labs EngineeringRead Article