OperationsGENERALSIMPLIFIEDILLUSTRATIVE

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.

The question

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?

The situation

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.

The reflex

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.

Why it stalls

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.

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

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.

addItem(cart, "laptop", 1) on [Laptop × 1]
  1. inputproductId = "laptop", quantity = 1. The catalog has "laptop"; quantity is positive; both checks pass.
  2. 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.
  3. branchEntry exists → take the "increase" path, not the "append" path. This branch is the rule "one entry per product".
  4. mutationitems[0].quantity: 1 → 2. Nothing appended; items.length stays 1.
  5. 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.

After the six columns
known
  • 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.
unknown → question → experiment
  1. ? 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.

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

  3. ? 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.

concept Shopping Cart beginner
Build it step by step →

Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.

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

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

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