Reference build 02 · Finance
Payments Ledger
Double-entry core with idempotent write paths and reconciliation designed in, not bolted on.
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.
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.
Payments Ledger, in one picture.
- 01Payment requestArrives carrying an idempotency key
- 02Unique constraintThe database rejects the duplicate — no application check to forget
- 03EntriesAppend-only double-entry rows; nothing is updated in place
- 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
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.
| Option rejected | Why it lost |
|---|---|
| Keeping a mutable balance column with row locks | Correct under load, but it discards history. When a balance is disputed there is nothing to reconstruct it from. |
| Event sourcing the entire domain | Right 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 fees | It expands PCI scope across the whole platform. The saving is far smaller than the cost of the audit surface. |
Other reference builds
Keep reading.
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
