Table of Contents

    isolation

    RELATIONAL DATA MODELING & SQL

    Isolation

    Learn how transaction isolation controls concurrent database operations, prevents dirty reads and lost updates, manages row and range conflicts, and balances data correctness against concurrency and performance.

    Introduction

    Production databases rarely process one transaction at a time. Several users, services, background jobs, and scheduled processes can read and update the same data concurrently.

    Each transaction can be individually correct but still produce an invalid result when its operations are interleaved with another transaction.

    Transaction isolation controls which changes concurrent transactions can observe and which interleavings the database permits.

    Isolation helps control problems such as:

    • Reading data that is later rolled back
    • Reading the same row twice and receiving different values
    • Receiving a different set of rows from the same repeated query
    • Overwriting another transaction's update
    • Overselling inventory or seats
    • Violating cross-row business invariants
    • Producing inconsistent reports

    Core idea: Isolation is not merely a database setting. It is part of preserving a business invariant while several transactions overlap.

    In your System Design curriculum, Isolation is Topic 5.6 under Relational Data Modeling & SQL. It follows transactions and precedes B-tree and composite indexes, query plans, and connection pools.

    Prerequisites

    # Prerequisite Why It Is Needed
    1 ACID Isolation is one of the four ACID transaction properties.
    2 Transactions Isolation controls interactions between concurrent transactions.
    3 Keys and constraints Constraints can prevent selected invalid concurrent outcomes.
    4 SQL queries and updates The examples use SELECT, UPDATE, COMMIT, and ROLLBACK.
    5 Concurrency fundamentals Isolation problems arise when operations overlap in time.
    6 Error handling and retries Strong isolation can block or abort conflicting transactions.

    What Is Transaction Isolation?

    Transaction isolation determines how concurrent transactions interact and which changes one transaction can observe from another.

    Transaction A
          |
          | reads and writes shared data
          v
    Database concurrency control
          ^
          | reads and writes shared data
          |
    Transaction B

    A stronger isolation policy prevents more unsafe interleavings. However, it can also increase blocking, version-storage usage, conflict detection, transaction retries, or other concurrency-control work.

    Isolation-design Flow
    define invariant → model concurrent transactions → identify anomaly → choose protection → test interleaving → handle conflicts

    Isolation vs Atomicity

    Property Primary Concern
    Atomicity Whether one transaction commits completely or rolls back completely
    Isolation How concurrent transactions observe and affect one another

    Two transactions can each be atomic while their combined concurrent result still violates a business rule.

    Final-seat Example

    Available seats:
    
    1
    
    
    Transaction A:
    
    Reads available seats = 1.
    Creates Booking A.
    Sets available seats = 0.
    Commits.
    
    
    Transaction B:
    
    Also reads available seats = 1.
    Creates Booking B.
    Sets available seats = 0.
    Commits.
    
    
    Final result:
    
    Two confirmed bookings
    for one available seat.

    Each transaction committed completely, but insufficient concurrency control allowed an invalid combined outcome.

    Concurrency Anomalies

    Anomaly Description
    Dirty read A transaction reads another transaction's uncommitted change
    Non-repeatable read The same row is read twice and returns different committed values
    Phantom read A repeated predicate query returns a different matching row set
    Lost update One transaction overwrites another transaction's update
    Write skew Transactions update different rows after reading a shared condition and violate a cross-row invariant

    Dirty Read

    A dirty read occurs when a transaction reads data written by another transaction that has not committed.

    Initial balance:
    
    500
    
    
    Transaction A:
    
    Updates balance to 100.
    Does not commit.
    
    
    Transaction B:
    
    Reads balance as 100.
    Makes a decision using that value.
    
    
    Transaction A:
    
    Rolls back.
    
    
    Result:
    
    Transaction B used a value
    that never became committed.

    Dirty reads can expose temporary, invalid, or rolled-back data to another transaction.

    Non-repeatable Read

    A non-repeatable read occurs when one transaction reads the same row twice and sees different committed values because another transaction updates the row between the reads.

    Transaction A:
    
    Reads Order 9001 status:
    pending
    
    
    Transaction B:
    
    Updates Order 9001 status:
    confirmed
    
    Commits.
    
    
    Transaction A:
    
    Reads Order 9001 again:
    confirmed

    Both values were committed when read, but Transaction A did not receive a repeatable value throughout its unit of work.

    Phantom Read

    A phantom read occurs when a transaction repeats a predicate query and receives a different set of matching rows.

    Transaction A:
    
    SELECT all pending orders.
    
    Result:
    10 rows.
    
    
    Transaction B:
    
    Inserts another pending order.
    Commits.
    
    
    Transaction A:
    
    Repeats the same query.
    
    Result:
    11 rows.

    The additional matching row is called a phantom.

    Lost Update

    A lost update occurs when two transactions read the same value, calculate new values independently, and one update overwrites the other.

    Initial balance:
    
    1,000
    
    
    Transaction A reads:
    
    1,000
    
    Adds 200.
    Plans to write 1,200.
    
    
    Transaction B reads:
    
    1,000
    
    Subtracts 100.
    Plans to write 900.
    
    
    Transaction A writes:
    
    1,200
    
    
    Transaction B writes:
    
    900
    
    
    Final balance:
    
    900
    
    
    Correct combined balance:
    
    1,100

    Transaction A's update was lost because Transaction B calculated from a stale value.

    Unsafe Read-modify-write

    1. SELECT balance.
    2. Calculate new balance in application.
    3. UPDATE balance with calculated value.

    Relative Database Update

    UPDATE accounts
    SET balance = balance + :amount
    WHERE account_id = :account_id;

    A relative database update can avoid the particular stale read-modify-write pattern, although the complete business rule can still require additional checks.

    Write Skew

    Write skew can occur when concurrent transactions read overlapping data but update different rows. Each transaction individually preserves its local condition, while the combined result violates a cross-row invariant.

    On-call Example

    Business rule:
    
    At least one assigned person
    must remain on call.
    
    
    Initial state:
    
    Person A = on call
    Person B = on call
    
    
    Transaction A:
    
    Reads both assignments.
    Sees Person B is on call.
    Sets Person A off call.
    
    
    Transaction B:
    
    Reads both assignments.
    Sees Person A is on call.
    Sets Person B off call.
    
    
    Both commit.
    
    
    Final state:
    
    Nobody remains on call.

    The transactions updated different rows, so a simple conflict on one row might not be detected.

    Invariant rule: When a business rule spans several rows, protecting only the row being updated might be insufficient.

    Standard Isolation Levels

    SQL commonly describes four isolation levels:

    • Read Uncommitted
    • Read Committed
    • Repeatable Read
    • Serializable

    These names provide a conceptual framework, but exact behaviour differs among database products and configurations.

    Isolation Level Dirty Reads Non-repeatable Reads Phantom Reads
    Read Uncommitted Can occur Can occur Can occur
    Read Committed Prevented Can occur Can occur
    Repeatable Read Prevented Prevented by the standard intent Platform behaviour must be verified
    Serializable Prevented Prevented Prevented through serializable execution semantics

    Platform rule: Never choose isolation from a generic comparison table alone. Verify the actual database engine, version, access pattern, row-versioning configuration, locking behaviour, and documented anomalies.

    Read Uncommitted

    Read Uncommitted provides weak isolation and can allow one transaction to observe another transaction's uncommitted changes.

    SET TRANSACTION ISOLATION LEVEL
    READ UNCOMMITTED;
    
    BEGIN;
    
    SELECT
        account_id,
        balance
    FROM accounts
    WHERE account_id = 101;
    
    COMMIT;

    This level can expose values that are later rolled back. It is unsuitable when a decision must be made from reliable committed data.

    Read Committed

    Read Committed prevents dirty reads by allowing a query to read committed data. Another transaction can still commit changes between two statements, so repeated reads can return different results.

    SET TRANSACTION ISOLATION LEVEL
    READ COMMITTED;
    
    BEGIN;
    
    SELECT
        order_status
    FROM orders
    WHERE order_id = 9001;
    
    -- Another transaction can commit an update.
    
    SELECT
        order_status
    FROM orders
    WHERE order_id = 9001;
    
    COMMIT;

    Depending on the database implementation, each statement can observe a different committed state.

    Repeatable Read

    Repeatable Read provides stronger stability for rows read during the transaction.

    SET TRANSACTION ISOLATION LEVEL
    REPEATABLE READ;
    
    BEGIN;
    
    SELECT
        order_status
    FROM orders
    WHERE order_id = 9001;
    
    SELECT
        order_status
    FROM orders
    WHERE order_id = 9001;
    
    COMMIT;

    The intended guarantee is that rereading a previously read row does not produce a different committed value during that transaction. Range-query, phantom, and conflict behaviour depends on the database implementation.

    Serializable

    Serializable provides the strongest standard isolation level. Committed transactions must have an outcome equivalent to some valid serial order.

    Concurrent execution:
    
    Transaction A and Transaction B overlap.
    
    
    Serializable requirement:
    
    The committed result must be equivalent to:
    
    A followed by B
    
    or
    
    B followed by A
    SET TRANSACTION ISOLATION LEVEL
    SERIALIZABLE;
    
    BEGIN;
    
    SELECT
        available_seats
    FROM courses
    WHERE course_id = :course_id;
    
    UPDATE courses
    SET available_seats =
        available_seats - 1
    WHERE course_id = :course_id;
    
    COMMIT;

    Serializable does not necessarily mean that transactions literally execute one at a time. The database can use locking, validation, versioning, or another concurrency-control method. Conflicting transactions can block or be aborted and require retry.

    Snapshot Isolation

    Some database systems provide snapshot-based isolation using row versions. A transaction reads a consistent database snapshot rather than waiting for every concurrent writer.

    Transaction starts
          |
          v
    Database establishes visible snapshot
          |
          v
    Transaction reads versions valid
    for that snapshot
          |
          v
    Concurrent commits create newer versions
          |
          v
    Transaction continues according to
    snapshot and conflict rules

    Snapshot techniques can reduce reader-writer blocking. However, snapshot visibility alone does not automatically protect every cross-row invariant. The exact guarantees must be verified for the selected database.

    Statement vs Transaction Snapshots

    Snapshot Scope General Behaviour
    Statement-level snapshot Each statement can receive a consistent view, but later statements can see newer commits
    Transaction-level snapshot Statements in one transaction can share a stable snapshot

    Product-specific documentation determines which isolation levels use each snapshot scope.

    Lock-based Concurrency Control

    Lock-based systems coordinate concurrent operations by granting compatible locks and delaying incompatible operations.

    Lock Concept Purpose
    Shared lock Supports reading while preventing incompatible modifications
    Exclusive lock Protects a resource being modified
    Row lock Protects selected rows
    Range or predicate protection Protects a qualifying key range against conflicting changes
    Table lock Protects a broader table-level resource

    Lock names, compatibility, escalation, duration, and range behaviour vary between database systems.

    Multi-version Concurrency Control

    Multi-version concurrency control, commonly abbreviated as MVCC, maintains several row versions so readers can access an appropriate committed version while writers create newer versions.

    Row version 7
    Valid for earlier snapshot
    
    Row version 8
    Created by newer committed transaction
    
    Reader:
    Uses the version visible to its snapshot
    
    Writer:
    Creates or updates the newer version

    MVCC can reduce reader-writer blocking but introduces version storage, cleanup, visibility, and conflict-management responsibilities.

    Pessimistic Locking

    Pessimistic concurrency assumes conflicts are sufficiently likely or costly that the database should lock the relevant data before modification.

    BEGIN;
    
    SELECT
        available_quantity
    FROM inventory
    WHERE product_id = :product_id
    FOR UPDATE;
    
    UPDATE inventory
    SET available_quantity =
        available_quantity - :quantity
    WHERE product_id = :product_id
      AND available_quantity >= :quantity;
    
    COMMIT;

    Suitable Characteristics

    • High contention
    • Short transaction duration
    • Conflict must be prevented before work proceeds
    • Contested resource is easy to identify

    Considerations

    • Blocking
    • Lock waits
    • Deadlocks
    • Reduced concurrency
    • Lock escalation or broader locking

    Optimistic Concurrency

    Optimistic concurrency allows work to proceed without holding a long-lived write lock and detects whether the data changed before applying the update.

    UPDATE customers
    SET
        display_name = :display_name,
        row_version = row_version + 1
    WHERE customer_id = :customer_id
      AND row_version = :expected_version;

    If the affected-row count is zero, the expected version no longer matches the current row.

    Client read:
    
    row_version = 7
    
    
    Another transaction updates row:
    
    row_version = 8
    
    
    Client update requires:
    
    row_version = 7
    
    
    Result:
    
    No row updated.
    Concurrency conflict detected.

    Pessimistic vs Optimistic Control

    Area Pessimistic Optimistic
    Conflict strategy Prevent conflict through locking Detect conflict before update or commit
    Waiting Conflicting transactions can block Conflicting operations can fail and retry
    Typical fit Frequent or high-cost conflicts Infrequent conflicts
    Primary risk Blocking and deadlock Repeated conflicts and retries

    Correct Final-seat Reservation

    A conditional update can atomically test and reserve the final seat.

    BEGIN;
    
    UPDATE courses
    SET available_seats =
        available_seats - 1
    WHERE course_id = :course_id
      AND course_status = 'published'
      AND available_seats > 0;
    
    -- Verify exactly one row was updated.
    
    INSERT INTO enrollments
    (
        enrollment_id,
        learner_id,
        course_id,
        enrollment_status
    )
    VALUES
    (
        :enrollment_id,
        :learner_id,
        :course_id,
        'active'
    );
    
    COMMIT;

    If no course row is updated, the transaction must not create the confirmed enrollment.

    Preventing Lost Updates

    Unsafe Pattern

    SELECT
        available_quantity
    FROM inventory
    WHERE product_id = :product_id;
    
    -- Application calculates a new value.
    
    UPDATE inventory
    SET available_quantity = :new_quantity
    WHERE product_id = :product_id;

    Atomic Relative Update

    UPDATE inventory
    SET available_quantity =
        available_quantity - :quantity
    WHERE product_id = :product_id
      AND available_quantity >= :quantity;

    Version-protected Update

    UPDATE inventory
    SET
        available_quantity = :new_quantity,
        row_version = row_version + 1
    WHERE product_id = :product_id
      AND row_version = :expected_version;

    Select the pattern that correctly protects the full business rule.

    Preventing Duplicate Relationships

    Unique constraints provide atomic protection against concurrent duplicate inserts.

    CREATE TABLE enrollments
    (
        enrollment_id BIGINT PRIMARY KEY,
        learner_id BIGINT NOT NULL,
        course_id BIGINT NOT NULL,
    
        CONSTRAINT uq_enrollment_learner_course
            UNIQUE
            (
                learner_id,
                course_id
            )
    );

    Two transactions can both check that no enrollment exists. The UNIQUE constraint decides atomically which insert can succeed.

    Constraint rule: Use database constraints for invariants the database can represent. Isolation alone should not replace primary, unique, foreign-key, or check constraints.

    Deadlocks

    A deadlock occurs when transactions wait on resources held by one another.

    Transaction A:
    
    Locks Account 1.
    Requests Account 2.
    
    
    Transaction B:
    
    Locks Account 2.
    Requests Account 1.
    
    
    Result:
    
    Circular wait.

    The database can detect the cycle and abort one transaction.

    Deadlock-reduction Practices

    • Access resources in a consistent order
    • Keep transactions short
    • Use selective queries
    • Create suitable indexes
    • Avoid remote calls while holding locks
    • Update only required rows
    • Implement safe bounded retries

    Blocking and Lock Waits

    Transaction A:
    
    Locks Order 9001.
    Performs additional work.
    
    
    Transaction B:
    
    Attempts to update Order 9001.
    Waits for Transaction A.
    
    
    Possible outcomes:
    
    - Transaction A commits and B continues
    - Transaction A rolls back and B continues
    - B reaches its lock timeout
    - A deadlock is detected

    Long lock waits increase response latency and can consume database connections and worker capacity.

    Transaction Duration and Isolation

    Strong isolation is easier to operate when transactions are small and focused.

    Long isolated transaction
    Begin transaction
    
    Lock inventory
    Call payment provider
    Generate document
    Send notification
    Wait for response
    
    Commit
    Focused transaction
    Perform safe external preparation
    
    Begin transaction
    
    Read current state
    Validate conditions
    Reserve inventory
    Create order
    Insert outbox event
    
    Commit
    
    Perform external follow-up asynchronously

    Isolation Failures and Retries

    Stronger isolation can reject a transaction when the database cannot preserve the required concurrent history.

    Transaction conflict
          |
          v
    Database aborts transaction
          |
          v
    Application rolls back
          |
          v
    Classify failure as retryable
          |
          v
    Wait with bounded backoff and jitter
          |
          v
    Start a new transaction
          |
          v
    Reread current database state
          |
          v
    Retry the complete safe operation

    A retried transaction must not continue from stale values captured during the failed attempt.

    Isolation and Idempotency

    Isolation controls concurrent database interactions. Idempotency controls repeated execution of one logical request.

    Isolation:
    
    Prevents concurrent transactions from
    producing an unsafe combined outcome.
    
    
    Idempotency:
    
    Prevents the same logical API operation
    from creating duplicate intended effects.

    A transaction retried after a deadlock or lost network response might need both isolation and a scoped idempotency key.

    Setting Isolation in SQL

    SET TRANSACTION ISOLATION LEVEL
    READ COMMITTED;
    
    BEGIN;
    
    SELECT
        order_id,
        order_status
    FROM orders
    WHERE customer_id = :customer_id;
    
    COMMIT;

    Exact syntax and whether the setting applies to one transaction, connection, or session depend on the database platform.

    PHP Optimistic-concurrency Example

    <?php
    
    declare(strict_types=1);
    
    function updateCustomerName(
        PDO $pdo,
        int $customerId,
        int $expectedVersion,
        string $displayName
    ): void {
        $pdo->beginTransaction();
    
        try {
            $statement =
                $pdo->prepare(
                    '
                    UPDATE customers
                    SET
                        display_name =
                            :display_name,
                        row_version =
                            row_version + 1
                    WHERE customer_id =
                            :customer_id
                      AND row_version =
                            :expected_version
                    '
                );
    
            $statement->execute([
                'display_name' =>
                    $displayName,
                'customer_id' =>
                    $customerId,
                'expected_version' =>
                    $expectedVersion
            ]);
    
            if ($statement->rowCount() !== 1) {
                throw new RuntimeException(
                    'The customer was modified by another transaction.'
                );
            }
    
            $pdo->commit();
        } catch (Throwable $exception) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
    
            throw $exception;
        }
    }

    Production code should distinguish a missing resource from a version conflict and map each result to an appropriate domain response.

    PHP Transaction-retry Pattern

    <?php
    
    declare(strict_types=1);
    
    function executeWithRetry(
        PDO $pdo,
        callable $operation,
        int $maximumAttempts = 3
    ): mixed {
        for (
            $attempt = 1;
            $attempt <= $maximumAttempts;
            $attempt++
        ) {
            $pdo->beginTransaction();
    
            try {
                $result =
                    $operation(
                        $pdo
                    );
    
                $pdo->commit();
    
                return $result;
            } catch (PDOException $exception) {
                if ($pdo->inTransaction()) {
                    $pdo->rollBack();
                }
    
                $retryable =
                    isRetryableConcurrencyError(
                        $exception
                    );
    
                if (!$retryable ||
                    $attempt === $maximumAttempts) {
    
                    throw $exception;
                }
    
                $maximumDelayMicroseconds =
                    min(
                        500000,
                        50000 * (2 ** ($attempt - 1))
                    );
    
                usleep(
                    random_int(
                        0,
                        $maximumDelayMicroseconds
                    )
                );
            } catch (Throwable $exception) {
                if ($pdo->inTransaction()) {
                    $pdo->rollBack();
                }
    
                throw $exception;
            }
        }
    
        throw new RuntimeException(
            'The transaction could not be completed.'
        );
    }

    The retryable database error codes and transaction-state rules are platform-specific. The complete operation must also be safe to retry and protected against duplicate external effects.

    Testing Isolation

    Isolation bugs require coordinated concurrent sessions. Single-session tests cannot reproduce most transaction interleavings.

    Session A

    BEGIN;
    
    SELECT
        available_seats
    FROM courses
    WHERE course_id = 101;
    
    -- Keep the transaction open
    -- while Session B runs.

    Session B

    BEGIN;
    
    UPDATE courses
    SET available_seats =
        available_seats - 1
    WHERE course_id = 101
      AND available_seats > 0;
    
    COMMIT;

    Session A Continues

    SELECT
        available_seats
    FROM courses
    WHERE course_id = 101;
    
    COMMIT;

    Repeat the experiment under approved isolation configurations and document the observed values, blocking, errors, and final database state.

    Choosing an Isolation Strategy

    1. Define the business invariant.
    2. Identify the rows and predicates involved.
    3. Draw two or more conflicting transaction timelines.
    4. Identify dirty reads, lost updates, phantoms, or write skew.
    5. Determine whether a constraint can enforce the invariant.
    6. Consider an atomic conditional update.
    7. Consider optimistic version checks.
    8. Consider explicit locking for highly contested resources.
    9. Select the minimum isolation that safely supports the operation.
    10. Test the actual database implementation.
    11. Add retry handling for abortable conflicts.
    12. Monitor contention, waits, failures, and transaction duration.

    Isolation Observability

    Useful concurrency metrics include:

    • Transaction count by isolation level
    • Transaction duration
    • Lock-wait duration
    • Blocked transaction count
    • Deadlock count
    • Serialization-failure count
    • Optimistic-concurrency conflict count
    • Retry count and retry success rate
    • Long-running transaction count
    • Version-store or row-version pressure
    • Lock-timeout count
    • Contested tables, operations, and resources

    Use bounded resource identifiers and trace information in diagnostics without exposing credentials or sensitive business values.

    Common Isolation Mistakes

    1

    Ignoring Concurrent Execution

    Code that is correct in one session can violate business invariants when several transactions overlap.

    2

    Choosing Isolation from the Level Name Alone

    The same named level can have product-specific implementation behaviour. Verify the selected database.

    3

    Using Read Uncommitted for Important Decisions

    A transaction can act on data that another transaction later rolls back.

    4

    Using Read-then-write without Conflict Protection

    Another transaction can change the row after it is read and before the update is applied.

    5

    Assuming Repeatable Read Prevents Every Anomaly

    Cross-row invariants, predicate changes, and implementation-specific behaviours still require analysis and testing.

    6

    Assuming Serializable Never Aborts Transactions

    Serializable execution can block or reject conflicting work, requiring safe retry handling.

    7

    Using Locks without Consistent Ordering

    Transactions that acquire the same resources in different orders increase deadlock risk.

    8

    Holding Locks during Remote Calls

    Network latency extends transaction duration and increases contention.

    9

    Retrying without Rereading Data

    State captured during an aborted transaction can be stale during the next attempt.

    10

    Retrying Non-idempotent External Effects

    A database retry can repeat a payment, notification, or remote operation unless those effects have their own protection.

    11

    Replacing Constraints with Isolation

    Unique and foreign-key constraints remain necessary even under strong isolation.

    12

    Testing with One Database Session

    Dirty reads, lost updates, blocking, deadlocks, and write skew require coordinated concurrent sessions.

    Recommended Test Cases

    Test Expected Evidence
    Dirty read Document whether uncommitted changes are visible
    Non-repeatable read Repeat one row lookup after another transaction commits an update
    Phantom read Repeat one range query after another transaction inserts a matching row
    Lost update Verify that one concurrent write does not overwrite another silently
    Write skew Verify that a cross-row invariant remains valid
    Optimistic conflict A stale version update affects no rows
    Pessimistic lock The conflicting transaction waits or fails according to policy
    Final-seat reservation Only one concurrent enrollment receives the final seat
    Duplicate enrollment The unique constraint permits one learner-course association
    Deadlock One transaction rolls back and retry handling is applied
    Serializable conflict The operation commits safely or receives a retryable conflict
    Retry after conflict The new transaction rereads current state before retrying

    Isolation Best Practices

    Recommended Practices

    • Begin with the business invariant rather than an isolation-level name.
    • Model the possible concurrent transaction interleavings.
    • Use constraints for representable uniqueness and relationship rules.
    • Use atomic conditional updates for counters, inventory, and capacity.
    • Check affected-row counts after conditional writes.
    • Use optimistic version checks when conflicts are uncommon.
    • Use pessimistic locking when conflict prevention is necessary.
    • Select isolation according to the operation's correctness requirements.
    • Verify exact behaviour for the selected database platform.
    • Keep transactions short and focused.
    • Acquire contested resources in a consistent order.
    • Avoid remote calls while holding database locks.
    • Expect blocking, deadlocks, or aborted transactions under contention.
    • Roll back before retrying a failed transaction.
    • Reread current data during every retry attempt.
    • Use bounded backoff and jitter for retryable conflicts.
    • Combine transaction retries with idempotency.
    • Test cross-row invariants for write skew.
    • Use at least two coordinated sessions for concurrency tests.
    • Monitor lock waits, deadlocks, conflicts, retries, and long transactions.

    Practice Exercise

    Test and protect concurrent course enrollment for the final available seat in your online learning platform.

    Requirements

    1. Create a course with one remaining seat.
    2. Open two independent database sessions.
    3. Start one enrollment transaction in each session.
    4. Demonstrate the unsafe read-then-write pattern.
    5. Record the final capacity and enrollment count.
    6. Replace the pattern with an atomic conditional update.
    7. Verify the affected-row count.
    8. Protect duplicate enrollment with a unique constraint.
    9. Test Read Committed behaviour.
    10. Test Repeatable Read behaviour.
    11. Test Serializable behaviour.
    12. Record blocking and transaction-conflict errors.
    13. Add bounded retry handling.
    14. Add an idempotency key for the enrollment request.
    15. Verify that only one enrollment receives the final seat.

    Safe Enrollment Transaction

    BEGIN;
    
    UPDATE courses
    SET available_seats =
        available_seats - 1
    WHERE course_id = :course_id
      AND course_status = 'published'
      AND available_seats > 0;
    
    -- Continue only when exactly one row was updated.
    
    INSERT INTO enrollments
    (
        enrollment_id,
        learner_id,
        course_id,
        enrollment_status,
        enrolled_at
    )
    VALUES
    (
        :enrollment_id,
        :learner_id,
        :course_id,
        'active',
        CURRENT_TIMESTAMP
    );
    
    COMMIT;

    Isolation Test Matrix

    Scenario Observed Behaviour Invariant Preserved? Protection Used
    Read uncommitted value Record evidence Record result Isolation level
    Repeat one row read Record evidence Record result Isolation level
    Repeat range query Record evidence Record result Isolation level
    Concurrent final-seat enrollment Record evidence Record result Conditional update
    Duplicate learner enrollment Record evidence Record result Unique constraint
    Optimistic version conflict Record evidence Record result Version column
    Deadlock Record evidence Record result Rollback and retry

    Frequently Asked Questions

    1

    What is transaction isolation?

    Transaction isolation controls how concurrent transactions observe and affect one another.

    2

    What is a dirty read?

    A dirty read occurs when a transaction reads another transaction's uncommitted data.

    3

    What is a non-repeatable read?

    It occurs when one transaction reads the same row twice and receives different committed values.

    4

    What is a phantom read?

    A phantom read occurs when a repeated predicate query returns a different set of matching rows.

    5

    What is a lost update?

    A lost update occurs when one transaction's write overwrites another transaction's change calculated from the same earlier value.

    6

    What is write skew?

    Write skew occurs when transactions update different rows after reading a shared condition and their combined result violates a cross-row invariant.

    7

    What does Read Committed prevent?

    Read Committed prevents dirty reads, but repeated statements can still observe newer committed data.

    8

    What does Repeatable Read provide?

    Repeatable Read provides stronger row-read stability. Exact phantom, snapshot, and conflict behaviour depends on the database implementation.

    9

    What does Serializable mean?

    Serializable requires committed outcomes equivalent to some valid serial order, though transactions can still execute concurrently internally.

    10

    Does stronger isolation eliminate retries?

    No. Strong isolation can block or abort conflicting transactions, so applications need safe retry handling.

    11

    Should every operation use Serializable?

    Not automatically. Select isolation based on the business invariant, workload, database implementation, and acceptable concurrency cost.

    12

    What comes after isolation?

    The next topic is B-tree and composite indexes, followed by query plans and connection pools.

    Key Takeaway

    Isolation protects business invariants while transactions execute concurrently. Understand dirty reads, non-repeatable reads, phantoms, lost updates, and write skew before choosing a strategy. Use database constraints, atomic conditional updates, optimistic version checks, or targeted pessimistic locking where appropriate. Select the isolation level from the operation's correctness requirements, verify the actual database implementation, keep transactions short, expect conflicts under contention, and retry aborted transactions only after rollback, rereading current state, and confirming idempotency.