Transaction Processing Systems: Consistency, Idempotency, and Scale
Post your first atomic split
Clone the Formance Ledger on GitHub, run the core ledger locally, and post the $2,400,000 atomic split shown above.
Post your first atomic split
Clone the Formance Ledger on GitHub, run the core ledger locally, and post the $2,400,000 atomic split shown above.
A transaction processing system (TPS) for payments enforces three write-path guarantees (consistency, idempotency, and scale) that turn at-least-once message delivery into once-only money movement. Every hop between a payment rail and your core ledger delivers messages at least once, but the money must move only once. The write path addresses the mismatch with a retried webhook, a replayed queue message, or a resubmitted payout.
In April 2018, TSB Bank migrated 5.2 million customer accounts onto a new core banking platform and locked people out of digital and mobile banking, with some staring at error messages, duplicate transactions, and, in a few cases, other people's account details. UK regulators later fined the bank £48.65 million for operational resilience failings tied to the migration.
TSB's £48.65 million fine is the visible cost; the invisible cost is what the same class of failure does inside your own stack every week. A duplicate posting burns a reconciliation team tracing it through application code, or a balance drifts and surfaces in an auditor's spot check. Consistency, idempotency, and scale are the three write-path guarantees a transaction processing system must enforce.
A TPS for payments must guarantee atomic multi-posting, idempotent retries, and per-account throughput on hot accounts. Every transaction the TPS commits must sum to zero across all accounts touched, atomically.
The TPS is the write path that turns an at-least-once delivery stream (webhooks, queue messages, batch files, retries from your own services) into once-only money movement in the ledger posting model.
A settlement that debits one account and credits two others is one transaction with three postings. If any posting is missing, the sum is not zero, and the write did not happen.
Consistency preserves the ledger's integrity and invariants, so balances never lie. Idempotency ensures every retry resolves to one effect, and scale keeps hot accounts and bursty payment rails from stalling the write path.
Formance is an open-source, programmable core ledger that unifies fiat and digital assets with regulatory-grade traceability. It enforces atomic multi-posting, idempotency, and per-account batching in the write path so retries cannot create duplicate or partial money movement and hot accounts stay fast under settlement bursts.
Consistency on a payment TPS means ledger invariants hold under concurrency. The core ledger preserves two invariants a payment TPS must enforce. These are debits-equal-credits (which breaks on partial commits) and the balance floor (which breaks under write skew).
Both are fixed by picking a named isolation level and enforcing it in the core ledger rather than the application. A worked example shows an atomic multi-posting split in Numscript.
For a payment TPS, consistency means every committed transaction preserves two invariants: the postings in each transaction sum to zero, and no account drops below zero, unless an overdraft is explicitly configured on that account. A named isolation level prevents two concurrent writers from leaving the core ledger in a state neither writer ever saw.
Debits-equal-credits breaks when a transaction commits in pieces. The debit leg (one posting in the multi-posting transaction) posts, the process dies, and the credit leg never arrives. The value is now orphaned in a transaction nobody can complete or reverse. Single-statement atomicity ensures every posting in the transaction commits together, or none does.
The balance floor breaks under write skew. Two payouts for the same account read a balance of $500,000. Each payout checks that its own $400,000 debit fits and then commits. The account sits at -$300,000 even though every writer saw a legal state. No row was overwritten, and neither transaction observed the other.
Run the write at Serializable and retry after a serialization abort. Alternatively, hold explicit row locks with SELECT FOR UPDATE and accept lock waits under contention.
Pick either Serializable or SELECT FOR UPDATE and document the chosen isolation level. A core ledger described as "safe" with no isolation level attached is a core ledger nobody has tested under concurrent debits.
Put the balance-floor check in the core ledger. An application-layer balance check is bypassed by the next service that writes postings directly. Application-layer bypass is a homegrown ledger pitfall every team rediscovers.
The worked example below uses Numscript, Formance’s purpose-built language for describing financial transactions, and describes the split as one transaction.
The account model assigns one account per role in the settlement:
A PSP settlement batch of $2,400,000 has to split atomically:
Amounts are in minor units (USD/2), so $2,400,000 is 240000000.
// PSP_SETTLEMENT
// Event: settle a PSP batch; record the platform fee and client-funds liability
send [USD/2 240000000] (
source = @counterparties:paymentServiceProviders:psp allowing unbounded overdraft
destination = {
max [USD/2 3600000] to @platform:revenue:fees
remaining to @platform:liabilities:clientFunds
}
)
set_tx_meta("event_type", "psp_settlement")
set_tx_meta("settlement_id", "batch001")
The send is a single atomic multi-posting with both destinations credited as part of the same transaction, or neither is.
Numscript describes the split, and the core ledger commits the split atomically, so the fee split cannot leave orphan value.
Idempotent ledger writes survive retries when four rules hold together: the client mints and persists the key, locks the key for a lease longer than the RPC timeout, rejects any reuse of the key with a changed payload, and keeps each key longer than the longest upstream retry window.
The caller generates the idempotency key and writes the key to its own durable store before the first request leaves the process.
If the server mints the key and returns it, a client that crashes between sending and receiving has nothing to retry with, and the next attempt is a brand-new request that posts again.
Derive the key from the business operation (one capture and one refund of one payment) rather than from the attempt, so every retry carries the same value. A fresh UUID per retry defeats the idempotency layer.
Take a lock on the key with a lease that outlives the longest downstream remote procedure call (RPC) timeout, and reject or park concurrent requests holding the same key until the first finishes.
Two instances that both check for the key, both find nothing, and both call the payment rail create the classic first-write race; the lease closes the race.
If the lease expires before the first attempt's RPC returns, a second attempt can acquire the key and post a second time. The lease has to outlive the downstream timeout.
Reusing a key with a different amount, account, or asset is a bug, and the endpoint must fail the request rather than guess which version the caller meant.
Store a hash of the request alongside the key and compare on every reuse. A PSP webhook for one capture fires twice under a flaky retry policy, and both deliveries carry the same key. The first call posts the transaction; the second returns the cached response with an Idempotency-Hit header.
The stored hash of the request decides what a same-key retry gets back. An identical payload replays the original result under an Idempotency-Hit: true header, and a changed payload fails validation instead of posting a second transaction.
Keys are scoped to a specific ledger, so the same string on two ledgers refers to two separate keys.
Retain each key for longer than any upstream retry schedule. A retry that arrives after its key has been reaped is indistinguishable from a new request and will post.
PSP webhooks, queue replays, and re-run batch files can all arrive well after a request times out, so size retention to the longest retry schedule plus a margin. Retention is your endpoint's job.
Scale on a payment TPS means per-account throughput that holds under settlement bursts without the write path collapsing on a single contended row.
Hot omnibus accounts stay fast when the write path replaces optimistic locking with per-account batching inside short time windows. Optimistic locking collapses on a hot account and creates per-write contention, while per-account batching turns that contention into a single serialized write.
Optimistic locking suits a consumer account. Writes to a consumer account are infrequent, conflicts are rarer, and a version check with retry costs almost nothing.
On a hot account, the same mechanism collapses. Every writer reads the same version, one commits, and the rest retry against the new version and collide again.
On a marketplace that routes every buyer payment through one @platform:liabilities:clientFunds account, 200 concurrent captures each read version N; one commits at N+1 and 199 retry.
New captures keep arriving, so the retry queue grows faster than it drains. Throughput on the hot account falls as load rises, the opposite of what a settlement window needs.
Per-account batching turns the retry storm into a single serialized writer per account, so throughput scales with batch depth instead of collapsing under concurrent writes. A high-volume double-entry accounting ledger hit this per-account limit.
Some accounts exceed the system's low per-account update ceiling. The redesign groups updates into time-bound batches by account affinity within a window of about 250 ms.
Each batch executes atomically against the account before the system persists the result and propagates it to reconciliation. Each batch commits multiple updates against the account in a single transaction, so per-account throughput scales with batch depth.
The new design handles more than 30 updates per second per account. The team also evaluated splitting one account across multiple rows and rejected the sharded-account architecture.
Verify a transaction processing system holds its guarantees by running eight checks against your current ledger. Two consistency checks reject bad transactions at the write path, four idempotency checks exercise the full key lifecycle, and two scale checks measure hot-account throughput under load.
Exercise the write path with transactions that should fail and confirm the core ledger rejects each one without application-layer help.
1. Post a transaction whose legs do not sum to zero and confirm the core ledger rejects the transaction at write time, with no application code in the path.
2. Fire two concurrent debits that each fit the balance alone but not together. Confirm one transaction fails safely: Serializable aborts and retries one transaction, or SELECT FOR UPDATE makes the second wait before rejecting or rolling back against the updated balance.
Both checks run against the core ledger alone, with no middleware shielding the write path.
Exercise the key lifecycle from client-side persistence through post-expiry retry and confirm each stage behaves correctly.
3. Kill the client between persisting the key and sending the request. Restart the client and confirm the retry carries the same key.
4. Compare the key lease to your longest downstream RPC timeout. The lease must be longer.
5. Replay a request with the same key and a changed amount, and confirm the write fails on hash mismatch.
6. Replay a request after your longest upstream retry window has passed, and confirm the key is still there to catch the retry.
Each of the four checks targets a distinct failure window, including client crash, concurrent delivery, mutated payload, and late arrival.
Identify the accounts that absorb the highest write pressure and confirm throughput holds under a realistic settlement burst.
7. List the accounts that appear on one side of a large share of postings. Each hot account needs a per-account batching window that is set and measured under load.
8. Confirm every committed batch propagates to reconciliation, and alert when propagation lags.
The two checks surface whether a settlement window will throttle the write path or sail through it.
Fail any of these checks and the issue is in the write path itself. Formance Ledger is built to pass all eight: atomic multi-posting, scoped idempotency keys, and per-account batching enforced in the core ledger.