EngineerGENERALCONTESTEDILLUSTRATIVE

V0 to V6, With a Reason for Each

Pure logic, then in memory, then the browser, then the server, then logged-in sync, then inventory validation, then high scale. Every stage exists because a requirement arrived, and every stage breaks an assumption the previous one made. A stage without a requirement is complexity without a reason.

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 could be built as V6 from the start. Why build V0 through V5 first — and how do you know when a stage is due, rather than merely possible?

The situation

You know what a production cart looks like: a service, a fast store, carts keyed by user, expiry of abandoned ones, inventory checks. You have the working array cart and it feels embarrassingly small. You start designing the production one, and two weeks later there is an architecture diagram, three services and no cart a shopper can use.

The reflex

Build the final version first. If the cart will end up server-side, keyed by user, backed by a fast store with expiry, then building the in-memory one is wasted work — start where you will finish, and skip the throwaway stages.

Why it stalls

The final version is designed against requirements that do not exist yet, so its shape is guessed. The fast store was added for a load nobody measured; the expiry was added for an abandonment policy nobody wrote; and when login turns out to merge carts differently than assumed, the keyed-by-user design is wrong in a way the simpler one would not have been.

What the reflex produces — and fails to produce
  • The final version is designed against requirements that do not exist yet, so its shape is guessed. The fast store was added for a load nobody measured; the expiry was added for an abandonment policy nobody wrote; and when login turns out to merge carts differently than assumed, the keyed-by-user design is wrong in a way the simpler one would not have been.
  • Nothing is shippable until everything is. Each stage of the progression is a working cart; the final-first design has no working cart until the last service is wired, and the first shopper feedback — that the reload empties the cart — arrives months late.
  • The reasons are lost. In the progression, every component has a "because" that names the requirement that brought it. In the final-first design the fast store, the expiry and the service boundary are all there from the beginning, and nobody can say which one could be removed — the complexity ledger has no entries (The Complexity Ledger).
  • The assumptions that each stage was supposed to break are never named, so they are never tested. "One shopper", "one device", "one writer", "the cart is the authority on stock" — each is silently made and silently kept until an incident breaks it.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Build the stages as answers to requirements, and let the requirement arrive before the stage. V0 exists because the behaviour must be right; V2 exists because a reload emptied the cart; V3 because checkout must trust it; V5 because selling what is not there is worse than an empty cart. The concept record gives every version an "adds", a "because" and a "breaks" — read the "because" column as the requirement that opens the stage.
  • Name the assumption each stage breaks, in advance. V1 breaks "one cart"; V3 breaks "the browser is the only writer"; V4 breaks "one owner for the cart's whole life"; V5 breaks "the cart is the only authority on what can be added". The list of assumptions is the list of things that will change, which is what When Assumptions Change wants written down before they do.
  • Keep the stages additive. Each one wraps the previous — persistence wraps the pure functions, the API wraps the persistence, the merge is an operation over two carts, the stock check is a rule asked of another service. If a stage rewrites the one below, either the lower stage was wrong or the new stage is not a stage.
  • Treat V6 as a measurement, not a milestone. The record says it arrives "only when the readings say the cart table is the bottleneck" and that for most stores a relational row per item "always is" cheap enough. The falsifiable form of "don't over-engineer" is: do not add a stage whose requirement has not arrived and cannot be measured — and the flip is that a stage whose requirement is certain and expensive to retrofit may be built early.

Seven stages, seven requirements, seven broken assumptions

The ladder is the concept record's versions, read as a sequence of requirements. Each "because" is the requirement that opened the stage; the assumption it breaks is stated in the what. The last level is the one most stores never reach, and it says so.

