Skip to content

Part 3 of 7 · Petty cash tracker series ~5 min read

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.

How a spend moves the float balance safelyA vertical chain of four steps entered by a box labelled A spend or a top-up, carrying an amount and a float. Step one writes the movement to an append-only DynamoDB movements table, together with the receipt reference, and never updates it. Step two moves the balance on a DynamoDB floats table using an atomic ADD rather than a SET. Step three asks whether the balance has gone below zero, which does happen, and exits to Flag but do not block, telling the custodian. Step four is Balance updated, shown on the phone. A note says ADD is atomic, so two spends in the same second both apply in whatever order they arrive.AWS ACCOUNTA spend or a top-upamount and floatWrite the movementappend-only, with the receiptDynamoDB movementsnever updatedMove the balanceADD, not SETDynamoDB floatsatomic incrementBelow zero?it happensFlag, do not blocktell the custodianyesBalance updatedshown on the phoneADD is atomic. Two spends in the same second both apply, in whatever order.
Fig 1. How a movement is recorded. The atomic increment is the whole concurrency story: two functions can both apply their own delta without either needing to know what the other read.
  • 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.

How an overdrawn petty cash float resolvesA horizontal row of five boxes. Spend: twelve pounds. Balance: minus three pounds forty. Flagged: the custodian is told. Top-up: fifty pounds is recorded. Clear: the balance is forty-six pounds sixty. A note says refusing the write would have lost a real spend in exchange for a tidier number.AN OVERDRAWN TIN RESOLVES ITSELFSpend£12.00Balance-£3.40Flaggedcustodian toldTop-up£50 recordedClear£46.60Refusing the write would have lost a real spend in exchange for a tidier number.
Fig 2. What happens when a float goes below zero. Recording the truth and flagging it is better than refusing to record something that has already happened.
  • 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