From Invariant to Validation
Rule → validation → code, for every cart rule. "Every quantity is greater than zero" becomes "reject a quantity ≤ 0" becomes if quantity <= 0: reject. Code is a precise encoding of a previously identified rule — which is why an if you cannot name the rule for is suspicious.
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 have the rule in words. How do you get from the sentence to the check — and how do you know the check says exactly what the sentence said?
The rules are written on a page: quantity > 0, one entry per product, no unknown product. You start typing if and freeze on the details. Is it < 0 or <= 0? Does zero throw, or do nothing? Where does "one entry per product" go — it is not a comparison at all.
Look at a tutorial's cart and copy its validation block. It has an if (!quantity) at the top, which looks like the same thing, and it compiles.
if (!quantity) is a different rule from "quantity > 0": it rejects zero and lets −1 through, and it rejects nothing when the quantity is the string "0". The tutorial's line encoded whatever the tutorial's author was thinking; it did not encode your rule.
if (!quantity)is a different rule from "quantity > 0": it rejects zero and lets −1 through, and it rejects nothing when the quantity is the string "0". The tutorial's line encoded whatever the tutorial's author was thinking; it did not encode your rule.- "One entry per product" has no copyable line, so it gets skipped, and the second add produces two entries. The rule that was not a comparison was the one that mattered most.
- The block at the top of the function cannot be explained line by line. Asked which rule
if (!productId)enforces, the answer is a shrug — so when the rules change, nobody knows which lines to touch.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Write the rule as a sentence about the state. Then write the validation as a sentence about the operation: what input, or what existing state, would break the rule, and what the operation does about it — reject, cap, convert, or look before writing. Only then write the check, and make it say exactly what the validation sentence said, boundary included.
- Read the boundary off the rule. "Greater than zero" excludes zero, so the rejection is
<= 0. If zero has a meaning of its own — a change to zero is a removal — that is a second rule with its own line, not an exception smuggled into the first. - Recognise the rules that are not comparisons. "One entry per product" is enforced by looking before writing: find the existing entry, and increase it rather than append. The validation sentence is still there — "before adding, look for an existing entry with the same product id" — it just produces a find and a branch instead of a reject.
- Check the encoding backwards. Read the line and translate it into English without looking at the rule; if the translation is not the rule, the line is wrong.
if (!quantity)reads as "if the quantity is missing or zero", which is not "if the quantity is not greater than zero".
Quantity: the rule that is a comparison
The simplest rule, written out in the three lines the whole module uses. The validation sentence is the one people skip, and it is the one that decides the boundary: "greater than zero" means zero is refused, so the comparison is <=, not <. A change to exactly zero is a second rule — a cart never holds quantity 0 — and gets its own line in changeQuantity, converting to a removal rather than rejecting.
rule Every quantity is greater than zero.
↓ becomes validation Reject an add or change whose quantity is ≤ 0. In changeQuantity, treat exactly 0 as a removal — a cart never holds quantity 0.
-- addItem if quantity <= 0: reject "quantity must be positive" -- changeQuantity if quantity < 0: reject "quantity cannot be negative" if quantity == 0: return removeItem(cart, productId)
One entry per product: the rule that is not a comparison
Not every rule is an if with a reject. "One logical entry per product" is enforced by looking before writing: find the entry, and if it exists, change it instead of appending. The validation sentence still exists — it just produces a find and a branch. The TypeScript is the same algorithm as the pseudocode with the language's syntax for "find"; the comment on the find is the rule's name, kept in the code on purpose.
rule One logical entry per product.
↓ becomes validation Before adding, look for an existing entry with the same product id and increase it instead of appending.
existing = find(cart.items, productId)
if existing: existing.quantity += quantity
else: append(cart.items, { productId, quantity })1const existing = cart.items.find((i) => i.productId === productId) // one entry per product2if (existing) existing.quantity += quantity3else cart.items.push({ productId, quantity })The find is O(n) over the items — a scan, invisible for a cart, and the reason a map would make this rule structural instead of procedural.
Unknown product: the rule that comes first, and again at the boundary
The third rule shows two things the others do not. Order: the catalog check comes before anything touches the items, so a bad product never reaches the find. And repetition: the API repeats this check because it cannot trust the browser. The pipeline is the whole move — the same four stops for every rule, and the last stop is the one that makes the encoding checkable.
rule An unknown product cannot be added.
↓ becomes validation Check the catalog before touching the items; the API layer repeats the check because it cannot trust the browser.
if not catalog.has(productId):
reject "unknown product"- 1Rule
A sentence about the state: what is always true afterwards.
fails by A sentence about the code ("check the input") instead of the state — nothing to encode.
- 2Validation
What input or existing state would break it, and what the operation does: reject, cap, convert, look-before-write.
fails by Skipped — and the boundary (
<or<=) gets guessed. - 3Code
The check, placed in the operations that can break the rule, before the state is touched.
fails by A check that reads back as a different sentence than the rule.
- 4Test
The invalid example runs against the check; the state is unchanged afterwards and the message names the rule.
fails by A test that only proves the happy path still works.
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.
- Three lines per rule, in order: the rule, the validation in words, the check in pseudocode. Never skip the middle line; it is where
<and<=get decided. - For each check, name the operation it belongs in and the response: reject with a message, cap, convert to another operation, or no-op. The concept record chooses reject for a bad quantity and no-op for removing an absent product; both are choices, both are written down.
- Translate each written line back into English and compare it with the rule. Do this with the negation too: what does the line let through?
- Put the check where the threat is, and repeat it at every boundary that cannot trust its caller — the API repeats the catalog check because it cannot trust the browser (Rules That Live Elsewhere).
- Run the invalid example against the check and confirm the state is untouched afterwards (Normal, Edge, Invalid).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Rule: every quantity > 0. Validation: reject an add or change whose quantity is ≤ 0. Check:
if quantity <= 0: reject "quantity must be positive". Read back: "if the quantity is zero or less, refuse" — that is the rule, so the line stays. - Rule: one logical entry per product. Validation: before adding, look for an existing entry with this product id; increase it instead of appending. Check:
existing = find(cart.items, productId); if existing: existing.quantity += quantity; else: append(...). No comparison, no rejection — and still a rule encoded. - Rule: an unknown product cannot be added. Validation: check the catalog before touching the items. Check:
if not catalog.has(productId): reject "unknown product". Order matters — this comes first, so a bad product never reaches the find.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every check in
addItemhas a rule sentence you can recite, and every rule on the page has a line, a structure, or a named owner. - The
<against<=question no longer arises, because the boundary is read off the rule rather than guessed. - A reviewer can rename a variable in your validation and the English translation still matches the rule.
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 input, or what existing state, would break this rule — and what does the operation do about it?
- ?Does this line, read back into English, say exactly what the rule says, boundary included?
- ?Which rules are comparisons, which are look-before-write, and which are someone else's to enforce?
- ?Where else does the same check have to be repeated because the caller cannot be trusted?
What can go wrong
- The validation sentence is skipped and the check is written from the rule directly — and the boundary drifts. "Greater than zero" becomes
< 0because the author was thinking about negatives. - Every rule becomes a rejection. A change to zero could be rejected, converted to a removal, or ignored; the record chooses conversion, and the choice is a design decision the lesson makes visible. Reflexively rejecting hides it.
- The check is written once, at the first boundary, and assumed to hold everywhere. A rule enforced only in the browser is a rule the API does not have.
- Three lines per rule is slower than one, and for a rule you have encoded a hundred times the middle line adds nothing.
- A validation written as a sentence commits you to a response — reject, cap, convert — that you might have preferred to leave to whoever writes the UI.
- Repeating a check at every boundary means the same rule lives in three places, and they can drift; the shared-library answer costs a package.
- "Validate early" means validate everything at the entry point. The slogan is precise only if "early" means "before the state is touched": the catalog check goes before the find, and the find goes before the append, inside the operation — not in a wall of checks before any function is called.
- "If the code has the check, the rule is enforced." The check enforces the rule in the process that runs it; a second tab, a second server or a direct database write does not run it. Enforcement is a property of where the check lives, not of its existence.
- "Garbage in, garbage out" means invalid input is the caller's problem. The rule says what stays true of the cart; the cart is responsible for refusing input that would break it, whoever sent it.
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.
- GENERALRule → validation → code applies to any check in any language; the middle step is where boundaries, responses and ownership get decided, and it is the same step whether the rule is about a cart quantity or a rate-limit window.
- TEAM-SPECIFICA learner needs the three lines written out; an engineer who has encoded the same rule many times does the middle line in their head, and the move becomes a review habit — reading a stranger's
ifback into English to see whether it matches any rule at all. - ILLUSTRATIVEThe rules, checks and messages quoted are the cart concept record's invented example; quantities such as −1 and 0 illustrate boundaries, not a real store.
Where the depth lives
This domain asks the question and hands the answer off by name.