OperationsGENERALSTAGE-SPECIFICILLUSTRATIVE

Operation Contracts

Each operation promises something and refuses something: add item promises one entry per product and refuses a non-positive quantity or an unknown product, leaving the cart untouched. Written as a contract — given, promises, refuses — the operation is already most of an API contract, and the endpoint can only keep promises the operation made.

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

What does each operation promise its caller, what does it refuse, and how does a promise made in a function become one an endpoint can keep?

The situation

The pure functions work and the tests pass. Now the frontend developer asks: "What does POST /cart/items return when the product is already in the cart? What if the quantity is zero — 400 or 200 with a warning? If I DELETE an item that is not there, is that an error?" You realise the answers are somewhere in your functions, and that you decided some of them by accident.

The reflex

Answer from the code. Read addItem, see that it throws on zero, say "400". Read removeItem, see it filters, say "200". Write the answers into the API doc as you find them, and where the code does not say, pick whatever the framework's default is.

Why it stalls

The contract is whatever the implementation happened to do, so the accidental decisions are now promises: an exception message becomes the error body, a filter that ignores absent items becomes "DELETE is idempotent", and neither was chosen.

What the reflex produces — and fails to produce
  • The contract is whatever the implementation happened to do, so the accidental decisions are now promises: an exception message becomes the error body, a filter that ignores absent items becomes "DELETE is idempotent", and neither was chosen.
  • The promises are discovered by callers one at a time and documented after the fact — the contract exists in a chat thread, not in a place the next caller will find.
  • The API contract and the operation contract drift: the endpoint returns 404 for an absent item while the function returns the cart unchanged, and the two behaviours are now both "correct" depending on which layer you ask.
  • Writing the doc from the code felt like documenting the API; it was promoting accidents to guarantees.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Write each operation as a contract with three parts: given (the preconditions on inputs and state), promises (what is true afterwards — the postconditions, including which rules still hold), refuses (the inputs it rejects, with the guarantee that the state is untouched). The six-column analysis supplies the material; the contract is the commitment.
  • Make the refusals name the rule. "Refuses quantity ≤ 0: quantity must be positive" — so that the API's error body can carry the rule and the client can show it, and so that a new rule (maximum quantity) is a new refusal, not a surprise.
  • Decide the ambiguous cases on purpose and write the decision down as part of the contract. Removing an absent item is the record's example: "a choice: error, or a no-op. V1 chooses nothing happens, because removing something absent leaves the cart in the state the caller wanted." The API inherits the choice — "removing an absent item returns success, matching the domain's no-op choice."
  • Map each contract to the endpoint's contract one to one: given → request validation; promises → response body; refuses → status code plus the rule. The endpoint can add promises about transport (idempotency keys, auth) but cannot promise behaviour the operation does not have — that is the direction of the mapping (From Cart.addItem() to POST /cart/items).

A rule becomes a refusal becomes a status code

The rule device shows the chain the contract makes explicit: the invariant in words, the validation that keeps it, the check in pseudocode. The API layer adds one more link — the 400 body — and it can only carry a rule the operation already refused.

Quantity must be positive

rule Every quantity in the cart is greater than zero.

becomes validation Reject an add whose quantity is ≤ 0 with the rule named; treat a change to 0 as a removal. The cart is untouched on rejection.

becomes code
if quantity <= 0:
    reject "quantity must be positive"    -- state unchanged
-- at the API: 400 { rule: "quantity-positive", message: "quantity must be positive" }

The contracts, side by side with the endpoints

Each operation's contract and the endpoint that keeps it. The check is line by line: every refusal has a status, every promise is in the response, and the ambiguous cases have the same answer in both columns.

Cart operation contracts → API contracts
1Add item given productId, quantity (default 1)
2 promises one entry for productId; quantity increased or set; other entries unchanged; returns the cart
3 refuses unknown product | quantity <= 0 | quantity > max (once that rule exists) — cart unchanged
4 POST /cart/items { productId, quantity } → 200 cart | 400 { rule, message }
5
6Remove item given productId
7 promises no entry for productId afterwards; returns the cart
8 refuses nothingabsent product is a no-op (V1 choice; an API might choose 404)
9 DELETE /cart/items/:productId200 cart
10
11Change quantity given productId, quantity
12 promises entry.quantity == quantity, or the entry removed when quantity == 0; returns the cart
13 refuses quantity < 0 | product not in cartcart unchanged
14 PATCH /cart/items/:productId { quantity } → 200 cart | 400 { rule, message }
15
16View items given
17 promises the entries as they are; empty list, never null
18 GET /cart200 { items, total }

The endpoint column adds transport — verbs, paths, codes — and no behaviour. If a status code appears with no refusal behind it, one layer is lying.

When the contract and the code disagree

Each row is a promise made by one layer and broken by another. The response in every row is to decide the contract at the operation and let the endpoint follow, not the reverse.

