Skip to content
Engineering9 min read

Designing a Double-Entry Ledger Core for High-Volume Payments

A reference architecture for a double-entry ledger that stays balanced, auditable, and fast under sustained payment load — the invariants, data model, and scaling choices that matter.

  • Double-entry accounting
  • PostgreSQL
  • Event sourcing
  • Idempotency keys
  • gRPC
  • Kafka

By TRAGenX Engineering

Why a ledger is the hardest easy problem

On the surface, a payments ledger looks trivial: money leaves one account and arrives in another. The difficulty is not the arithmetic — it is holding a single, non-negotiable invariant true across millions of concurrent operations, partial failures, retries, and years of history. Every unit of value that enters the system must be accounted for at every instant, and the sum of all movements must always be zero.

The pattern that has held up for centuries is double-entry bookkeeping. Every economic event is recorded as a transaction made of two or more entries whose signed amounts sum to zero: a debit to one account is matched by an equal credit to another. Value is never created or destroyed inside the ledger — it only moves between accounts. That constraint is what makes a ledger trustworthy, and it is also what makes it unforgiving to build. A design that treats balances as a mutable number you increment and decrement will eventually drift; a design that treats balances as the derived consequence of an immutable list of entries can be proven correct.

This write-up describes how a system like that is architected at the depth we operate — the invariants first, then the data model, then the concurrency and scaling decisions that let it hold under real payment volume. It is a capability piece, not a report on any specific deployment: there are no client names, no throughput figures, and no benchmark claims here, only the engineering reasoning.

The core data model: accounts, transactions, entries

The model has three primitive tables and one rule. Accounts hold identity and a currency; they do not hold a balance you write to directly. Transactions are the atomic unit of change — a single business event, such as a transfer, a fee, or a settlement. Entries are the individual signed movements that belong to a transaction, each referencing one account and carrying an amount in the smallest currency unit (minor units, never floating point).

The rule binds them together: the entries of a single transaction must sum to exactly zero within each currency. A transaction is either committed with all of its entries or it does not exist at all — there is no valid state in which half of a transfer is recorded. This is what lets the ledger reason about correctness locally, one transaction at a time, without ever needing to trust a running total.

Entries are append-only. Nothing in the ledger is updated or deleted after it is committed; a correction is itself a new transaction that reverses the original, so the history remains a faithful record of what actually happened and when. Balances become a projection over that history — the sum of an account's entry amounts up to a point in time. For hot paths you cache that projection in a balances table that is only ever advanced by the same transaction that writes the entries, so the cached figure and the entry history can never disagree.

Guaranteeing atomic balance under concurrency

The zero-sum invariant is only useful if it holds under concurrent writers competing for the same accounts. The foundation is the database transaction: all entries of a business transaction, plus any balance-projection update, are written inside one atomic commit. If any part fails, the whole thing rolls back and the ledger is untouched.

Contention is the real design problem. A popular account — a treasury, a fee pool, an omnibus account — becomes a hotspot every writer needs to touch. Pessimistic row locking is correct but serializes throughput on that row; optimistic concurrency with a version check and bounded retries keeps most transactions parallel and only pays a cost when two genuinely collide. Where a single account is unavoidably hot, the projection can be sharded into buckets whose sum is the true balance, spreading the write contention while keeping the invariant exact.

Retries introduce the second hazard: a client that times out and resends a transfer must not move money twice. Every write carries a caller-supplied idempotency key, stored under a unique constraint. The first request commits; a retry with the same key returns the original outcome instead of creating a duplicate. Combined with the append-only entries and the zero-sum check, this makes the write path safe to retry aggressively — which is exactly what a payment network needs, because retries are not an edge case but the normal weather.

Scaling for sustained payment volume

A ledger that is correct but slow simply moves the failure from your books to your queue. Scaling one starts with accepting that reads and writes have opposite shapes. Writes are small, contended, and must be strongly consistent. Reads — statements, reporting, reconciliation — are large, bursty, and tolerant of a little staleness.

We separate them. The write path stays lean: validate, check idempotency, enforce zero-sum, commit entries, advance the projection. Everything else is derived asynchronously. Committed transactions are published as immutable events onto a durable log; downstream consumers build statement views, analytics aggregates, and search indexes from that stream at their own pace. Because the event log is the same ordered truth the ledger committed, these projections are eventually consistent with the ledger without ever putting read load on the write path.

Partitioning follows the access pattern. Entries partition cleanly by time, which keeps the write-hot recent data small and lets old partitions age into cheaper storage without touching live indexes. Accounts partition by identity when a single database can no longer hold the working set, with the deliberate constraint that a transaction's entries stay within one partition where possible so the atomic commit remains a local operation rather than a distributed one. Cross-partition movements, when unavoidable, are handled as an explicit two-phase workflow with a reversing entry as the compensating action — never as a silent best-effort update.

Auditability and reconciliation as first-class features

In a payments system, being able to prove what happened is as important as making it happen. The append-only design gives this almost for free: the entry history is a complete, ordered, immutable record, so any balance at any past instant can be reconstructed by replaying entries up to that point. There is no separate audit log to keep in sync because the ledger itself is the audit log.

Reconciliation is the routine that turns that property into operational confidence. Internally, a continuous check re-derives every account balance from raw entries and compares it to the cached projection; any divergence is a bug caught immediately rather than a discrepancy discovered at month end. Externally, the ledger is reconciled against the real world — bank statements, card-scheme settlement files, payment-processor reports — by matching each external movement to the internal transaction that should mirror it, and flagging anything unmatched on either side.

Because corrections are modelled as reversing transactions rather than edits, the reconciliation trail stays honest even when things go wrong: the original mistake, the reversal, and the correct entry are all visible, in order, forever. That is the difference between a system you audit and a system that is inherently auditable.

Operational realities and where the effort actually goes

The interesting engineering in a ledger is rarely the happy-path transfer. It is the surrounding discipline that keeps the invariant intact through everything a production system endures. Schema changes must preserve the append-only contract, which means additive migrations and versioned entry shapes rather than in-place rewrites. Deploys must be safe against in-flight transactions, so the write path is designed to be forward-compatible across a rolling release.

Multiple currencies mean the zero-sum rule is enforced per currency, and any foreign-exchange movement is two balanced transactions linked by a rate record — never a single entry that quietly mixes units. Failure handling is explicit: a downstream projection can fall behind or be rebuilt from the event log without the write path ever noticing, because the log, not the projection, is the source of truth.

None of this requires exotic infrastructure. A relational database with real transactions, a durable ordered log, and a disciplined append-only model carry a great deal of load before anything more specialised is warranted. The value is in getting the invariants right early, because a ledger that has drifted is far more expensive to repair than one that was built to make drift impossible. That is the kind of foundational systems work we take on — designing the core that everything else in a financial product has to trust.

Building something like this?

This is the kind of foundational systems work TRAGenX takes on. If you have a project that needs a core other things can trust, tell us about it and we'll get back to you.