What It Is
Vertical slice architecture is a way of organizing software around features instead of technical layers. Rather than splitting an application into broad folders like controllers, services, and repositories, you keep everything needed for one feature together in the same place.
This approach is designed to make change safer and faster when features evolve independently. In a layered architecture, shared classes often connect unrelated features. That can make simple updates riskier, because changing shared code for one feature may unintentionally affect another.
Why You Should Use It
Vertical slices keep each feature self-contained. That makes the codebase easier to understand, because the structure reflects what the system actually does for users. Instead of piecing together one feature from several technical folders, you can often understand it by opening a single feature area.
Pros
- Changes stay smaller and more localized because features do not rely heavily on shared classes
- Each feature can evolve with less risk of breaking unrelated behavior
- It is easier to understand one capability without tracing code across multiple technical layers
- New team members can often understand the system faster by reading feature names and boundaries
- It fits well when requirements are still changing and the product is still being shaped
Cons
- You may duplicate code across slices, which is often intentional but still requires discipline
- Shared infrastructure such as authentication, logging, and other cross-cutting concerns still needs coordination
- If feature boundaries are not managed carefully, the structure can become inconsistent over time
When to Choose It
Choose this approach when requirements are still emerging, features change often, and the team needs to move quickly. It is especially useful when you are still learning the domain and want an architecture that supports fast iteration.
Examples
A common example is a checkout flow in an e-commerce application. Instead of splitting the work across several technical layers, the team keeps the UI, validation, business rules, and persistence for checkout together.
Another example is a password reset feature. The request form, email workflow, token validation, and password update logic can all live in one cohesive slice.
A good example of intentional duplication is user-related validation or lookup logic. In a layered architecture, teams often create one shared UserService or UserRepository for registration, login, profile updates, and password reset. In vertical slices, each of those features may keep its own request model, validation rules, and database query because the business rules are different and they change at different times.
That can look repetitive at first, but it helps prevent unrelated features from becoming tightly coupled through a shared abstraction. Registration should not become harder to change just because profile editing happens to use the same user table.
In both cases, the goal is the same: treat each feature as a complete unit that can be understood, changed, and shipped with minimal impact on the rest of the system.
Code and File Structure Examples
One simple way to picture vertical slices is to group files by feature instead of by technical role.
src/
features/
checkout/
CheckoutPage.tsx
checkoutService.ts
checkoutValidator.ts
checkoutRepository.ts
passwordReset/
PasswordResetPage.tsx
passwordResetService.ts
passwordResetValidator.ts
passwordResetRepository.tsHere is a small example of a feature-oriented service:
// passwordResetService.ts
export async function resetPassword(email: string) {
const user = await findUserByEmail(email);
if (!user) return { success: false, message: 'User not found' };
const token = createResetToken(user);
await sendResetEmail(user.email, token);
return { success: true, message: 'Reset email sent' };
}Intentional duplication often shows up when two features both work with users but solve different problems. Instead of forcing both features through one shared service, each slice keeps its own small query and validation logic.
src/
features/
registerUser/
RegisterUserPage.tsx
registerUserHandler.ts
registerUserValidator.ts
registerUserRepository.ts
updateProfile/
UpdateProfilePage.tsx
updateProfileHandler.ts
updateProfileValidator.ts
updateProfileRepository.tsBoth slices may query the same users table, but they do it independently because the business rules are different.
// registerUserRepository.ts
export async function findUserByEmail(email: string) {
return db.user.findUnique({ where: { email } });
}
export async function createUser(input: RegisterUserInput) {
return db.user.create({ data: input });
}
// updateProfileRepository.ts
export async function findUserProfileById(userId: string) {
return db.user.findUnique({ where: { id: userId } });
}
export async function updateUserProfile(userId: string, input: UpdateProfileInput) {
return db.user.update({ where: { id: userId }, data: input });
}In another architecture, these functions might be merged into one shared UserRepository. In a vertical slice approach, keeping them separate is often the point. Registration and profile updates evolve independently, so their code stays close to the feature that owns it.
Conclusion
Vertical slice architecture is a strong choice when you want software to stay practical, feature-oriented, and easy to change. It works especially well for fast-moving teams, as long as the team also maintains clear conventions and a shared understanding of the architecture.