Vertical Slice Architecture in .NET — From N‑Tier Layers to Feature Slices
==========================================================================
Most .NET developers grow up on classic N‑Tier or Clean / Hexagonal architectures:
- Presentation layer
- Application / Services layer
- Domain
- Data access
Each layer is neatly stacked… but a single feature like "Get weather forecasts" ends up scattered across controllers, services, repositories, DTO folders, validators, and view‑models.
Vertical Slice Architecture (VSA) turns that on its head.
Instead of organizing code by technical layer, you organize it by feature. Each feature owns everything it needs — from the HTTP endpoint down to the data access — in one cohesive folder or slice.
In this post we’ll explore how to apply Vertical Slice Architecture in .NET using a minimal Web API, and we’ll refactor the classic WeatherForecast example into feature slices:
GetForecastsSearchForecasts
Treat this as both a conceptual guide and a practical starter template you can drop into your next .NET Web API.
Table of Contents
- What Is Vertical Slice Architecture?
- From N‑Tier Layers to Slices (Mental Model)
- Why Vertical Slice? Benefits & Trade‑offs
- Starting Point: The Default .NET Web API
- Step 1 — Create a Features Folder
- Step 2 — Implement the GetForecasts Slice
- Step 3 — Implement the SearchForecasts Slice
- Wiring Slices into
Program.cs - Pros, Cons, and When to Use VSA
- Next Steps: MediatR, CQRS, and Beyond
1. What Is Vertical Slice Architecture?
Jimmy Bogard (creator of MediatR) describes Vertical Slice Architecture like this:
“In this style, my architecture is built around distinct requests, encapsulating and grouping all concerns from front‑end to back. You take a normal ‘n‑tier’ or hexagonal/whatever architecture and remove the gates and barriers across those layers, and couple along the axis of change…”
Put simply:
- A slice = a request / feature (e.g.,
GetForecasts,CreateOrder,SearchCustomers) - That slice owns everything required to fulfill that request:
- HTTP endpoint (minimal API / controller)
- Request & response contracts
- Handler / application logic
- Domain rules needed for that feature
- Data‑access logic (repository, Dapper query, EF Core query, search index call, etc.)
Instead of this:
- Controller folder
- Services folder
- Repositories folder
- DTOs folder
…you get this:
Features/
WeatherForecasts/
GetForecasts/
SearchForecasts/
Orders/
CreateOrder/
CancelOrder/
Each subfolder is a slice that cuts vertically through the horizontal layers.
2. From N‑Tier Layers to Slices (Mental Model)
Imagine a typical N‑Tier application as a cake with four horizontal layers:
- Presentation
- Application
- Domain
- Data Access
+---------------------+ Presentation
+---------------------+ Application
+---------------------+ Domain
+---------------------+ Data Access
Now imagine you cut the cake into slices.
A Vertical Slice is one of those cuts: it touches all the layers, but only for a single feature.
| Slice: GetForecasts |
+-------------------------+ Presentation
+-------------------------+ Application
+-------------------------+ Domain
+-------------------------+ Data Access
| Slice: SearchForecasts |
Within a slice we embrace coupling: the endpoint, request, handler, and data access all work together very closely.
Between slices we aim for low coupling: features shouldn’t know about each other’s internal details.
Mental rule of thumb:
Maximize coupling inside a slice, minimize coupling between slices.
3. Why Vertical Slice? Benefits & Trade‑offs
Vertical Slice Architecture is about organizing code along the axis of change — features — instead of technical layers.
Key benefits:
Feature isolation
You build and modify one slice without worrying about breaking another. Each feature is “master of its own destiny”.Parallel development
Teams can work on different slices without constantly touching shared service/repository layers.New features only add code
Done right, you almost never have to edit some giantServicesorInfrastructureclass. New feature? → new slice.Screaming architecture
Open theFeaturesfolder and the use cases of the system literally “scream” at you (CreateInvoice,RefundPayment,SearchForecasts, …).
Trade‑offs / costs:
Possible duplication
Without discipline, utility helpers, validation rules, or mapping code might be duplicated across slices.Learning curve
Teams used to layered architectures may initially find slices “weird” or “too coupled”.Harder to over‑standardize
Because each slice can choose how it talks to persistence or other services, standardization requires good guidelines and code reviews.
As with any architecture choice, you’re managing trade‑offs to deliver business value faster and safer.
4. Starting Point: The Default .NET Web API
We’ll start from the default dotnet new webapi / minimal API template with the classic WeatherForecast example.
Initially the project looks roughly like this:
Vertical.Slice.WebApi/
Properties/
appsettings.json
Program.cs
WeatherForecast.cs
The single Program.cs file usually contains the endpoint:
app.MapGet("/weatherforecast", () =>
{
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool" };
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]))
.ToArray();
return forecast;
});
Our goal is to pull this endpoint out into a proper slice and then add another slice for searching forecasts.
5. Step 1 — Create a Features Folder
First, create a screaming architecture root folder:
Vertical.Slice.WebApi/
Features/
WeatherForecasts/
GetForecasts/
SearchForecasts/
Program.cs
appsettings.json
Start with one slice:
Features/
WeatherForecasts/
GetForecasts/
GetForecasts.cs
We’ll keep everything for the GetForecasts request inside this file: request, response, handler, and endpoint.
6. Step 2 — Implement the GetForecasts Slice
Inside Features/WeatherForecasts/GetForecasts/GetForecasts.cs we can define the full slice:
namespace Vertical.Slice.WebApi.Features.WeatherForecasts.GetForecasts;
public static class GetForecastsEndpoint
{
public const string Route = "/weatherforecast";
public static void MapGetForecasts(this IEndpointRouteBuilder app)
{
app.MapGet(Route, Handle)
.WithName("GetForecasts")
.WithSummary("Gets the next 5 weather forecasts")
.Produces<GetForecastsResponse>();
}
public static IResult Handle()
{
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool" };
var forecasts = Enumerable.Range(1, 5)
.Select(index => new ForecastDto(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
var response = new GetForecastsResponse(forecasts);
return Results.Ok(response);
}
}
public sealed record ForecastDto(DateOnly Date, int TemperatureC, string Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
public sealed record GetForecastsResponse(IReadOnlyList<ForecastDto> Items);
What lives in this slice?
-
Endpoint (
MapGetForecasts,Handle) -
DTOs (
ForecastDto,GetForecastsResponse) - No extra service/repository types — just enough to fetch and shape the data for this feature.
Later, if you introduce a database or a search index, you can refactor inside this slice without leaking those details to others.
7. Step 3 — Implement the SearchForecasts Slice
Now we’ll add a second slice that lets clients search by minimum temperature.
Folder structure:
Features/
WeatherForecasts/
GetForecasts/
GetForecasts.cs
SearchForecasts/
SearchForecasts.cs
SearchForecasts.cs:
namespace Vertical.Slice.WebApi.Features.WeatherForecasts.SearchForecasts;
public static class SearchForecastsEndpoint
{
public const string Route = "/weatherforecast/search";
public static void MapSearchForecasts(this IEndpointRouteBuilder app)
{
app.MapGet(Route, Handle)
.WithName("SearchForecasts")
.WithSummary("Searches forecasts with a minimum temperature filter")
.Produces<SearchForecastsResponse>();
}
public static IResult Handle([AsParameters] SearchForecastsRequest request)
{
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool" };
var forecasts = Enumerable.Range(1, 20)
.Select(index => new ForecastDto(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.Where(f => f.TemperatureC >= request.MinTemperatureC)
.ToArray();
var response = new SearchForecastsResponse(forecasts);
return Results.Ok(response);
}
}
public sealed record SearchForecastsRequest(int MinTemperatureC = -10);
public sealed record SearchForecastsResponse(IReadOnlyList<ForecastDto> Items);
// We can reuse ForecastDto from the GetForecasts slice,
// or define a local version if we want full isolation.
public sealed record ForecastDto(DateOnly Date, int TemperatureC, string Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Notice a few things:
- The request is explicitly modeled as
SearchForecastsRequest. - The slice is free to shape data differently than
GetForecastsif needed. - Internally, this slice might later query a completely different data store (e.g., ElasticSearch, Azure Cognitive Search) without affecting
GetForecastsat all.
That’s the power of slices: each feature decides how it gets its work done.
8. Wiring Slices into Program.cs
Finally, Program.cs turns into a thin composition root:
using Vertical.Slice.WebApi.Features.WeatherForecasts.GetForecasts;
using Vertical.Slice.WebApi.Features.WeatherForecasts.SearchForecasts;
var builder = WebApplication.CreateBuilder(args);
// Add services here (DbContext, logging, etc.)
var app = builder.Build();
// Map feature slices
app.MapGetForecasts();
app.MapSearchForecasts();
app.Run();
The app startup doesn’t know about controllers, services, or repositories. It only knows it’s mapping feature endpoints.
As you add more slices (CreateForecast, DeleteForecast, etc.), you simply call their mapping extension methods here.
9. Pros, Cons, and When to Use VSA
Pros
1. Creation of slices in isolation
Each feature can be designed, implemented, and tested in its own folder, with its own dependencies. This scales very well for larger teams: multiple developers can work on different features without trampling on each other’s shared service layers.
2. Safe modification of logic within a slice
Because a slice doesn’t leak its internals, changes are localized. You can swap a SQL query for a search index call without breaking other features.
3. New code mostly adds new files
Mature VSA projects trend towards “append‑only” feature development: new requirements → new slices, not edits to a global application service that every endpoint uses.
Cons / considerations
1. Potential duplication across slices
Without careful refactoring, shared logic (e.g., common validation, mapping, error handling) might be copied into multiple slices. Periodic extraction of true cross‑cutting concerns into shared libraries is important.
2. Architecture learning curve
Teams raised on strictly layered architectures might initially feel uncomfortable with a handler talking directly to a DbContext or Dapper query that lives inside the same folder as the endpoint.
3. Harder to over‑standardize
Because each slice solves a specific problem, it can be tempting to let every slice “do its own thing”. Lightweight standards (e.g., “each slice defines Request, Handler, Response, Endpoint”) and good reviews help preserve consistency without killing flexibility.
When Vertical Slice shines
- Product or SaaS codebases with many evolving features
- API‑first systems with lots of request/response endpoints
- Teams that want to parallelize development by feature
- Codebases adopting CQRS + MediatR (each handler naturally maps to a slice)
10. Next Steps: MediatR, CQRS, and Beyond
Once you’re comfortable with the basic folder structure, you can layer more patterns on top of VSA:
-
MediatR / CQRS — Model each request as a command or query (
IRequest<TResponse>) and let MediatR dispatch to handlers. Your slice then holds:GetForecastsQueryGetForecastsQueryHandlerGetForecastsEndpointGetForecastsResponse
Validation — Use FluentValidation or custom validators per slice. Each feature owns its input validation and error shaping.
-
Persistence — Inside the slice, you’re free to:
- Use EF Core DbContext
- Call stored procedures
- Hit a read‑optimized search store
- Call external services
Testing — Because slices are self‑contained, you can write focused unit and integration tests that exercise the feature end‑to‑end.
Conclusion
Vertical Slice Architecture is not a silver bullet, but it’s a powerful way to align your .NET code with how your product actually changes: one feature at a time.
By organizing around slices instead of layers you get:
- Architecture that “screams” your use cases
- Safer, more isolated changes
- Better parallelization for teams
- Flexibility to choose the right persistence or integration strategy per feature
Start small:
- Take an existing Web API feature.
- Move its endpoint, request, response, and handler into a single folder.
- Repeat for the next feature.
Before long, you’ll find your codebase reads more like a product specification than a wiring diagram of controllers and services.
Happy slicing, and happy coding in .NET! 🍰
✍️ Written by: Cristian Sifuentes — .NET / Vertical Slice / Clean Architecture enthusiast.

Top comments (0)