The Horizontal Layers Trap
Building layer by layer feels safe because each layer is a real thing that gets finished. The trap is that a finished layer proves nothing about the product, and every question about the product is deferred to the last week, when they all arrive together.
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.
Why does building one layer at a time feel like progress, and what specifically does it hide until it is too late?
Your team split the store into three tracks — data, API, UI — and each has been shipping. Standups are cheerful. Then someone tries to wire the cart page to the cart endpoint and discovers the endpoint takes a session id, the page has a user id, and the table has neither.
Finish the layers, then integrate. The mismatch is a known cost; the layers are mostly done; an integration phase will sort out the seams. Stopping now to re-plan feels like throwing away weeks of good work.
The "integration phase" is where every deferred product decision is made, at once, under a deadline, by whoever is wiring things that day. The cart's identity model, whether prices are captured, whether checkout is one call or three — all decided in a week, none of them written down.
- The "integration phase" is where every deferred product decision is made, at once, under a deadline, by whoever is wiring things that day. The cart's identity model, whether prices are captured, whether checkout is one call or three — all decided in a week, none of them written down.
- Each layer was built against a guess about the other two. The guesses were reasonable and different. Reconciling them means rework in all three, and the rework is blamed on the people doing the wiring.
- Nothing was demonstrable until the end, so the founder's first sight of the store is also the first time anyone learns that the catalog needs categories.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Name what layer-first defers. Every product question — what does a customer see, what is stored, how do the layers agree on an identity, a price, an error — is answered only when a feature runs through all the layers. Layer-first postpones every one of those to integration; slice-first answers one or two per slice, early.
- Recognise the trap by its symptoms: standups that report layers done; a schema with columns nobody has a page for; endpoints returning fixtures; an "integration" milestone near the end. Any one of these means product questions are queuing.
- Escape it with a slice through the existing layers, however partial they are: pick the thinnest feature — show one product — and force it through data, API and UI as they stand. The mismatches it finds are the queued questions, now answerable one at a time (The Walking Skeleton).
- Keep the layers; change the schedule. The layers were never the problem. Building them in isolation, against imagined needs, was.
What the trap looks like, and what it hides
The symptoms are all things that look healthy. The table names each, says what it is hiding, and gives the slice that would surface it.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Standup: "schema is done" | Tables with columns no page reads | Columns designed from imagined features | Slice the first page through it; keep the columns it reads, question the rest |
| Endpoints return fixtures | API "complete", nothing real behind it | API built against an imagined page and an imagined schema | Wire one endpoint to a real row and a real page; the fixture shape versus the row shape is the first finding |
| UI keeps state locally "for now" | Pages work in isolation; nothing persists | No endpoint to call, so the page invented its own model | Slice add-to-cart through the real endpoint; the identity seam appears immediately |
| An "integration" milestone near the end | Every cross-layer question queued for one week | The plan never had a first end-to-end moment | Move the first end-to-end request to this week, on the thinnest feature |
The slice that springs the trap
The escape slice is deliberately built through the layers as they stand, not after they are fixed. Its job is to break at the seams, and the required last line says what it leaves untouched.
- PageCalls the real add-to-cart endpoint instead of local storage; renders whatever the cart endpoint returns.
- APIAccepts product id and quantity; must decide what identifies the cart — this is where it broke.
- LogicFinds or creates the cart for that identity; upserts the line.
- DatabaseThe carts table as it stands, with whichever identity column the decision picks.
- ResponseThe cart lines, so the page can render them and a refresh re-fetches the same.
The questions that were queued
Everything the integration phase would have decided in a week, as an unknowns board — each already sharpened into the slice that answers it. This is the conversion of a horizontal plan into a slice plan.
- ✓The layers exist and mostly work in isolation.
- ✓No feature has crossed all three yet.
? The cart identity thing.
becomes What identifies a cart — a session, a user, a cookie — and what happens to a guest cart on login?
experiment The add-to-cart slice above, then a second slice that logs in with a guest cart present.
? Prices.
becomes Does an order carry the price at add-to-cart or at checkout, and which layer captures it?
experiment A checkout slice with a price changed in admin between add and checkout; look at what the order row holds.
? Errors.
becomes What does the page show when the API returns a not-found, a validation failure or a timeout — and does the API return them in one shape?
experiment One slice that requests a missing product and one that adds quantity zero; compare what the page renders (Loading, Error, Empty — The States You Did Not Render in Frontend).
Every item on this board was going to be decided in the integration week, silently, by whoever was wiring. Now each has a slice and a place to write the answer.
How to do it
Most important first.
- Ask, for the current plan: when is the first moment a real request goes from a page to a row and back? If the answer is a date, not "already", the trap is set.
- List the product questions that only integration can answer — identity across layers, price capture, error shape, what a page shows when a row is missing. Each is an unknown; make it a question with an experiment, and the experiment is a slice (Unknown, Question, Experiment).
- Convert layer tickets to slice tickets without discarding work: the data track's next ticket becomes "the columns the product page needs", the UI track's becomes "the product page against the real endpoint".
- Demand a demonstrable thing every few days. If nothing can be shown, the plan has gone horizontal again.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The cart mismatch, traced: the data track chose a
session_idon carts because guests should shop; the API track choseuser_idbecause auth was in scope; the UI track kept the cart in local storage because it was easiest before endpoints existed. Three reasonable guesses about one question — "who owns a cart?" — that a single add-to-cart slice would have forced on day three. - The escape: one slice, "add a product to the cart and see it after a refresh", through the layers as they are. It broke immediately at the identity seam, which is the point; the team answered "guest carts keyed by a cookie, merged into the account on login", wrote it down, and fixed all three layers for that one feature. The next slice found the next seam.
- What was recovered: most of the schema, most of the endpoints, most of the pages. What was lost: the columns and routes that no slice ever needed, which were never real progress.
How you know it worked
What now exists that did not before, and what question you can now ask.
- The first end-to-end request happened in the first days, not the last.
- The product questions that used to be discovered at integration are now discovered one per slice and recorded.
- The layer tracks still exist, but every ticket names the feature it serves.
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.
- ?When does the first real request travel from a page to a row and back — and if that is a date, why?
- ?Which product questions is this plan deferring to integration, and which slice would answer each one now?
- ?Can I convert the next layer ticket into a slice ticket without discarding what is built?
What can go wrong
- Overcorrecting into slices so thin they never share a model, and the layers fragment into per-feature schemas and per-feature endpoints.
- Declaring the trap escaped because a slice exists, while the tracks continue horizontally beside it and the slice is a demo nobody extends.
- Blaming the layer split itself where the real issue was team structure; if the tracks are three teams, the fix is slice tickets across teams, not one team (Decomposition by Layer).
- Slicing through partial layers means working in three codebases at once, which is slower per feature than working in one layer you know.
- Some horizontal work is genuinely shared — auth middleware, a migration tool — and treating it as a slice distorts it; the trap is layer-first for features, not layer-shaped infrastructure.
- "Layers are the trap." The trap is building layers in isolation before any feature crosses them. Layers as structure are fine and necessary (Transport, Application, Domain, Infrastructure in Backend).
- "Integration phases are always wrong." An integration phase after slices — hardening, load, security — is normal. An integration phase that is the *first* time the layers meet is the trap.
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.
- GENERALDeferring every cross-layer question to integration hurts any layered system; the more layers, and the more teams per layer, the more it hurts.
- TEAM-SPECIFICA solo builder rarely falls in — they naturally move between layers — while a team with one person per layer falls in by default, because each person's most productive day is spent inside their own layer. The fix scales with team size: slice tickets across people, and a shared demo cadence.
- ILLUSTRATIVEThe three tracks, the cart identity mismatch and the day-three slice are invented to show the trap's shape; a real team's seams would be different but no fewer.
Where the depth lives
This domain asks the question and hands the answer off by name.