How to Structure Production Apps With Vertical Slice Architecture in .NET (2026)
A simple, friendly guide to structuring real production .NET 10 apps with vertical slice architecture in 2026 — folders, code, diagrams, and tables.
A food court with many small stalls
Think about a food court in a big mall.
A bad food kitchen is one giant shared kitchen. Every cook walks into the same room. The dosa cook, the biryani cook, and the chaat cook all share the same stove, the same fridge, and the same shelf. It sounds efficient. But on a busy evening it is chaos. When the biryani cook moves the salt, the chaat cook cannot find it. When one burner breaks, three dishes stop. Everything is tangled together.
A good food court gives each dish its own small stall. The dosa stall has its own batter, its own pan, its own counter, and its own bill book. The biryani stall has its own pot and its own spices. Each stall is small and complete. A customer walks up to one stall, orders, pays, and gets the food — all in one place. If the chaat stall closes for a day, dosa and biryani keep running happily.
Vertical slice architecture packs your code exactly like that food court. Each feature is a small stall. It has everything it needs to serve one request, kept together in one folder. When you want to change "place order," you walk to the "place order" stall and nothing else moves.
This article shows you how to build that food court in a real .NET 10 production app, step by step, in plain words.
Two ways to cut the same app
Before any code, picture a tall layered cake. There are two ways to cut it.
The old way cuts the cake flat, layer by layer. You get a plate of just sponge, a plate of just cream, a plate of just jam. This is the classic layered (horizontal) style: all controllers in one folder, all services in another, all repositories in a third. To add one feature, you run around and touch every layer.
The vertical slice way cuts straight down, top to bottom. Each slice has a little sponge, cream, and jam together. This is vertical slice architecture: each feature keeps its request, its logic, its validation, and its data access in one place. To add a feature, you add one slice. To delete it, you delete one folder.
In the layered picture, one feature is spread across many boxes. In the slice picture, one feature lives in one box. That single difference is what makes large apps easier to grow.
What lives inside one slice
A slice is a tiny vertical column through your whole app. For a single feature like "create order," the slice usually holds four things.
| Part of the slice | What it does | Example file |
|---|---|---|
| Request | The data that comes in | CreateOrderCommand.cs |
| Validation | Checks the data is sane | CreateOrderValidator.cs |
| Handler | The actual work and rules | CreateOrderHandler.cs |
| Endpoint | The HTTP door, like POST /orders | CreateOrderEndpoint.cs |
Everything for that one feature sits in one folder. When a new teammate opens the CreateOrder folder, they see the whole story of that feature without hunting through five other folders.
A request flowing through one slice
Steps
Endpoint
HTTP door receives POST /orders
Validate
Reject bad input early
Handle
Run the business rule
Save
Write to the database
Respond
Return the new order id
A folder structure that scales
Here is a folder layout that holds up well in production. Group by feature, not by file type.
src/
Orders/
Features/
CreateOrder/
CreateOrderCommand.cs
CreateOrderValidator.cs
CreateOrderHandler.cs
CreateOrderEndpoint.cs
GetOrder/
GetOrderQuery.cs
GetOrderHandler.cs
GetOrderEndpoint.cs
CancelOrder/
CancelOrderCommand.cs
CancelOrderHandler.cs
CancelOrderEndpoint.cs
Domain/
Order.cs
Shared/
OrdersDbContext.csNotice the Features folder. Each subfolder is one stall in the food court. The Domain and Shared folders hold the few things that many slices truly need, like the Order entity and the database context. Keep Shared small on purpose. The moment it grows fat, your slices start leaning on each other again.
Writing a slice without MediatR
For years, most .NET teams used MediatR to pass a request to its handler. That changed. MediatR went commercial on 2 July 2025. Version 13 and later need a paid license for many professional teams, though older versions stay under their original open-source license. Because of this, a lot of teams in 2026 build slices without MediatR. The good news: you never needed it. A slice is just a small class.
Here is a complete "create order" slice in modern C# 14 on .NET 10. It uses a minimal API endpoint and a plain handler class — no extra library.
// CreateOrder/CreateOrderCommand.cs
public sealed record CreateOrderCommand(string Product, int Quantity);
// CreateOrder/CreateOrderValidator.cs
public sealed class CreateOrderValidator
{
public IReadOnlyList<string> Validate(CreateOrderCommand cmd)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(cmd.Product))
errors.Add("Product is required.");
if (cmd.Quantity <= 0)
errors.Add("Quantity must be greater than zero.");
return errors;
}
}Now the handler. It does the real work: validate, build the entity, and save it. Notice it depends only on what this one feature needs.
// CreateOrder/CreateOrderHandler.cs
public sealed class CreateOrderHandler(OrdersDbContext db)
{
private readonly CreateOrderValidator _validator = new();
public async Task<Result<Guid>> HandleAsync(
CreateOrderCommand cmd,
CancellationToken ct)
{
var errors = _validator.Validate(cmd);
if (errors.Count > 0)
return Result<Guid>.Invalid(errors);
var order = new Order(cmd.Product, cmd.Quantity);
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result<Guid>.Success(order.Id);
}
}Finally the endpoint, which is the HTTP door. In a minimal API you register the route POST /orders and call the handler.
// CreateOrder/CreateOrderEndpoint.cs
public static class CreateOrderEndpoint
{
public static void Map(IEndpointRouteBuilder app) =>
app.MapPost("/orders", async (
CreateOrderCommand cmd,
CreateOrderHandler handler,
CancellationToken ct) =>
{
var result = await handler.HandleAsync(cmd, ct);
return result.IsSuccess
? Results.Created($"/orders/{result.Value}", result.Value)
: Results.BadRequest(result.Errors);
});
}That is the entire feature. Three small files, one folder, zero magic. A junior teammate can read it in a minute.
CQRS: split reading from writing
Inside slices, a very useful idea is CQRS — Command Query Responsibility Segregation. The fancy name hides a simple rule: commands change data, queries only read data. Keep them apart.
A command slice (like create or cancel) goes through your domain rules and saves changes. A query slice (like get or list) can skip the heavy rules and read straight from the database, often returning a flat shape built just for the screen. This keeps reads fast and writes safe.
You do not need separate databases to do CQRS. In most production apps, one database is fine. You only split the code paths: a command path that protects your rules, and a query path that is plain and quick.
Cross-cutting concerns the simple way
Some jobs are needed by almost every slice — logging, timing, error handling, checking the user is allowed. These are called cross-cutting concerns. You do not want to copy them into every handler.
The clean trick is a pipeline: a thin wrapper that runs around every handler. The request goes in, passes through each wrapper, hits the handler, and the result comes back out the same way. Each wrapper does one small job.
A request passing through the pipeline
Steps
Logging
Record what came in
Validation
Stop bad input
Auth
Check the user is allowed
Handler
Do the real feature work
If you use a library like Wolverine (a free, open-source option), it gives you this pipeline out of the box. If you stay library-free, you can wrap handlers with small decorator classes registered in dependency injection. Either way, the feature handler stays clean and only holds business logic.
Talking between slices without tangling them
Slices should not call into each other's private code. The dosa stall does not reach into the biryani pot. So how does "place order" tell "update inventory" that stock changed?
The safe answer is domain events. One slice raises a small event like OrderPlaced. Other slices that care subscribe to it. The two slices never reference each other directly. They only know about the shared event contract.
This keeps each slice deletable. If you remove the "send email" slice tomorrow, "place order" does not break, because it never knew that listener existed. It only published an event into the air.
How vertical slices compare with other styles
Teams often ask which architecture to pick. Here is an honest side-by-side.
| Style | Organized by | Best for | Watch out for |
|---|---|---|---|
| N-layered | Technical layer | Small CRUD apps | Features smeared across layers |
| Clean architecture | Rings of dependency | Big, rule-heavy domains | Lots of ceremony and mapping |
| Vertical slice | Feature | Most product apps, modular monoliths | Some honest duplication |
For most production product apps in 2026, vertical slices hit a sweet spot: fast to build, easy to test, and easy to delete. When one slice grows genuinely complex, you can still apply clean-architecture rules inside that one slice — the rest of the app stays simple.
Testing slices in production apps
Because a slice is a focused unit with clear inputs and outputs, it is lovely to test. The highest-confidence test is an integration test that calls the real endpoint, hits a real test database, and checks the result. One test covers the whole slice end to end.
[Fact]
public async Task CreateOrder_returns_201_for_valid_input()
{
// Arrange: a test server pointing at a throwaway database
using var app = new TestApp();
var client = app.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/orders",
new { Product = "Dosa", Quantity = 2 });
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}Because the test exercises the real path through the slice, it catches wiring bugs that unit tests miss. And since each slice is independent, your test suite can grow one slice at a time without the tests fighting each other.
A few rules that keep production apps healthy
Vertical slices are simple, but a few habits keep them tidy as the app grows:
- Keep
Sharedthin. Only put something there when three or more slices truly need it. - Prefer duplication over a wrong abstraction. Two slices copying ten lines is cheaper than two slices chained to a shared base class.
- Make slices deletable. You should be able to remove a feature by deleting one folder.
- Use events for cross-slice talk. Never let one slice reach into another's internals.
- Keep reads and writes apart. Commands protect rules; queries stay fast and flat.
Follow these and your food court keeps running smoothly, stall by stall, even as you add dozens of new dishes.
Quick recap
- Vertical slice architecture organizes code by feature, not by layer. Each slice keeps its request, validation, handler, and endpoint together in one folder.
- A real .NET 10 slice is just a few small classes — you do not need MediatR, which went commercial in July 2025. Plain handlers, Wolverine, or a tiny dispatcher all work.
- Use CQRS inside slices: commands change data through rules, queries read fast and flat.
- Handle cross-cutting concerns with a pipeline or decorators, so handlers stay clean.
- Let slices talk through domain events, never direct calls, so each slice stays independent and deletable.
- Test whole slices with integration tests for the highest confidence.
- Keep
Sharedthin and prefer a little duplication over a premature abstraction.
References and further reading
- .NET application architecture guidance — Microsoft Learn
- Vertical Slice Architecture in .NET — Milan Jovanović
- Vertical Slice Architecture in ASP.NET Core — Code Maze
- Build Your Own CQRS Dispatcher in .NET 10 (No MediatR) — codewithmukesh
- Vertical Slice Architecture — Wolverine docs
- N-Layered vs Clean vs Vertical Slice Architecture — antondevtips
Related Posts
Vertical Slice Architecture in .NET: The Easy Guide
Learn Vertical Slice Architecture in .NET in simple words. Organise code by feature instead of by layer, with diagrams, real examples, a comparison with Clean Architecture, and when to use each.
Building a Modular Monolith With Vertical Slice Architecture in .NET
Learn to build a modular monolith using vertical slice architecture in .NET. Simple words, real-life analogy, diagrams, tables, and clean C# code examples.
Vertical Slice Architecture: The Best Ways to Structure Your Project
A simple, friendly guide to the best ways to structure a .NET 10 project with vertical slice architecture — folders, diagrams, tables, and real code.
Where Vertical Slices Fit Inside the Modular Monolith
A simple guide to how vertical slices live inside the modules of a modular monolith in .NET, with diagrams, code, tables, and everyday examples.
How to Avoid Code Duplication in Vertical Slice Architecture in .NET
Learn how to avoid code duplication in Vertical Slice Architecture in .NET without breaking your slices. Rule of three, pipeline behaviors, shared infrastructure, and clear examples.
Vertical Slice Architecture: Where Does the Shared Logic Live?
A beginner-friendly guide to where shared logic lives in Vertical Slice Architecture in .NET — domain, infrastructure, pipelines, and the rule of three.