← All writing

December 5, 2025

Designing Reliable Transaction Systems

Most of the hard problems in a transaction system aren't about the happy path. They show up when a request is retried, a process crashes mid-write, or two operations touch the same balance at the same time.

Idempotency is the baseline, not an optimization

Any operation that moves value needs to be safe to retry. A client that times out and retries, a queue that redelivers a message, a webhook fired twice — all of these are normal, not edge cases. Idempotency keys tied to the operation, checked before any state changes, are what make retries safe instead of dangerous.

Locks protect invariants, not performance

Distributed locks around a wallet or account exist to enforce an invariant — that two transactions don't read the same balance and both proceed as if they had exclusive access to it. The lock scope matters more than the lock implementation: too coarse, and unrelated operations queue behind each other; too fine, and the invariant it was supposed to protect leaks through the gaps.

Transaction state has to be explicit

A transaction usually passes through created, signed, broadcast, confirmed, and settled — and can fail or stall at any of those points. Modeling this as an explicit state machine, persisted and queryable, makes recovery a matter of looking up where a transaction is stuck and resuming from there, instead of guessing from logs.

Recovery is a designed path, not a fallback

Systems that handle failure well usually decided in advance what "resume after a crash" looks like: what gets replayed, what gets reconciled against the source of truth (the chain, in this case), and what requires a human to look at it. Designing that path before it's needed is the difference between an incident and a non-event.