Ground Truth

Entries Are the Record, the Balance Is a View

Mostafa DhouibMostafa Dhouib·10 min read
The short answer

A balance held in a column is a cache with no source behind it. Every correction becomes an edit, and nothing in the system can reconstruct how the number reached its current value. Derive the balance from entries instead and it is explainable at any date, while corrections become entries rather than edits. The cost is real: derivation is slower than reading a column, and a stored snapshot is an optimisation of the derivation rather than a replacement for it.

Entries Are the Record, the Balance Is a View

The short answer. A balance stored as a column on a row is a cache with nothing behind it. There is no source it can be recomputed from, so every correction has to be an edit to the cached value, and the system can never say how the number reached its current state. Make the entries the record and the balance a derivation over them, and two things change. Any past balance can be reconstructed and explained at a date, and a correction becomes another entry rather than an overwrite. The cost is that derivation is slower than reading a column, which is why a materialised snapshot earns its place. A snapshot is an optimisation of the derivation, not a substitute for it.

Almost every system that tracks a quantity starts with a column. Credits remaining, days available, units in hand, points earned. It is the obvious model, it is one integer, and it reads in constant time.

The trouble arrives the first time somebody disputes the value, and it is not a performance problem.

A stored balance is a cache with no origin

A cache is a derived value kept close to hand for speed. What makes it safe is that it can be thrown away and rebuilt from the thing it was derived from.

A balance column has no such thing. It was not derived from anything. It was arrived at, by a long sequence of increments and decrements that were applied and then forgotten. The column is the only surviving artefact of that sequence.

That has three consequences, and they compound.

Corrections have to be edits. If the figure is wrong, the only available repair is to write a different figure. There is no bad entry to reverse, because there are no entries. The edit leaves no trace of what the value was, who changed it, or why, unless somebody remembered to build a separate audit log, which is itself a second record that can disagree with the first.

The value cannot be explained. Asked why a balance is seventeen, the system can only say that it is seventeen. Asked what it was at the end of the last period, it cannot answer at all. For anything a person is accountable for, that is the whole problem rather than a missing feature.

Bugs become permanent. A defect in an increment path writes a wrong value into the column and then the evidence is gone. Fix the code and the data stays wrong, with no way to tell which rows were affected or by how much. The usual outcome is a one-off repair script written against guesses.

A system can live with all three for a long time. It lives with them right up until a figure matters to somebody outside engineering.

A column holding the balance
Corrections
must be edits
there is no bad entry to reverse, because there are no entries
Why is it seventeen
no answer available
the system can only say that it is seventeen, and cannot say what it was at the end of the last period
A defect in an increment path
permanent
fix the code and the data stays wrong, with no way to tell which rows were affected or by how much
The repair
a one off script
written against guesses
Entries, with the balance derived
Corrections
a reversing entry plus the correct one
both visible, both with an author and a reason
Why is it seventeen
sum the entries
any past balance reconstructs by summing entries whose effective date falls on or before that date
A defect in an increment path
tractable
the entries it produced are identifiable by type and date range, so reverse them and post replacements
The repair
auditable and reversible
state is a projection
Nobody needs write access to a balance, because no balance is stored as truth. Double entry bookkeeping has worked this way since the fifteenth century.
FigureA cache is safe because it can be thrown away and rebuilt from the thing it was derived from. A balance column has no such thing. It was not derived, it was arrived at, by a long sequence of increments that were applied and then forgotten.

The entries are the record

The alternative is old. Double-entry bookkeeping has worked this way since the fifteenth century, and the reason is not aesthetic. A ledger records transactions, and the balance of an account is a function of the transactions in it. You do not store the balance as the truth. You store what happened, and the balance falls out.

In software this reappears under several names. Event sourcing stores the sequence of facts and derives current state by folding over them. The accounting patterns literature describes the account and entry pattern, where an account holds entries and its balance is computed from them, with adjustments modelled as reversing and replacement entries rather than as edits. Append-only ledgers in payments systems are the same idea with a narrower scope.

The common claim in all of them is this. The record is immutable and additive. State is a projection of the record. Nothing that has been recorded is ever rewritten.

Applied to an entitlement balance, the model is small.

An entry has an amount, a sign or a direction, a type, an effective date, a recorded-at timestamp, a reference to what caused it, and an author. Allocations are entries. Usage is an entry. A pending request is an entry with a status, or a pair of entries that commit and settle. An administrative adjustment is an entry with a reason. A cancellation is a reversing entry that names the entry it reverses, and crucially restores the amount that was actually taken rather than recomputing it from today's calendar.

The balance at any date is then the sum of entries whose effective date is on or before that date. That is the whole of it. There is no separate balance to keep in step, and therefore no way for a balance to drift away from its own history.

Two timestamps, not one

The part people skip is the second date, and skipping it undoes most of the benefit.

An entry needs an effective date, which is when the thing happened in the world, and a recorded-at timestamp, which is when the system learned about it. They are not the same. An adjustment entered in October that applies from July has a July effective date and an October recorded timestamp.

With both, two different and both-correct questions can be answered. What is the balance as at the end of July, using everything known now. And what did we believe the end-of-July balance was when we reported it in August. The first is for correctness. The second is for explaining why a report issued in August said something different, which is the question that actually gets asked in a dispute.

With only one date, those questions collapse into each other and the system silently rewrites history every time a backdated entry lands.

Corrections become entries

This is the operational difference, and it is the one that changes how support works.

