Vertical Slices
One folder per use case, containing everything that use case needs. Change locality is close to maximal, and shared concepts have nowhere obvious to live.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
If grouping by capability is good, is grouping by individual use case better?
A team wants every change to be one folder. They propose a folder per use case — PauseSubscription/, CancelSubscription/, ResumeSubscription/ — each with its own request type, handler, validation, persistence access and tests.
Every use case gets a folder containing everything it needs. Nothing is shared, so nothing can break anything else, and a change is always one folder.
The rules are shared whether or not the code is. Three slices each with their own copy of "a paused subscription cannot be paused again" is three chances to get it wrong and no mechanism to keep them aligned (Duplicate Knowledge).
- The rules are shared whether or not the code is. Three slices each with their own copy of "a paused subscription cannot be paused again" is three chances to get it wrong and no mechanism to keep them aligned (Duplicate Knowledge).
- The first time a rule changes, the change is N folders — which is exactly the amplification the layout was adopted to avoid, arriving on the axis nobody priced (Change Amplification).
- Slices multiply faster than capabilities. A product with forty use cases has forty top-level folders, and the root of the repository stops summarising anything (Over-Decomposition).
- Shared concepts do not disappear when you delete their folder; they reappear as copied code, which is harder to find than a badly-named package (Shotgun Surgery).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- The three subscription use cases share a state machine, and that state machine is the part most likely to be got wrong (State Machines).
- The team is eight people and the product is early, so the set of use cases is still churning.
- Any layout has to keep the domain rules enforceable in one place, because the rules are what the auditors look at (Enforcing Invariants).
- A domain rule holds regardless of which slice is executing. Two slices that both enforce it must agree, and the only reliable way to make them agree is for them to share the enforcement (Where Invariants Live).
- A slice may own its own orchestration; it does not own the meaning of the domain concepts it touches.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- A slice owns the orchestration of one use case: parse the request, call the domain, persist, respond.
- The domain owns the rules, and it is shared by every slice that touches the same concept — this is the part the naive version gets wrong.
- Whoever adds the second slice touching a concept owns noticing that the concept now has two users and needs a home.
- The slice boundary is around a use case and is genuinely good at containing orchestration, which is the part that varies per use case.
- The domain boundary cuts across slices and must survive them, or the invariants have no single enforcement point (Consistency Boundaries).
- The practical shape is slices for orchestration over a shared domain model — which is a capability package with slice-shaped internals, not an alternative to one (Package by Feature).
What is sliced, and what cannot be
The picture below is the whole lesson. Orchestration slices cleanly because each use case really does coordinate a different sequence. Domain rules do not slice, because a rule is a property of the concept rather than of the operation, and the moment two slices hold their own copy they are two rules that happen to agree today.
The dotted region is the part teams delete when they adopt slices enthusiastically, and re-create eighteen months later after a bug that only appeared through one endpoint.
- Each slice owns its request type, its transport validation, its persistence call and its tests — all things that genuinely differ per use case.
- None of them owns "a paused subscription cannot be paused again", because that sentence is about subscriptions and not about an endpoint (Where Invariants Live).
- No slice points at another slice. That edge is the one to forbid with a tool, because it is how the top level acquires a dependency graph (Circular Dependencies).
The rule that lives in three places
The failure is not dramatic. Each slice is short, readable and correct on the day it is written, and the drift happens later, when one of them is updated and the others are not found.
What makes it expensive is that nothing in the code looks wrong afterwards. There is no missing import, no failing test and no reviewer who can see both files at once — which is why this class of bug is normally found by a customer.
// PauseSubscription/handler.ts
if (sub.status === 'paused') throw new Error('already paused')
if (sub.pausesUsedThisYear >= 3) throw new Error('limit reached')
await store.update(sub.id, { status: 'paused', pausedAt: now })
// ResumeSubscription/handler.ts
if (sub.status !== 'paused') throw new Error('not paused')
await store.update(sub.id, { status: 'active', pausedAt: null })
// AdminForcePause/handler.ts <- added later, by someone else
if (sub.status === 'paused') throw new Error('already paused')
await store.update(sub.id, { status: 'paused', pausedAt: now })
// the annual limit is missing. Nothing here looks wrong.// subscriptions/Subscription.ts (shared by every slice)
export function pause(sub: Subscription, now: Instant): Subscription {
if (sub.status === 'paused') throw new AlreadyPaused()
if (sub.pausesUsedThisYear >= 3) throw new PauseLimitReached()
return { ...sub, status: 'paused', pausedAt: now,
pausesUsedThisYear: sub.pausesUsedThisYear + 1 }
}
// PauseSubscription/handler.ts
const updated = pause(sub, clock.now())
await store.put(updated)
// AdminForcePause/handler.ts
const updated = pause(sub, clock.now()) // cannot forget the limit
await store.put(updated)The rule now has one address, so the fourth slice cannot omit it and a change to the limit is one edit rather than a search. The slices keep everything that genuinely differs — their request shapes, their authorisation, their responses — so the locality that made this layout attractive is intact. The cost is a shared module that every slice depends on, which becomes a coordination point and a place where an ill-considered change breaks four use cases at once (Stability and Dependency Direction).
How far to slice
The choice is not slices versus packages; it is what the top level is named after and how much each slice is allowed to own. Four positions are common, and they differ mainly in what happens to shared concepts.
The useful question is not which is purest. It is: when the same rule is needed by a second use case, what does the layout make you do?
When a second use case needs the same rule, where does the layout put it?
when The domain has real rules that must not vary by endpoint.
cost A shared domain module per capability, which becomes a coordination point and needs its own care. This is the default that works (Package by Feature).
when Use cases read and write genuinely different shapes, or reads and writes are separated deliberately.
cost A storage change touches every slice, and two slices writing the same rows can violate an invariant nothing in application code can see (State Ownership).
when Use cases are independent by nature — an integration gateway, a set of unrelated admin operations — or the code is short-lived.
cost Rules drift silently. Only defensible when the real enforcement lives elsewhere, typically in database constraints or one transactional boundary (Enforcing Invariants).
when Use cases within a capability are small and share most of their orchestration.
cost Individual use cases are less independently reviewable and deletable, and a package accumulates endpoints until someone splits it (Module Granularity).
How to build it
Most important first.
- Slice the orchestration, share the domain. That single rule resolves almost every argument about this layout.
- Let a slice own its own request and response types, its own validation of transport-level input, and its own persistence calls — these genuinely differ per use case (Parse, Do Not Validate is the backend name for the input half).
- Keep the domain model and its invariants in one place per capability, imported by whichever slices need it (Where Invariants Live).
- Allow deliberate duplication between slices where the similarity is coincidental — two validations that both check a date range for unrelated reasons are not one thing (DRY: Knowledge, Not Lines).
- Group slices under the capability they belong to, so the root of the repository still names the business rather than listing forty verbs.
- Watch for the second slice that copies domain logic; that copy is the signal that a concept needs extracting, and it is the cheapest moment to do it (The Rule of Three).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Changing one use case costs one folder, and this is genuinely excellent: the change is trivially reviewable, trivially testable, and trivially deletable when the use case is retired.
- Changing a domain rule costs one place if the domain is shared, and N slices if it is not. That single distinction decides whether this layout is cheap or expensive, and it is decided on day one.
- Adding a use case costs one new folder and touches nothing existing, which is the strongest property this layout has and the reason it feels so good early.
- Changing persistence costs every slice, because each one talks to the store directly. That is the deliberate price of removing the repository layer, and it should be paid knowingly (Cost-Aware Interfaces).
- Removing the repository indirection makes each slice simpler to read and makes a storage change touch everything. That is a real trade with a real losing side.
- A root directory listing forty use cases is less informative than one listing six capabilities, so navigability drops as the count grows (Module Granularity).
- Slices encourage duplication, and distinguishing coincidental duplication from duplicated knowledge is a judgement the layout asks you to make constantly (DRY: Knowledge, Not Lines).
What can go wrong
- Domain rules are copied into slices, drift, and produce behaviour that differs depending on which endpoint you used — the worst kind of bug, because it is correct-looking in every individual file.
- A
Shared/folder appears next to the slices and accumulates everything, restoring the original problem (The Common Module). - Slices start calling each other to reuse orchestration, and the top level acquires a dependency graph nobody designed (Dependency Cycles).
- The mitigation fails on its own terms: a team extracts the shared domain, but leaves the persistence writes in each slice, so two slices write inconsistent rows for the same concept and the invariant is violated below the level the domain can see (State Ownership).
- Slices depend on the domain; the domain depends on no slice. That direction is the whole design (Dependency Direction).
- Slices should not depend on each other. A slice calling another slice is orchestration calling orchestration, and it is how a cycle starts (Circular Dependencies).
- Each slice depends directly on persistence, which is a deliberate trade: it removes a repository indirection and it means a storage change touches every slice (When the Repository Is Just Indirection argues the same trade from the backend side).
- "Vertical slices mean no shared code." They mean no shared *orchestration*. Domain rules are shared in every version of this that works, and the ones that do not share them fail in a specific, well-documented way.
- "This is CQRS." Separating read and write models is a different decision about consistency and data shape; you can slice without it and use it without slicing (CQRS in Architecture covers the read/write split itself).
- "Slices remove the need for layers." Each slice has layers inside it — transport, domain call, persistence. What has gone is the top-level grouping by layer, not the layers (Package by Layer).
- "Every change becomes one folder." Every *use-case* change does. Domain changes, cross-cutting technical changes and storage changes all still fan out, and the layout makes the last two worse (Change Amplification).
- duplicate-knowledge
- shotgun-surgery
Testing it, and how it ages
- Test each slice end to end through its entry point; slices are unusually easy to test this way because they have no hidden collaborators (Where a Test Must Be Real).
- Test the shared domain rules once, directly, with no slice involved — that suite is what stops the rules drifting (What a Unit Is).
- Add a test that fails when one slice imports another, since slice-to-slice dependencies are the specific way this layout degrades.
- Early on, slices feel unambiguously right: few use cases, little shared domain, and every change in one folder.
- The turn comes with the third or fourth slice over the same concept, when copied rules start to drift. Teams that extract the domain then do well; teams that add a
Shared/folder instead spend the next two years there. - Very long-lived slice layouts tend to converge on capability packages with slice-shaped internals, which is where the two ideas stop being alternatives (Package by Feature).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThat slicing by use case maximises locality for use-case change and provides no home for concepts shared between use cases is a consequence of the grouping and holds in any language.
- DOMAIN-SPECIFICWhere use cases genuinely share little — an integration gateway, a reporting surface, a set of independent admin operations — pure slices work well and the shared-domain problem barely arises. Where a rich domain model is the product, the same layout copies rules into every slice, and the identical structure that was clean in the first system is the source of the worst class of bug in the second (Domain Modeling).
- LIFETIME-SPECIFICFor code with a short life, or for use cases expected to be deleted rather than maintained — a campaign flow, a migration-period endpoint — a self-contained slice is ideal because deleting it is one folder and nothing else notices. The argument against slices assumes the rules inside them must stay consistent for years.
- CONTESTEDThe strongest case for pure slices, argued by people who have shipped large systems this way: shared domain models are where coupling accumulates, and a shared model that must satisfy every slice becomes a compromise nobody wants — so it is better to let each slice own its own view of the data, keep the rules in the one or two places that actually enforce them at the database or transaction boundary, and accept duplication as the price of independence. That is a serious position and it is close to how successful event-sourced and CQRS systems are built. The disagreement is narrow: it works when the enforcement point is genuinely elsewhere, and it fails when the shared rule is enforced only in application code, because then N copies really do drift.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — a slice is the natural unit to extract when one operation genuinely needs separate scaling or isolation, which is the main reason to draw the boundary before you need it.