Skip to content
ApexSutra

Reference build 02 · Finance

Payments Ledger

Double-entry core with idempotent write paths and reconciliation designed in, not bolted on.

Reference architecture · illustrative
The problem

What we'd be solving.

A platform is moving money on behalf of merchants using a balance column and a payments table. Retries occasionally double-charge, month-end reconciliation is manual, and no one can reconstruct why a balance is what it is.

NestJSTypeScriptPostgreSQLKafkaTerraformKubernetes

The approach

Four decisions that shape everything else.

  • 01

    Balances become derived, not stored

    An append-only journal of paired debit and credit entries is the source of truth. Balances are a projection, which means any historical balance can be recomputed exactly.

  • 02

    Idempotency enforced by the database

    A unique constraint on the idempotency key, inside the same transaction as the entry write. Application-level checks lose to concurrency; a constraint does not.

  • 03

    Payment state modelled explicitly

    Authorised, captured, settled, refunded and failed are distinct states with legal transitions, rather than booleans that can contradict each other.

  • 04

    Reconciliation runs on a schedule

    A nightly job compares the internal journal against processor settlement reports and raises discrepancies as work items — so drift surfaces in a day, not a quarter.

Shape of it

Payments Ledger, in one picture.

  1. 01Payment requestArrives carrying an idempotency key
  2. 02Unique constraintThe database rejects the duplicate — no application check to forget
  3. 03EntriesAppend-only double-entry rows; nothing is updated in place
  4. 04BalanceDerived by summing entries, so it cannot disagree with them

Deliberately off the main path

  • Payment stateModelled as explicit states rather than a boolean that cannot express "we are not sure yet".branches from 01 Payment request
  • ReconciliationA scheduled job replays entries against the processor. Designed in from the start, not bolted on after a mismatch.branches from 03 Entries
The balance is not a column anybody writes to. Entries are appended, the balance is summed from them, and duplicate submissions are stopped by a database constraint rather than by application code.

What we rejected

The alternatives, and why they lost.

Anyone can list the technology they chose. What tells you whether a team can be trusted with your system is whether they can explain what they didn't choose.

Architecture options rejected for the Payments Ledger reference build, and the reasoning for rejecting each
Option rejectedWhy it lost
Keeping a mutable balance column with row locksCorrect under load, but it discards history. When a balance is disputed there is nothing to reconstruct it from.
Event sourcing the entire domainRight for the ledger, needless complexity for everything else. Scoping immutability to where it pays keeps the rest of the system ordinary.
Handling raw card data to reduce processor feesIt expands PCI scope across the whole platform. The saving is far smaller than the cost of the audit surface.

Start here

Tell us the problem.
We'll be honest about the fit.

A first conversation costs nothing and is useful even if you go elsewhere — you'll leave with an architecture opinion and a realistic sense of scope.

  • Reply within 24 hours
  • No sales sequence