Why a wallet balance should never be a column
A balance you update is a balance you have to trust. A balance you derive from postings is one you can prove. Here is what that difference looks like in production.
, 7 minute read
The column everyone starts with
The first version of almost every wallet is a table with a balance column. A credit adds to it; a debit subtracts. It works in the demo, and for a while in production.
Then two requests arrive in the same millisecond. Or a payout times out and is retried. Or someone fixes a customer complaint with a quick update in the database. The column is now a number, and nobody can say how it got there.
Postings first, balances second
In a double-entry ledger, money never appears or disappears. Every movement is an entry with at least two postings, and the debits equal the credits in every currency the entry touches. A balance is just the sum of an account’s postings.
LIA:WAL:NGN:0193
NGN 350,025.00
Fee income NGN 25.00
Because postings are only ever added, the balance at any moment in history can be recomputed and checked. A correction is a new entry that points at the old one, so the mistake and the fix are both on the record.
But isn’t summing slow?
Not if you don’t do it on every read. PanoQore keeps a running balance per account, updated in the same database transaction as the postings, with a version number for optimistic locking. The derived balance is the cache; the postings are the truth. A nightly job re-sums every account and alerts if any cached balance disagrees.
The derived balance is the cache. The postings are the truth.
What we’d tell our past selves
- Use integer minor units as strings. Floats will round a kobo away eventually.
- Put the idempotency key on the entry, not the HTTP request, and keep it long enough to survive a weekend.
- Never let a balance go negative by accident. Make overdraft a limit you set, not a bug you find.
- Give every account a code a human can read, like
LIA:WAL:NGN:0193. Your operations team will thank you.
The concepts page walks through entries, holds and balances in the API.