A warehouse count tells you what is on the shelf. It does not tell you what you can sell, and the gap between those two numbers is made of reservations. A reservation is the record that says a specific quantity is already spoken for by a specific order, and almost every stock problem that is not a counting problem is a reservation lifecycle problem.
The concept of promising from available rather than on-hand is covered in available-to-promise and why overselling happens. This post is about the reservation itself: when it is created, what releases it, what happens when nothing does, and why two systems each holding their own reservations will oversell no matter how fast they talk to each other.
What a reservation is, precisely
An inventory reservation is a durable row, not a flag and not a calculation. It names an item, a quantity, a location and the order that holds it, and it persists across restarts, deploys and the sudden unavailability of whoever created it.
Three properties follow from that definition, and each one is a design constraint:
- Reserved units are still on-hand. Nothing has physically moved. A cycle count will find them on the shelf, and it should.
- Available is derived, never stored. Available equals on-hand minus reserved. Storing available as its own field creates a third number that can disagree with the other two.
- A reservation has an owner. If you cannot name the order a reservation belongs to, you cannot release it when that order dies.
The third property is the one most often skipped, and it is the one that produces the failure below.
Create and release: the four transitions
| Transition | Trigger | Effect on on-hand | Effect on reserved |
|---|---|---|---|
| Create | Order confirmed, or a scarce item added to a cart | No change | Increases |
| Consume | Shipment picked and dispatched | Decreases | Decreases |
| Release, cancellation | Order cancelled, voided, or payment failed | No change | Decreases |
| Release, expiry | Time-limited hold reaches its deadline | No change | Decreases |
The middle row is the only one where on-hand moves. That is the whole reason reservations exist: they let availability respond to a commitment immediately, hours or days before the physical movement that would otherwise be the first signal.
The two release rows are the ones systems get wrong, because neither is triggered by a person doing something. A cancellation release fires on an event the customer causes; an expiry release fires on nothing at all happening, which requires a scheduled sweep that must itself be durable.
The failure case: stock you own and cannot sell
Overselling gets all the attention. The mirror failure is quieter and just as expensive.
A retailer reserves stock at add-to-cart to stop scarce items being lost between browse and checkout. The reservation is created correctly. Checkout completion converts it to an order. Cancellation releases it. Expiry, however, is implemented as a nightly cleanup job that was disabled during a deployment eight days ago and never re-enabled, because nothing about a disabled cleanup job is visible to anyone.
Abandoned carts accumulate. After eight days, 63 units across 40 SKUs sit reserved against sessions that ended days ago. On-hand is correct, the shelves are full, and the storefront is showing “out of stock” on items a customer is standing in front of in the shop. Purchasing sees availability at zero and raises a replenishment order for stock already in the building. The discrepancy is found during a stock count, when the counted quantity matches on-hand exactly and nobody can explain why the site says zero.
Notice what did not happen: no unit was lost, no count was wrong, no order was mishandled. A release path stopped running and the system kept behaving as designed, with a growing set of holds that nothing would ever remove.
The design lesson is that every reservation needs a release path that does not depend on a person or an unmonitored job, and that expiry should be evaluated when availability is read, not only when a sweep runs. A hold whose deadline has passed is not a hold, whether or not the cleanup has got to it yet.
Concurrency: why check-then-reserve is not enough
The other half of reservation correctness is concurrency, and it has a precise failure mode. Two orders for the last three units arrive milliseconds apart. Both read available as 3. Both pass. Both write a reservation. On-hand is 3, reserved is 6, and available is negative.
The fix is that reading availability and writing the reservation must not be separable. The read has to take a lock the second transaction blocks on, so that the second order reads the numbers as they are after the first commits rather than as they were before it started. Retrying, validating twice, or checking again after writing all reduce the frequency and none of them close the window.
This is also why the check cannot live one layer up. A user interface that greys out the unavailable option is showing a number that was true when the page rendered. The refusal has to happen at the write, where the current state is.
Two systems, two copies, one shelf
Now put the same item in two systems: a storefront with its own stock cache and an operations system with the authoritative numbers.
Each system checks availability against its own copy. The storefront confirms an order for 3 and reserves 3 locally. Inside the same sync window, a phone order is taken in the operations system, which also sees 3 and reserves 3. Both checks passed against a copy that was accurate at the moment it was written and stale by the time it was read.
There is no sync frequency that fixes this. A five-minute sync makes the collision rare; a thirty-second sync makes it rarer and much harder to reproduce; a real-time push makes it rare enough to be blamed on the customer. The property that would fix it, a single serialisation point where both reservations contend for the same row, is precisely the property two copies do not have. This is the inventory instance of the general problem described in connecting CRM, invoicing and accounting: duplicated state cannot be made consistent by communication alone.
How Oneop handles reservations
In Oneop’s inventory module, available is on-hand minus reserved, computed under a row lock. Five code paths change stock, and every one of them takes a row-level lock before it reads the number it is about to change. A reservation that would exceed on-hand minus reserved is rejected at the write with an insufficient-stock error rather than being greyed out in the interface.
Two details matter more than the headline. First, the reservation accepts the caller’s transaction, so a sales order’s reservation commits with the order itself rather than as a separate step that can succeed while the order fails. Second, issuing reserved stock at shipment releases the hold in the same operation, clamped so that reserved cannot go negative, which prevents a double-release from corrupting the number in the other direction.
Every stock change also writes a movement row carrying the quantity and value before and after, across four movement types: receipt, issue, transfer and adjustment. That makes a reservation history readable as a sequence of events rather than reconstructed from a current balance, which is the difference between investigating a discrepancy and guessing at one.
Stock, warehouses and fulfilment are available on every plan including the free one, with plan limits on products, warehouses and carrier connections rather than on the invariant. For what happens after the reservation becomes a shipment, and how one order turns into several invoices, see partial shipments and partial invoices.