Inputs, Outputs and Side Effects
Every operation, analysed the same six ways: inputs, state read, state changed, output, side effects, errors. Add item takes a product id and a quantity, reads the items and the catalog, changes one entry, returns the cart, has no side effects in V0, and fails on an unknown product or a non-positive quantity. Six answers per operation, and the function writes itself.
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.
An operation has a name and a sentence. What do you need to know about it before you can implement it — and how do you know when you have all of it?
You are about to write addItem. You know it adds an item. You type function addItem( and stop: does it take a product or an id? Does it return the cart, the item, or nothing? Should it check the catalog, or does the caller? What if the product is already there? What if the quantity is zero? Each question sends you back to the sentence, which does not answer it.
Write the function and let the questions answer themselves as you go. Take whatever the calling code has handy — probably the whole product object — return void for now, skip the catalog check because the UI only shows real products, and handle the duplicate case when a test finds it.
The signature was decided by what the caller had handy, so the cart now depends on the product object's shape; when the product gains a field, the cart's tests break for no cart reason.
- The signature was decided by what the caller had handy, so the cart now depends on the product object's shape; when the product gains a field, the cart's tests break for no cart reason.
- The errors were never listed, so they were never handled: a quantity of zero produces an entry with quantity 0, an unknown product produces an entry that total cannot price, and both were "found by a test" only if someone thought to write one.
- What the operation reads was not stated, so nobody noticed the catalog dependency until the function needed it in a place where the catalog was not available — the unit test, then the browser.
- Writing the function felt like implementing the operation; it was discovering the operation's shape by trial, in the one medium where every wrong guess costs a rewrite.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Analyse each operation in the same six columns before writing it: inputs (what the caller must supply), state read (what the operation looks at, including other concepts), state changed (exactly which fields), output (what comes back), side effects (anything outside this concept's state), errors (every way it can refuse). The columns are the record's and the CRUD and Domain Actions lab's; six answers make a function derivable.
- Keep inputs minimal and by reference: an id, not an object. The cart points at products; passing a product object into addItem makes the cart know the product's shape, which it has no reason to.
- Separate "state read" from "state changed" and be exact about the changed column. "items: a new entry, or the existing entry's quantity" is precise enough to become a before/after example and a test; "updates the cart" is not.
- List errors last but do not skip them: the errors column is where the rules from Rules Determine Implementation show up as refusals, and an operation with an empty errors column is either a read or has not been thought through.
Add item through the five stops
The six columns describe the operation at rest; the trace shows it running. Input, lookup, branch, mutation, output — the same five stops every operation passes through, here for the edge case that decides the rule.
- inputproductId = "laptop", quantity = 1. The catalog has "laptop"; quantity is positive; both checks pass.
- lookupScan cart.items for an entry whose productId equals "laptop" — an O(n) find over a handful of entries. It is found at index 0.
- branchEntry exists → take the "increase" path, not the "append" path. This branch is the rule "one entry per product".
- mutationitems[0].quantity: 1 → 2. Nothing appended; items.length stays 1.
- outputThe updated cart: [Laptop × 2]. The caller renders it; nothing else in the system changed.
Every operation, six ways
The full table for the record's cart. Empty cells are deliberate — "none" is an answer — and the errors column is the one to read twice.
Operation Inputs Reads Changes Output Side effects Errors Add item productId, quantity=1 items; catalog one entry: new, or its quantity the cart none in V0; V5 reserves unknown product; qty <= 0 Remove item productId items the entry is gone the cart none absent: no-op (V1) or error Change quantity productId, quantity items entry.quantity, or removed at 0 the cart none not in cart; qty < 0 View items - items - the entries none none Calculate total a way to price items; catalog prices - the sum none a product with no price Clear cart - - items: empty the empty cart none none
What the columns leave unknown
The table answers what each operation needs and does. It does not answer where the catalog comes from, what the API does with an error, or what two simultaneous adds do — and each of those is a sharpened unknown with an experiment, not a reason to stop.
- ✓Every mutation returns the updated cart; every read returns a value and changes nothing.
- ✓Add item and total depend on the catalog; nothing else does.
- ✓Every error leaves the state untouched.
? The catalog.
becomes How does addItem reach the catalog — a parameter, an injected dependency, a global — and what does the unit test use instead?
experiment Write addItem with the catalog as a parameter and a test that passes a set of three ids; see whether the signature is tolerable.
? Errors in the API.
becomes Which status code and body does "quantity must be positive" become, and does the response name the rule?
experiment Map the errors column to one 400 body shape with a rule field and check every error fits it (From Cart.addItem() to POST /cart/items).
? Two adds at once.
becomes If two requests add the same product to a persisted cart simultaneously, is the result quantity 2, quantity 1, or two rows?
experiment Two concurrent inserts against the cart_item table with and without the unique constraint; observe.
None of these blocks the pure implementation — the next lesson — which is why the columns are enough to start.
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.
- Draw the six columns for every operation on the list at once, in a table; the empty cells are the questions you have not asked yet.
- In the "state read" column include other concepts explicitly — "catalog — does the product exist?" — because every entry there is a dependency the function will need injected, faked or reached.
- Write the output as the thing the caller needs next. The record returns "the updated cart" from every mutation so the caller can render; returning nothing forces a second read.
- Distinguish side effects by version: "none in V0; in V5 an inventory reservation." A side effect that appears later is a reason the function signature will change, and knowing that now is cheap.
- Turn each error into an invalid-case example — "add Laptop with quantity 0 → rejected, cart unchanged" — and note that "cart unchanged" is part of the contract (Normal, Edge, Invalid).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The record's add-item, in the six columns: inputs — productId, quantity (default 1). Reads — "items — is this product already present?", "catalog — does the product exist?". Changes — "items: a new entry, or the existing entry's quantity". Output — the updated cart. Side effects — "none in V0; in V5 an inventory reservation". Errors — unknown product; quantity ≤ 0; quantity above the allowed maximum "once that rule exists".
- Change quantity: inputs — productId, quantity; reads — items; changes — "items[].quantity, or the entry is removed when quantity becomes 0"; output — the updated cart; side effects — none; errors — product not in cart, quantity < 0. The changes column is where "zero removes" was written down, before any code.
- Calculate total: inputs — "a way to price a product — the catalog"; reads — items, catalog prices; changes — nothing; output — the sum; errors — "a product whose price is unknown — the catalog changed under the cart". The inputs column is why the implementation takes a
priceOffunction: the analysis said the operation needs a way to price, not a price table.
How you know it worked
What now exists that did not before, and what question you can now ask.
- A table exists with every operation in six columns, and no cell says "updates the cart".
- You can write each function's signature from the inputs and output columns without looking at any caller.
- Every entry in the errors column has an invalid-case example that ends with the state unchanged.
- The dependencies (catalog, inventory) are visible in the reads and side-effects columns before they are visible in an import.
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 must the caller supply — and is any of it an object where an id would do?
- ?What does this operation read, including from other concepts?
- ?Exactly which fields change, and what does the caller get back?
- ?What can this operation refuse, and is the state untouched when it does?
What can go wrong
- The columns are filled with implementation: "reads — calls items.find()". The analysis is about what the operation needs, not how the code will get it; the how belongs to Plain-English Logic Is the Algorithm.
- Side effects are listed for the future version and then built now — an inventory reservation in V0 because the column mentioned V5. The column records when a side effect arrives so that it does not arrive early.
- The output column is filled in as "success/failure" for every operation, and the caller has to do a read after each write.
- The reads column omits the concept's own state ("reads — the catalog") and the find-before-append is forgotten, because "is this product already present?" was never written down.
- Six columns per operation is a page of analysis before a line of code; for a read with no errors, four of the columns say "none" and the page was mostly confirmation.
- Minimal inputs by id mean the operation must reach the catalog itself, which is a dependency to inject; passing the object in would have avoided the injection at the cost of coupling.
- Returning the updated cart from every mutation is convenient and hides which fields changed; the state-change device in Before, Operation, After — and Exactly What Changed exists to put that back.
- "This is just writing the docstring first." A docstring describes the function; this decides it. The output column decided that mutations return the cart; the inputs column decided that total takes priceOf.
- "Side effects means anything the function does." It means effects outside this concept's state — an inventory reservation, an email, a log line. Changing items is the operation, not a side effect (Side Effects in the design domain is the long version).
- "Errors are exceptions." Errors are refusals; whether they are thrown, returned or reported as a 400 is a representation decision. The column says what is refused and that the state is untouched.
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.
- GENERALThe six columns describe any operation on any concept; only the entries change. A rate limiter's "allow" reads a counter and a clock, changes the counter, outputs a decision, and errors on nothing — the same table.
- SIMPLIFIEDSix columns leave out concurrency (what if two calls interleave?) and authorisation (may this caller?), which the engineering stage adds; the model is complete for pure in-memory logic and deliberately not beyond it.
- ILLUSTRATIVEThe six-column entries and the laptop examples are the shopping-cart record's; a real analysis would have its own catalog and its own errors.
Where the depth lives
This domain asks the question and hands the answer off by name.