To CodeGENERALSTAGE-SPECIFICILLUSTRATIVE

The Framework Comes Last

Only after the core logic works: React holds the cart in state and calls addItem; an Express route calls it and returns JSON; a repository loads the cart, calls it, and saves. The record's note says it plainly — "the framework wraps behaviour that already works, which is why it comes last, and why the same five functions survive every framework change."

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

The cart works in memory with its tests. When does the framework enter, what does it wrap, and what should it never be allowed to contain?

The situation

The five functions pass their tests in a file with no imports. Now the app needs a cart page and the store needs an endpoint, and a familiar pull sets in: surely the real work is the React part, and the functions were a warm-up.

The reflex

Move the logic into the framework where the "real" code lives: a useCart hook with the find and the checks inside it, or an Express handler that parses the body and does the add inline. One place, no indirection.

Why it stalls

The rule "one entry per product" now lives in a hook, and the tests that took two lines each need a rendering harness. The server needs the same rule and gets a second copy in the route handler, and the two copies drift on the first change.

What the reflex produces — and fails to produce
  • The rule "one entry per product" now lives in a hook, and the tests that took two lines each need a rendering harness. The server needs the same rule and gets a second copy in the route handler, and the two copies drift on the first change.
  • The framework's idioms edit the algorithm. The hook spreads a new array on every add because that is how React likes state; the find is replaced with a map; the invalid-zero example is now a toast rather than a rejection, and the "state unchanged" half of the example is no longer tested anywhere.
  • When the framework changes — a migration, a rewrite, a second client — the cart goes with it, because it was never separable. What was five functions and six examples is now a component tree.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Draw the boundary before integrating: the five functions and their tests are the *core*; the framework is a *wrapper* that decides where the cart is held, how operations are triggered and how rejections are shown. Nothing in the wrapper changes items; it calls a function that does (Pure Logic First).
  • Integrate one wrapper at a time, each as a thin layer that holds, calls and displays. React: const [cart, setCart] = useState(createCart()), and on click setCart({ ...addItem(cart, id) }) — the hook holds and triggers; addItem still has the find. Express: parse the body, call addItem, return the cart or a 400 with the rejection's message. A repository: load the cart, call addItem, save — three steps around an unchanged function (Where Should This Code Live?).
  • Keep the tests where they were. The core's tests do not import the framework; the wrapper's tests check the wrapping — that a rejection becomes a 400, that a click calls the function, that the save happens after the call — and never re-test the find. Two test suites, two responsibilities.
  • Read the record's frameworkNote as the check: "nothing above knows about React, Express or a database." If the core file gains an import from a framework, something has moved across the boundary, and the thing to ask is which decision of the framework's just entered the cart. The discovery-stage lesson Framework Independence is about learning without leaning on a framework; this one is about the finished operation staying separable from the one it runs in.

The claim "the real code is the React part"

The reflex, run through the why ladder. It is not silly — the shopper does click a button in React — but it answers a question about where behaviour is *triggered* as if it were a question about where behaviour *lives*.

Why ladder

The cart's logic belongs in the React hook, because that is where the app actually runs it.

  1. Why in the hook? Because the click happens there and the state is held there; the logic is next to what uses it.
  2. Why does proximity matter? Fewer files, no indirection — a reader sees the click and the add together.
  3. Why is that not enough? Because the server needs the same add, and a hook cannot run there; the logic gets a second copy in the route.
  4. Why is a second copy the problem? Two finds, two quantity checks, and the first change to a rule updates one of them. The cart now has two behaviours.
real requirement One place that defines what adding does, callable from a click, a request and a test, with each caller deciding only how to trigger it and show the result.
simpler Five functions in a file with no imports; a hook that holds and calls; a route that parses, calls and maps; a repository that loads, calls and saves.

the claim was right when The app is one component, has no server, and will be thrown away — a prototype to see whether the cart page is wanted at all. Then the hook is the app, and a separate core is a file nobody needs yet.

The same five functions, wrapped three ways

The record's versions, read as wrappers arriving around an unchanged core. Each level adds a wrapper with its own concern, and the because says what forced it; the core's five functions and six examples are the same at every rung.

Wrappers around the core
  1. V0 — the core
    createCart, addItem, removeItem, changeQuantity, total in one file; six examples as tests; no imports.The behaviour has to be right before anything wraps it — the record's own reason for V0.
  2. React holds it
    A hook with the cart in state; each button calls a core function and replaces the state with the result; names and prices looked up for rendering.A shopper needs a page; the page needs somewhere to hold the cart and a way to trigger operations — and nothing else.
  3. Express wraps it
    GET /cart, POST /cart/items, PATCH and DELETE per product; each route parses, calls the core, maps rejections to 400 and success to JSON.The cart must be trusted for checkout and reachable from more than one device, so the authoritative copy moves behind an API.
  4. A repository saves it
    load(owner) → Cart, call the core, save(cart) to cart and cart_item rows with UNIQUE (cart_id, product_id).The cart must survive restarts; the constraint is the find's rule getting a second home where a second writer can reach the state.
  5. The rules repeat at the boundary
    The route re-checks the catalog and the quantity before calling the core, "because it cannot trust the browser".A wrapper that faces an untrusted caller repeats the core's checks; it does not replace them, and the core's tests do not change.

One feature through all the wrappers

"Add to cart" as a vertical slice, with the core in the middle and a wrapper on each side. The slice proves the wrappers are thin; it does not prove they are correct, which is what their own tests are for.

