Read Committed Isolation Low-Level Design: Statement-Level Snapshots, Lock Mechanics, and Non-Repeatable Read Behavior

What Is Read Committed Isolation?

Read committed is the weakest practical isolation level. It guarantees that a transaction only reads data that has been committed by other transactions — dirty reads (reading uncommitted data) are prevented. However, it allows non-repeatable reads and phantom reads. Read committed is the default isolation level in PostgreSQL and Oracle.

Statement-Level Snapshot

Under read committed, each SQL statement acquires a fresh snapshot of the committed database state at the moment the statement begins — not when the transaction began. This means:

  • Two identical SELECT statements within the same transaction may return different results if another transaction commits a change between them.
  • This is by design: read committed trades consistency within a transaction for reduced lock contention.

In contrast, repeatable read and serializable isolation levels take the snapshot at transaction start and use it for all statements within the transaction.

Shared Locks Under Read Committed

Under a traditional lock-based implementation:

  • Shared locks (S-locks): Acquired on rows being read. Released immediately after the read completes — the lock is not held for the duration of the transaction. This is sometimes called a “short-duration shared lock.”
  • Releasing shared locks immediately allows other writers to modify and commit those rows before the current transaction completes, enabling non-repeatable reads.

Under MVCC (Multi-Version Concurrency Control) implementations like PostgreSQL, readers do not acquire shared locks at all. Instead, they read from an immutable snapshot, so readers never block writers and writers never block readers.

Exclusive Locks

Write operations (INSERT, UPDATE, DELETE) acquire exclusive locks (X-locks) on affected rows. These locks are held until the transaction commits or rolls back, not released immediately. This means:

  • Two concurrent writers on the same row will block — the second writer waits for the first to commit or rollback.
  • A reader (under lock-based implementation) waiting to read a row being written must wait for the exclusive lock to be released.

Non-Repeatable Reads

A non-repeatable read occurs when transaction T1 reads a row, transaction T2 updates and commits that row, and T1 then reads the same row again and sees the new value. This is explicitly allowed under read committed.

Example sequence:

  1. T1: SELECT balance FROM accounts WHERE id = 42; — returns 1000
  2. T2: UPDATE accounts SET balance = 900 WHERE id = 42; COMMIT;
  3. T1: SELECT balance FROM accounts WHERE id = 42; — returns 900 (different result)

This is not a bug under read committed — it is the defined behavior.

Phantom Reads

A phantom read occurs when a range query returns different sets of rows across two executions within the same transaction because another transaction inserted or deleted rows in that range and committed. Read committed allows phantom reads.

Example:

  1. T1: SELECT COUNT(*) FROM orders WHERE amount > 100; — returns 5
  2. T2: INSERT INTO orders (amount) VALUES (200); COMMIT;
  3. T1: SELECT COUNT(*) FROM orders WHERE amount > 100; — returns 6

Use Cases and When Read Committed Is Appropriate

Read committed is appropriate for most OLTP workloads where:

  • Each operation is logically independent and non-repeatable reads are not a business problem.
  • Maximum throughput and minimal lock contention are priorities.
  • The application does not rely on seeing a consistent snapshot of data across multiple statements in a transaction.

It is inappropriate when a transaction must make decisions based on a consistent view of multiple rows (e.g., computing a derived value from several related rows and writing a result that must be consistent with the inputs).

Lock Contention and MVCC

Under MVCC, read committed has very low lock contention: readers and writers do not block each other. Each reader sees a snapshot of the committed state as of its statement start. Writers acquire row-level locks and block only concurrent writers to the same rows. This combination makes read committed the highest-throughput isolation level in MVCC databases.

SQL Schema

-- Tracks transaction metadata
CREATE TABLE Transaction (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    isolation_level VARCHAR(32) NOT NULL DEFAULT 'READ_COMMITTED',
    status          VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',  -- ACTIVE, COMMITTED, ABORTED
    started_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    completed_at    TIMESTAMPTZ
);

CREATE INDEX idx_transaction_status ON Transaction(status, started_at);

