How the float balance stays right
A petty cash balance looks like the simplest possible piece of state and contains the one bug that makes these systems untrustworthy. Two people photograph receipts within the same second, both functions read a balance of £54.80, both subtract, and the tin is now recorded as £46.60 when it should be £38.40. Nobody notices until the count, and then the count is what gets doubted.
Key takeaways
- The balance moves by a conditional increment, never by a read, subtract and write.
- Every movement is an immutable row; the balance is a convenience, not the truth.
- The balance can always be rebuilt from the movements, and is, after any correction.
- A correction is a new row, never an edit. Editing history is how a float stops being evidence.
- A negative balance is allowed and flagged, because tins do go overdrawn.
Movements are the truth
The design keeps two things: an append-only list of movements, and a balance. The movements are the record; the balance exists so that a phone can show a number without summing four hundred rows. If they ever disagree, the movements win and the balance is rebuilt.
- Database
- App integration
- Machine learning
- Management
- Analytics
ADD rather than SET
The bug is in the read. A function that reads £54.80, subtracts £8.20 and writes £46.60 has embedded an assumption about what the balance was, and that assumption is wrong the moment somebody else writes in between. Two of those and one spend disappears.
An atomic increment carries no assumption. Each function says “reduce this by 8.20” and “reduce this by 12.00”, and the database applies both in some order, arriving at the right answer either way. It is one line of difference in the update expression and it is the difference between a balance you can trust and one you cannot.
Why movements are immutable
Because the moment a movement can be edited, the float stops being evidence. A corrected amount that overwrites the original leaves no trace that anything was corrected, which is precisely the situation an audit is designed to detect. So a correction is a new row of type adjustment, referencing the row it corrects, with a reason and a name.
That produces a slightly longer ledger and a completely defensible one. The balance after a correction is recomputed by summing the movements rather than by patching the stored figure, which is the one place the system deliberately does the expensive thing.
Overdrawn floats
A tin going below zero sounds impossible and happens regularly: somebody pays out of their own pocket intending to reimburse from the tin later, a top-up is recorded a day after it physically happened, or a spend is photographed twice. The system allows it, flags it, and tells the custodian — because blocking the write would mean refusing to record a spend that has already occurred, which loses information in exchange for tidiness.
- App integration
- Machine learning
- Management
The movement row
- Type — spend, top-up, adjustment, or count.
- Amount, signed, so summing the column is the balance with no case analysis.
- Receipt reference, for a spend, pointing at the stored photograph.
- Who and when, always, including for a system-generated row.
- Corrects, for an adjustment, naming the row it fixes and why.
- Never a delete. There is no code path in the system that removes a movement row.
Next: what a count actually does, and why weekly beats monthly.
All posts