Vertical Slices
Not the database in week one, the backend in week two and the frontend in week three — but one product, from page to API to database and back, visible on day one. A slice is one narrow feature through every layer, and it is the unit in which a system actually becomes real.
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.
What is the smallest unit of work that, when finished, means something works — and why is it a path through the layers rather than one layer?
You have the store's decomposition and a plan that says schema first, then endpoints, then pages. It is logical: each layer needs the one below. Three weeks in, the schema is elegant, the endpoints return fixtures, and there is still nothing anyone could click.
Build the foundation first. Get the database right, because everything sits on it; then the API, because the UI needs it; then the UI. It is the order of dependencies and it means nothing is built on sand.
The foundation is designed for needs that have not been discovered, because no feature has run through it. The products table gets columns for variants, images and SEO metadata on speculation, and the first real page needs a column nobody thought of.
- The foundation is designed for needs that have not been discovered, because no feature has run through it. The products table gets columns for variants, images and SEO metadata on speculation, and the first real page needs a column nobody thought of.
- Nothing works until everything does. The first moment the layers meet is the last week, and every mismatch — the API returns cents, the page expects a formatted price; the schema has no cart, the page needs one — is found when there is least time.
- Progress is reported as layers done. "Database: complete" is true and meaningless; the founder asks to see a product and there is nothing to show.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Pick one narrow feature and build it through every layer it needs, and no wider. "Show one product": a page, an endpoint, a query, a row. Finish that path before starting any other, and check it works by looking at the page.
- Then pick the next feature and run it through the same layers, extending each only as far as the new feature requires. The schema grows one column at a time, each one with a page that needs it.
- Let the slices come from the decomposition's leaves (Decomposition by Capability). A capability is already a vertical thing; a slice is a capability, or a piece of one, taken through the stack.
- After each slice, say what it proved and what it did not. "Show one product" proves the layers connect and a product renders; it does not prove that the list scales, that the cart works, or that a missing product is handled. Those are the next slices, not defects.
One product, through every layer
The first slice, as a slice device. Notice how little each layer does, and notice the two required lines at the bottom: what passing proves, and what it does not. The second line is where the next slice comes from.
- PageFetches
/products/{id}for a hard-coded id and renders name and price; nothing else. - APIOne route,
GET /products/{id}, returning name and price or a not-found. - LogicNone yet — the handler calls the query and maps the row; a stub-shaped layer that will grow.
- DatabaseA products table with id, name, price; one row inserted by hand.
- ResponseThe row, as JSON, back through the API to the page, which repaints.
The same three weeks, two ways
The comparison is the reason the module exists: identical effort, one of which produced a store you can click and one of which produced three finished layers and no store.
A complete schema designed from imagination; an endpoint set returning fixtures; pages rendering fixtures. First integration in the last week; every mismatch found then; nothing demonstrable until the end.
A visible product on day one; each layer grown only as far as a feature needed; mismatches found the afternoon they were created; a demonstrable store at every point.
Integration is where the unknown lives — the API returns cents, the page expects a string — and a slice does integration every day in small pieces, while layer-first defers all of it to the moment with the least slack.
The order of slices
Slices are chosen from the decomposition's leaves, in an order that follows dependencies and then risk. The sequence below is one; the alternative names when you would choose another.
- 1Show one product
because The thinnest path through every layer; proves the environment before anything depends on it.
- 2List products; admin creates one
because Extends the same layers one step; the catalog exists and products come from somewhere real.
- 3Add to cart, cart persists
because First slice that writes on behalf of a customer; forces the guest-cart decision early.
- 4Checkout to a pending order with a fake payment
because The core workflow becomes demonstrable end to end while the external edge is still stubbed.
- 5Real payment, then decline and no-answer
because The riskiest slice, built once the path around it is proven (Decomposing Checkout).
How to do it
Most important first.
- Name the feature as something you could see: "a product page shows a real product". If the name is a layer — "the products endpoint" — it is not a slice.
- List the layers the feature needs, from the user inward: page, request, handler, logic, storage, and back. Build exactly those, minimally (The Smallest Executable Thing).
- Stub anything outside the path. The page can hard-code the product id; the handler can skip auth; both are written down as stubs.
- Check by observation, then by test (A Slice Is Testable). The observation is the page; the test is the same path driven by code.
- Write the doesNotProve list before choosing the next slice; the next slice is usually the most important item on it.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The store's first slice: a product row in the database; an endpoint that returns one product by id; a page that fetches and shows its name and price. One column set, one route, one component. It took an afternoon and it can be shown to the founder. It proved: the three layers connect; the environment works; a product renders. It did not prove: the list works, images work, anything about cart or checkout.
- The second slice: "the catalog page lists all products". The endpoint gains a list route; the query gains an ordering; the page gains a list. The schema did not change. The slice proved listing works; it did not prove filtering, paging or what happens with a thousand products.
- The layer-first alternative, for contrast: the same three weeks would have produced a products table with a dozen columns, an endpoint set for products, carts and orders, and pages that render fixtures — and the first real product would still not be visible.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Something is clickable at the end of the first day, and it is backed by a real row.
- Each layer grew only because a feature needed it; you can name the feature for every column and route.
- For every slice there is a written list of what it did not prove, and the next slice was chosen from that list.
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.
- ?What is the narrowest feature I could see working, and which layers does it need?
- ?What did the slice I just finished prove — and what did it leave unproven?
- ?Which stub did I introduce to make this slice thin, and where is that written down?
- ?What is the next slice, and is it the most important thing on the doesNotProve list?
What can go wrong
- Slices too wide: "checkout" as one slice, through every layer, is the whole hard part at once. Slice narrower: create a pending order with a fake payment is a slice; checkout is a set of them.
- Slices that never widen: a store built as thirty independent slices with no shared model, so the product row that the catalog slice created and the one the cart slice created are different tables.
- Treating the first slice's shortcuts as decisions: the hard-coded product id and the skipped auth ship because nobody kept the stub list.
- Each slice touches every layer, so a specialist works outside their specialty, and a shared layer is edited by every slice in turn — more merges, more small migrations.
- The schema grows incrementally, and some incremental growth is a migration that a designed-once schema would have avoided.
- A visible slice invites feedback on things that are stubs — "the price has no currency symbol" — and the feedback has to be triaged against the plan.
- "Never design the schema up front." Design the entities up front — from the requirements (Entities From Requirements) — and build the tables slice by slice. The model is thought about early; the columns arrive with the features that need them.
- "Vertical slices mean no shared code." Slices share the model, the layers and the infrastructure; what they do not share is a schedule. The second slice reuses the first slice's endpoint machinery; it does not wait for an endpoint layer to be finished.
- "A slice is a sprint." A slice is whatever narrow feature runs through the stack; some take an hour. Tying it to a calendar unit makes slices wide to fill the time.
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.
- GENERALOne narrow feature through every layer it needs is the unit of progress for any system with layers — a web app, a CLI with a parser and an evaluator, a pipeline with ingest and transform and output.
- CONTESTEDSome practitioners hold that for a system whose data model is well understood and whose consistency rules are strict — a ledger, a payments core — the schema and its constraints should be designed and built completely first, because incremental columns and migrations are where invariants get lost, and a slice that adds a column without its constraint has created a bug the next slice will meet. Their strongest point: a schema is the one layer whose mistakes are hardest to undo. The reply here is that the entities and invariants are designed first; the *tables* still arrive with features, each carrying its constraint.
- ILLUSTRATIVEThe afternoon, the three weeks and the thousand products are invented to show the shape of the argument; no real schedule is being described.
Where the depth lives
This domain asks the question and hands the answer off by name.