What is Serializable
It guarantees that the execution result of any number of concurrent transactions will be mathematically equivalent to some strictly sequential (serial) execution order (as if tr...
🟢 Junior Level
Serializable is the highest, strictest transaction isolation level defined by the ANSI/ISO SQL standard.
It guarantees that the execution result of any number of concurrent transactions will be mathematically equivalent to some strictly sequential (serial) execution order (as if transactions executed one after another with zero temporal overlap).
Key Characteristics:
- Total Elimination of Anomalies: Completely prevents all known concurrency anomalies: Dirty Reads, Non-Repeatable Reads, Phantom Reads, Lost Updates, and Write Skew.
- Cost of Integrity: The most resource-intensive level. Under high write contention, transactions may conflict, forcing the database engine to abort one of the transactions.
- Application Responsibility: The application must implement automated retry logic to handle transient serialization conflict aborts.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TicketBookingService {
// Guarantees absolute prevention of overbooking (double booking a single seat)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void bookSeat(Long flightId, String seatNumber, Long passengerId) {
boolean isTaken = seatRepository.isSeatTaken(flightId, seatNumber);
if (isTaken) {
throw new IllegalStateException("Seat already booked!");
}
seatRepository.assignSeat(flightId, seatNumber, passengerId);
}
}
Analogy: Serializable is like a single-occupancy fitting room in a clothing store with a locked door. Even if a crowd of 100 shoppers waits outside, they enter strictly one by one, lock the door, and conduct their business in complete privacy without anyone interfering or observing their state.
🟡 Middle Level
Two Fundamental Implementations of Serializable
Historically and architecturally, database engines achieve Serializability via two contrasting paradigms:
| Criterion | Pessimistic (Strict 2PL) | Optimistic (SSI) |
|---|---|---|
| Primary Algorithm | Two-Phase Locking (Strict 2PL) | Serializable Snapshot Isolation (SSI) |
| Reader/Writer Interaction | Readers block writers; writers block readers | Readers NEVER block writers (Lock-free reads) |
| Underlying Engine | Traditional physical lock managers | Multi-Version Concurrency Control (MVCC) |
| Conflict Tracking | Physical Predicate & Range Locks | In-memory Dependency Serialization Graph (rw-antidependencies) |
| Representative Engines | Microsoft SQL Server (default), MySQL InnoDB | PostgreSQL (9.1+), CockroachDB |
The Core Requirement: Handling SQLSTATE 40001 and Retries
Under Serializable, successful first-try completion of a transaction is not guaranteed by the database. When the engine detects a serialization hazard, it terminates the transaction with a standardized error:
\(\text{SQLSTATE 40001 (serialization\_failure)}\) \(\text{"could not serialize access due to read/write dependencies among transactions"}\)
Enterprise applications must catch CannotSerializeTransactionException and retry the transaction from the beginning:
import org.springframework.dao.CannotSerializeTransactionException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SafeFinancialService {
@Retryable(
retryFor = { CannotSerializeTransactionException.class },
maxAttempts = 5,
backoff = @Backoff(delay = 50, multiplier = 2.0, random = true)
)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void executeCriticalTransfer(Long fromId, Long toId, BigDecimal amount) {
accountRepository.withdraw(fromId, amount);
accountRepository.deposit(toId, amount);
}
}
Ideal Use Cases for Serializable:
- Multi-Row Invariants & Financial Ledgers: Where the sum across multiple accounts must satisfy a global condition ($A + B \ge 0$).
- Resource Allocation & Booking Engines: Hotel rooms, flight seats, or warehouse inventory where overselling is catastrophic.
- Defense Against Write Skew: When explicit pessimistic row locking (
SELECT FOR UPDATE) cannot cover missing or dynamic rows.
🔴 Senior Level
PostgreSQL Serializable Snapshot Isolation (SSI) Architecture
Introduced in PostgreSQL 9.1 based on research by Cahill, Röhm, and Fekete (2008), SSI delivers serializability on top of MVCC without blocking readers:
1. Non-Blocking SIREAD Locks
Instead of taking physical shared read locks that stall concurrent writers, PostgreSQL registers lightweight in-memory markers called SIREAD locks:
- Allocated in shared memory (
shared_buffers) on individual tuples, pages, or entire relations upon executing aSELECT. - SIREAD locks NEVER block any other thread. Writers proceed to insert or update without waiting. SIREAD locks exist solely as diagnostic breadcrumbs: “Transaction $T$ read this data range.”
2. Tracking rw-antidependencies and Dangerous Structures
The SSI engine continuously tracks edges in an in-memory Serialization Dependency Graph:
- An anti-dependency edge $T_1 \xrightarrow{rw} T_2$ forms when $T_1$ reads a version of a tuple that $T_2$ subsequently modifies or deletes. In any equivalent serial schedule, $T_1$ must precede $T_2$.
- The engine flags a Dangerous Structure (Pivot) whenever two consecutive $rw$-edges appear: \(T_{in} \xrightarrow{rw} T_{pivot} \xrightarrow{rw} T_{out}\)
- If this structure threatens to form a cycle, the PostgreSQL kernel aborts $T_{pivot}$ with
SQLSTATE 40001.
3. Lock Coarsening (Escalation)
Because SIREAD memory is bounded (governed by max_pred_locks_per_transaction):
- If a query reads thousands of tuples, individual tuple locks are promoted to a single Page Lock.
- Reading too many pages escalates the lock to a Relation Lock on the entire table.
- Side-Effect: Lock coarsening increases the false-positive conflict rate, triggering spurious aborts on non-conflicting concurrent transactions.
4 Tricky Questions
1. What are SIREAD locks in PostgreSQL, and why do they never cause thread waiting?
Answer: SIREAD locks in PostgreSQL SSI are not resource synchronization primitives (such as mutexes or standard table locks). They are informational flags recorded in shared memory that log the fact that a transaction read a tuple, page, or index predicate. When a concurrent transaction writes to a row tagged with an SIREAD lock, it does not wait; it writes the new tuple version immediately, while recording an anti-dependency edge ($T_{reader} \xrightarrow{rw} T_{writer}$) in the SSI conflict tracker. SIREAD locks serve solely to evaluate graph acyclicity.
2. Can a strictly READ ONLY transaction be aborted on SERIALIZABLE with error 40001?
Answer:
Yes.
In SSI, read-only transactions participate in the serialization dependency graph. If a READ ONLY transaction observes an intermediate, uncommitted state between two concurrently writing transactions, it can complete a dangerous dependency cycle. PostgreSQL will abort the read-only transaction to protect global serializability.
(PostgreSQL Solution: Declare the transaction as SET TRANSACTION READ ONLY DEFERRABLE;. The database will delay starting the read-only transaction until a safe global snapshot is established, guaranteeing zero aborts).
3. Are sequence counter increments (Sequence.nextval) rolled back when a transaction fails with a serialization error?
Answer:
No, never.
Database sequences (sequence) operate outside transaction contexts to maximize concurrent scalability.
The nextval() function atomically advances the counter on disk and in memory immediately. If a transaction obtains ID 105 and subsequently aborts due to a serialization conflict, ID 105 is permanently discarded, leaving a gap in the numeric sequence. The retry attempt (@Retryable) receives the next sequence number (e.g., 106).
4. How does classic Serializable in MySQL InnoDB differ from PostgreSQL?
Answer:
- MySQL InnoDB: Operates pessimistically. When
SET TRANSACTION ISOLATION LEVEL SERIALIZABLEis set, InnoDB implicitly converts every standardSELECTinto a blockingSELECT ... FOR SHARE(acquiring Shared S-Locks and Next-Key Gap Locks). Readers block writers, and writers block readers. - PostgreSQL: Operates optimistically via SSI. Queries run lock-free over MVCC snapshots. Read-write conflicts are detected retrospectively via serialization graph analysis, aborting conflicting transactions.
🎯 Interview Cheat Sheet
- Core Guarantee: Mathematically equivalent to strictly sequential execution; eliminates 100% of concurrency anomalies (including Write Skew).
- PostgreSQL SSI Engine: Non-blocking MVCC execution tracking
rw-antidependenciesvia in-memorySIREAD locks. - MySQL Engine: Pessimistic execution; implicitly transforms all
SELECTqueries intoSELECT ... FOR SHAREwith Gap Locks. - Mandatory Application Pattern: Services running at
Isolation.SERIALIZABLEmust be paired with@Retryableto handleSQLSTATE 40001serialization aborts. - Read-Only Aborts: Even
READ ONLYtransactions can abort under SSI unless markedDEFERRABLE. - Performance Trade-off: High write contention can degrade throughput by $2\times$ to $10\times$ due to conflict aborts and transaction retries.