Vertical slice
Add to cart, from the click to the row
  1. ReactClick → setCart({ ...addItem(cart, productId) }), or shows the rejection message; state holds a copy, the server's cart is the truth.
  2. ExpressPOST /cart/items → parse { productId, quantity }, repeat the catalog and quantity checks, call the core, return the cart or 400 with the message.
  3. CoreaddItem: catalog check, quantity check, find, branch, increment or append, return — unchanged since V0.
  4. Repositoryload the owner's cart before the call, save it after; a UNIQUE violation on the insert becomes a retry as an increment.
proves
The same addItem serves the page, the endpoint and the row; every wrapper is hold-or-load, call, show-or-save, and the find lives in exactly one place.
does not prove
That the optimistic click rolls back correctly on a 400, that the route's repeated checks agree with the core's, or that the repository's retry-as-increment is right when two tabs race — each wrapper has its own contract and its own tests, and this slice ran none of them.

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.

  • Write the core in a file with no framework imports, and treat any such import appearing later as a review finding (Language-Neutral Pseudocode).
  • Each wrapper does three things — hold or load the state, call the operation, show or save the result — and if it does a fourth, name the rule it just took over.
  • Translate rejections at the boundary: the operation throws a message; the route maps it to a status; the hook maps it to an error state. The mapping is the wrapper's only logic (From Cart.addItem() to POST /cart/items).
  • When the wrapper needs something the core lacks — a cart id for the route, an owner for the repository — add it to the concept's state with a reason, not to the wrapper's closure (Who Owns It, and How Long Does It Exist?).
  • Re-run the core's tests after every integration step; if one fails, the wrapper edited the algorithm.

Worked on a concrete problem

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

  • React, from the record's frontend notes: "the UI holds a copy of the cart in component state and renders items with names and prices looked up from the catalog"; every button calls the operation and replaces the local copy with the result. The hook is a dozen lines: state, a handler per operation, a name lookup for rendering. The find is not in it and never was (The UI Holds a Copy).
  • Express, from the record's API: POST /cart/items with { productId, quantity } returns "the cart, or 400 with the rule that rejected it"; DELETE /cart/items/:productId returns success even for an absent item, "matching the domain's no-op choice". The route is parse, call, map — and the domain's choice about absent removal was made in removeItem, not in the handler.
  • A repository, for V3: load(ownerId) returns a Cart, addItem(cart, …) is the unchanged function, save(cart) writes rows with the UNIQUE (cart_id, product_id) constraint that repeats the find's rule at the database. Three steps around the core; the constraint is a second home for a rule the core still enforces (The Cart Disappears).

How you know it worked

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

  • The core file has no framework import and its tests run in milliseconds with no harness.
  • Each wrapper is a few lines that hold, call and show or save; you can name its one piece of logic (the rejection mapping).
  • Swapping a wrapper — a second client, a different server framework — touches the wrapper and nothing in the core, and the core's tests do not notice.

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
  • ?Where is the boundary between the cart and the thing that runs it — and could I draw it as an import list?
  • ?For each wrapper: what does it hold or load, what does it call, and how does it show or save the result?
  • ?Which rules does the wrapper repeat because it cannot trust its caller, and which does it own?
  • ?If this framework were replaced tomorrow, which files would change?

What can go wrong

How the move itself fails
  • The boundary is drawn and then the wrapper is given "just one" rule — a quantity cap in the route "because it is closer to the request" — and the cart now has a rule it does not know about.
  • The core is kept pure at the cost of the wrapper being unable to do its job: the route needs a cart id and the core has none, so the route invents a lookup table instead of the concept gaining an owner.
  • The separation is treated as a law for a project that is one component and will stay one; a fifty-line demo with the find inside the hook is not wrong, it is a demo.
What the move costs
  • Wrapping an existing core means more files and one more layer to read on every change, which on a one-screen app is indirection with no payoff yet.
  • Framework idioms sometimes exist for reasons the core ignores — immutable state for React's change detection — and the wrapper has to bridge them, here with a spread on every call.
  • A core with no framework knowledge cannot use the framework's conveniences — an ORM's unique constraint, a form library's validation — without those being written twice, once in each vocabulary.
Misreads
  • "Framework last means learn the framework last." The discovery lesson makes the learning point; this one is about order of *integration* for an operation you already understand. Learning React early is fine; letting React hold the find is the mistake.
  • "Don't couple to the framework" means write an abstraction layer over it. The wrapper *is* the coupling, kept small; a second layer that hides the framework from the wrapper is the over-application, and it is usually where the slogan goes wrong in practice.
  • "Business logic in the service layer" means the core should be a class with a framework-shaped interface. The core here is five functions and two types; the layer is a name for where the wrapper calls them, not a shape the core must adopt.

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.

  • GENERALA framework wrapping an understood core — hold, call, show or save — is the same shape for a cart in React and Express, a rate limiter in a middleware, or a job queue behind a worker runtime; the boundary is drawn the same way.
  • STAGE-SPECIFICA demo that is one component may keep the logic in the hook and lose nothing; the boundary starts paying at the first second client, the first server-side copy or the first migration, which for a store arrives with V3 and for a throwaway never does.
  • ILLUSTRATIVEThe dozen-line hook, the route shapes and the V3 repository are the concept record's invented progression; the frameworks are real, the store is not.

Where the depth lives

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