Concept CasesILLUSTRATIVESCALE-SPECIFICDOMAIN-SPECIFIC

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 moveWorked exampleNext questions

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.

The question

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 situation

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.

The reflex

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.

Why it stalls

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.

What the reflex produces — and fails to produce
  • 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.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

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.
reserve 2 on a product with 5 on hand
before
inventory[laptop] = { onHand: 5, reserved: 0 }  (available = 5)
reserve(laptop, 2) →
after
inventory[laptop] = { onHand: 5, reserved: 2 }  (available = 3)
what changed reserved: 0 → 2 · available (derived): 5 → 3 · onHand: unchanged — nothing has been sold yet
Available is never negative

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.

becomes code
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.

Alice and Bob reserve the last unit, interleaved
  1. inputinv = { onHand: 1, reserved: 0 }; Alice: reserve(laptop, 1); Bob: reserve(laptop, 1)
  2. lookupAlice reads available = 1. Before Alice writes, Bob reads available = 1.
  3. branchAlice: 1 > 1 is false → proceed. Bob: 1 > 1 is false → proceed. Both take the success branch.
  4. mutationAlice: reserved = 0 + 1 = 1. Bob: reserved = 1 + 1 = 2 (or, with a stale read, reserved = 1 and Alice's write is lost).
  5. outputTwo successful reservations against one unit; available = −1. The rule is violated and no line of the function is wrong — the interleaving is.
reserve, single-writer version
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.reserved
6 if amount > available: reject "not enough stock"
7 inv.reserved = inv.reserved + amount
8 return inv

Correct 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.

Inventory for a store, by version
  1. V1 — one number, one writer
    onHand per product; increase and decrease with reasons; in memory.The behaviour and the rule are testable before anything shares the number.
  2. V2 — reservations
    reserved 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.
  3. V3 — atomic enforcement in the database
    One 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.
  4. V4 — several locations
    One 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.
Making check-and-change one step

Two writers can both read "available = 1". How is the check and the change made atomic?

Single thread owns the inventory

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).

Conditional update in the database

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).

Row lock inside a transaction

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).

Compare-and-swap on a version

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.

concept Inventory intermediate
Build it step by step →

Inventory = The quantity of each product that is actually available to sell, kept truthful as units arrive, are promised to buyers, and leave.

Identity, ownership, lifetime
  • 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.
State it must remember
  • 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.
Operations
  • 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
Rules that must always hold
  • 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.

Next questions
  • ?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

How the move itself fails
  • 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.
What the move costs
  • 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.
Misreads
  • "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.