Inventory lifecycle
Commerce does not own stock — it holds what the ERP published, and reserves against it while a customer checks out. The interesting part is the reservation, which changes character halfway through.
The states
stateDiagram-v2
[*] --> Available: ERP push sets stock on hand
Available --> Held: Stage checkout
Held --> Available: expires · abandoned · re-staged
Held --> Committed: Place order
Committed --> [*]: ERP push absorbs the order
note right of Held
Temporary.
Expiry set to now + TTL.
end note
note right of Committed
Permanent — no expiry.
Ends only when the ERP's stock push
LOWERS stock on hand AND EXPIRES
this reservation, in one transaction.
The ERP asks for the expiry by naming
the reservation on that same push.
end note
| State | What it means | How it ends |
|---|---|---|
| Available | On hand from the last push, less anything live-reserved | Somebody stages a checkout |
| Held | Reserved with an expiry — a customer is mid-checkout | Expires, is abandoned, or the order is placed |
| Committed | Reserved with no expiry — the order exists and the ERP has not caught up | The ERP's next push lowers stock and names this reservation to be expired, in the same transaction |
Note the direction of that last one. Commerce does not decide when a committed reservation ends — the ERP asks for it, by naming the reservation on the push that carries the reduced stock figure. The two arrive together because they have to; the reasoning is below.
Availability is computed, never stored
available = on hand (from the last push) − live reservations
A reservation is live when its expiry is in the future or it has no expiry at all. That single condition is what makes the whole design work without a cleanup job.
Nothing sweeps expired rows. They stay in the table and simply stop counting, because every read filters on that condition. Rows accumulate; they are harmless, and they preserve the history of what was held when.
Nothing is ever deleted, and there is no released flag. Releasing a reservation means setting its expiry to now — the same field the live condition already reads. One field carries the whole state, so there is nothing to keep in step.
Note
A basket does not count against itself. When Commerce works out what a basket may have, it excludes that basket's own reservation from the reserved total. Without it, a customer holding the last ten units would be told there are none left — their own hold would deduct from their own availability.
Stage: the hold
Staging a checkout reserves each line, keyed by a reference type and id, with an expiry of now plus the reservation TTL — 30 minutes by default, and a live setting.
Two behaviours worth knowing:
Re-staging supersedes rather than stacks. An existing live reservation for the same reference is expired first, then a new one is written. A customer who re-stages three times holds stock once, not three times.
The deadline is captured once and written to three places — the order, the reservation and the response — from a single reading of the clock. Three clocks would produce three answers, and the gap between them is a window where the order is valid and the stock is not.
If a line cannot be satisfied, the attempt fails outright rather than partially reserving.
Place: the hold becomes permanent
At the moment an order is placed, the reservation is finalised: its expiry is set to null.
Why not simply keep extending it. The order now exists and is a commitment. If the reservation expired, the stock would appear available while an order for it was already in flight, and the next customer could buy the same units. Removing the expiry removes that possibility.
Why not decrement stock instead. Commerce does not own stock. It holds a copy of what the ERP published, and the ERP will publish a new figure that already reflects the order. Adjusting the copy locally would mean two systems disagreeing until the next push, and the local edit being silently overwritten.
So the reservation is the mechanism that keeps the picture honest in between: on hand is still the old number, and the reservation accounts for the difference.
Release: bringing the expiry forward
There is only one way a reservation ever stops counting: its expiry is in the past. Every "release" is the same single act — set the expiry to now. Nothing is deleted, and there is no separate released flag or status.
| What happens | When | |
|---|---|---|
| It just lapses | Nothing at all. The expiry set at Stage passes on its own | A customer abandons a checkout, or takes too long |
| Re-staged | The basket's own live rows have their expiry set to now, then new rows are written | A customer re-stages |
| Explicitly released | Live rows for that reference have their expiry set to now | A basket is abandoned deliberately |
| Absorbed by the ERP | The push carries the reduced stock figure and names the reservation; both are applied in one save | The ERP has the order and its own stock now reflects it |
That is why a committed reservation can be released even though it has no expiry. Null counts as
live, so the release matches it and writes an actual timestamp — moving it from "live forever" to
"expired a moment ago" in one step.
The last row is the one that matters, and it is why the inventory push carries reservation identifiers.
Important
The release happens in the same transaction as the stock update. One save applies the new on-hand figures and releases the named reservations together.
If those were two operations, either order is wrong. Release first and there is a window where stock is the old figure with nothing reserved — the same units look available twice. Update first and there is a window where the new, already-reduced figure is also reduced by the reservation, so stock appears lower than it is and customers are told there is none.
Doing both in one transaction means neither window exists. This is the single most important thing on this page.
That also explains the shape of the integration contract: the reservation identifier travels with the stock figures, on the same call, rather than being a separate release endpoint. It has to, or the guarantee is lost.
Incoming stock
Separate from on-hand: expected deliveries, each with a location, a quantity, an expected date and the upstream reference behind it. Keyed on location plus reference, so two deliveries into one warehouse stay distinct.
It is presentational — it tells a customer when something out of stock is expected back. It does not enter the availability calculation, and nothing can be reserved against it.
Consequences worth carrying
| Stock is a snapshot, not a promise | It reflects the last push. Nothing is held until a checkout stages |
| A location absent from a push is removed | Inventory pushes are snapshots. Send zero explicitly rather than omitting a location you still have |
| Availability lower than expected usually means reservations | In-flight checkouts and placed-but-not-yet-absorbed orders both deduct |
| Availability that stays low | A committed reservation whose order the ERP has not yet acknowledged. It has no expiry, so it will not clear itself |
| Expired rows are normal | The table grows and nothing cleans it. Not a leak |
Related
- Integration API › Inventory — the push contract, including reservation identifiers
- Support › Wrong data — diagnosing a stock complaint