Skip to content
← Writing

A wallet balance is a derived number, not a column

The moment balance becomes a field you write to, you have two sources of truth and no way to tell which one is lying.


Almost every wallet I have been handed starts the same way. There is a users table, and on it a balance column. Money arrives, the column goes up. Money leaves, it goes down. It is obvious, it is fast, and it is the decision that costs the most later.

The problem is not that the column can drift. It is that when it drifts, there is nothing to compare it against. The number is the only record of itself. You can see that a customer says they have less than they should, and you have no way to find the moment it went wrong.

What the column throws away

A balance is not a fact. It is the result of every movement that came before it. Write only the result and you discard the movements, which is where every answer you will eventually need actually lives: which transfer, in what order, against which counterparty, reversed by what, at what time.

The alternative is older than software. Record the movements, never the total. Each entry is immutable and names both sides, because money does not appear, it moves from somewhere to somewhere. The balance is then a sum over those entries. It cannot disagree with the movements, because it is made of them.

What this buys, concretely

  • Every balance is explainable. "Why is this number what it is?" has an answer that is a list of rows, not a theory.
  • Reversals stop being edits. A refund is a new pair of entries, not an UPDATE that erases what happened. The original is still there, and the history still adds up.
  • Concurrency gets simpler, not harder. Two simultaneous debits against a column is a lost-update problem. Two inserts are just two inserts, and the constraint you enforce sits in one place instead of being scattered across every code path that touches money.
  • Reconciliation becomes possible at all. You can compare your entries against the provider's statement line by line. With a column you can only compare one number to another and shrug when they differ.

The objection, and the honest answer

Summing entries on every read is slower than reading a column. True. The fix is a cached balance that is explicitly a cache: recomputable, rebuilt on a schedule, checked against the entries, and never the thing a decision is based on when the two disagree.

That is a very different object from a column you write to and hope. One of them can be wrong and tell you. The other can only be wrong.