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 investigate
A PAYOUT STATE MACHINE
every state named, every transition explicit, unknown is a state
createdfunds reservedhold placedsubmittedsent to railunknowntimeoutcompletedpostedfailedhold releasedreversedrefunded
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

statemax time before actionaction
reservedminutessubmit, or fail and release (a stuck worker)
submittedthe rail's SLA (seconds to minutes)move to unknown and start status enquiry
unknownhourskeep enquiring with backoff; after N hours escalate to operations
hold on a card authorisation7 days typicallyauto-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.