BlackgradeSystems

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.

01

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.

02

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.

FieldMeaningNote
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
03

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.

04

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.

Suspense is not a dumping ground A suspense account is legitimate and should be small, aged, and monitored. An entry that has been in suspense for sixty days is an unresolved question about real money. Alert on the age of the oldest item, not on the total.
05

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.

06

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.

07

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.

Contact

Working on something in this territory?

Financial infrastructure, regulated systems, AI in controlled environments, cryptography, platforms at scale.

Get in touch