Table of Contents

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