Subscribe to get high-signal insights on how modern fintech is built.

engineering

The Ledger Is the Product

Every fintech product is a UI on top of a ledger. Get it wrong and every layer above inherits the error.

By Alex Kugell ·

Why does a $25 transfer between two accounts in your app require seven database writes?

You could move $25 with a single UPDATE statement. Decrement one row, increment another. Two writes, done. Every web developer's instinct says this is correct. In fintech, it's the kind of correct that gets your banking partner to revoke API access.

The real answer starts with a 700-year-old constraint that most engineers have never thought about, and it shapes every architecture decision in every system that touches real money.

The constraint that built modern finance

Double-entry bookkeeping has one rule: money is moved, never created or destroyed. Every transaction records a source account and a destination account, and the amounts must be equal. If $25 leaves Account A, exactly $25 arrives in Account B. The constraint doesn't bend for eventual consistency or deferred reconciliation.

This sounds obvious. It's also the single most important invariant in financial software, because it makes an entire category of bugs structurally impossible.

When a balance is derived from the sum of all entries rather than stored as an independent number, phantom money can't exist. If someone reports a balance of $10,000 and the entries only sum to $9,500, the entries win. There's no ambiguity, no judgment call. The ledger entries are the source of truth.

The seven writes in that $25 transfer? A transaction record, two ledger entries (debit and credit), a hash-chain link for tamper evidence, two timestamp records (value time and booking time — settlement comes later from the processor), and a reference linking the whole operation back to the user action that triggered it. Each one exists because someone once shipped a system without it and lost money, or lost an audit, or both.

Three tables hold the entire financial model

A ledger's physical schema is small. Three core tables handle everything:

TablePurpose
AccountsThe chart of accounts. Every bucket money can sit in, tagged with its type (asset, liability, equity, revenue, expense) and its normal balance direction
TransactionsThe atomic unit of work. A group of entries that must sum to zero. Carries the idempotency key, timestamps, and a human-readable description
EntriesIndividual debits and credits, each belonging to exactly one transaction and referencing exactly one account
Three Tables, One Invariant
Accounts
id
name
type (asset | liability | equity | revenue | expense)
normal_direction (debit | credit)
Transactions
id
idempotency_key
value_time
booking_time
description
Entries
id
transaction_id → Transactions
account_id → Accounts
direction (debit | credit)
amount
SUM(debits) = SUM(credits) per transaction
Enforced at write time, inside the database transaction

Every transaction is a set of entries whose debits and credits net to zero. Enforce this at write time, in the database transaction, before the commit. If you validate after the fact, you've already created a window where your books don't balance. However briefly that window exists, it will surface during reconciliation.

A single user-facing action, like paying a subscription, might create half a dozen entries. The $9.99 subscription payment is one entry. The $0.30 processing fee is another. The platform's revenue share is a third.

Each one moves money between accounts, and each one must balance against the others within the same transaction.

The balance you want to store is the balance you shouldn't

Every engineer's first instinct is to add a balance column to the accounts table. Read one row, return the number. It's the obvious optimization, and it will eventually cost you an audit.

A stored balance is a cache with no invalidation policy. The moment a write fails halfway, or a concurrent transaction reads between two entries of the same logical operation, the stored balance and the derived balance diverge. Unlike a web cache that serves stale data for a few seconds, a diverged financial balance means your system is reporting money that doesn't exist, or hiding money that does.

The correct architecture: balance is always the sum of an account's entries. You can cache the derived result for read performance, but the cache must be fully rebuildable from the entries themselves. If the two ever disagree, the entries win.

At low volume, this is straightforward. Sum the entries, return the result. At high volume, it's an O(n) scan that grows with every transaction the account has ever processed.

The performance pattern is balance snapshots: periodically compute and store the sum at a known point in time. To read a current balance, load the most recent snapshot and sum only the entries posted since then. An O(n) problem becomes O(recent).

