Skip to main content
Banks and blockchains do not take part in distributed transactions, so a cross-border payment cannot be one atomic commit. Corridor runs each plan as a saga: a sequence of legs, each with a compensating action, persisted in Postgres so it survives crashes. See ADR 0004.
Saga forward steps and reverse compensationSaga forward steps and reverse compensation

Fig. 07 · Forward legs, a failure at the partner, and compensation in reverse order with an on-chain refund.

The leg contract

Every call gets an idempotencyKey of ${sagaId}:${stepIndex}. Bookings without an external reference are keyed on it, so re-running a step never double-books. Legs that move money first look for the movement by its Corridor Reference, so a retry after a crash between “sent” and “recorded” finds the transfer instead of sending it again.

State

Failure semantics

The step is retried up to maxAttempts with backoff. Network errors and RPC timeouts land here.
The step fails at once (a validation that a retry cannot change), and the saga compensates.
Completed steps are undone from last to first, each through its own compensate with its own bookings. Order matters: unwinding out of order can leave a balanced ledger with a false audit trail. A forced partner failure on testnet refunds the Tempo leg on-chain and reverses every booking.
No retries and no automatic compensation. Used when money has already left Corridor’s control, for example a partner rejecting a payout it was already funded for. Undoing earlier legs would be wrong until the partner’s refund is confirmed, so a human closes it.

Leases

A saga is driven by one worker at a time. run() takes a lease (lease_owner, lease_until, default 120 s) with a conditional update; another live worker that tries gets the current status back without doing work. If a worker dies, its lease expires and any worker can resume from the last durable step.
A worker whose lease expired mid-leg could still finish its call. Legs are idempotent through the memo lookup, so the chain is never double-spent, but fencing tokens on bookings are on the roadmap to make this airtight.

Fault injection

SagaInput.faults.failAt makes a chosen leg kind fail, which is how the demos prove compensation on-chain. The console button “Payout with partner failure” uses it; see Failure and compensation.