Part 3 · 2 chapters · ~12 min
Payment State Machines
Payouts, transfers and card authorisations as explicit state machines, persisting state before external calls, holds and captures, the unknown state and status enquiry, reversals and chargebacks, transition tables enforced in code and database, and timeouts per state.
7
States, transitions and the unknown
code
// transitions as data: anything not listed is impossible
const transitions = {
created: ['reserved', 'failed'],
reserved: ['submitted', 'failed'],
submitted: ['completed', 'failed', 'unknown'],
unknown: ['completed', 'failed'],
completed: ['reversed'],
failed: [], reversed: [],
} as const;
// enforce in the database too: an UPDATE that only succeeds from an allowed state
UPDATE payouts SET state = 'completed', updated_at = now()
WHERE id = $1 AND state IN ('submitted', 'unknown')
RETURNING id; -- zero rows = an illegal or duplicate transition: log and investigateA PAYOUT STATE MACHINE
every state named, every transition explicit, unknown is a state
swipe the figure sideways, or tap expand for full screen
1/6
created to reserved
Validation passes and a hold reserves the funds. If funds are insufficient, the payout fails immediately with a clear reason.
validate, then reserve fundsinsufficient funds fails early
8
Timeouts, enquiries and reversals
| state | max time before action | action |
|---|---|---|
| reserved | minutes | submit, or fail and release (a stuck worker) |
| submitted | the rail's SLA (seconds to minutes) | move to unknown and start status enquiry |
| unknown | hours | keep enquiring with backoff; after N hours escalate to operations |
| hold on a card authorisation | 7 days typically | auto-release expired holds |
Chargebacks and reversals arrive days or weeks later from processors and banks. Model them as new entries and new state machines (dispute: opened → evidence submitted → won or lost) linked to the original, never as edits.