Skip to main content
SEMastery
Architectureintermediate

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.

11 min readUpdated September 25, 2025

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.

Horizontal layers cut the app sideways. Vertical slices cut top to bottom, by feature.

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 sliceWhat it doesExample file
RequestThe data that comes inCreateOrderCommand.cs
ValidationChecks the data is saneCreateOrderValidator.cs
HandlerThe actual work and rulesCreateOrderHandler.cs
EndpointThe HTTP door, like POST /ordersCreateOrderEndpoint.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

Endpoint
Validate
Handle
Save
Respond

Steps

1

Endpoint

HTTP door receives POST /orders

2

Validate

Reject bad input early

3

Handle

Run the business rule

4

Save

Write to the database

5

Respond

Return the new order id

Each step lives in the same folder, so the whole feature reads top to bottom.

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

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

Commands change state through domain rules. Queries read straight for speed.

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

Logging
Validation
Auth
Handler

Steps

1

Logging

Record what came in

2

Validation

Stop bad input

3

Auth

Check the user is allowed

4

Handler

Do the real feature work

Each wrapper adds one cross-cutting job, then passes the request along.

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.

Slices stay independent by talking through events, not direct calls.

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.

StyleOrganized byBest forWatch out for
N-layeredTechnical layerSmall CRUD appsFeatures smeared across layers
Clean architectureRings of dependencyBig, rule-heavy domainsLots of ceremony and mapping
Vertical sliceFeatureMost product apps, modular monolithsSome 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 Shared thin. 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 Shared thin and prefer a little duplication over a premature abstraction.

References and further reading

Related Posts