Docs/Use casesv0.0.1-preview
Payment switch settlement
The ledger inside an instant-payment or interbank switch — RRN-deduplicated postings, refusals on the books, and positions that survive faults exactly.
A payment switch sits between institutions and keeps the one number that must never be wrong: each participant’s position. The workload is unforgiving in a specific way — every message is delivered at-least-once, every refusal must be accounted for, and the switch cannot stop settling because a node died. Those three properties map directly onto what the engine already enforces.
This is TulaDB’s lead use case: a real-time posting ledger in front of a batch core. An institution joining an instant-payment rail posts every payment as it happens into one balance per account, with limits enforced on each payment, while the core banking system keeps settling in batch behind it. What sits between the rail and the ledger is yours: TulaDB has no rail adapters and does not parse ISO 20022 messages — your integration maps each message onto a transfer, a hold or an explicit reject.
The model
Each participant institution is an account; a posting moves value between the payer’s and payee’s institution positions. The scheme’s own reference — the RRN or transaction id — is the txn_id, which makes the scheme’s identity the ledger’s identity:
S = c.create_ledger(LedgerInstance(ledger_id=0, currency=840)).ledger_id
BANK_A, BANK_B, SWITCH = 15001, 15002, 9 # integer ids
for a in (BANK_A, BANK_B):
c.create_account(AccountCreate(id=a, ledger_id=S, currency=840,
limit_low=-10**12, limit_high=10**12))
c.transfer(TransferCreate(source_id=SWITCH, ledger_id=S, currency=840,
debit_account_id=BANK_A, credit_account_id=BANK_B,
amount=10_000, txn_id="rrn-000123456789"))
The engine’s answer: the first posting applies; the redelivered message returns OK_DUPLICATE — the scheme’s retry storm settles once. Duplicate suppression by RRN is usually a bespoke subsystem inside a switch; here it’s the write path itself, and it lasts as long as history does — forever by default.
Refusals are positions too
When the beneficiary institution refuses (insufficient funds, account closed, compliance hold), the refusal is part of the settlement record — post it as an explicit reject carrying the scheme’s response code verbatim:
c.reject(RejectCreate(source_id=SWITCH, ledger_id=S, currency=840,
txn_id="rrn-000123456790", amount=4_000,
reason_code=51)) # the scheme's own refusal code, recorded as-is
# → status 41, durable; replaying this RRN returns the refusal, not a retry
End-of-day, “every refusal with response code 51 today” comes from the ledger, not from log archives. Search can’t filter on the reason code itself, so search for explicit rejects (result_code=41) over the day’s time range and filter the rows on reject_reason_code — or run it as SQL over the analytics export:
rows = q.search_transactions(TxnSearch(ledger_id=S, currency=840, result_code=41))
code_51 = [r for r in rows if r.reject_reason_code == 51]
Net settlement and exposure caps
Participant limit_low is a net debit cap — the engine refuses, atomically, any posting that would push an institution past its exposure limit, which is precisely the risk-management primitive deferred-net-settlement schemes require. For gross settlement against a central bank account, model it as the FBO/omnibus pattern; the two-phase lifecycle covers schemes with an accept/confirm handshake, with a TTL expiring unanswered requests.
Why this workload suits the engine
- Conservation is the product. Positions must sum to zero across participants at every instant — the double-entry invariant, held exactly through kills, partitions, and double-faults in the published campaigns.
- Availability through faults: in the August 2026 campaign (an earlier development build, 3 × m8gd.2xlarge), settlement continued at 7,400–9,300 TPS on the pessimal single-account shape, at 16 transfers per request, through the full fault schedule; a real switch’s load spreads across many participant accounts. Measure your own build and hardware before relying on a figure.
- The audit posture is built in: append-only history, hash-chained WAL, and an offline store verifier (
tuladb_verify_store) — the evidence artifacts a scheme operator’s auditors ask for.
Related
Idempotency · Resilience · FBO sub-ledgering · Limits and sizing