الدرس 1 من 2
Cutting the Other Way
A layer groups code that does the same kind of work. A slice groups code that changes for the same reason. Most change requests follow the second grouping.
Vertical slice architecture, described by Jimmy Bogard, starts from an observation about change: almost every request a team receives is a feature — "let learners retake a failed quiz", "add a discount code" — and almost every layered codebase makes that feature touch four directories. The proposal is to cut the other way, so that one feature is one place.
Layers cut horizontally, slices cut vertically
A feature change follows the vertical grain. A technology change follows the horizontal one.
A grid showing four layers across three features. In the layered arrangement, a feature change cuts across all four layers in four directories. In the sliced arrangement, each feature is a column containing its own handling, rules and data access, and a change stays inside one column.
Layered: change cuts across
controllers/
all features
services/
all features
repositories/
all features
Sliced: change stays inside
retake-quiz/
handler, rules, queries
enrol-learner/
handler, rules, queries
issue-refund/
handler, rules, queries
// features/list-published-courses/handler.ts — thin, and correctly so.
export async function listPublishedCourses(input: ListInput) {
const query = listSchema.parse(input);
return db.course.findMany({
where: { status: 'PUBLISHED', trackSlug: query.trackSlug },
select: { slug: true, title: true, estimatedMinutes: true },
orderBy: { position: 'asc' },
});
}
// features/grade-attempt/handler.ts — deep, because the rules are real.
export async function gradeAttempt(input: GradeInput) {
const command = gradeSchema.parse(input);
const principal = await authorize('attempt.submit');
return withTransaction(async (tx) => {
const revision = await quizRevisions(tx).published(command.quizId);
const attempt = Attempt.from(revision, command.answers, principal.id);
// A domain object with real invariants: it can refuse.
const graded = attempt.grade(revision.passingScore);
await attempts(tx).save(graded);
await progress(tx).record(graded.outcome());
return graded.summary();
});
}Layers and slices, judged on the same criteria
Layered
Horizontal grouping. Every feature has the same shape, and each layer can be replaced as a unit.
- Predictable placement, which helps new joiners and large teams
- Technology replacement within one layer is contained
- Cross-cutting rules can be applied uniformly at a layer
- Every feature change touches several directories
- Simple features pay the same ceremony as complex ones
- Shared service classes accumulate unrelated responsibilities
Choose when: Feature complexity is uniform, technology change is a real driver, or the team benefits more from predictability than from locality.
Vertical slices
Grouping by feature. Each slice contains everything it needs and may be as thin or deep as its problem.
- A feature change is one directory, usually one or two files
- Complexity is paid where it exists rather than everywhere
- Slices can be deleted cleanly when a feature is removed
- Similar code appears in several slices
- Consistency must come from review, templates or lint rules
- Cross-cutting concerns need a deliberate home outside the slices
Choose when: Changes arrive as features, complexity varies widely between them, and the team is comfortable deciding per slice rather than following one shape.
In practice
Slicing a system that was drowning in its service layer
A logistics dashboard had 60 endpoints, one `DashboardService` of 3,100 lines, and a change-failure rate around 20%. Most defects were in features nobody had intentionally touched.
Constraints
- Five engineers, one deployable, no scale pressure
- Feature requests arrive weekly and are mostly independent
- A rewrite was rejected by leadership
- The service class is called from 60 handlers
Decision
Freeze the service class. Every new feature becomes a slice with its own handler, validation and queries. When an existing feature is modified, extract it into a slice first.
Why
The defects came from shared private helpers inside the service class: a change for one feature altered behaviour for another with no visible connection. Slices break that transmission path because each feature owns its code path outright. Freezing rather than rewriting meant the migration was paid for by work that was happening anyway.
What it cost
Eighteen months later, roughly the same validation logic appeared in eleven slices, and two of them had drifted apart in a way that produced inconsistent error messages. The team accepted that and added a shared validation vocabulary — small, dependency-free helpers with no business rules in them — rather than reintroducing a service layer. Change-failure rate fell to about 6%. The duplication was the price and they judged it the cheaper of the two costs.
Seeing the same query written in three slices
As a developer
Extracts it into a shared repository so the query lives in one place, since three copies is exactly the duplication that principles warn about.
As an architect
Applies the DRY test from Chapter 2 before extracting: must all three change together, always, for the same reason? Often the answer is no — the three slices select different columns for different screens and will diverge. Extracting produces a shared query with three optional parameters within a year, which is the wrong abstraction and reintroduces the transmission path slicing removed. Where the answer is genuinely yes, extraction is right; the discipline is asking rather than reacting.
In a vertically sliced codebase, where do authorisation, transactions and audit logging live?RevealHide
Not in the slices, and this is the question that decides whether slicing succeeds. Cross-cutting concerns still need a single definition and an application point every path crosses — exactly as the separation-of-concerns module argued. In a sliced codebase the usual arrangement is a thin pipeline that every slice handler runs inside: it resolves the principal, opens a transaction, and emits an audit entry, while the slice supplies only what is specific to it, such as which permission it requires. What you must not do is let each slice implement its own authorisation, because you will then have sixty implementations and three of them will be wrong. Slicing distributes the *feature* logic, not the platform rules.
Key takeaways
- A layer groups code by kind; a slice groups code by the reason it changes.
- Slices may differ in depth, so complexity is paid where it exists rather than uniformly.
- Slicing removes the shared-helper transmission path that causes unrelated features to break each other.
- Duplication across slices is expected; apply the must-change-together test before unifying.
- Cross-cutting concerns live in a pipeline every slice runs inside, never in the slices themselves.