The uniqueness rule was doing concurrency work and nobody had noticed
The first version of the dispatch module allowed exactly one dispatch per order. A second attempt against the same order returned a conflict. That was written down as a scope decision — partial shipment was a later feature, and refusing the second dispatch kept the first version small.
When partial dispatch was built, that guard came off. Orders could now be shipped across several drops, each drop carrying a subset of the order’s lines, and the validation became “the quantity on this dispatch plus everything already dispatched must not exceed the ordered quantity”.
Which is a read, then a decision, then a write. Two requests arriving together both read the same already-dispatched total, both decide there is one unit left, and both ship it. The one-per-order rule had been silently preventing that, and nobody had listed concurrency among the things it was for.
The pattern that was already right elsewhere
Stock decrement in the same system had never had this problem, because it was never written as a read-modify-write.
The obvious implementation of “reduce stock by five if there are at least five” is to select the current quantity, compare it in the application, and write the new value. That is correct in a single-threaded test and wrong under any concurrency, and the window is exactly as long as your application logic takes.
The implementation used instead puts the condition into the statement:
UPDATE stock
SET on_hand = on_hand - :qty
WHERE variant = :v AND branch = :b AND on_hand >= :qty
Then it checks how many rows the statement reported changing. One row means the decrement happened. Zero rows means the condition was not satisfied, which is returned as a conflict with an insufficient-stock code. There is no moment between the check and the change, because the check is the change. The database evaluates the predicate while holding the row.
The increment side uses the equivalent trick from the other direction — an insert that falls back to an update on conflict — so receiving stock into a branch that has never held that item does not need a “does the row exist yet” read either.
That property is the whole reason a warehouse can run two receipts and a sale at the same time without a reconciliation job. It is also why the ledger balances: every movement writes a row into an append-only movement table in the same transaction as the level change, so the current quantity is always reproducible from the movements.
Why the dispatch case could not use the same trick
The stock pattern works because the quantity being guarded lives in one column on one row. The dispatch case is a sum across rows — everything already dispatched for that order line — and you cannot put a sum of rows you are about to add to into the WHERE clause of the insert that adds them.
So the dispatch path takes an explicit row lock on the order line before it computes the cumulative figure. The second concurrent request waits, then recomputes against a total that now includes the first one, and correctly refuses. Slower than the guarded update, and unavoidable when the invariant spans a set rather than a value.
The general rule that came out of it: decide whether your invariant is a property of one row or a property of a set, before choosing how to enforce it. A single-row invariant belongs in a conditional statement or a database constraint and costs nothing. A set invariant needs the set held still, which means a lock, and the lock needs to be on something that both writers will contend for — here, the order line, because that is what the sum is grouped by.
The rule that stopped the double count
There is a second class of error in the same area that is not a race at all, and it did more damage in the old system this replaced: counting the same physical movement twice.
An order confirmation decrements stock. A dispatch then ships it. If dispatch also decrements, the stock is wrong by the whole quantity of every dispatched order, and it is wrong quietly, because each individual step looks correct in isolation. The same trap sits in demonstration stock: issuing a demo unit takes it out of the branch, and converting that demo into a sale is a change of category, not a second physical movement.
So both were settled as explicit rules and written into the service that owns them. Dispatch never touches stock — it was already decremented at order confirmation. Converting a demonstration into a sale nets the outstanding quantity and writes no movement at all. Marking a demo lost writes it off, and the stock stays out, because it is out.
Each of those has a test that asserts the quantity does not change across the transition. That sounds like a strange thing to assert until you have debugged the alternative. A test that stock is unchanged after a dispatch is the only thing standing between you and a plausible future refactor that decides dispatch ought to be where stock leaves the building.
The transferable rule: for any physical quantity, exactly one step in the workflow is allowed to move it, that step is named in writing, and every other step in the workflow has a test asserting it moves nothing. The rule is easy. Making the negative case a test is the part people skip.
What the removed guard should have said
Going back to the beginning: the one-dispatch-per-order rule was documented as “partial dispatch is out of scope for version one”. That is true and it is not the whole truth. The rule was also the only thing making the dispatch quantity a single-row invariant instead of a set invariant.
When a constraint is removed, the reasons it was introduced get reviewed. The reasons it was keeping true do not, because nobody wrote them down. This is the same failure as a comment that explains what a line does rather than what breaks without it.
What I now write next to a temporary restriction is not why it exists but what becomes possible when it goes. One sentence: “removing this makes the dispatched quantity a sum across rows, which will need a lock”. That sentence costs nothing to write while the restriction is fresh, and it is the sentence you cannot reconstruct six weeks later.
Rules
Never read, decide, then write, for a quantity anyone else can change. Put the condition in the statement and check the affected-row count. Zero rows is a conflict, not an error.
Classify the invariant before choosing the mechanism. One row: a conditional write. A set: an explicit lock on the thing the set is grouped by.
Name the single step that moves a physical quantity, and test that every other step does not.
When you add a temporary restriction, record what it silently guarantees, not just why it exists. The scope note survives. The invariant does not.