Let's talk
concurrency

Two tills sold the last case and stock fell only once

A sale, a production run and an adjustment each changed stock their own way. Only one took a lock, and the movement report was missing every sale.

· · updated

A tablet on a stand at a plant showing the inventory dashboard in Sazinga Factory.

Two people at two tills sell the last case of the same product at the same moment. Both sales go through. Two cases leave the building, and the stock count goes down by one. You now show a case you do not have.

There is a second half to the problem, and it is what makes it hard to catch. The report that is supposed to show where your stock went is missing every one of those sales. Ask it where stock went and it will say the product has only ever gone up.

What was actually going on

Taking stock out was written three times in one system, by three people at three points in the project, and the three versions do not agree about what safe means.

The first is a shared routine. It locks the stock record, checks there is enough, refuses the change if there is not, and writes the new figure. That is correct: a second person attempting the same change waits until the first has finished, then sees the reduced figure.

The second is for production runs. It has the same shape but does not lock the record while it checks. It does write a movement entry first, so it keeps a better trail and gives a worse guarantee.

The third is tucked inside the code that creates a sale. It finds the stock record, compares, subtracts and saves, with no lock and no movement entry. It runs inside a transaction, which makes it look safe and is exactly why nobody questioned it.

A transaction does not stop two people reading the same figure at the same time. Both read one case left, both pass the check, both write a new figure worked out from what they read, and the second write overwrites the first.

I had assumed that because a stock routine existed, it was being used. That assumption is wrong in almost any system older than about six months. Searching for what actually called it settled the matter: the movement table, designed as the trail for every stock change, is written from exactly one place. Everything else changes the balance directly. Of the five ways stock moves on this system, one was recorded.

Nobody duplicated it deliberately. The sale code came first, when there was one warehouse and one till. The production routine came next, from someone who cared about the trail. The shared one came last, from someone who cared about locking. Nobody went back to change a working sale path.

The order chosen was to instrument first and consolidate second: every path writes a movement entry before any path is rewritten to use the shared routine, so the trail becomes true before the code becomes tidy. The sale code also works out totals, tax, delivery state and payment state in the same place, so pulling the stock part out of it is a change to the most sensitive step in the product.

Why the duplicates survived

Changing a working sale path to go through a newer routine is a risky change with no visible benefit on the day you make it. So each author solved the problem they arrived to solve, and the three versions stayed. The fix is organisational more than clever: there is nothing hard about the correct version. The hard part is having exactly one of it.

What it did not fix

The three versions were not merged in one pass, and the record of where stock went was still wrong for the paths not yet instrumented. That is the honest state of it. A wrong number in a record that people trust outlives any tidy-up of the code.

The pattern, for anyone running stock through software

Ask who is allowed to change a stock figure. If the answer is “several parts of the system”, you have this problem whether or not it has bitten yet. A quantity that more than one part can change needs a single writer, enforced by the system rather than by a note in a guide that nobody reads under deadline.

And distrust any history that only some changes write to. A trail with production movements and no sales will report, with complete confidence, that stock has only ever gone up. The test of a movement record is not “does my new module write to it” but “does every path that changes stock write to it, and have we listed the paths”. Listing them takes an afternoon.

Where this ends up

Every path that moves stock in Sazinga Factory writes a movement entry, whether it is a sale at the counter, a production run or a manual adjustment, because the batch traceability the product is sold on is only as true as the least recorded of them.

This came out of building Sazinga Factory

From raw material to finished batch, with the yield accounted for. The problem above is one we met while building it, and what we did about it is in the product.

If you run something like this, there is one thing you can do without a call: send one batch record.