-- Tracks lock grants for audit and debugging
CREATE TABLE LockGrant (
    id          BIGSERIAL PRIMARY KEY,
    txn_id      UUID NOT NULL REFERENCES Transaction(id),
    resource_id VARCHAR(256) NOT NULL,  -- e.g. "table:row_id"
    lock_type   VARCHAR(8) NOT NULL,    -- SHARED, EXCLUSIVE
    granted_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    released_at TIMESTAMPTZ            -- NULL if still held
);

CREATE INDEX idx_lockgrant_txn ON LockGrant(txn_id);
CREATE INDEX idx_lockgrant_resource ON LockGrant(resource_id, released_at);

Python Implementation

import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Optional

class IsolationLevel(Enum):
    READ_COMMITTED = "READ_COMMITTED"
    REPEATABLE_READ = "REPEATABLE_READ"
    SERIALIZABLE = "SERIALIZABLE"

class LockType(Enum):
    SHARED = "SHARED"
    EXCLUSIVE = "EXCLUSIVE"

@dataclass
class TxnContext:
    txn_id: str
    isolation: IsolationLevel
    snapshot_xid: Optional[int] = None  # For RC: per-statement; for RR: per-txn

class ReadCommittedEngine:
    def __init__(self):
        self.transactions: Dict[str, TxnContext] = {}
        self.committed_data: Dict[str, dict] = {}   # row_id -> {data, version}
        self.row_locks: Dict[str, str] = {}          # row_id -> exclusive holder txn_id

    def begin_transaction(self, isolation: IsolationLevel = IsolationLevel.READ_COMMITTED) -> str:
        txn_id = str(uuid.uuid4())
        self.transactions[txn_id] = TxnContext(txn_id=txn_id, isolation=isolation)
        return txn_id

    def _get_current_snapshot_xid(self) -> int:
        """Under read committed, each statement gets the current committed snapshot."""
        return id(object())  # Placeholder for actual XID

    def read_row(self, txn_id: str, row_id: str) -> Optional[dict]:
        """
        Read a row. Under read committed, we get a fresh snapshot for this statement.
        Under MVCC, no shared lock is acquired.
        """
        ctx = self.transactions[txn_id]
        if ctx.isolation == IsolationLevel.READ_COMMITTED:
            # Fresh committed snapshot per statement
            snapshot_xid = self._get_current_snapshot_xid()
        else:
            # Use transaction-level snapshot (set at BEGIN)
            snapshot_xid = ctx.snapshot_xid

        row = self.committed_data.get(row_id)
        if row is None:
            return None
        # In real MVCC: filter to rows visible to snapshot_xid
        return row["data"]

    def write_row(self, txn_id: str, row_id: str, data: dict) -> bool:
        """Acquire exclusive lock and stage the write."""
        holder = self.row_locks.get(row_id)
        if holder and holder != txn_id:
            # Block: another transaction holds exclusive lock
            # In production: use wait queue; here we return False
            return False
        self.row_locks[row_id] = txn_id
        # Stage write in transaction buffer (not yet committed)
        return True

    def commit(self, txn_id: str) -> None:
        """Release exclusive locks and make writes visible."""
        locks_to_release = [rid for rid, holder in self.row_locks.items()
                            if holder == txn_id]
        for rid in locks_to_release:
            del self.row_locks[rid]
        del self.transactions[txn_id]

    def rollback(self, txn_id: str) -> None:
        """Release locks and discard staged writes."""
        self.commit(txn_id)  # Same lock release; staged writes are discarded

FAQ

  • Non-repeatable read example: T1 reads row, T2 updates and commits, T1 re-reads same row — different result; permitted under read committed.
  • Statement vs transaction snapshot: RC gets a fresh snapshot per statement; RR and serializable get one snapshot at transaction start.
  • Lock release timing: Shared locks released immediately after read; exclusive locks held until commit/rollback.
  • MVCC vs lock-based: MVCC readers never acquire locks; they read from versioned snapshots, eliminating reader-writer blocking.

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

Scroll to Top