← All insightsFintech · Architecture

Your ledger is foundational. Treat it that way.

Teleport-X·January 22, 2026·5 min

Most fintech reconciliation nightmares do not start in reconciliation. They start on day 40, when a reasonable engineer adds a balance column to the accounts table because the balance endpoint is slow. Two years later, the team is writing increasingly elaborate jobs to figure out which of seventeen code paths updated the balance, why the ledger disagrees with it, and which of those answers to show a regulator.

A ledger is not a reporting view. It is the source of truth about money movement, and it has exactly one job: make it impossible to describe a state of the world that could not have actually happened. Every design decision either upholds that invariant or quietly undermines it.

Double entry is not a formatting choice

The rule is simple and unforgiving. Every posted transaction consists of entries that sum to zero across accounts, in every currency, at every point in time. There is no such thing as an orphan debit. There is no such thing as a credit that “will balance later.” If your schema allows a single sided write, it allows a bug that creates money from nothing, and eventually, someone will write it.

The table you want is ledger_entries, append only, with columns for transaction_id, account_id, direction (debit or credit), amount, currency, posted_at, and a stable idempotency_key. Balances are derived. Never stored. A database constraint enforces that entries with the same transaction ID sum to zero per currency. Writes happen only through a posting API that accepts a whole transaction or rejects it. Partial posts are not representable.

Idempotency is the first primitive, not the last

Every payments system eventually retries. The network hiccups, the worker restarts, the webhook gets redelivered. A ledger that has not been designed for this will double post, and double posts in production are discovered by customers, not by alerts.

Every posting call carries a caller supplied idempotency key, scoped to the originating event (a Stripe charge ID, a card network authorization, an internal transfer request), not to the request envelope. The posting API persists the key, the request hash, and the resulting transaction ID in a unique index. A retry with the same key returns the original transaction unchanged. A retry with the same key but a different request hash is a loud error, not a silent overwrite.

Immutability beats “updates” every time

Ledgers do not get edited. A mistaken transaction is corrected by posting its reversal, not by mutating the original row. The history of what was believed at every point in time is as important as the current state. It matters for audits, for disputes, for regulators, and for the oncall engineer trying to understand what the system did at 03:17 last Tuesday.

Follow the same rule for account metadata. If an account changes status, emit a new versioned row. Do not overwrite. Time series state is cheap. Forensic debugging on a mutated ledger is not.

FX, rounding, and the accounts everyone forgets

Any system that touches more than one currency needs explicit FX mechanics, and it needs them in the ledger, not upstream. A cross currency transaction posts at least three entries: the debit in the source currency, the credit in the destination currency, and a posting to an FX P&L account that captures the rounding residue and the spread. If the spread is booked implicitly (“we apply a rate and move on”), it will be off the ledger and off the P&L, which means it will be wrong and invisible at the same time.

Pick a rounding mode (banker’s rounding is a reasonable default) and apply it consistently at the ledger boundary. Store amounts as integers in the minor unit of the currency: cents, pence, satoshis. Floats are not a currency type. They never were.

Closing the books is a function, not a cron job

A close is a read over an immutable slice of the ledger as of a cutoff timestamp. If a ledger is append only and entries carry a posting time, closing period P is a three step operation: define the cutoff, sum entries up to the cutoff, freeze the report. It does not require “locking” the ledger. It does not require “freezing” writes. Late entries that belong to period P are posted as prior period adjustments into period P+1, with explicit accounts. This is how accountants already think about the problem; it is worth making the software match.

The test you should run this week

Pick a random day in the last quarter. Can you reconstruct, from your ledger alone, the exact balance of a specific account at 14:00 UTC that day? Can you explain every entry that contributed to it, traced back to the upstream event that caused the posting? Can you do it without joining to any table outside the ledger?

If the answer is yes, your ledger is foundational in the way a ledger should be. If the answer is “mostly” or “we’d have to check,” the bill for that comes due the first time someone asks a question that depends on the answer being unambiguous: a dispute, a regulator, a board deck, an acquirer’s diligence. Ledgers are the part of the system you cannot retrofit under pressure. Get them right before you need them to be right.