Vertical Slice Architecture

Most .NET developers learn Clean Architecture or N-tier architecture early, and it becomes the default way to structure a solution. You get a Domain project, an Application project, an Infrastructure project, and a Presentation or API project. It works well on paper, but anyone who has added a single field to a feature and then had to touch five different folders across four projects knows the friction this creates. Vertical Slice Architecture takes a different starting point: organize the code around features, not technical layers.

This article walks through why layered architectures create this friction, what Vertical Slice Architecture actually looks like in a .NET solution, and how the REPR pattern (Request-Endpoint-Response) gives you a concrete way to structure each slice. I have used this approach on a few production APIs, so I have also added some practical notes on where it helps and where it does not.

The problem with layered architectures

A layered architecture splits the system into technical layers, typically Domain, Application, Infrastructure, and Presentation, with each layer usually mapped to its own project. Clean Architecture is the most common implementation of this idea in the .NET world, and it comes with a strict rule about dependency direction: the Domain layer has no dependencies, the Application layer can reference the Domain, and Infrastructure and Presentation can reference both Application and Domain.

Clean Architecture maps entities, use cases, API endpoints, and external services to their respective layers
Clean Architecture maps entities, use cases, API endpoints, and external services to their respective layers

This structure genuinely helps with separating concerns, and it gives you a system that is easier to reason about at the boundary level. The trade-off is coupling inside each layer. A single feature, say adding a new field to a workout entity, usually means changes to an entity class, a validator, a MediatR handler, a repository, a DTO, and a controller action, each living in a different project. The cohesion between the pieces of one feature is low, while the coupling inside each layer keeps growing as the codebase grows.

None of this makes layered architecture wrong. It is a reasonable default for small to medium APIs where the team is comfortable navigating between projects. The problem shows up mostly on larger codebases, where every new feature means touching the same five folders in the same order, and onboarding a new developer means explaining the same five-hop mental map before they can ship anything.

What Vertical Slice Architecture actually changes

Vertical Slice Architecture flips the grouping. Instead of a Domain folder, an Application folder, and an Infrastructure folder, you get a Features folder, and inside it, one folder per use case. Everything a single feature needs, its request, its handler, its validation, and its response shape, lives together in that one folder. Jimmy Bogard, known for MediatR and AutoMapper, is usually credited with popularizing this approach, and the underlying principle is straightforward: minimize coupling between slices, and maximize coupling inside a slice.

Each feature slice cuts across Presentation, Application, Domain, and Infrastructure concerns instead of being split by them
Each feature slice cuts across Presentation, Application, Domain, and Infrastructure concerns instead of being split by them

When all the files for a use case sit in one folder, cohesion for that use case goes up sharply. You do not have to jump between four projects to understand what a single API endpoint does. In practice, this matters most during code review and debugging, where finding every file relevant to a bug becomes a single folder lookup instead of a solution-wide search.

Implementing vertical slices in an API

If you are building a typical REST API, the requests already split naturally into commands (POST, PUT, DELETE) and queries (GET). Structuring each slice around a single command or query gives you the benefits of the CQRS pattern without needing to adopt full CQRS with separate read and write databases. Each slice is free to pick its own implementation strategy, which is one of the underrated benefits of this approach.

For example, one slice that reads a single activity by id can use Dapper with a raw SQL query for speed, while a slice that creates a new activity can use EF Core with a rich domain model for validation and business rules. A slice that deletes a batch of records could use EF Core’s ExecuteDeleteAsync directly, skipping change tracking altogether. None of these choices affect the other slices, because there is no shared repository abstraction forcing every use case through the same code path.

Four activity API slices, each using a different data access approach: EF Core, Dapper, a rich domain model, and ExecuteDeleteAsync
Four activity API slices, each using a different data access approach: EF Core, Dapper, a rich domain model, and ExecuteDeleteAsync

This flexibility is also where the approach needs discipline. New features only add code, since you are not modifying a shared repository or a shared service that every other feature depends on, which reduces the risk of side effects when you ship. But because a lot of business logic ends up sitting inside a single handler, it is easy for a handler to keep growing until it becomes a small god class. Watch for this early: when a handler starts doing five unrelated things, push the logic that belongs to the domain into the domain model instead of leaving it in the handler.

