Skip to content

Double-entry for physical units

Accounting solved this 500 years ago: every movement gets a witness, and the books must balance. Most inventory software still runs on single entry. Here is what the double-entry version looks like, and the two bugs it caught in our own code.

Accounting solved this 500 years ago. Every movement of money gets two entries, a debit and a credit, so the books always balance and every number has a counterpart that explains it. Nobody runs a business on single-entry books anymore, because a ledger that only says “the balance changed” is not accounting. It is a rumor with formatting.

Inventory software never got the memo. In most systems, stock is a number in a column. It goes up when a receipt is posted. It goes down when an order ships. Between those two facts there is usually nothing: no row that says who moved it, no row that says from where to where, no row at all. When the count is wrong, and it will be wrong, the system has no story to tell. Someone recounts, someone adjusts, the number changes again, still with no witness. The adjustment is itself a single entry.

We built DockingWare the other way, and the ledger has already caught us twice.

The shipment that was recorded twice

The first catch was a double ship. On the wholesale path, the pick step wrote a SHIP row to the ledger, and then Ship Confirm wrote another SHIP row for the same units, in the same transaction as the bucket deduction. Stock fell once. The ledger said twice. Reconciliation put it in writing: replay expected one number, the stock record showed another, and the gap was exactly the duplicated rows.

The fix became a law: every unit that leaves the building produces exactly one SHIP row, in the same transaction that moves its bucket. A pick that has not shipped moves no SHIP. Replay equals the stock record after pick, after ship, after cancel, at any hour. The books balance, or the build is red.

The case that opened itself

The second catch was quieter. An order needed 48 pieces. The stock sat on the shelf as cases, unopened. Ship Confirm broke a case to get the pieces, which is correct warehouse work, but the first version did it silently: cases went down, pieces went up, and the ledger had no row explaining the repackaging.

Now the case break is written as a pair of transfer rows inside the same transaction: cases down by the broken amount, pieces up by the conversion factor. The pair nets to zero in base units. Nothing was created or destroyed; the packaging changed, and the ledger says so, twice, in matching rows. A silent repackaging is just another single entry. The pair makes it double.

One writer per fact

Both fixes rest on the same rule. There is a registry of the 36 places in the code allowed to touch a stock bucket, and the checks fail if a 37th appears without being registered and reviewed. Every movement has a witness, and every witness is on the list.

This is stricter than it sounds, and that is the point. A system that lets five different screens adjust the same bucket will eventually have five different theories about what the bucket holds. A ledger with one writer per fact has one theory, and it is written down, and replaying it reproduces the stock record. When it does not, the system says so. That is what double-entry buys: not perfection, but the inability to be wrong quietly.

Questions

Is this just an audit log with a fancier name?
No. An audit log records that something happened, usually after the fact and usually incompletely. A ledger is the record the operation writes as it happens: every stock change is a row, written by exactly one place in the code, in the same transaction as the work itself. And the books must balance: replaying the rows must reproduce the stock record, or the system says so.
Does writing a row for everything slow down the floor?
The row is written inside the transaction that already does the work, so there is no extra round trip and nothing for the scanner operator to wait on. The cost is one database row per movement. The cost of not having it is a number nobody can explain.
What stops a developer from changing stock without writing a row?
A registry. Every location in the code that is allowed to touch a stock bucket is on a list, 36 of them today, and the checks fail if a new one appears without being registered. One writer per fact. That is the whole rule.

Keep reading

See how DockingWare handles this →