Question 6 · Section 11

What is Serializable?

All anomalies are completely eliminated.

Language versions: English Russian Ukrainian

🟢 Junior Level

Serializable is the strictest transaction isolation level. It guarantees that the result of concurrent transactions will be equivalent to some single sequential execution (not necessarily the actual chronological order).

Key Properties

  • Dirty Read: Prohibited
  • Non-Repeatable Read: Prohibited
  • Phantom Read: Prohibited

All anomalies are completely eliminated.

Simple Example

If two transactions work with the same data, at the Serializable level they will execute as if working strictly one after another.

When to Use

  • Financial transactions
  • Booking systems
  • When the cost of a data error is very high

Main Drawback

The slowest level. On write-heavy loads, throughput can be 2-10x lower than on Read Committed. On read-only — the difference is minimal. Transactions may conflict and be rolled back. The application must be able to retry such transactions.

When NOT to Use Serializable

  1. Read-heavy reporting — excessive overhead
  2. >50 concurrent writers — constant serialization failures
  3. Transactions >1 sec — the longer the transaction, the higher the chance of conflict
  4. Hot-spot updates (counters, queues) — everyone writing to the same row

🟡 Middle Level

Implementation Mechanisms

Historically, there are two approaches:

Pessimistic: Two-Phase Locking (2PL)

  • All data that a transaction reads or writes is locked
  • Reading blocks writing, writing blocks reading and writing
  • Drawback: Low performance and frequent deadlocks
  • Used in older DBMS (SQL Server by default for this level)

Optimistic: Serializable Snapshot Isolation (SSI)

  • Modern method (PostgreSQL since 9.1)
  • Doesn’t block reads/writes, allows parallel execution via MVCC
  • Tracks dependencies between transactions
  • On conflict detection, one transaction is aborted with an error

The Main Challenge: Retries

At the Serializable level, the DBMS doesn’t guarantee successful transaction completion. The database may throw an error:

ERROR: could not serialize access due to read/write dependencies among transactions

The application must implement retry logic.

When to Use Serializable

  1. Financial transactions: Transfers between many accounts with invariants
  2. Booking systems: Cannot allow double-selling
  3. Complex business rules: Logic depends on a dataset that may change

How to Minimize Conflicts

  • Keep transactions as short as possible
  • Don’t perform long network requests inside a transaction
  • Update data in the same order across all transactions

🔴 Senior Level

Serializable Snapshot Isolation (SSI) — Internal Implementation

Dependency Graph Construction

PostgreSQL SSI builds a Serializable Graph tracking:

  • RW-conflicts: When transaction T1 reads a row that T2 later writes
  • Dangerous structures: Cycles in the dependency graph that could cause anomalies
T1 reads row X (T2 hasn't written yet)
T2 writes row X
T2 reads row Y (T3 hasn't written yet)
T3 writes row Y
T3 reads row X (that T2 wrote)

This forms a cycle → potential anomaly → one transaction must abort.

A cycle is dangerous because it means: the result depends on the execution order, and there is no serial order that would produce the same result. Therefore, the DBMS aborts one transaction with a serialization failure error.

Predicate Locks

Unlike traditional row/table locks, SSI uses predicate locks:

  • Locks are on index ranges or full table scans
  • If predicates overlap → potential conflict detected
  • Locks are tracked in memory, not on disk
  • Coarsening: individual page locks may be promoted to relation-level locks to save memory

Conflict Detection Algorithm

On each read:
  - Register predicate lock on accessed data

On each write:
  - Check for RW-dependencies with other transactions
  - If dangerous structure detected:
    - Mark one transaction for abortion
    - Error code: SQLSTATE 40001 (serialization_failure)

Performance Characteristics

Overhead Analysis

  • Memory: Predicate locks stored in shared memory (~few MBs typically)
  • CPU: Dependency graph traversal on each read/write
  • Abort rate: 1-10% under moderate contention, can spike to 30%+ under heavy writes
  • Throughput: 2-10x lower than Read Committed depending on workload

When SSI Performs Well

  • Read-heavy workloads (predicate locks don’t conflict)
  • Transactions accessing disjoint data sets
  • Short transactions with minimal overlap

When SSI Struggles

  • High write contention on same rows
  • Long-running transactions (more conflict window)
  • Hot-spot updates (counters, queues)

Production Retry Implementation

@Configuration
@EnableRetry
public class RetryConfig {
}

@Service
public class TransactionService {

    @Retryable(
        value = {CannotSerializeTransactionException.class},
        backoff = @Backoff(
            delay = 50,
            maxDelay = 2000,
            multiplier = 3,
            random = true  // Jitter to prevent thundering herd
        ),
        maxAttempts = 5
    )
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public Result performCriticalOperation(Input input) {
        // Business logic here
    }

    @Recover
    public Result recover(CannotSerializeTransactionException e, Input input) {
        // Fallback or throw business exception
        throw new BusinessException("Could not complete after retries", e);
    }
}

Serialization Failure Metrics to Monitor

-- PostgreSQL: Track serialization failures
SELECT
    datname,
    xact_commit,
    xact_rollback,
    conflicts
FROM pg_stat_database
WHERE datname = 'your_db';

-- Application-level: Track retry rate
// Use Micrometer/Prometheus
Counter.builder("transaction.serialization.retries")
    .tag("operation", "transfer")
    .register(meterRegistry);

Serializable vs Alternative Approaches

Approach Guarantees Performance Complexity
Serializable (SSI) Full ACID Lowest Medium (retries)
Repeatable Read + FOR UPDATE Row-level Medium Low
Optimistic Locking (@Version) Application-level High Medium
Application-level locking Varies High High

Edge Cases and Gotchas

  1. Read-only transactions: Can still cause serialization failures if they read data being written
  2. Deferred constraints: Checked at COMMIT time, can trigger serialization failures
  3. Temporary tables: Not tracked by SSI, not protected
  4. Advisory locks: Not tracked by SSI, not protected
  5. Sequence values: nextval() is NOT rolled back on abort

Migration Strategy from RC to Serializable

  1. Start with monitoring: track potential conflicts under RC
  2. Implement retry logic first
  3. Enable Serializable for new endpoints only
  4. Gradually migrate critical paths
  5. Measure abort rates and adjust

🎯 Interview Cheat Sheet

Must know:

  • Serializable — the strictest level, eliminates all anomalies (dirty, non-repeatable, phantom)
  • PostgreSQL uses SSI (Serializable Snapshot Isolation) since version 9.1
  • SSI tracks RW-dependencies and on cycle detection aborts the transaction with error 40001
  • The application MUST implement retry logic when using Serializable
  • Throughput on write-heavy loads can be 2-10x lower than on Read Committed
  • Predicate locks are used instead of traditional row/table locks

Common follow-up questions:

  • What is SSI? — Serializable Snapshot Isolation: dependency graph + selective abortion
  • When to use Serializable? — Finance, booking, where the cost of error is high
  • When NOT to use? — Read-heavy reporting, >50 concurrent writers, transactions >1 sec
  • What happens on conflict? — serialization_failure error (40001), transaction is aborted

Red flags (DO NOT say):

  • “Serializable = just add the annotation and it works” — retry logic is needed
  • “Serializable fits any load” — on write-heavy, throughput drops significantly
  • “PostgreSQL uses 2PL for Serializable” — SSI is used, not two-phase locking

Related topics:

  • [[2. What transaction isolation levels exist]]
  • [[5. What is Repeatable Read]]
  • [[11. What is the default isolation level in PostgreSQL]]
  • [[16. What is the @Transactional annotation]]