Contract drift
TriggerSymptomCauseResponse
Endpoint returns 404 for an absent item; the function returns the cart unchangedA retried DELETE fails on the second attempt; the client shows an error for a cart that is exactly as requested.The no-op choice was made in the function and the framework's default was used at the endpoint.Record the choice in the contract; make the endpoint return success, as the record does — or change both to 404 deliberately.
A new maximum-quantity rule added to addItem onlyThe API returns a 500 with a stack trace for quantity 11.A new refusal with no status code and no rule name behind it.One refusal line, one 400 case with the rule, one test — the contract makes the change three edits.
Checkout reserves stock, then payment failsStock is held for an order that does not exist.A refusal without "state unchanged" — the operation half-succeeded.Make the contract say what is undone on failure, and implement the undo before the endpoint promises anything.
The response body copies the exception messageError text changes when a developer rewords a throw; clients that matched on it break.The contract's refusal had no rule name, so the message became the interface.A rule field in every error body; messages are for humans and may change.

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.

  • For each operation, write "given / promises / refuses" in three lines under the six columns. The promises line must mention every rule the operation preserves — one entry per product, quantity > 0 — because those are what a caller may rely on.
  • For every refusal add "state unchanged". If an operation can half-succeed — check out reserving stock and then failing payment — that is a contract problem to solve now (What Can Go Wrong With a Cart) rather than a surprise later.
  • Choose the no-op-or-error cases explicitly and record why. Idempotent removal is friendlier to retries; an error is more informative. Either is a contract; neither is a default.
  • Write the endpoint contract next to the operation contract and check them line by line. Any status code with no refusal behind it, or any refusal with no status code, is drift.
  • Give the errors a single shape — a rule name and a message — so that every refusal across every operation reads the same to the client (Validation Errors: Feedback, Not Verdicts in the API domain is the long version).

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • Add item, as a contract. Given: a product id and a quantity (default 1). Promises: the cart contains exactly one entry for that product, with quantity increased by the given amount or set to it if new; every other entry unchanged; the updated cart is returned. Refuses: an unknown product ("unknown product"); quantity ≤ 0 ("quantity must be positive"); and — once the rule exists — a quantity above the maximum. On refusal the cart is unchanged.
  • The endpoint that keeps it, from the record: POST /cart/items with body { productId, quantity }, response "the cart, or 400 with the rule that rejected it." The 400 body carries the refusal's rule name; the 200 body is the promise (the cart). Nothing in the endpoint promises anything the function does not.
  • Remove item and the deliberate choice: the operation promises "no entry for this product afterwards" and refuses nothing — an absent product is a no-op, chosen because "removing something absent leaves the cart in the state the caller wanted." DELETE /cart/items/:productId therefore "returns success" for an absent item, and the record notes the alternative: "an API might choose 404." The choice is written in both places, and they agree.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • Every operation has three lines — given, promises, refuses — and the frontend developer's questions are answered by reading them.
  • Every refusal names a rule, ends with "state unchanged", and maps to exactly one status code and body shape.
  • The ambiguous cases (absent item, repeat add, empty clear) have a recorded decision and a reason.
  • Adding a rule — maximum quantity — is one new refusal line, one new 400 case and one new test, and nothing else changes.

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 this operation promise its caller that they may rely on — and does the promise name the rules?
  • ?What does it refuse, by rule, and is the state untouched when it does?
  • ?Which ambiguous cases did I decide by accident, and what should the decision be?
  • ?Does the endpoint promise anything the operation does not?

What can go wrong

How the move itself fails
  • The contracts are written in a formal language nobody reads; three lines of prose per operation are the goal, not a specification suite.
  • Promises are written as restatements of the code ("returns the cart") without the rules ("with one entry per product"), so callers cannot rely on the thing that matters.
  • The endpoint invents promises the operation does not make — "PATCH always succeeds" — because the framework made it easy, and the drift begins at the first call.
  • The refusals are written and the "state unchanged" clause is assumed; the first operation that half-succeeds — checkout — breaks the assumption where it costs most.
What the move costs
  • A contract is a promise, and promises constrain: once "removing an absent item succeeds" is published, changing it is a breaking change for every caller that retries.
  • Writing refusals by rule means the error shape must carry a rule name from the start, which is a small design decision made before the second consumer exists to justify it.
  • Contracts per operation are more text than a method list; on a private helper called from one place, the three lines document a conversation you could have had.
Misreads
  • "The API contract is the contract." The API contract is the operation contract plus transport; if they disagree, the operation is the truth and the endpoint is wrong. The API domain's What an API Contract Actually Is starts where this lesson ends.
  • "Refuses means throws." Refusal is a behaviour — this input is rejected and the state is untouched; how it is signalled (exception, result type, 400) is a representation choice made per layer.
  • "Idempotent removal is obviously right." It is a choice: the record picks it for V1 and names 404 as the alternative an API might choose. A client that needs to know whether the item was there is a reason to pick the other one.

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.

  • GENERALGiven / promises / refuses describes any operation on any concept and maps to any transport — HTTP, a message, a function call across a module boundary; the cart is the small case.
  • STAGE-SPECIFICIn V0–V2 the contract is between your own functions and your own component and can stay in the code and the tests; from V3, when the browser calls the server, it needs to be written where the other side can read it, and the cost of an accidental promise rises with every external caller.
  • ILLUSTRATIVEThe frontend developer's three questions are invented; the contracts, the endpoints and the no-op choice are the shopping-cart record's.

Where the depth lives

This domain asks the question and hands the answer off by name.