Field note Ledger
Designing a ledger for electronic money
Balances are a consequence, not a field you update.
The shortest description of a correct ledger is that it has no balances in it. It has movements, and balances are what you get when you add them up.
Why a stored balance always drifts
A stored balance is a cache of a computation, and like every cache it can diverge from the thing it caches. The divergence does not require a bug in the arithmetic. A retry that writes twice, a transaction that commits the movement but fails before the balance update, a repair script run by someone with good intentions, a schema migration that reorders writes: any of these produces a balance that no sequence of movements explains.
Once that happens, the system has lost the ability to answer the only question that matters, which is why. You can see that a user has forty euros. You cannot say where they came from, and neither can anyone else.
Deriving the balance removes the failure mode by removing the second copy. There is one representation of truth, and reading it is a query.
The entry is the atom
Everything the ledger knows is expressed as immutable entries, each one a signed amount against an account, grouped into transactions that must sum to zero.
Amounts are integers in minor units. Floating point in a ledger is not a shortcut, it is a decision to be wrong by an amount you cannot predict. If you support currencies with different exponents, carry the exponent explicitly rather than assuming two.
The reason field is an enumerated value, not prose. It is what lets you answer questions of the form "how much did we move because of chargebacks last quarter" without a text search.
| Field | Meaning | Note |
|---|---|---|
| transaction_id | Groups entries that must succeed or fail together. | Immutable |
| account | Which internal or user account this side touches. | From the chart of accounts |
| amount | Signed minor units, with an explicit currency. | Integers only |
| event_time | When the underlying fact occurred. | Outside world |
| recorded_time | When the ledger learned about it. | Append order |
| value_date | The date on which it takes accounting effect. | Reporting |
| idempotency_key | Client-supplied reference, unique per logical operation. | Retry safety |
| reason | The business event that justifies the movement. | Not free text |
Nothing is ever deleted or edited
A mistake in a ledger is corrected by a compensating entry, never by modifying or removing the original. This feels wasteful the first time and obvious the tenth, because the alternative destroys the property that makes the ledger useful: that the past is stable.
A reversal carries a link to what it reverses and its own reason. A user who sees a movement and its reversal understands more than a user who sees nothing, and an auditor who sees both learns that the institution detects and corrects its own errors, which is a good thing to demonstrate rather than hide.
The chart of accounts is the design
Most of the intelligence of a ledger lives in the account structure, because the invariants are expressed as statements about groups of accounts. If the sum of all user liability accounts must equal the sum of safeguarded asset accounts, that equality is only checkable if the accounts are classified.
Give every account a type, a normal side, and a place in a hierarchy. Then the invariant checks are queries over the hierarchy rather than hand-maintained lists that fall out of date when someone adds an account for a new payment provider.
Internal accounts deserve as much care as user accounts. Money in transit, fees earned but not yet swept, provider float, rounding, suspense: each of these is a real place value can sit, and if it has no account, value will be forced into an account where it does not belong and the invariant will fail for a reason nobody can find.
Performance without giving up derivation
The usual objection to derived balances is that summing millions of entries per read is impossible. The answer is snapshots, which are a cache with a rule: a snapshot records the balance of an account as of a specific entry sequence number, and reading a current balance means taking the latest snapshot and applying entries after it.
The difference from a stored balance is that a snapshot is reproducible. It can be deleted and recomputed from the journal at any time, and a background job can verify old snapshots against a fresh computation and alert on any disagreement. The cache is checkable, so the cache cannot silently lie.
In practice snapshots per account per day are enough for most workloads, and the verification job becomes one of the more useful alerts you own, because a snapshot mismatch usually means something upstream wrote history it should not have.
Authorisation and capture are different movements
Card and transfer flows are rarely atomic. An authorisation reserves availability without transferring value, a capture transfers it, an expiry or void releases the reservation. Modelling this as a single balance with a mutable available amount reintroduces exactly the drift the journal was designed to eliminate.
Model reservations as entries against a dedicated account. Available balance becomes settled balance minus active reservations, which is still a fold over entries. Expiry is a movement with a time, not a flag someone flips.
The gain shows up during incidents. When a provider replays six hours of webhooks, a system with reservations as entries and idempotency keys absorbs the replay and changes nothing. A system with mutable available amounts spends the evening reconstructing what the balance should have been.
The property worth protecting
Every rule above exists to preserve one property: any balance, at any point in time, is explained by the entries that produced it, and that explanation does not change when you look at it again.
Systems that keep that property are boring to operate for years. Systems that trade it away for a faster first release spend those years reconciling against themselves.
Related
Read next
Wallet, account, ledger
Three words that get used interchangeably and should not be.
Field noteBuilding an electronic money system
What has to exist before a single unit of electronic money can move.
Field noteAutomating regulatory reporting
Structure the data once, and every report becomes a projection of it.
Field noteContact
Working on something in this territory?
Financial infrastructure, regulated systems, AI in controlled environments, cryptography, platforms at scale.