Skip to content
← All patterns

commerce

Stock Counter

Can we fulfil this order?

A counter that decrements atomically and refuses to go negative.

Stock levels under concurrent checkouts. The count is changed with ADD rather than read-modify-write, so two simultaneous purchases both land, and a condition on the same request stops the total falling below what is actually there.

The model

StockLevel

How many of one product are available to sell.

pk
PRODUCT#<sku>
sk
STOCK

Attributes: quantity (N), reorderAt (N), updatedAt (N), GSI1PK (S), GSI1SK (N)

Access patterns

  • Query · GSI1What needs reordering

    Products at or below their reorder point, lowest stock first.

  • UpdateItemTake stock for an order

    Decrement by the quantity ordered, but only if that much is there.

  • GetItemRead a stock level

    The current count for one product.

Design notes

ADD is not read-then-writewhy

Reading 42, subtracting one and writing 41 loses a sale whenever two checkouts overlap: both read 42 and both write 41, so two items left the shelf and the count fell by one. ADD sends the delta instead of the result, and the database applies them one after another. Neither request needs to know the current value, which is exactly why neither can lose the other's update.

The guard belongs on the same requestwhy

Checking availability first and then decrementing reopens the gap: between the check and the write, someone else takes the last one. ConditionExpression quantity >= :n is evaluated where the item lives, at the moment of the write, so a decrement that would oversell is refused rather than applied. sku-2044 has three left; two orders for two each cannot both succeed.

The buying list maintains itselfwhy

The same guarded write that decrements stock adds the index keys when the count falls to the reorder point, and removes them when a delivery takes it back above. So the index holds exactly the products needing attention and nothing else - run the Query and it returns two of four, having read only two. A filtered Scan would read all four to tell you the same thing.

A refusal is an answer, not an errortrade-off

The failed condition is how the caller learns the stock ran out - it is the checkout's normal 'sorry, gone' path, not an exception to alert on. sku-3390 sits at zero, where the guard will keep refusing until a delivery restocks it, rather than being carried negative by orders that should never have succeeded.

Taking stock is a design here, not a live demonstrationtrade-off

The engine performs guarded counters correctly - an ADD with a condition applies, and refuses with 'The conditional request failed' when the guard fails. The console cannot show it: the resolver templates writes without emitting them as runnable, so this page prices the mechanism rather than racing two checkouts in front of you.

Taught in the course

stock-counterDynamoDB workbench
Ready to run
Explore an access patternSelect to load & run

Products at or below their reorder point, lowest stock first.

Request
Execute against the local Dynoxide engine
ReturnedFiltered outChanged
PK(pk)
SK(sk)
quantity
reorderAt
updatedAt
GSI1PK
GSI1SK
PRODUCT#sku-1180
STOCKS
42N
10N
1790026560N
—
—
PRODUCT#sku-2044
STOCKS
3N
5N
1790000640N
REORDER#dueS
3N
PRODUCT#sku-3390
STOCKS
0N
8N
1789862400N
REORDER#dueS
0N
PRODUCT#sku-4127
STOCKS
156N
20N
1789931520N
—
—
Awaiting request
Your next query starts here.

Choose an access pattern above, or build your own request. See what comes back and what it costs.

Write transactions, streams, tags and TTL are among the operations this browser build leaves out. dynoxide's native build has them.