Serializable means as if one at a time, in some order
Not necessarily commit order. A transaction that committed later may go first, as long as everything each one read fits that order.
Each payment is correct alone. Code review passes it; a solo test passes it.
The truth
The rule breaks only in the mix. SERIALIZABLE is the one level where “works alone” means “works in production”.
Three ways to get the one-at-a-time result
Run one transaction at a time, lock what you read until commit, or run on snapshots and abort when dependencies line up dangerously.
SSI hunts for a pivot, not a whole cycle
T1 missed T2’s newer write; T2 missed T3’s. Once T3 commits, PostgreSQL prefers to abort the pivot, T2, so its retry can’t hit the same conflict.
≈97%
of snapshot isolation’s throughput, kept by SSI
Requests a second on the RUBiS auction benchmark, snapshot isolation 435 and SSI 422. Strict two-phase locking managed 208, and aborted 25 times as often as SSI.
Aborts are normal. Retry properly.
- Restart from BEGIN: the reads caused the failure
- Retry only 40001 and deadlocks; surface the rest
- Capped exponential backoff, full jitter, bounded attempts
- Side effects after commit; idempotency key inside
Who must run at SERIALIZABLE?
Bad: Only the payment API
A batch job inserting transfers at READ COMMITTED escapes the tracking.
Good: Every writer
Set default_transaction_isolation to serializable; let no other level sneak in.
Remember
Some one-at-a-time order, not commit order. Expect aborts, and retry the whole transaction from BEGIN with jittered backoff.