The cart, V0 to V6
  1. V0 — cart functions
    The five functions over a plain data structure, in memory, with the rules enforced. Assumes one shopper and one process.The behaviour has to be right before anything wraps it.
  2. V1 — cart object in memory
    A cart with an owner, several carts at once, and a catalog to look prices up from. Breaks: there is one cart.Two shoppers is the first requirement that makes "which cart?" a question.
  3. V2 — saved in the browser
    Serialise to localStorage on every change; load on start. Breaks: the cart only exists while the page does; introduces stale products from storage.A reload emptying the cart is the first thing a real shopper notices.
  4. V3 — stored server-side
    The API, a cart table, cart_item rows with a unique (cart_id, product_id) constraint. Breaks: the browser is the only writer — two tabs now race.The cart must be trusted for checkout and seen from more than one device.
  5. V4 — logged-in sync
    An anonymous cart merged into the user's cart at login, with a merge rule for overlapping products. Breaks: a cart has exactly one owner for its whole life.Shoppers start anonymous and log in at checkout.
  6. V5 — inventory validation
    The stock rule, asked of the inventory service at add and again at checkout. Breaks: the cart is the only authority on what can be added — inventory is.Selling what is not there is worse than an empty cart.
  7. V6 — high-scale cart
    Carts keyed by owner in a fast store, expiry of abandoned carts, and a measured reason for each. Breaks: a relational row per item is cheap enough — which for most stores it always is.Only when the readings say the cart table is the bottleneck.

The stage that wanted to be built first

V6 is the stage the reflex reaches for, and the why ladder is how it is examined. The claim is not refused — the ladder ends with the case where it was right. It is asked what requirement it answers, until either a requirement appears or a simpler stage satisfies the real one.

"The cart needs a fast store"

We need to put the cart in an in-memory key-value store with expiry from the beginning.

  1. Why a fast store? Because the cart is read on every page and a database round trip per read will be slow.
  2. Why will it be slow? Because a real store has many shoppers and the cart table will be under load.
  3. How many, and what load? Unknown — nothing has been measured; the store has not launched.
  4. What is the cart read actually doing? One indexed lookup by cart id returning a handful of rows, on every page that shows the badge.
  5. Why is that too slow? It probably is not; the badge count could also come from the response of the last mutation, which the UI already holds.
real requirement The cart must load fast enough not to be noticed on every page — a requirement the indexed row lookup meets until a reading says otherwise.
simpler V3 as written: a table with an index on cart id, the UI's cached copy for the badge, and a reading of cart-read latency taken after launch.

the claim was right when The reading exists and says the cart table is the bottleneck under real load — the record's own condition for V6 — or the business needs abandoned-cart expiry and the store's database makes that awkward. Then the fast store has a because, and the ledger has an entry.

One order, and the one you might prefer

The stages are also an implementation order, and like every order it is one of several. The steps below are the record's sequence with the reason each precedes the next; the alternative is the collapse that a certain destination permits — and the condition for choosing it.

Building the cart by stage
  1. 1
    V0: the five functions and the tests from the examples

    because Every later stage is verified against these; nothing above them is trusted until they pass.

  2. 2
    V1: owner and several carts

    because The first thing a server does is serve two shoppers; the owner field the concept marked "depends" now has its requirement.

  3. 3
    V2: browser storage

    because The reload complaint arrives before login does; it is cheap, it wraps V0, and it surfaces the stale-product failure early.

  4. 4
    V3: server, API, table, constraint

    because Checkout must trust the cart; the race arrives here and so does the constraint.

  5. 5
    V4: merge at login

    because It needs V3's server cart and login, and its merge rule is the first new operation since V0.

  6. 6
    V5: stock rule via inventory

    because It needs an inventory service to ask, and it re-checks at checkout — which needs V3.

  7. 7
    V6: only on a reading

    because Its requirement is a measurement that cannot be taken before V3 has run under load.

a different valid order When login and server-side checkout are certain and imminent, collapse V2 into V3: build V0, V1 and the server cart directly, and skip browser storage. Choose this when the anonymous-shopper reload requirement is genuinely absent (an internal ordering tool behind a login) — not because the browser stage feels beneath the project.

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 stage table for your concept before building V0: version, what it adds, why, what assumption it breaks. Leave "why" blank where no requirement exists; blanks are stages you are not building yet.
  • Build V0 as pure functions with tests from the examples. Everything above it wraps it; nothing above it changes it (Pure Logic First).
  • Before each stage, confirm the requirement is real: a shopper complained, checkout needs it, the business asked, or a reading showed it. "It is how real stores do it" is not one of these.
  • After each stage, write the assumption it broke and check the code for places that still make it — the browser copy that still thinks it is the truth after V3.
  • For V6-shaped stages, name the reading that would justify them and how to take it, and take it before building (Measure Before You Optimize in Performance).

