Skip to main content
SEMastery
Architectureintermediate

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.

11 min readUpdated May 8, 2026

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.

Layered architecture spreads one feature across many folders.

This works fine for small apps. But as the app grows, two problems show up:

  1. Hard to find things. To change one feature you open four or five folders.
  2. Scary to change things. The OrderService class 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.

Vertical slices group all the code for one feature together.

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.cs

Each 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

HTTP POST
Endpoint
Handler
Database
Response

Steps

1

HTTP POST

Client sends order JSON

2

Endpoint

Minimal API binds the command

3

Handler

Runs the order logic

4

Database

Saves via EF Core

5

Response

Returns 201 Created

One request, one slice, top to bottom.

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.

Commands write, queries read. Slices keep them apart.

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

QuestionLayered (horizontal)Vertical slice
Group code byTechnical job (controller, service)Feature (PlaceOrder, GetOrders)
Add a featureEdit many foldersAdd one folder
Where is the logic?In big shared service classesInside the slice
Risk of breaking othersHigher (shared classes)Lower (slices are separate)
Best forSmall apps, simple CRUDApps that keep growing
Coupling styleHigh between layersHigh 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:

OptionWhat it isNotes
Plain handlersSimple classes you call directlyNo extra package. Easiest to start.
Minimal API onlyLogic inside the endpoint lambdaFine for tiny slices.
MediatRIn-process command/query busPopular, but now moves to commercial licensing for some use. Check the license before adopting.
WolverineMessaging + handler frameworkOpen 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

Repeated code?
Truly stable?
Used by many?
Move to Shared
Keep in slice

Steps

1

Repeated code?

You spot duplication

2

Truly stable?

Will it stop changing?

3

Used by many?

3+ slices need it

4

Move to Shared

Only if both yes

5

Keep in slice

Otherwise leave it

A simple decision path for sharing code.

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.

The full vertical-slice app: many independent slices over a thin shared core.

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

Related Posts