Vertical Slice Architecture: Organize Code Around Features

Many codebases start with a familiar structure: controllers/, services/, repositories/, models/. That looks neat at first, but as the application grows, one feature gets scattered across five or six folders. Understanding "how checkout works" or "how order cancellation works" means jumping horizontally through the whole codebase.
Vertical Slice Architecture solves that by organizing code around features and use cases, not technical layers. Instead of placing all controllers together and all repositories together, each feature owns its request handler, validation, business logic, and data access in one slice.
This usually leads to a codebase that is easier to navigate, easier to change, and friendlier to teams shipping product features continuously.
Series: Software Architecture Patterns Roadmap
Related: Layered (N-Tier) Architecture, Hexagonal Architecture, Clean Architecture
What You'll Learn
✅ What vertical slice architecture is and what problem it solves
✅ Why feature-based organization often scales better than horizontal layers
✅ A practical folder structure for real projects
✅ How a request flows through a single slice
✅ When vertical slices fit well and when they do not
✅ Common mistakes: fake slices, shared-kernel sprawl, and over-abstraction
✅ How vertical slice architecture relates to Clean and Hexagonal architecture
What Vertical Slice Architecture Is
Vertical slice architecture organizes a system around complete user-facing behaviors.
A slice is usually something like:
CreateOrderCancelSubscriptionPublishPostResetPassword
Each slice contains everything needed for that use case:
- request/command model,
- validation,
- handler or application logic,
- data access for that use case,
- response mapping,
- and sometimes tests.
The key idea is simple:
Code that changes together should live together.
That sounds obvious, but many layered codebases violate it constantly.
The Problem With Horizontal Layers
In a traditional layered structure, one feature is spread across horizontal folders:
src/
├── controllers/
│ └── order-controller.ts
├── services/
│ └── order-service.ts
├── repositories/
│ └── order-repository.ts
├── validators/
│ └── order-validator.ts
└── dtos/
└── order-dto.tsThis looks organized, but it optimizes for technical role grouping, not for understanding a feature end to end.
Now imagine you need to change the "cancel order" behavior:
- find the controller,
- find the service method,
- find validation,
- find repository logic,
- find DTO mapping,
- and hope the business rule is not duplicated elsewhere.
That cost grows with every new feature.
Vertical slices try to reduce that cognitive tax.
The Core Idea
Instead of organizing code like this:
You organize it like this:
Each slice is a mini-module for one use case.
A typical slice might look like this:
src/features/orders/create-order/
├── create-order.route.ts
├── create-order.request.ts
├── create-order.validator.ts
├── create-order.handler.ts
├── create-order.repository.ts
└── create-order.response.tsThis does not mean every slice must duplicate everything. It means each slice should own the code that is specific to its behavior.
A Practical Example
Let us imagine a blog platform with a "Publish Post" use case.
src/
├── features/
│ └── posts/
│ ├── publish-post/
│ │ ├── publish-post.route.ts
│ │ ├── publish-post.request.ts
│ │ ├── publish-post.handler.ts
│ │ └── publish-post.repository.ts
│ └── get-post-details/
│ ├── get-post-details.route.ts
│ ├── get-post-details.query.ts
│ ├── get-post-details.handler.ts
│ └── get-post-details.repository.ts
└── shared/
├── db/
├── auth/
└── kernel/Notice what is missing:
- no giant
services/folder, - no giant
controllers/folder, - no giant
repositories/folder.
You can open one feature directory and understand one behavior without mentally stitching together the entire application.
How a Request Flows
In vertical slice architecture, a request usually flows through one feature module with minimal detours:
The handler is often the center of the slice. It coordinates the use case. It may call domain objects or shared infrastructure, but the feature's behavior stays localized.
Here is a practical TypeScript example:
// src/features/posts/publish-post/publish-post.handler.ts
type PublishPostCommand = {
authorId: string;
title: string;
content: string;
};
type PostRecord = {
id: string;
author_id: string;
title: string;
content: string;
status: "draft" | "published";
published_at: Date | null;
};
export class PublishPostHandler {
constructor(private readonly repository: PublishPostRepository) {}
async execute(command: PublishPostCommand): Promise<PostRecord> {
if (command.title.trim().length < 5) {
throw new Error("Title must be at least 5 characters");
}
const draft = await this.repository.findDraftByAuthorAndTitle(
command.authorId,
command.title
);
if (draft) {
throw new Error("A draft with this title already exists");
}
return this.repository.insertPublishedPost({
authorId: command.authorId,
title: command.title.trim(),
content: command.content.trim(),
publishedAt: new Date(),
});
}
}And the repository used only by that slice:
// src/features/posts/publish-post/publish-post.repository.ts
export class PublishPostRepository {
constructor(private readonly db: DbClient) {}
findDraftByAuthorAndTitle(authorId: string, title: string) {
return this.db.post.findFirst({
where: { authorId, title, status: "draft" },
});
}
insertPublishedPost(input: {
authorId: string;
title: string;
content: string;
publishedAt: Date;
}) {
return this.db.post.create({
data: {
authorId: input.authorId,
title: input.title,
content: input.content,
status: "published",
publishedAt: input.publishedAt,
},
});
}
}This repository is not trying to become a giant reusable abstraction for the entire application. It serves one use case well.
Why Teams Like Vertical Slices
1. Better Feature Ownership
Each feature has a natural home. When a teammate asks, "Where does post publishing live?" the answer is one directory, not five.
2. Easier Change Isolation
If a requirement changes, you often edit one slice instead of touching shared horizontal layers across the whole app.
3. Lower Cognitive Load
Developers can understand a use case without loading unrelated parts of the system into working memory.
4. Better Fit for Product Work
Most teams ship features, not layers. Organizing by feature often matches how work is planned, reviewed, and tested.
5. More Honest Reuse
Layered systems often create "shared" services too early. Vertical slices push you to keep code local until reuse is truly proven.
Important Clarification: It Is Not Anti-Layer
Vertical slice architecture does not mean architecture disappears.
Inside a slice, you may still have:
- a route/controller entry point,
- application logic,
- domain rules,
- and data access.
The difference is that these concerns are grouped within a feature, not split into massive application-wide folders.
This is why vertical slice architecture often combines well with:
- Clean Architecture
- Hexagonal Architecture
- CQRS-style handlers
- DDD aggregates and value objects
You can absolutely have a vertical slice whose handler talks to ports, whose domain model is clean, and whose infrastructure is hidden behind adapters.
Vertical slices answer "how should we organize features?"
Clean/Hexagonal answer "how should dependencies flow?"
These are complementary questions.
When Vertical Slice Architecture Works Well
Vertical slice architecture is especially effective when:
- the product has many distinct use cases,
- the team ships changes feature by feature,
- the codebase is suffering from large shared service classes,
- onboarding is slow because logic is scattered,
- and teams want stronger feature ownership.
It is a strong fit for:
- business applications,
- internal tools,
- SaaS products,
- CRUD-heavy systems with meaningful workflows,
- and APIs where use cases matter more than generic entity reuse.
When It May Not Be the Best First Choice
Vertical slices are not automatically the best answer for every project.
Be careful if:
- the system is still tiny,
- the team is very inexperienced and needs simpler conventions first,
- the domain is mostly a thin data wrapper with little behavior,
- or you are forcing slices while the real complexity is in shared infrastructure.
For a very small application, a simple layered structure might be enough. Architecture should solve real pain, not create ceremony.
Common Mistakes
1. Fake Slices
Some teams create feature folders, but still route all real logic through giant shared services/ and repositories/ directories.
That is not vertical slicing. That is layered architecture wearing a feature-folder costume.
2. Shared Folder Explosion
When every third file gets moved into shared/, the slices become empty shells. Shared code should be earned through repeated use, not created preemptively.
3. Generic Repository Obsession
Trying to force one generic repository for every slice often removes useful business meaning. Sometimes a use-case-specific query is exactly the right design.
4. Copy-Paste Without Boundaries
Keeping code local is good. Blind duplication is not. If three slices share the same policy or domain concept, extract that concept intentionally.
5. Confusing "Feature" With "Database Table"
A slice should represent a behavior or use case, not just a table. orders/ by itself is often too broad. create-order/, cancel-order/, and ship-order/ are usually clearer.
Vertical Slice vs Layered vs Clean vs Hexagonal
| Architecture | Main organizing idea | Best question it answers |
|---|---|---|
| Layered | Horizontal technical layers | How do I separate broad responsibilities? |
| Vertical Slice | Feature/use-case modules | How do I keep each behavior easy to find and change? |
| Clean | Dependency rule, inner vs outer policies | How do I protect business logic from frameworks? |
| Hexagonal | Ports and adapters at the boundary | How do I isolate the core from external systems? |
The practical takeaway:
- use Layered when the system is small or the team wants simple conventions,
- use Vertical Slice when feature sprawl and cognitive load become painful,
- use Clean/Hexagonal when dependency boundaries and long-term core protection matter deeply,
- and remember that Vertical Slice can coexist with Clean or Hexagonal.
A Pragmatic Adoption Path
You do not need a big-bang rewrite.
One practical approach is:
- Leave old features where they are.
- Build new features as slices.
- Extract only truly shared code.
- Gradually move the most painful old workflows into slices.
This keeps momentum high and avoids architecture refactors becoming their own project.
Final Thoughts
Vertical slice architecture is popular because it aligns code structure with the way teams actually work: by delivering features and changing use cases.
It will not magically fix bad domain modeling or unclear boundaries. But it does solve a very real problem: in many layered codebases, the code for one feature is scattered everywhere.
If your team keeps asking "where does this behavior live?" or every change requires touching controllers, services, repositories, and mappers in different corners of the app, vertical slices are worth serious consideration.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.