Worked on a concrete problem

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

  • V0 → V1. Requirement: a second shopper in the same process — the first thing a server does. V1 adds an owner and several carts, and breaks "there is one cart". The five functions are unchanged; a map from owner to cart wraps them.
  • V1 → V2 → V3. Requirement for V2: a reload emptied the cart, the first thing a real shopper notices. Adds serialise-and-load; breaks "the cart only exists while the page does"; introduces stale products from storage. Requirement for V3: checkout must trust the cart and a second device must see it. Adds the API, the table and the unique constraint; breaks "the browser is the only writer" — two tabs now race, and What Can Go Wrong With a Cart is due.
  • V3 → V4 → V5. Requirement for V4: shoppers start anonymous and log in at checkout. Adds the merge with a rule for overlapping products; breaks "one owner for the cart's whole life". Requirement for V5: a sold-out item reached an order. Adds the stock rule asked of inventory at add and again at checkout; breaks "the cart is the only authority on what can be added".
  • V5 → V6, examined. The requirement would be a reading: the cart table is the bottleneck. No reading exists; the store's carts are small and read a few times per visit. V6 is not built, and the decision is recorded with the reading that would reopen it — which is what makes "we might need a fast store for the cart" a plan instead of a worry.

How you know it worked

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

  • Every stage in your table has a requirement in the "because" column that someone actually stated or measured.
  • Every stage names the assumption it breaks, and the code was checked for places still making it.
  • V0 is unchanged from the first commit; every stage above it wraps it.
  • At least one stage is deliberately not built, with the reading that would justify it written beside it.

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
  • ?Which requirement opens the next stage of this concept, and has it actually arrived?
  • ?Which assumption does the next stage break, and where in the current code is that assumption still made?
  • ?Does the next stage wrap the current one, or rewrite it — and if it rewrites, which of the two is wrong?
  • ?What reading would justify the stage I am tempted to build, and can I take it first?

What can go wrong

How the move itself fails
  • The progression becomes a ritual. V2 is built for a cart that will be server-side next week because "you have to do the stages"; the stages are answers to requirements, and a requirement that is certain and imminent can open its stage early.
  • The stages are built but not wrapped. V3 rewrites addItem around SQL, V0's tests stop running, and the "progression" is five carts with five sets of bugs.
  • The "breaks" column is filled in and never acted on. The assumption "one writer" is written beside V3, and the browser code that writes to storage first is still there a year later.
  • V6 is refused on principle. The store genuinely has the reading, the cart table genuinely is the bottleneck, and the fast store is rejected because "premature optimisation" — the slogan applied after the measurement it was supposed to wait for.
What the move costs
  • Building by stages means some stages are replaced — the browser-storage cart is superseded by the server one — and that work is real, even when the wrapper is thin.
  • Waiting for a requirement means occasionally being caught by one that arrives faster than the stage can be built; the stage table is the warning, not a guarantee.
  • Naming and checking assumptions after each stage is bookkeeping that final-first designs do not pay, because they made every assumption at once and wrote none of them down.
Misreads
  • "Progressive means slow." The progression ships a working cart at every stage; the final-first design ships one at the end. Over the same weeks, the progression has had more carts in front of more shoppers.
  • "V6 is the goal." V6 is what the record says a store builds only when the readings demand it, and most never do; a cart that stays at V5 forever is a cart that was engineered correctly.
  • "Skipping V0 saves time." V0 is where the rules and the tests from the examples live; every later stage is verified against them. Skipping it means each stage tests the cart's behaviour again, from scratch, with infrastructure in the way.

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.

  • GENERALStages opened by requirements, each naming the assumption it breaks, is the shape for growing any concept — a job queue from an array to a durable store, search from a filter to an index.
  • CONTESTEDSome engineers hold that for a concept whose destination is certain — a cart on a store that will have login and checkout — the intermediate stages are waste, and the strongest form of that view is that a thin wrapper rewritten three times costs more than a schema written once by someone who has built carts before. The reply is that certainty about the destination is usually certainty about its shape, not its requirements, and the merge rule, the stale-product rule and the stock race are all discovered by stages, not by experience; both sides agree the stage table should be written even if some stages are collapsed.
  • ILLUSTRATIVEThe two-week diagram, the sold-out item and the absent bottleneck reading are invented to show the shape of the progression; no real store's timeline is described.

Where the depth lives

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