Case: Implement Inventory
Inventory is the available quantity of a product. Increase, decrease, reserve, release — then inject "stock = 1, Alice buys, Bob buys at the same moment" and watch the concept meet concurrency, and route the mechanism to Concurrency and Database.
The situation, the reflex, and why it stalls
Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.
You need to track how many of each product are left. How do you derive inventory as a concept, and what happens to the derivation when two customers want the last unit at the same time?
The store sells things that run out. You need a number per product, and you need it to go down when someone buys. It sounds like a counter. You have a feeling it is not just a counter and cannot say why.
Add a stock column to the product table and decrement it at checkout with stock = stock - quantity. It is one column and one line, and it works on your machine every time.
It works with one customer. The example "stock = 1, Alice and Bob both check out" was never written, so the case where both reads see 1, both decrements run, and stock becomes −1 is discovered in production as an order that cannot be fulfilled.
- It works with one customer. The example "stock = 1, Alice and Bob both check out" was never written, so the case where both reads see 1, both decrements run, and stock becomes −1 is discovered in production as an order that cannot be fulfilled.
- There is no "reserve" operation, so a product in someone's cart is either counted as sold (and disappears for everyone while they dither) or not counted at all (and is sold twice). The tutorial chose one of those silently.
- The concept was never stated, so when a return, a restock or a damaged unit arrives, there is no rule for which operations may change the number and by how much — only a column that anything can write.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Define inventory as a concept: the quantity of a product that is available to sell. "Available" is the load-bearing word, and it splits the state the moment reservations exist: on hand, reserved, available = on hand − reserved.
- Discover the operations from the events that change the number: increase (restock, return), decrease (sold, damaged), reserve (in a cart or a pending checkout), release (cart abandoned, checkout failed). Each has an amount, a reason and a rule about what it may not do.
- Write the rules that follow from "available": available is never negative; a decrease or reserve larger than available is rejected; a release never exceeds what was reserved. Then write the examples, and include the one that breaks the counter: stock = 1, two buyers at once.
- When that example fails under the single-process model, name the mechanism you need — an atomic check-and-decrement — and route it: the in-process version to Concurrency, the database version to isolation levels and row locks. The concept does not change; where the rule is enforced does (Invariants Under Concurrency).
Meaning and state — the word "available" splits the number
The reflex stored one number. The meaning — the quantity available to sell — asks whether a unit sitting in a cart is available, and the honest answer for most stores is "for a while, no". That produces two numbers and a subtraction, and the state change below is the first example that needs both.
The rule device encodes the one invariant every operation must respect. Note what it does not say: it does not say how the check and the change are made atomic. That is deliberate — the rule is the concept's, the mechanism is Concurrency's and Database's.
- release(amount) is rejected when amount > reserved — you cannot un-reserve what was never reserved.
- decrease for a sale takes the amount out of both onHand and reserved when the sale completes a reservation; a decrease for damage takes it from onHand only and may reduce available below a pending reservation — an example worth writing.
- Every operation records a reason; V1 does not read it, and the first "why is stock 3?" will.
inventory[laptop] = { onHand: 5, reserved: 0 } (available = 5)inventory[laptop] = { onHand: 5, reserved: 2 } (available = 3)rule onHand − reserved is never below zero; a decrease or a reservation larger than what is available is rejected.
↓ becomes validation Compare the requested amount against onHand − reserved before changing either; reject rather than clamp, so the caller learns the truth.
available = inv.onHand - inv.reserved if amount > available: reject "not enough stock" inv.reserved = inv.reserved + amount
Operations and examples — then the example that breaks the counter
Four operations, each with normal, edge and invalid cases, make a concept that is complete for one writer. The injected example is what Inject a Constraint and Follow It Through asks for: take a working concept, add one constraint — two writers — and follow it through the state, the rules and the implementation.
The trace runs the naive reserve for Alice and for Bob against the same one unit, interleaved the way a scheduler would interleave them. Watch the lookup: both read available = 1 before either writes.
- inputinv = { onHand: 1, reserved: 0 }; Alice: reserve(laptop, 1); Bob: reserve(laptop, 1)
- lookupAlice reads available = 1. Before Alice writes, Bob reads available = 1.
- branchAlice: 1 > 1 is false → proceed. Bob: 1 > 1 is false → proceed. Both take the success branch.
- mutationAlice: reserved = 0 + 1 = 1. Bob: reserved = 1 + 1 = 2 (or, with a stale read, reserved = 1 and Alice's write is lost).
- outputTwo successful reservations against one unit; available = −1. The rule is violated and no line of the function is wrong — the interleaving is.
1function reserve(inventory, productId, amount):2 if amount <= 0: reject "amount must be positive"3 inv = inventory.get(productId)4 if inv is missing: reject "unknown product"5 available = inv.onHand - inv.reserved6 if amount > available: reject "not enough stock"7 inv.reserved = inv.reserved + amount8 return invCorrect for one writer at a time; O(1) average per call with a map. The gap between the "available" read and the "reserved" write is where the second writer fits.
Where the rule is enforced — the decision, routed
The concept is finished; the enforcement is not. The decision below names the options and their costs, and each option is taught elsewhere. The lesson's job is to make sure you arrive at those lessons with the example in hand, so that you read them for an answer rather than for orientation.
The ladder says how far inventory goes and why. Reservations are V2 for a store because V1 carts do not hold stock; for a ticketing system they are V1. The version numbers are for a store.
- V1 — one number, one writeronHand per product; increase and decrease with reasons; in memory. — The behaviour and the rule are testable before anything shares the number.
- V2 — reservationsreserved and the derived available; reserve and release; a release on cart abandonment. — A pending checkout must not sell the unit twice, and a cart must not hold it forever.
- V3 — atomic enforcement in the databaseOne row per product; the conditional UPDATE or a row lock; the injected example as an integration test. — The second writer exists — two servers, or two connections — and the single-thread guarantee is gone.
- V4 — several locationsOne record per product per location; available summed across them for the storefront. — The one-warehouse assumption written down in V1 has changed, and it changes the identity of the record.
Two writers can both read "available = 1". How is the check and the change made atomic?
when One process, one event loop, all operations serialised through it.
cost The concept stays exactly as written; the guarantee is lost the moment there are two processes (The Atomicity Illusion in Concurrency).
when One row per product; the UPDATE carries the check in its WHERE clause and reports rows affected.
cost The rule is written in SQL; the application must treat zero rows affected as "not enough stock" rather than success (Optimistic Concurrency: Versions and If-Match in Backend).
when Several rows must change together — inventory and an order line — under one isolation level.
cost Lock waits, deadlock potential and an isolation level to choose deliberately (Isolation Levels, Locks and Deadlocks in Database).
when A fast store without transactions, or an in-memory structure shared across threads.
cost A retry loop and a version field on every record (Compare-and-Swap: The Primitive Everything Is Built On in Concurrency).
The implementation ladder
Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.
Inventory = The quantity of each product that is actually available to sell, kept truthful as units arrive, are promised to buyers, and leave.
- Does an inventory record have identity? It borrows the product's. There is exactly one stock record per product; two records for the same product would be two sources of truth for one number, which is the failure the concept exists to prevent.
- Who owns it? The business, not a buyer. A buyer only ever holds a reservation — a promise of some units — and the reservation belongs to that buyer. Ownership of the count and ownership of a promise are different things and appear as different state.
- How long does it exist? As long as the product is sold. A reservation is much shorter-lived: it is created at "add to cart" or "begin checkout" and ends when it is consumed by an order or released — by cancellation or by expiry.
- Should it survive reload? Always. A count that resets to its starting value is a count that sells the same unit again. Inventory is the concept where in-memory is only ever V0 and V1, never a destination.
- Does it depend on who is asking? No. Available is the same number for Alice and Bob — which is precisely why they can race for it.
- productIdidkeepThe record is keyed by the product; the catalog owns everything else about it.
- onHandinteger ≥ 0keepPhysical units in the warehouse. Increase and decrease change this.
- reservedinteger ≥ 0keepUnits promised to buyers who have not yet paid. Without it, a unit in Alice's checkout is still "available" to Bob.
- availableinteger ≥ 0deriveEvery screen and every reserve call asks "how many can I still promise?"
- reservationscollection of { id, productId, quantity, expiresAt }dependsRelease must know how much to give back and whether it already did.
- warehouseIdiddropA product may be stocked in several places.
- lowStockThresholdintegerdropSomeone wants an alert.
- update Increase stock — the updated record
- update Decrease stock — the updated record
- create Reserve units — a reservation id
- delete Release a reservation — the updated record
- domain Consume a reservation — the updated record
- read Available quantity — onHand − reserved, or 0 for an unknown product
- • Available is never negative.
- • Reserved never exceeds on-hand.
- • A reservation is released or consumed exactly once.
- • Every quantity is positive.
- • A reservation does not live forever.
How to do it
Most important first.
- Write the meaning and underline "available". Ask whether a unit in a cart is available. If the answer is "no", you have a reserved quantity and a second field; if "yes", you have decided that carts do not hold stock, which is also a decision.
- Challenge the state: onHand (keep), reserved (depends — V1 without reservations has none), available (derive — never store both it and its parts), location (drop — one warehouse in V1), lastRestockedAt (drop — no operation reads it) (Derived vs Stored).
- List the operations with their reasons as an input, so the history question ("why is stock 3?") has an answer later without redesigning the concept.
- Write the rules as sentences and then as checks. The check for reserve and decrease is the same shape: compare the requested amount against available, reject if larger.
- Write the concurrent example explicitly — two operations, one unit — and predict what your current implementation does. If the prediction is "stock becomes −1", that is not a bug to fix by hand; it is the signal to route to the domain that owns atomicity (Scale Thought Experiments).
- Choose the enforcement point: in memory, a single-threaded process is the lock; in a database, a conditional UPDATE or a row lock inside a transaction. Read the linked lessons before choosing between them.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Meaning: the quantity of a product available to sell. Identity: inventory belongs to a product — one record per product per location; in V1 one location. Lifetime: as long as the product is sold.
- State: onHand (keep), reserved (keep once carts or pending checkouts hold units — V2), available (derive: onHand − reserved). The reflex stored one number called stock; the derivation shows it was two numbers and a subtraction, and that the reflex's number was neither of them consistently.
- Operations: increase(productId, amount, reason), decrease(productId, amount, reason), reserve(productId, amount), release(productId, amount). Rules: available ≥ 0 always; decrease and reserve are rejected when amount > available; release is rejected when amount > reserved; amounts are positive integers.
- Examples: onHand 5, reserved 0 → reserve 2 → onHand 5, reserved 2, available 3. → decrease 2 (sold, releasing the reservation) → onHand 3, reserved 0. onHand 1 → reserve 2 → rejected, unchanged. The injected one: onHand 1, reserved 0 → Alice reserve 1 and Bob reserve 1 at the same moment → exactly one succeeds and the other is rejected; a naive implementation lets both succeed and reserved becomes 2 against onHand 1.
- Representation and enforcement: in memory, a map from product id to { onHand, reserved }, O(1) average per operation, and a single thread as the lock. In the database, one row per product and
UPDATE inventory SET reserved = reserved + 1 WHERE product_id = ? AND on_hand - reserved >= 1— the check and the change in one statement, with the rows-affected count as the verdict. Which of those is right, and what "at the same moment" means under each isolation level, is Database's lesson, linked below.
How you know it worked
What now exists that did not before, and what question you can now ask.
- You have two numbers where the reflex had one, and you can say which operation moves which.
- The concurrent example is written down with a predicted outcome, and your implementation either passes it or cites the lesson that will make it pass.
- Every change to the number carries a reason, so "why is stock 3?" is answerable from the operations alone.
- You can say where the rule "available ≥ 0" is enforced — the process, the statement, the transaction — and what would happen if that layer were bypassed.
The questions you can now ask
The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.
- ?What does "available" mean here — and is a unit in a cart available?
- ?Which events change the number, and does each carry a reason?
- ?What should happen when two customers want the last unit at the same moment — and what does my implementation actually do?
- ?Where is "available is never negative" enforced, and what bypasses that layer?
- ?Which two numbers was the single stock column hiding?
What can go wrong
- The concurrent example is written and then "handled" with a check-then-write in application code: read available, compare, write. That is the race, restated in two statements; the check and the change must be one atomic step, and the lesson that says how is not this one.
- Reservations are added to V1 before any cart holds stock, and the store now has a release timer, an expiry rule and a reconciliation job for a requirement nobody has.
- The routing becomes abdication: "concurrency is Database's problem", and the inventory concept ships without the example that would have shown it failing — so the Database lesson is never read.
- Two fields and four operations are more than one column and one decrement, and for a store that never reserves and never races they are pure overhead — the derivation exists to find out whether that store is yours.
- Enforcing the rule in the database statement means the rule is written in SQL and again in the application for the error message; the duplication is the price of a last line of defence.
- Carrying a reason on every change is a field the V1 operations do not read; it is kept because the first "why is stock 3?" arrives before V2.
- "Inventory is a counter." A counter has increment and decrement; inventory has a rule about a derived quantity that must not go negative under concurrent writers. The rule, not the number, is the concept.
- "Just lock the row and the problem is solved." A lock is one mechanism; a conditional update is another; an optimistic version check is a third — and they behave differently under load and under retries. The concept says what must hold; the mechanism is chosen with the linked lessons open.
- "The cart should decrement stock when an item is added." That is a reservation, and it needs a release when the cart is abandoned — an operation the reflex did not write. Decide whether carts hold stock before deciding how.
Where this applies
Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.
- ILLUSTRATIVEAlice, Bob, one unit and onHand 5 are invented for the shape of the argument; the isolation-level behaviour they point at is real and lives in the linked Database lessons.
- SCALE-SPECIFICA single-threaded in-memory service has no race and needs no lock; the race appears with the second writer — a second process, a second server, or a database with concurrent connections — and the enforcement point moves with it.
- DOMAIN-SPECIFICA store with unlimited digital goods has no inventory rule at all; a ticketing system for a fixed number of seats has this rule as its entire business, and reservations with expiry are V1 there rather than V2.
Where the depth lives
This domain asks the question and hands the answer off by name.