Docs/Get startedv0.0.1-preview
Modelling common flows
Authorization and capture, wallet top-ups, marketplace splits, and treasury funding — mapped onto the ledger API.
Most value-movement products reduce to a handful of ledger shapes. This page maps the common ones onto TulaDB operations, so you can see your own system in the API before writing integration code.
Card-style authorization and capture
The authorization is a reserve; the capture is its commit; the void is its rollback. The customer’s available balance reflects the hold the moment it’s placed; posted balance moves only at capture.
authorize → reserve(txn_id="auth-7781", amount=5000) # PENDING, available -5000
capture → commit_by_key(..., "auth-7781") # POSTED
— or —
void → rollback_by_key(..., "auth-7781") # VOIDED, funds released
Partial capture is not a single engine operation yet — commit settles the full reserved amount. Capture the full hold, then refund the difference as its own transfer (its own txn_id, e.g. auth-7781-refund), keeping every movement individually attributable and leaving no unprotected window; see partial capture for the alternative and its trade-off. The settlement worker retries safely forever: commit is idempotent, and a hold never silently leaks if the worker dies between steps.
Wallet top-up and spend
Money enters the ledger through a control account when the PSP confirms the top-up; spending is ordinary transfers between user wallets and merchant accounts.
top-up → transfer(control → wallet, txn_id=psp_payment_id)
spend → transfer(wallet → merchant, txn_id=order_id)
withdraw → reserve(wallet → control, txn_id=payout_id) # hold during payout processing
then commit (payout sent) or rollback (payout failed)
Use the PSP’s payment id as txn_id and webhook redelivery becomes harmless — the duplicate replays instead of double-crediting. The wallet UI reads available_balance (holds included); finance reads posted_balance; both are the same account, no shadow bookkeeping.
Marketplace split with escrow
A checkout that must end up 85/12/3 across seller, platform, and processor is a reserve at purchase, then a batch of transfers at release:
checkout → reserve(buyer → escrow, txn_id=order_id) # buyer funds held
delivery → commit_by_key(..., order_id) # escrow funded
release → transfer_batch([
escrow → seller (txn_id=order_id+"-seller"),
escrow → platform (txn_id=order_id+"-fee"),
escrow → processor (txn_id=order_id+"-proc"),
])
refund → rollback_by_key(..., order_id) # before release
Each leg has a deterministic txn_id derived from the order, so a crashed release job reruns its batch and the landed legs answer OK_DUPLICATE. A declined refund (already released) comes back ERR_WRONG_PHASE — a durable, supportable answer rather than a race. Retrying it is safe but gets the same answer; get_transaction_by_key on the order shows the hold POSTED.
Treasury and settlement tiers
When you track both positions (who owes whom) and settlement (what actually moved at the bank), use a position–settlement ledger and tiered transfers: one transaction moves both tiers atomically, so positions can never drift from settlement — the reconciliation between tiers is zero by construction, not by a nightly job.
settle → tiered_transfer(dr_position, cr_position,
dr_settlement, cr_settlement,
amount, txn_id=settlement_ref)
Declines your ops team can see
Whatever the flow, route business declines through the ledger rather than swallowing them: let limit checks decline naturally (ERR_LIMIT is recorded), and record upstream refusals — a fraud block, a compliance hold — with an explicit reject. Six months later, “show every declined payout for this account” is a query, not an archaeology project.
One habit that makes all of this work
Every pattern above leans on the same discipline: derive txn_id deterministically from the business event — the order id, the payment id, the settlement reference — never from a random UUID minted at send time. Deterministic identity is what turns crashes, retries, and redeliveries from correctness problems into noise. Read Idempotency once and this habit becomes obvious.