Structuring slices with the REPR pattern

Layered architectures like Clean Architecture organize the solution around technical concerns, so you end up with a folder structure grouped by layer. Vertical Slice Architecture organizes the same code around features or use cases instead, but that raises an obvious question: how do you structure the code inside each slice consistently?

The REPR pattern, short for Request-Endpoint-Response, gives a consistent answer. It states that every API endpoint should have three components: a Request that captures the input, an Endpoint that handles the HTTP concerns and orchestration, and a Response that shapes the output. This maps directly onto the vertical slice idea, and you can implement it with MediatR, or with dedicated minimal API libraries built specifically around this pattern.

RunTracker.API
|__ Database
|__ Entities
    |__ Activity.cs
    |__ Workout.cs
    |__ ...
|__ Features
    |__ Activities
        |__ GetActivity
            |__ ActivityResponse.cs
            |__ GetActivityEndpoint.cs
            |__ GetActivityQuery.cs
            |__ GetActivityQueryHandler.cs
        |__ CreateActivity
            |__ CreateActivity.cs
                |__ CreateActivity.Command.cs
                |__ CreateActivity.Endpoint.cs
                |__ CreateActivity.Handler.cs
                |__ CreateActivity.Validator.cs
    |__ Workouts
    |__ ...
|__ Middleware
|__ appsettings.json
|__ appsettings.Development.json
|__ Program.cs

The Features folder is the important part here. Each subfolder under it, GetActivity or CreateActivity for instance, is a complete vertical slice with its own request, handler, endpoint, and validator. Notice that CreateActivity uses a slightly different naming style, grouping related files by a shared file name prefix (CreateActivity.Command.cs, CreateActivity.Endpoint.cs) rather than separate descriptive names. Both styles work; pick one convention for the team and stay consistent, since mixing naming styles across slices makes the codebase harder to navigate than either style alone.

If you want a library that enforces this structure for you rather than relying on folder discipline, FastEndpoints and ApiEndpoints are two options built around the REPR pattern for .NET. FastEndpoints in particular has gained traction because it also gives you built-in validation and reduces some of the MediatR boilerplate, though it does mean learning another library’s conventions on top of ASP.NET Core’s own.

When this approach makes sense, and when it does not

Vertical Slice Architecture is not a universal replacement for layered architecture, and treating it as one is a common mistake. It earns its keep on APIs with many independent, loosely related use cases, where forcing every feature through the same repository and service abstractions creates more overhead than value. Reporting APIs, admin backends, and CRUD-heavy services with dozens of endpoints are good candidates.

It is a weaker fit when your domain has heavy cross-cutting business rules that many use cases share, since you can end up duplicating that logic across slices, or reaching for a shared domain layer anyway, which quietly turns your vertical slices back into a layered architecture with extra folders. If your team already has strong discipline around a rich domain model and shared invariants, a hybrid works better in practice: keep a shared Domain project for entities and business rules, but organize the Application and Presentation concerns into feature folders instead of technical ones.

Testing strategy also needs a small rethink. Instead of unit-testing a service layer and mocking a repository, you test each slice’s handler with its actual dependencies where practical, and lean on integration tests hitting the endpoint directly for the slices where the value is in verifying request-to-response behavior rather than isolated logic. This tends to produce fewer, more meaningful tests per feature compared to the layered approach, where you often end up testing the same logic at both the service and controller level.

Adopting it without a full rewrite

You do not need to migrate an entire existing codebase to try this. The core idea, grouping files by feature instead of by technical concern, applies even if you keep a shared Domain project for your aggregates and entities. Start by moving one bounded area of the application, ideally a set of related endpoints that do not share much logic with the rest of the system, into a Features folder and see how it feels for your team before going further.

For new projects, it is easier to start with vertical slices from day one, since retrofitting this structure onto an established layered codebase means untangling shared services that multiple layers currently depend on. Either way, the goal is not to eliminate layers entirely but to stop organizing your primary folder structure around them.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading