Case: Build an Online Store
One sentence — "build an online store" — taken from actors and actions through data, state, interfaces, a walking skeleton, the cart decision, checkout in pseudocode, payment research, failure cases, and scale only on evidence. The whole loop, on the running example, with nothing added before it was needed.
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.
You have "build an online store" and nothing else. How do you grow it from a sentence into a system, and how do you know at each step that the next piece was justified rather than merely available?
The founder wants a store. You have the domain's moves but you have only ever used them one at a time, on someone else's example. Now it is one problem, from the first minute, and every move has to hand something to the next one — you do not want to end the week with a good unknowns board and no store, or a store and no idea why it is shaped the way it is.
Sketch the final architecture. You know what stores look like — a frontend, an API, a database, a cache for the catalog, a queue for orders, a search service, a payment integration — so drawing all of it feels like understanding the problem. The diagram is complete on the first day and it looks like a plan.
The diagram has seven boxes and no workflows. It cannot say what happens when the customer clicks Buy, because "checkout" is an arrow, not a sequence, and arrows do not have failure cases.
- The diagram has seven boxes and no workflows. It cannot say what happens when the customer clicks Buy, because "checkout" is an arrow, not a sequence, and arrows do not have failure cases.
- The cache, the queue and the search service are solutions to problems the store has not shown it has. Each will need building, operating and debugging, and none of them is on the path to the first order.
- Payment — the one part nobody on the team has done — is a box labelled "Stripe", which is a vendor name where a question should be. The riskiest unknown has been hidden by being drawn.
- When the first requirement arrives that the diagram did not anticipate — guest checkout, say — there is nowhere to put it, because the diagram was built from a generic store rather than from this one.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Grow the store in the order the loop dictates, and let each stage produce exactly the artefact the next stage needs: actors and actions produce the core workflow; the workflow produces the data; the data and the workflow produce the first state machine; the workflow produces the first interface; the first interface produces the walking skeleton; the skeleton produces the first real decision (the cart); the cart produces checkout; checkout produces the payment unknown; payment produces the failure cases; and only real traffic produces the scale question.
- At every stage ask two questions before adding anything: which requirement asks for this, and what is the simplest thing that meets it? A component with no requirement behind it goes on a list labelled "pending a reason" (Add Complexity Only When Required). A component with a requirement behind it still has to beat the simpler alternative.
- Keep the unknowns visible and sharpen them as they arrive. The first hour produces "payments" and "inventory"; by the checkout stage those must be questions with experiments, and by the payment stage the experiments must have been run (Unknown, Question, Experiment).
- Treat scale as a stage with its own evidence requirement. The question "what happens with more traffic?" is legitimate at every stage; the answer "so add a cache" is legitimate only after a measurement has named the bottleneck (Scale Thought Experiments, Architecture From Requirements).
From the sentence to a skeleton
The first four stages produce lists, not code, and they produce them fast. The decomposition below is the store after the actions are known: each capability has a testable leaf, and the walking skeleton is the thinnest path through the first one. Nothing in the tree is a layer, and nothing in it is a technology.
The skeleton is deliberately trivial — one product, one endpoint, one page. Its job is to prove the layers connect and to force the first decision, which is the cart. Everything the reflex drew — cache, queue, search service — is absent because no leaf needs it yet.
- ├Catalog— the first customer action and the walking skeleton's path
- └List productstestable GET /products returns the rows in the database and a page renders them; an empty catalog renders an empty list, not an error.
- └View one producttestable A real id shows name, price and stock; a missing id gives a clear not-found.
- ├Cart— the bridge from browsing to buying, and the first real decision
- └Add, change quantity, removetestable Add twice, remove once: one line with the right quantity, and the total matches current prices.
- └Cart identity without an accounttestable A second tab and a refresh show the same cart; a cart abandoned overnight is still there within the window and gone after it.
- ├Checkout— the workflow that makes it a store
- └Validate and create ordertestable A cart with an out-of-stock line is rejected with the line named; a valid cart creates exactly one Order(PENDING) with prices snapshotted.
- └Take paymenttestable A test-mode success moves the order to PAID via the provider's confirmation; a test-mode failure leaves it PENDING and the customer is told.
- ├Admin— someone has to create the products
- └Create and edit a producttestable A product created in the admin appears in the catalog with the same price and stock; a stock change is visible on the product page.
Warehouse, support and seller are absent by decision, and the reasons are written next to them. The tree grows when a requirement arrives, not when a component is available.
The order the store was actually built in
The sequence below is the one the case followed, with the reason each step came where it did. The payment spike sits in the middle rather than at the end because it was the only unknown with the power to change the design — a provider that insists on a hosted checkout page would have changed the cart and the order flow.
The alternative is real and the device says so: a team that has integrated this provider before has no risk there and would push payment to the end, spending the middle on the cart persistence question instead.
- 1Actors, actions, core workflow, data, first state machine, first interface
because Every later stage consumes these lists; together they take an afternoon and they are the store's definition.
- 2Walking skeleton: one product → GET /products → one page
because Proves the layers connect, and forces the cart decision as the next visible gap (The Walking Skeleton).
- 3Cart, with the persistence and identity decision written down
because Checkout cannot exist without a cart, and the guest requirement decides the cart's shape.
- 4Checkout without payment: validate, snapshot prices, create Order(PENDING)
because "An order exists" is reachable and testable before the riskiest integration (Decomposing Checkout).
- 5Payment research spike, then integration, then the webhook receiver
because The spike answers "who says it is paid?" before the code depends on the answer; the receiver is idempotent from its first version because the spike showed repeats (What If Payment Fails?).
- 6Failure injection: timeout, duplicate click, sold out, database down, webhook repeat
because Each failure is injected against a working path, so the response can be observed rather than imagined (Failure Injection).
- 7Production concerns: logging, deployment, backups, the obvious security
because Real customers arrive next; an MVP simplifies scope, it does not skip the things that cannot be ignored (What Cannot Be Simplified).
- 8Measure, then grow the architecture from the readings
because The first bottleneck names itself under real traffic; a cache, a replica or a queue is added when a reading asks for it and not before (Growing From the MVP).
Scale, on evidence
The reflex's diagram had a cache, a queue and a search service on the first day. The case added an index and a small listing cache after a measurement, and nothing else. The ladder below is the argument that would have justified the queue, run honestly: it ends in a real requirement and a simpler thing that meets it — and in the case where a queue was right.
The habit this stage installs is not "never add components". It is: more traffic → find the bottleneck → optimise what the evidence names. A component added without a reading is a guess with an operational bill attached (The Why Ladder).
“We need a message queue between checkout and order processing.”
- ↓Why a queue? So checkout does not wait for order processing.
- ↓Why does checkout wait? After payment we send the confirmation email and notify the warehouse in the request.
- ↓Why is that slow enough to matter? The email provider sometimes takes a while; the customer sees a spinner after paying.
- ↓What did the measurement say? The checkout request spends most of its time in the email call; the database and payment steps are fast.
the claim was right when Several consumers need every order event — warehouse, analytics, fraud, email — with independent retry and replay, and the jobs table is measured as the new bottleneck or the polling is visibly hurting the database. Then a real queue is the simpler thing (Message Queues).
How to do it
Most important first.
- Actors and core actions first, in plain verbs: customer browses, views, adds to cart, checks out, sees the order; admin creates products and changes stock. Warehouse, support and seller are written down as later actors so their absence is a decision (Requirement Discovery).
- Name the data from the actions — User, Product, Cart, CartItem, Order, OrderItem, Payment — and mark which must persist and which are snapshots. OrderItem carries the price at the time of the order; that is a snapshot, not a reference (Snapshots vs References).
- Write the first state machine for Order before writing any checkout code: PENDING → PAID → FULFILLED. Add CANCELLED and REFUNDED only when a requirement names them (The Order Lifecycle, Built).
- Write the first interface as a sentence — Browser → GET /products → Backend → Database — and build the walking skeleton for exactly that path (The Walking Skeleton).
- Make the cart decision explicitly: client-side or server-side, anonymous or per-user, persistent or not. It is the first decision the skeleton forces, and the answer depends on who the buyer is, not on what is easiest.
- Write checkout in pseudocode with payment as a single line that says "take payment (provider TBD)". Then sharpen the payment unknown, run its experiment, and only then replace the line (Pseudocode Before Code).
- Inject failures against the working checkout, one at a time: payment fails, the browser closes, the webhook repeats, the database is down (Failure Injection). Then ship. Then measure, and grow the architecture from what the measurements say (Growing From the MVP).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Stage one, the sentence taken apart. Actors: customer, admin. Later: warehouse, support, seller — each written down with the reason it can wait (one warehouse in V1; support works from the admin screen; no third-party sellers until there is a first-party store). Core workflow: browse → view → add to cart → checkout → order created → confirmation. Data: User, Product, Cart, CartItem, Order, OrderItem, Payment. First state machine: Order is PENDING, then PAID, then FULFILLED. First interface: Browser → GET /products → Backend → Database. None of this took a technology, and all of it will be used.
- Stage two, the skeleton and the cart. The walking skeleton is one product row, one endpoint, one page — and it forces the cart question, because the next customer action is "add to cart". The decision: the buyer is a guest, so the cart cannot hang off a user id; it is keyed by a session token, stored server-side so that a refresh and a second tab agree, and persisted for a fixed window so an abandoned cart is recoverable. Client-side only was rejected because two tabs would disagree; per-user was rejected because the buyer has no account. The decision is written down with its reasons so it can be revisited when accounts arrive (The Decision Journal).
- Stage three, checkout and payment. Checkout in pseudocode: load cart; for each item, check stock and current price; compute total; create Order(PENDING) with OrderItems snapshotting price; take payment (provider TBD); on success mark PAID and clear cart; on failure leave PENDING and tell the customer. The one line "take payment" hid three questions, sharpened in the payment research: who says payment succeeded — my code after the API call returns, or the provider's later confirmation? What if the browser closes between the charge and my confirmation page? What if the confirmation webhook arrives twice? The experiment — a test-mode charge and a webhook receiver with no store around them — answered all three: the webhook is authoritative, the browser is irrelevant to whether the order is paid, and the receiver must be idempotent on the provider's event id (Which Dependency Must Answer Before the User Can Be Told Anything?, Duplicate Requests).
- Stage four, failure and then scale. Failures, injected against the working store: provider times out → order stays PENDING, customer told to wait, a reconciliation job asks the provider later; duplicate click on Pay → an idempotency key on the checkout request returns the same order; last unit bought twice → a conditional decrement in the database rejects the loser (Invariants in an Online Store). Then the store ships with one application and one database. When traffic grows, the first measurement shows the product listing query dominating database CPU; the response is an index and a short-lived in-process cache of the listing, because that is what the evidence named. A queue was not added, because checkout latency was fine; a separate search service was not added, because "filter by name" was a single indexed query. Each was put on the ledger with the reading that would justify it (The Complexity Ledger).
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every component in the store can be traced to a requirement and to the simpler thing it beat, in one sentence each; anything that cannot is on the pending list, not in the code.
- The Order state machine, the checkout pseudocode and the payment experiment agree with each other about who says an order is paid.
- The cart decision is written down with the alternatives it rejected and the condition that would reopen it.
- The first scaling change was chosen from a measurement, and you can name the reading that justified 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.
- ?Which artefact does this stage have to hand to the next one, and do I have it yet?
- ?For each component I am about to add: which requirement asks for it, and what simpler thing did it beat?
- ?Who is authoritative for the state I am about to change, and what happens if the message that changes it arrives twice or never?
- ?What measurement would justify the next piece of architecture, and am I recording it?
What can go wrong
- The stages become phases. A week of actor analysis, a week of data modelling, a week of state machines — and no skeleton until the fourth week. Each stage should take as long as it needs to hand the next one its artefact, and for a store that is hours, not weeks.
- The payment unknown is deferred because checkout "works" with the TBD line. The line is a placeholder for the riskiest integration in the system; the experiment has to run before the code around it hardens.
- The cart decision is made by the framework — whatever its session helper does — and never written down. Later, when accounts arrive, nobody can say why the cart is shaped as it is or what would break if it changed.
- "Scale later" becomes "scale never": the store grows, the listing query gets slow, and nobody measures because measuring was not a habit. Scale-later only works if measure-first is real (Measure Before You Optimize).
- Growing the store stage by stage produces a system that looks under-built to anyone comparing it with the seven-box diagram, right up until the diagram's components would have needed debugging.
- A store shipped with one application and one database will need to change when traffic arrives, and the change will happen under load. The bet is that the change is cheaper than building for a load that may never come — and it is a bet.
- Writing every decision down with its reasons is slower than deciding. On a solo project the notebook is often skipped, and the cost only arrives when someone else — including future you — has to change the cart.
- "So the store ends up as one server and one database, and that is the lesson." The lesson is that each piece is justified by a requirement or a reading. A store with real traffic will end up with a cache and probably a queue; it will have them because the readings asked, and the difference shows when they break.
- "Payment is the hard part, so do it first." Sometimes — the alternative order below says when. But payment research can run alongside the skeleton, and a store that has reached "an order exists" is a better place to integrate payment than an empty repository.
- "The cart decision is minor." It decides whether the store can serve a guest, whether two tabs agree, and what survives a restart. Several later requirements — accounts, saved carts, abandoned-cart email — hang off it.
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 stage sequence — actors, workflow, data, state, interface, skeleton, first decision, core workflow, riskiest unknown, failures, evidence-driven scale — applies to any product-shaped system; the store is the instance, and the marketplace capstone runs the same sequence with a third actor.
- STAGE-SPECIFICOn a greenfield store every stage is built; on an existing store the actors, data and state machine are read from the system and the move becomes finding where the new requirement — say, refunds — attaches to the existing Order lifecycle.
- CONTESTEDSome experienced teams argue that for a store the shape is so well known that the sequence is theatre: build the standard four-box architecture with a hosted checkout and a search service from the outset, because every store eventually needs them and the cost of adding them later under load is higher than the cost of running them idle. That view is strongest for a team that has built several stores and whose expected traffic is known from the previous ones.
- ILLUSTRATIVEThe founder, the guest buyer, the listing query that dominated database CPU and the sequence of events are invented to show the shape of the argument; the numbers, where any appear, are not measurements.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — The capstone at /thinking/capstone runs these ten stages with feedback only; the marketplace at the end adds the seller actor and asks the same questions again.
- — The manifesto's "What Are You Delegating?" cards at /manifesto/delegating are worth reading before the payment spike: the SDK handles the request, and the "who says it is paid?" question stays yours.