Under the column model, a wrong balance is fixed by somebody with database access writing a new number, usually out of hours, usually with no record beyond a message in a chat thread.

Under the entry model, a wrong balance is fixed by posting a reversing entry and then the correct one. Both are visible. Both have an author and a reason. The balance moves because the ledger moved, through the same path as every other change. Nobody needs write access to a balance, because no balance is stored as truth.

That also makes the fix for a historical bug tractable. Find the entries the defect produced, because they are identifiable by type and date range, reverse them, and post replacements. The repair is auditable and reversible, which a repair script is not.

Derivation is slower, and the snapshot is how you pay for it

Here is the cost, stated plainly, because the pattern is usually sold without it.

Reading a column is one row lookup. Deriving a balance is an aggregate over every entry for that account, and the number of entries grows without bound. On a dashboard that shows balances for several hundred people, the naive derivation is the slowest query in the application, and it gets slower every month.

So you materialise. A snapshot row per account per period boundary, holding the derived balance as at that date, and the current balance computed as the nearest snapshot plus the entries after it. Reads become bounded. The snapshot is written by the same derivation everyone else uses rather than by the code paths that post entries.

The distinction that matters is what the snapshot is allowed to be. It is a cache with a source. It can be deleted and rebuilt, and rebuilding it must produce the same figures. Two properties follow and both have to be enforced.

Nothing writes to a snapshot except the derivation. The moment a feature updates a snapshot directly, it has become a second balance column and the whole arrangement has collapsed back to where it started.

A job recomputes snapshots and compares. Rebuild from entries, compare to the stored snapshot, alert on any difference. That check is the only thing standing between a cache with a source and a cache that has quietly become the source. It also catches backdated entries landing before an existing snapshot, which must invalidate every snapshot after their effective date.

A snapshot without that rebuild-and-compare is just a balance column with extra steps.

The gate
Delete the snapshot, rebuild it from the entries, and compare. Do the figures come back identical?
A cache with a source
Reads stay bounded as the nearest snapshot plus the entries after it, and the model is intact. This holds only while nothing writes a snapshot except the derivation itself.
A balance column with extra steps
Some feature updated a snapshot directly, or a backdated entry landed before an existing snapshot and nothing invalidated the ones after its effective date. The cache has quietly become the source.
The scheduled rebuild and compare is the only thing standing between the two outcomes, and it is the part most often skipped.
FigureDerivation is slower than reading a column, so a materialised snapshot earns its place. What decides whether the snapshot is an optimisation or a relapse is one test, and the test has to run on a schedule rather than when somebody asks.

The honest limit

This is not free and it is not always the right call. The entry model costs more to build, it is harder for a newcomer to read, and it makes simple queries less obvious. For a quantity nobody will ever dispute, such as an unread-notification count or a view counter, a column is the correct engineering decision and an entry ledger is over-building.

The test I use is whether a person outside engineering could plausibly challenge the number and expect an explanation. Leave balances, account balances, inventory positions, credits that were paid for, anything that feeds payroll or invoicing: those get entries. A counter on a dashboard does not.

There is also a failure mode specific to this pattern. Derivation rules live in code, and if the rules change, old entries get reinterpreted under new rules and a historical balance silently changes. Rule versioning is the fix, and it is genuinely awkward. I have seen it skipped more often than implemented, and it is the part of the pattern that most often undermines the audit story it was adopted for.

Finally, the migration. Moving an existing system from a column to entries means inventing an opening-balance entry per account to account for history nobody recorded. That entry is unexplainable by construction. It is still better than the column, because everything after it is explainable, but it is worth saying out loud rather than pretending the ledger goes back to the beginning.

FAQ

Why is a stored balance column described as a cache? Because it is a derived value kept for fast reading, which is exactly what a cache is. What makes a cache safe is that it can be discarded and rebuilt from its source. A balance column has no source, having been arrived at by a sequence of updates that were applied and then forgotten, so it can never be rebuilt or verified.

What does deriving the balance from entries actually give you? Two things. Any balance can be reconstructed as at any past date by summing entries up to that date, which makes the figure explainable rather than merely assertable. And a correction becomes a reversing entry plus a replacement, with an author and a reason, instead of an untraceable edit by somebody with database access.

Where does this idea come from? Double-entry bookkeeping, where an account's balance has always been a function of its entries rather than a stored fact. In software it reappears as the account and entry pattern in the accounting patterns literature, as event sourcing, and as the append-only ledger used in payments systems. All three make the same claim: the record is immutable and additive, and state is a projection of it.

Is deriving the balance not too slow? It is slower, and on a page showing many balances the naive aggregate becomes the slowest query in the application and degrades as entries accumulate. The answer is a materialised snapshot per account per period boundary, with the current figure computed as the nearest snapshot plus later entries. That bounds the work read-side without changing the model.

How is a snapshot different from the balance column you just argued against? A snapshot is a cache with a source, so it can be deleted and rebuilt and must produce identical figures. Two rules keep it that way. Nothing writes a snapshot except the derivation itself, and a scheduled job rebuilds snapshots from entries and alerts on any difference. Without that rebuild-and-compare, a snapshot is a balance column with extra steps.

When is a plain column the right choice? When no person outside engineering would ever dispute the number or expect an explanation for it. Unread counts, view counters and similar figures are better as a column, and an entry ledger for them is over-building. Anything feeding payroll, invoicing, entitlement or a trading position belongs in entries, because those numbers get challenged and the challenge needs an answer.

Carrying a program like this one?

Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.

Bring us the program