Transaction Isolation
Banking flows that read state and write back based on that state — money movement, hold consumption, settlement — need stronger semantics than the PostgreSQL default ofREAD COMMITTED. CrateStack exposes the two pieces
this requires: explicit per-transaction isolation levels and a retry
loop for serialization failures.
run_in_isolated_tx
The helper wraps a closure in BEGIN, SET TRANSACTION ISOLATION LEVEL ...,
the closure body, and COMMIT:
Supported isolation levels
Serializable. Lighter
“consistent snapshot” reads use RepeatableRead. The default level
(without the helper) remains PG’s READ COMMITTED.
Retry on serialization failure
UnderSerializable (SSI), Postgres can refuse to commit a transaction
that participates in a read-write dependency cycle, raising SQLSTATE
40001. The same code is raised on deadlock detection (40P01). Both
are transient — the PG docs are explicit
that the entire transaction must be retried.
The wrapper retries automatically:
- up to
MAX_RETRIES_DEFAULT(3) times viarun_in_isolated_tx - up to a caller-chosen budget via
run_in_isolated_tx_with_retries(pool, level, retries, body) - on errors raised from any statement inside the body
- on errors raised from
tx.commit()itself — SSI can defer the conflict to commit time (write-skew)
Procedure-level opt-in
Procedures declare their required isolation level inline:- one
@isolationattribute per procedure - the level argument is a quoted string:
"serializable","repeatable_read", or"read_committed" - case-insensitive; underscores tolerated
ProcedureMetadata::isolation and decides whether to wrap
its body in run_in_isolated_tx. Auto-wrapping the dispatcher is on the
roadmap; today the choice is explicit.
Body must use the supplied transaction
Every statement in the body should run through&mut *tx. Statements
that escape to the pool will not see the snapshot the wrapper opened, and
won’t roll back on retry. The closure signature pins this:
When commit-time retry matters
Two scenarios surface 40001 fromtx.commit() rather than from a
statement:
- Write-skew anomaly. Two transactions read overlapping rows, write disjoint rows, and SSI detects the read-write dependency only at the commit boundary.
- Predicate-lock contention. A long-running SELECT participates in conflicts that aren’t visible until the transaction tries to land.
What this is not
- not a replacement for application-level conflict handling — some business logic genuinely needs the user to re-confirm after a stale read; the retry loop is the safety net, not the policy
- not a distributed transaction coordinator — PG isolation only applies inside one database
- not free —
Serializableadds locking overhead; benchmark before applying it to read-heavy procedures
Read Next
- Optimistic locking — row-level version checks complement transaction-level isolation
- Idempotency — duplicate-execution protection at the request boundary