Rules That Live Elsewhere
Not every cart rule is the cart's to enforce. Stock belongs to inventory, price to the catalog, and uniqueness — once there are rows — also to a database constraint. Ask who is authoritative for each rule; a cart that enforces stock alone is a race the inventory service has to resolve at checkout anyway.
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 cart has five rules. Which of them are the cart's to enforce, which belong to another component, and what happens when the cart enforces one that is not its own?
You are writing the stock check. if quantity > inventory.available(productId): reject. It works in the demo. Then you imagine two shoppers, one unit, both carts saying yes — and you cannot tell whether the cart check is wrong, useless, or just insufficient.
Put every rule in the cart, because the cart is the code you are writing and the rules were found while writing it. Stock, price, uniqueness — all checked in addItem, all in one place, easy to find.
The cart now has an opinion about stock that inventory does not know it holds. When inventory says 3 and two carts each add 2, both pass — the check answered a question that was already stale when it was asked.
- The cart now has an opinion about stock that inventory does not know it holds. When inventory says 3 and two carts each add 2, both pass — the check answered a question that was already stale when it was asked.
- The price rule is enforced in the cart by storing the price at add time, so the cart shows the price from last Tuesday and the catalog shows today's. Two components each believe they own the number.
- "One entry per product" is enforced in code and nowhere else. With rows in a database and two tabs adding at once, both finds return nothing, both appends succeed, and the rule that lived in code is gone — the code ran correctly, twice.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- For each rule, ask who is authoritative: which component could answer the question "is this still true?" without asking anyone else. Stock: inventory. Price and product existence: the catalog. Quantity > 0 and one entry per product: the cart, because they are about the cart's own state.
- A rule the cart is not authoritative for can be *asked about* but not *enforced*. The cart can ask inventory "is there stock?" and refuse the add on a no — good feedback for the shopper — but the yes is advisory. The enforcement happens where the state lives, at the moment it changes: inventory decrements at checkout, and that is where two shoppers and one unit is settled.
- A rule the cart is authoritative for still gets a second home once the state leaves the process. "One entry per product" is a find-before-append in memory and a UNIQUE (cart_id, product_id) constraint in a database — the last line of defence against a buggy client or a race. The code and the constraint are the same rule, encoded twice, because two writers can now reach the state.
- Write the answer beside the rule. The concept record calls it
whereElse: "inventory owns stock; the cart asks", "the catalog owns prices; the cart inherits the guarantee", "the API layer repeats this check because it cannot trust the browser". A rule with nowhereElseis one you believe you fully own; check that belief.
Who could answer "is this still true?"
The decision below is the module's question applied to each rule. The criterion is authority — which component could answer without asking anyone — and the cost column is what goes wrong when the cart takes on a rule that is not its own.
For this rule, does the cart enforce, ask, or inherit?
when quantity > 0, one entry per product. The cart is the only thing that changes its items, so the check in the code is the enforcement — until a second writer appears.
cost Once the state is rows, the code check alone does not survive two tabs; the same rule needs a constraint.
when stock. The cart asks inventory at add for the shopper's sake; inventory decides at checkout, inside the transaction that decrements.
cost A call per add; an advisory yes that may be stale by checkout, which the shopper must be told about then.
when the total is never negative. Prices are non-negative because the catalog says so, and the cart derives prices rather than storing them.
cost Nothing in the cart to test; the guarantee is only as good as the catalog's own check.
The claim "the cart must enforce stock"
The reflex to enforce stock in the cart is worth running through the why ladder, because it is not wrong — it is an answer to a different question than the one it seems to answer. What the shopper needs and what the store needs turn out to be two rules with two homes.
“The cart must enforce that quantity never exceeds available stock.”
- ↓Why must the cart enforce it? So a shopper cannot add more than exists.
- ↓Why does that matter at add time? So the shopper finds out early instead of at checkout.
- ↓Why is early feedback the same as enforcement? It is not — stock can change between add and checkout, so the cart's yes is stale by the time it matters.
- ↓Why then does anything need to be enforced? Because two shoppers can each be told yes for the same last unit; only the component that decrements can refuse one of them.
the claim was right when Inventory reserves stock at add time — a hold with an expiry — so the cart's add really does change inventory state. Then "enforce at add" is right, and it is inventory enforcing it through the cart's call, not the cart alone.
Where each rule lives, version by version
The ladder follows the concept record's versions and shows the rules moving. The rules themselves do not change from V0 to V5; the number of writers and the number of components do, and each level exists because one of those numbers changed.
- V0 — functions in memoryAll five rules live in the code: two enforced, one delegated to the catalog check, one inherited, stock not yet asked. — One process and one writer; the code is the only place a rule could be, and it suffices.
- V3 — rows on a serverUniqueness gains UNIQUE (cart_id, product_id); the API repeats quantity > 0 and the catalog check because the browser cannot be trusted. — Two tabs are two writers, and the find-before-append cannot see the other tab's append.
- V5 — inventory validationaddItem asks inventory and rejects on a no; checkout asks again inside inventory's transaction, and that answer decides. — The cart is no longer the only authority on what can be added — inventory is, and the rule moves to where the unit lives.
- Later — per-product maximaThe injected "at most 10" cap becomes a number the catalog owns per product; the cart asks for it rather than holding the constant. — The moment the limit varies by product, the catalog is the component that could answer "what is the cap?" alone.
The implementation ladder
Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.
Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.
- Does a cart have identity? Yes, weakly. Two carts with the same items are still two carts, because each belongs to someone and will become a different order. It needs an id once it leaves memory; in memory the variable is the identity.
- Who owns it? A shopper — a logged-in user or an anonymous session. The owner is part of the state because "my cart" has to be findable again.
- How long does it exist? From the first add until checkout or abandonment. Whether it survives a reload, a closed browser or a login is not a property of the concept; it is a persistence decision made later, and each answer changes where the cart lives.
- Should it survive reload? Usually yes for a store, usually no for a demo. V1 in memory says no; V2 browser storage says yes on one device; V3 server storage says yes everywhere the user is logged in.
- Should it survive login? Only if an anonymous cart and a logged-in cart are merged — a rule that does not exist in V1 and appears as a modification later.
- itemscollection of CartItemkeepThe cart is its items; without them nothing else means anything.
- items[].productIdidkeepThe reference to what is being bought. The catalog owns the product; the cart only points at it.
- items[].quantityinteger > 0keepTwo laptops is one entry with quantity 2, not two entries — the rule "one entry per product" needs a quantity to hold.
- owneruser id or session iddependsSo the cart can be found again by the person it belongs to.
- items[].productNamestringderiveIt would be convenient to render the cart without a catalog lookup.
- items[].pricemoneydependsThe total needs a price per item.
- totalmoneydropEvery screen shows the total.
- currencycodedependsPrices need a currency to be added.
- createdAttimestampdropAbandoned carts might be expired or emailed about.
- update Add item — the updated cart
- delete Remove item — the updated cart
- update Change quantity — the updated cart
- read View items — the list of entries — product id and quantity — for rendering
- domain Calculate total — the sum of price × quantity over the entries
- delete Clear cart — the empty cart
- • Every quantity is greater than zero.
- • One logical entry per product.
- • The total is never negative.
- • An unknown product cannot be added.
- • Quantity cannot exceed available stock — if inventory is enforced here.
How to do it
Most important first.
- For every rule, name the component that could answer "is this still true?" alone. That component enforces; everyone else asks (Source of Truth).
- Distinguish the shopper-facing check from the enforcement: the cart may say "not enough stock" early, but the sale is refused at checkout by inventory, inside a transaction.
- When a rule is about your own state, ask how many writers can reach that state in this version. One process, one writer: the code suffices. Rows and two tabs: add the constraint.
- Derive rather than copy. The price is looked up from the catalog every time the total is computed, so there is nothing for the cart to enforce about it (Derived vs Stored).
- When a rule moves, leave the cart's check as feedback and remove its authority — the message stays, the decision does not (Invariants Under Concurrency).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Stock. Authority: inventory. V0–V4: the cart does not ask at all. V5:
addItemasks inventory and rejects on a no, and checkout asks again inside the transaction that decrements. Alice and Bob both add the last unit; both carts say yes; one checkout succeeds and the other is told "sold out" — by inventory, where the unit is. - Price. Authority: the catalog. The cart stores product ids and computes the total through
priceOf, so the current price is always shown and there is no cart-side rule to enforce. The order, unlike the cart, snapshots the price — because an order is authoritative for what was paid. - Uniqueness. Authority: the cart — but in V3 the state is rows and two tabs are two writers. The find-before-append stays, and cart_item gets UNIQUE (cart_id, product_id). The constraint turns the race's second insert into an error the API can turn into a retry as an increment.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every rule has a
whereElsenote, and for each you can say whether the cart enforces, asks, or inherits. - The two-shoppers-one-unit question has an answer that names a component and a moment — inventory, at checkout, in a transaction — rather than a check in the cart.
- There is a list of rules that will need a second home in a later version, with the version named.
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.
- ?Which component could answer "is this still true?" without asking anyone else?
- ?Is my check an enforcement or a courtesy — and does anything downstream re-check where the state actually changes?
- ?How many writers can reach this state in this version, and does the rule survive two of them?
- ?Which of my rules will need a second home when the cart is persisted, and which will leave the cart entirely?
What can go wrong
- Every rule is delegated, including the cart's own. Quantity > 0 ends up as a database CHECK only, and the shopper learns about it from a 500.
- The cart's advisory check is removed because "inventory enforces it", and the shopper discovers the sold-out laptop at checkout instead of at add — correct, and worse.
- The database constraint is added and the code's find is deleted as redundant; now every duplicate add is a constraint violation that has to be caught and reinterpreted, and the in-memory tests have no rule at all.
- Asking another component costs a call per add, and a slow inventory service makes Add to Cart slow for a check that was advisory anyway.
- A rule encoded twice — code and constraint — can drift, and the drift is invisible until the constraint fires on input the code accepted.
- Deciding authority early is a systems decision made from inside one component, and it can be wrong; V5 may reveal that inventory would rather reserve at add than refuse at checkout.
- "So the cart should not validate stock." It should ask, for the shopper's sake, and not decide. The misread drops the courtesy along with the authority.
- "The database constraint makes the code check redundant." The constraint catches what the code missed — a race, a buggy client. The code catches the ordinary case with a message the shopper can read. Both, because they fail differently.
- "Single source of truth" means one place checks each rule. The slogan is precise about *authority* — one component decides — and wrong about *checks*: the same rule is legitimately checked in the browser, the API and the database, as long as only one of them is believed.
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.
- GENERALEvery concept has rules about state it does not own — login checks a password the user store owns, a job queue checks a worker capacity the scheduler owns — and "who could answer this alone?" is the same question in each.
- STAGE-SPECIFICIn V0 with one process, every rule can live in the code and the whole lesson is a note for later; from V3 rows and two tabs make uniqueness a constraint, and from V5 inventory makes stock a question the cart asks rather than a rule it holds — the version decides where each rule lives.
- CONTESTEDSome practitioners hold that the database is the only trustworthy enforcer and that application-level checks are duplicated logic that drifts, so every invariant that can be a constraint should be one and the code should simply handle the violation. Their strongest point is that the database is the one place every writer must pass through; the lesson's answer is that shopper-readable feedback and in-memory tests need the code check too, and that the two fail differently.
- ILLUSTRATIVEAlice, Bob, the last unit and the store's version numbers are the concept record's invented progression; no real inventory system is described.
Where the depth lives
This domain asks the question and hands the answer off by name.