Vertical Slice Architecture: How to Structure Your Slices in .NET
Learn vertical slice architecture in .NET with a simple tiffin-box analogy, feature folders, CQRS, code examples, diagrams, and clear structuring rules.
Think about a school lunch. Some kitchens cook in stations. One person only boils rice. Another only makes dal. A third only fries the sabzi. To get one full meal, your plate must travel to every station. If the dal station is slow, your whole lunch waits.
Now picture a tiffin box instead. Each tiffin holds one complete meal: rice, dal, sabzi, and a sweet — all packed together in one box. You grab one tiffin and you have everything you need. You do not run between stations.
Vertical slice architecture is the tiffin-box way of writing code. Each feature gets its own little box that holds everything that feature needs. That is the whole idea, and the rest of this article shows you how to pack those boxes well in .NET.
The old way: layers (the kitchen stations)
Most of us learned to build apps in layers. You make folders by job type:
Controllers/holds all the API endpoints.Services/holds all the business logic.Repositories/holds all the database code.Models/holds all the data shapes.
This is called layered or horizontal architecture. To build one feature, like "Place an order", you touch a file in every folder. Your one idea is scattered across the whole project.
This works fine for small apps. But as the app grows, two problems show up:
- Hard to find things. To change one feature you open four or five folders.
- Scary to change things. The
OrderServiceclass slowly grows huge because every order-related feature dumps code into it. Touching it for one feature might break another.
The new way: slices (the tiffin boxes)
Vertical slice architecture turns the folders sideways. Instead of grouping by job (controllers, services), you group by feature.
A slice is one full vertical cut through the app — from the web request at the top, down to the database at the bottom — for one use case. Each slice lives in its own folder. This idea was made popular in .NET by Jimmy Bogard around 2018.
Notice the big change. The web request, the logic, and the database code for PlaceOrder all sit inside one box. If you only care about placing an order, you open one folder and you see the whole story.
The guiding rule, in Jimmy Bogard's words, is:
Minimize coupling between slices, and maximize coupling within a slice.
In plain English: things that change together should live together. Things that have nothing to do with each other should stay apart.
A real folder layout
Here is what a small order system looks like with slices. The key folder is Features/.
src/
Features/
Orders/
PlaceOrder/
PlaceOrder.cs // command + handler + endpoint
PlaceOrderValidator.cs // input checks
CancelOrder/
CancelOrder.cs
GetOrders/
GetOrders.cs
Customers/
RegisterCustomer/
RegisterCustomer.cs
Shared/
AppDbContext.cs // shared database
Money.cs // shared value type
Program.csEach leaf folder (like PlaceOrder/) is one tiffin box. The Shared/ folder is for things that truly belong to everyone, like the database context. We will talk more about Shared/ later, because it is where slices can go wrong.
What goes inside one slice?
Let me show a full slice in one file. This uses ASP.NET Core Minimal APIs and a simple handler class — no extra library needed. Many tutorials reach for MediatR here, but you do not have to. We will cover that choice in its own section.
// Features/Orders/PlaceOrder/PlaceOrder.cs
namespace Shop.Features.Orders.PlaceOrder;
// 1. The input (the "command")
public record PlaceOrderCommand(int CustomerId, string Product, int Quantity);
// 2. The output
public record PlaceOrderResult(Guid OrderId);
// 3. The logic (the "handler")
public class PlaceOrderHandler(AppDbContext db)
{
public async Task<PlaceOrderResult> Handle(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = cmd.CustomerId,
Product = cmd.Product,
Quantity = cmd.Quantity,
CreatedAt = DateTime.UtcNow
};
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return new PlaceOrderResult(order.Id);
}
}
// 4. The endpoint (how the web calls this slice)
public static class PlaceOrderEndpoint
{
public static void MapPlaceOrder(this IEndpointRouteBuilder app)
{
app.MapPost("/orders", async (
PlaceOrderCommand cmd,
PlaceOrderHandler handler,
CancellationToken ct) =>
{
var result = await handler.Handle(cmd, ct);
return Results.Created($"/orders/{result.OrderId}", result);
});
}
}Read that one more time. Everything about "placing an order" is on this single screen: the input, the output, the rule, and the URL. A new teammate can understand the whole feature without hunting through the project.
In Program.cs you just register the handler and the endpoint:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddScoped<PlaceOrderHandler>();
var app = builder.Build();
app.MapPlaceOrder(); // wire up the slice
app.Run();How a request flows through a slice
Here is the journey of one HTTP request as it moves through a slice, step by step.
Request flow through the PlaceOrder slice
Steps
HTTP POST
Client sends order JSON
Endpoint
Minimal API binds the command
Handler
Runs the order logic
Database
Saves via EF Core
Response
Returns 201 Created
The request never leaves the slice. Compare this to layers, where the request would bounce out to a shared OrderService and a shared OrderRepository that dozens of other features also depend on.
CQRS fits slices like dal fits rice
Many .NET teams pair vertical slices with a pattern called CQRS — Command Query Responsibility Segregation. It sounds scary but the idea is small:
- A command changes data (place order, cancel order). It writes.
- A query reads data (get orders, get one order). It only reads.
You keep the two apart. A read slice can be simple and fast — sometimes just a single SQL query straight into a small DTO, skipping the heavy domain model entirely. A write slice can hold the real business rules.
Here is a tiny query slice. Notice how plain it is — no repository, no service, just the read it needs.
// Features/Orders/GetOrders/GetOrders.cs
namespace Shop.Features.Orders.GetOrders;
public record OrderSummary(Guid Id, string Product, int Quantity);
public static class GetOrdersEndpoint
{
public static void MapGetOrders(this IEndpointRouteBuilder app)
{
app.MapGet("/orders", async (AppDbContext db, CancellationToken ct) =>
{
var orders = await db.Orders
.Select(o => new OrderSummary(o.Id, o.Product, o.Quantity))
.ToListAsync(ct);
return Results.Ok(orders);
});
}
}A read slice and a write slice can use the database in totally different ways, and that is allowed. They do not share a forced "one model fits all" design. This is the freedom slices give you.
Slices vs layers: a side-by-side
| Question | Layered (horizontal) | Vertical slice |
|---|---|---|
| Group code by | Technical job (controller, service) | Feature (PlaceOrder, GetOrders) |
| Add a feature | Edit many folders | Add one folder |
| Where is the logic? | In big shared service classes | Inside the slice |
| Risk of breaking others | Higher (shared classes) | Lower (slices are separate) |
| Best for | Small apps, simple CRUD | Apps that keep growing |
| Coupling style | High between layers | High inside a slice, low between slices |
Neither is "wrong". Layers are great when an app is tiny. Slices shine when an app keeps adding features and many people work on it at once, because two people can build two slices without stepping on each other.
The DI and library question
A common myth is that you need MediatR to do vertical slices. You do not. The slice is a folder idea, not a library.
Here are your main options in 2026:
| Option | What it is | Notes |
|---|---|---|
| Plain handlers | Simple classes you call directly | No extra package. Easiest to start. |
| Minimal API only | Logic inside the endpoint lambda | Fine for tiny slices. |
| MediatR | In-process command/query bus | Popular, but now moves to commercial licensing for some use. Check the license before adopting. |
| Wolverine | Messaging + handler framework | Open source, built for slices, adds retries and outbox. A common MediatR replacement. |
Because MediatR (and the related MassTransit) now have commercial licensing for certain usage, do not assume they are free for your company. Read the license. For learning and many real apps, plain handler classes like the one above are perfectly good and have zero licensing worries.
Where slices go wrong: the giant Shared folder
The biggest trap is over-sharing. When you see two slices repeat a little code, the tempting move is to yank it into a Shared helper that everyone calls. Do this too eagerly and you slowly rebuild the very tangle of shared classes you were escaping.
A good habit: let a small amount of duplication live. Two slices having two similar-but-separate validators is often healthier than one shared validator both slices must agree on forever. Only promote code to Shared/ when it is truly stable and truly common — like the AppDbContext or a Money value type.
When to move code into Shared
Steps
Repeated code?
You spot duplication
Truly stable?
Will it stop changing?
Used by many?
3+ slices need it
Move to Shared
Only if both yes
Keep in slice
Otherwise leave it
Another rule: slices should not call each other. If PlaceOrder needs to "also send an email", it should not reach into the SendEmail slice and call its handler. Instead, raise an event or call a small shared service. Slices that call slices quietly become the spaghetti you wanted to avoid.
A quick checklist for a healthy slice
When you finish a slice, ask yourself these questions:
- Can I understand this whole feature by opening one folder?
- Does this slice avoid reaching into other slices?
- Did I share code only when it was truly common and stable?
- Can I delete this entire feature by deleting one folder, with little fallout?
If you can say yes to all four, your tiffin box is packed well.
Putting it together: the full picture
Here is the whole shape of a slice-based app in one diagram. The web layer maps many endpoints, each endpoint belongs to a slice, and slices lean on a thin shared core only for the database and a few stable types.
Each slice is a tiffin box. The shared core is the small set of things every meal needs, like the plate and the spoon. You add features by adding boxes, not by stretching shared classes thinner and thinner.
Quick recap
- Vertical slice architecture groups code by feature, not by technical layer. One feature, one folder.
- It is like a tiffin box: each feature carries everything it needs — endpoint, logic, and data access — in one place.
- The golden rule: maximize coupling inside a slice, minimize coupling between slices.
- CQRS pairs well: commands write, queries read, and they can use the database differently.
- You do not need MediatR. Plain handlers work great. MediatR and MassTransit now carry commercial licensing for some use, so check before adopting; Wolverine is an open-source alternative.
- The main danger is over-sharing. Allow a little duplication. Promote code to
Shared/only when it is stable and common. - Slices should not call each other. Use events or thin shared services instead.
References and further reading
- Jimmy Bogard — Vertical Slice Architecture — the original .NET write-up of the idea.
- Milan Jovanovic — Vertical Slice Architecture — a clear modern walkthrough with examples.
- Code Maze — Vertical Slice Architecture in ASP.NET Core — feature-folder structure in detail.
- nadirbad — Vertical Slice Architecture template (.NET 10) — a ready-to-read solution template.
- Wolverine — Vertical Slice Architecture — an open-source library built around slices.
Related Posts
Refactoring a Modular Monolith Without MediatR in .NET
Learn to remove MediatR from a .NET modular monolith using plain handlers and a tiny dispatcher, with CQRS, pipeline behaviors, and clear module boundaries.
CQRS Pattern in .NET: The Way It Should Have Been From the Start
A friendly, hands-on guide to the CQRS pattern in .NET 10. Learn commands, queries, handlers, diagrams, and when to actually use it.
Scheduling Background Jobs with Quartz.NET in ASP.NET Core
Learn Quartz.NET step by step in ASP.NET Core: jobs, triggers, cron schedules, dependency injection, and database persistence, explained for beginners.
How to Customize ASP.NET Core Identity With EF Core for Your Project Needs
Learn to customize ASP.NET Core Identity with EF Core: add user fields, change the key type, extend roles, and run migrations safely on .NET 10.
TickerQ: The Modern .NET Job Scheduler That Beats Quartz and Hangfire
Learn TickerQ, the fast, reflection-free .NET job scheduler with cron and time jobs, EF Core storage, retries, and a live dashboard, explained for beginners.
Getting Started with Event Sourcing in .NET with Marten and PostgreSQL
Learn event sourcing in .NET using Marten and PostgreSQL. Store events, build aggregates and projections, and read state the easy, beginner-friendly way.