Back to blog

EngineeringAug 31, 2026

Your balance is not stored. It is derived.

ZenbooxOfficial blog

There is no column in our database that holds your balance.

That sounds like an odd choice until you have watched a stored balance and a transaction history disagree. When they do, you cannot tell which one is right. Given enough time and enough concurrent writes, they always do.

So Zenboox keeps a double-entry ledger, and a balance is a query over it: the sum of the postings on your account.

ONE RULE

Every entry has at least two postings, and the total debit must equal the total credit. If they are not equal the entry is not written at all. The whole operation fails rather than leaving half a movement behind. Entries for zero are refused as well.

This is not bookkeeping aesthetics. In a ledger that is out of balance, "where did the money go" has no answer, and the older the imbalance the harder it is to find the entry that caused it.

Because balances are derived, direction matters. Your balance is a liability of the platform: crediting your account increases what we owe you. The treasury reserve is an asset. The normal side of every account is declared in one place, because the moment two parts of the code disagree about it, a balance shows up negative on screen.

WHAT DOUBLE ENTRY DOES NOT CATCH

Here is the part usually left out of posts like this.

Suppose two withdrawal requests for the same account arrive at the same instant, and the account holds enough for one of them. Both read the balance, both see enough, and both write a balanced entry. The consistency check stays green, because nothing about the ledger is inconsistent: each entry balances perfectly.

What broke is a different rule, that a user cannot withdraw money they do not have. Double entry has nothing to say about it.

The fix is not in the ledger, it is in the isolation level. The money path for withdrawals runs in a serializable transaction and the balance is read inside that transaction. Both halves are necessary. Read the balance outside the transaction and those rows never enter its view, so even serializable cannot detect the conflict. Run at a weaker level and the two transactions cannot see each other's debit yet, because postings are inserts and there is no shared row to lock, so both pass the check.

We know this because our first test for it was worthless. It went through the HTTP endpoint, where the one-time code is consumed first, and that effectively put the two requests in a queue. The test was measuring the code gate, not the race: deleting the balance check did not make it fail. So the money path was moved into its own module where two concurrent calls can actually be made, and the protection is now tested directly.

A guarantee you have never tried to break is not a guarantee. It is a comment.