Balance Is Derived, Never Stored
Entries
Deposit+500.00
Subscription-9.99
Transfer in+250.00
Wire out-200.00
Fee-2.50
SUM$537.51
=
Balance
$537.51
derived from entries
Snapshot (cache)
Rebuildable from entries. If suspect, discard and recompute.

But the snapshot is a performance optimization, not a source of truth. If it's ever suspect, you discard it and rebuild from the entries. The entries are the product. Everything else is a view.

Time is not one field

A card payment at a coffee shop touches three different moments in time. The customer taps their card at 8:02 AM (value time). The payment processor sends a webhook at 8:04 AM and your system records the event (booking time). The processor settles funds to your account two days later (settlement time).

Most applications would store this as created_at: 8:04 AM and move on. In fintech, collapsing those three timestamps into one destroys information you'll need during a dispute, a reconciliation, or an audit.

A transaction booked on March 31 but valued on March 30 might fall into different reporting periods. An auditor asking "what was the balance at end of day March 30?" needs value-time arithmetic. An engineer debugging a failed webhook needs booking-time ordering. A finance team reconciling against the bank statement needs settlement time.

One Payment, Three Timestamps
Value time
8:02 AM
Customer taps card
Booking time
8:04 AM
System records webhook
Settlement time
Oct 9
Funds arrive in your account
2 min
~2 days
Collapse these into one created_at and you lose the ability to answer three different questions

Three timestamps, three different questions, three different correct answers. Drop any one of them and you've created a category of questions your system can't answer.

Immutable means you fix forward

An entry posted to the ledger is permanent. You don't update it. You don't delete it. If you need to correct a $50 charge that should have been $45, you post a new entry: a reversal of the original $50 and a new entry for $45. Both the mistake and the correction are visible in the history, linked to each other with a reference that explains why.

An auditor doesn't want to see clean data. They want to see what happened, including the mistakes, and verify that corrections were handled properly. An editable ledger proves nothing because there's no way to distinguish between "this is what happened" and "this is what someone edited it to say."

The immutability constraint also solves a practical engineering problem. When posted entries can't change, every read against a past point in time returns the same result, forever. No cache invalidation for historical queries. No worrying about whether a background job mutated something. The past is frozen.

Hash chaining extends this guarantee. Each entry stores a SHA-256 hash of its contents plus the previous entry's hash. A single tampered record breaks the chain, and a background integrity checker can detect it automatically. Periodic checkpoints to S3 with Object Lock provide an external witness, and a SOC 2 auditor can verify the entire chain with a SQL query.

You don't need a blockchain for this. You need an append-only Postgres table, a hash function, and an external checkpoint.

Scaling breaks in exactly one place

The double-entry model scales well in theory. In practice, it has a single chokepoint.

Your average customer account might receive a few transactions per day. But a platform has accounts that every transaction touches: the omnibus wallet that pools user funds, the fee collection account that records every processing charge, the settlement account that reconciles with the bank. These accounts might receive thousands of writes per second. Engineers call them hot accounts.

Every write to a hot account contends for the same rows, and serializable transactions, which you need for correctness, serialize those writes.

Three patterns address this:

Batching. Buffer entries at short intervals, a few hundred milliseconds, and write them as a single batch. This reduces contention at the cost of a small delay in balance freshness. For accounts that don't need real-time balance reads, it's the simplest fix.

Partitioning. Split entries by account ID or time range. Cold entries, anything older than the most recent balance snapshot, move to cheaper storage. Hot accounts get dedicated partitions that spread the write load.

Optimistic locking. Read the current balance, attempt the write, retry on conflict. Uncontested writes proceed without waiting. Fall back to pessimistic locking only for accounts with extreme contention where the retries themselves become the bottleneck.

None of these change the data model. The invariants hold: every transaction balances, entries are immutable, balances are derived. The scaling patterns are execution strategies, not schema changes.

Sub-ledgers are how you stay sane at scale

A startup with three products and a hundred users can track everything in a single general ledger. A company processing $50 million per month across lending, payments, and deposits cannot.

Sub-ledgers break the general ledger into domain-specific books: accounts receivable, accounts payable, customer deposits, fee revenue. Each sub-ledger has its own chart of accounts and its own entries. The general ledger aggregates from them.

This is also where product teams get their boundaries. The lending team owns the loan sub-ledger. The payments team owns the transaction sub-ledger. Each team can reason about its own entries in isolation, but the general ledger enforces a global constraint: the sum of all sub-ledger balances for a given account type must equal the general ledger balance for that type. When it doesn't, you have a reconciliation break.

Reconciliation breaks are the most important alarm in a financial system. They mean money entered or left a sub-ledger without a corresponding entry somewhere else. The fix is never to adjust the general ledger to match — it's to find the missing entry in a sub-ledger and post it. The general ledger is a derived view, not an editable record.

Multi-entity structures, holding companies with subsidiaries, each need their own books. When entity A pays entity B, both ledgers record the transaction simultaneously.

A's books show cash leaving and a receivable appearing. B's books show cash arriving and a payable appearing. The entries are distinct, but the transaction is one logical event that must commit or roll back atomically across both sets of books. Get this wrong and you've created money in one entity's books without removing it from the other's — the double-entry constraint, violated across an organizational boundary.

Why this is the first decision

Most engineering teams treat the ledger as a data model choice, something you figure out when you get to the database schema.

The ledger determines what questions your system can answer. Can you compute a balance at any arbitrary point in the past? Can you trace a user's complaint back to the exact entries that created their balance? Can you prove to an auditor that no one edited a record after the fact? Can you split revenue across three parties in a single atomic operation?

If your ledger can't do these things, no amount of application code on top will fix it. Timestamps you didn't record are gone. Balances derived from overwritten entries are fiction. Immutability claims for a table that accepts UPDATEs are theater.

Every product feature in fintech, from a simple balance display to a complex multi-party settlement, is a query against the ledger. The ledger is not the plumbing underneath the product. It is the product. Everything else is a view.

Sources

Frequently Asked Questions

Why do fintech systems use double-entry bookkeeping?
Double-entry bookkeeping ensures money is only moved, never created or destroyed. Every transaction records both a source and a destination, so the books always balance. This makes entire categories of bugs, like phantom balances or missing funds, structurally impossible rather than just unlikely.
Should you store account balances in a fintech database?
No. A balance should be derived from the sum of ledger entries, never stored independently. You can cache the derived balance for read performance, but the cache must be fully rebuildable from entries. If the cached balance and the derived balance ever disagree, the entries win.
What is the difference between value time, booking time, and settlement time?
Value time is when the transaction occurred. Booking time is when your system recorded it. Settlement time is when money actually moved between institutions. These three timestamps almost always differ, and collapsing them into a single created_at field loses information you cannot reconstruct during an audit.
How do you correct a mistake in an immutable ledger?
You post a new compensating entry that offsets the original, then link them in both directions. The original entry stays visible in the history. Immutable ledgers are corrected by addition, never by editing or deleting existing records.
Why does Postgres work for fintech ledgers instead of blockchain?
Tamper evidence requires a hash chain and an independent checkpoint, not a distributed consensus mechanism. An append-only Postgres table with hash chaining and periodic S3 Object Lock checkpoints provides identical audit guarantees with standard tooling. A SOC 2 auditor can verify it with a SQL query.

Built by Trio, a fintech-native engineering partner helping teams build the next generation of financial technology and infrastructure.

Subscribe to Ledger Drift for high-signal insights into how modern fintech is built, from systems to code to teams.

Keep reading

engineeringGold Certificates, Usage Rights, and the Cooperative StackWhat if the floor price for art came from the gold in the certificate, not the artist's reputation?
analysisWhat Happens When You Make Gold SpendablePhysical gold you can spend at 5,000 merchants, in an economy where the dollar lost 22% of its purchasing power since 20...
analysisThe $42.8 Billion Sort — Where Fintech VC Went and Where It's GoingFintech VC hit its highest mark since 2022 in 2025, and two quarters into 2026 the pattern is clearer.
View more ›