Skip to main content
SEMastery
Architectureintermediate

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.

12 min readUpdated September 18, 2025

A tiffin box, not a buffet table

Picture two ways to pack lunch for a school trip.

The buffet way keeps all the rice in one giant pot, all the dal in another pot, all the sabzi in a third pot, and all the rotis in a big stack. To serve one child, a teacher must run to four different pots and assemble the plate. If the dal pot tips over, every single plate is affected. Everything is shared, so everything is tangled.

The tiffin box way gives each child one small box. Inside that one box is their rice, their dal, their sabzi, and their roti — all together, all in one place. A child opens one box and the whole meal is right there. If one box spills, only that one child is affected. The other boxes are perfectly fine.

Vertical slice architecture packs your code like tiffin boxes. Each feature gets its own box. Inside that box lives everything the feature needs to do its job: the request that comes in, the logic that runs, the rules that check it, and the database call that saves it. To work on a feature, you open one box. Nothing else moves.

This guide walks through the best ways to structure that tiffin-box project in a real .NET 10 app. We will look at folders, files, naming, and the small choices that make a slice easy to live with.

Two ways to slice the same cake

Before any code, imagine a tall layered cake.

The old way cuts the cake flat. 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 must run across every layer and touch a little of each.

The slice way cuts straight down. Each slice has a bit of sponge, cream, and jam together. This is vertical slice architecture: each feature keeps its request, its logic, its rules, and its data access in one place. To add a feature you add one slice. To delete it you delete one folder.

Layers cut the app sideways. Slices cut top to bottom, one feature at a time.

Notice the difference. In the layered picture, one feature lives spread across many boxes. In the slice picture, one feature lives inside one box. That single idea is the heart of everything below.

What lives inside one slice

A slice is small but complete. For a typical write feature, one slice holds four things.

PartJobEveryday meaning
RequestThe data that comes inThe order ticket a customer hands over
ValidationThe rules that check the dataThe waiter checking the ticket makes sense
HandlerThe logic that does the workThe cook who prepares the dish
EndpointThe door the request enters byThe counter where you place your order

Keep these four together and you have a slice. When you read the slice, you see the whole story of one feature from top to bottom. You never have to jump to a far-away folder to understand what happens.

Here is the shape of a single slice that creates an order. This example keeps everything in one file, which is the simplest place to start.

// Features/Orders/CreateOrder.cs
public static class CreateOrder
{
    public record Command(string ProductId, int Quantity) : IRequest<Guid>;
 
    public class Validator : AbstractValidator<Command>
    {
        public Validator()
        {
            RuleFor(c => c.ProductId).NotEmpty();
            RuleFor(c => c.Quantity).GreaterThan(0);
        }
    }
 
    public class Handler(AppDbContext db)
    {
        public async Task<Guid> Handle(Command command, CancellationToken ct)
        {
            var order = new Order
            {
                Id = Guid.NewGuid(),
                ProductId = command.ProductId,
                Quantity = command.Quantity,
            };
 
            db.Orders.Add(order);
            await db.SaveChangesAsync(ct);
            return order.Id;
        }
    }
}

Everything for "create an order" sits in one place. The request, the rules, and the work are all visible at a glance. This is the tiffin box, in code.

The best ways to structure the project

There is no single correct folder tree, but a few patterns work well in practice. Let us look at the three most common, from simplest to most organised.

Way 1: One folder per feature group

Group slices by the area of the app they belong to. Orders go in an Orders folder, payments go in a Payments folder, and so on. Inside each area, every slice is one file.

Folder per feature group

src
Features
Orders
CreateOrder.cs

Steps

1

src

project root

2

Features

all features live here

3

Orders

one business area

4

CreateOrder.cs

one full slice

Each business area is a folder; each slice inside is one file.

This is the friendliest starting point. A newcomer opens the Orders folder and sees every order feature listed by name. The structure reads like a table of contents for the business.

Way 2: One folder per slice

When a feature grows, give it its own folder and split it into separate files. The request lives in one file, the handler in another, the validation in a third.

FileHolds
CreateOrderCommand.csthe request shape
CreateOrderHandler.csthe logic
CreateOrderValidator.csthe rules
CreateOrderEndpoint.csthe route

This keeps each file short and focused. The cost is more files to open. Use this only when a single-file slice starts to feel too tall to scroll comfortably.

Way 3: Shared kernel plus slices

Most apps need a little common code: the database context, base classes, and small helpers. Put those in a shared place, and keep every feature as a slice that leans on the shared parts only when it must.

Slices stay independent and only reach into a small shared kernel when needed.

The golden rule for the shared kernel is patience. Do not move code into it the first time you see a repeat. Wait until the same logic shows up in about three slices. A little copied code is cheaper to fix later than a wrong shared abstraction that every feature now depends on.

How a request travels through a slice

It helps to picture one request moving through the system from start to finish. The path is short and straight, because everything lives in one place.

A request flows in a straight line through a single slice.

There is no detour into a far folder. The endpoint receives the request, the validator checks it, the handler does the work, the database stores it, and the user gets an answer. If you ever need to debug POST /orders, you open one slice and read it top to bottom.

Wiring a slice into the app

A slice needs a door. In .NET 10, the cleanest door is a minimal API endpoint. Keep the endpoint inside the slice so the whole feature stays together.

// Features/Orders/CreateOrder.cs (continued)
public static class CreateOrderEndpoint
{
    public static void MapCreateOrder(this IEndpointRouteBuilder app)
    {
        app.MapPost("/orders", async (
            CreateOrder.Command command,
            CreateOrder.Handler handler,
            CancellationToken ct) =>
        {
            var id = await handler.Handle(command, ct);
            return Results.Created($"/orders/{id}", new { id });
        });
    }
}

Then register every slice's endpoint in one tidy place. Because each slice exposes a small Map method, the startup file stays short and readable.

// Program.cs
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddScoped<CreateOrder.Handler>();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
 
var app = builder.Build();
 
app.MapCreateOrder();
// app.MapGetOrder();
// app.MapCancelOrder();
 
app.Run();

Notice the route uses $"/orders/{id}" inside the code block, which is the safe place for curly braces. In prose you would write that route as GET /orders/{id} with backticks around it.

Do you need MediatR?

You may have seen vertical slice examples that use MediatR to send a command to a handler. That still works, but there is news to know.

On 2 July 2025, MediatR went commercial. Newer versions now need a paid licence for many professional teams. MassTransit announced the same move, with its version 9 turning commercial too. None of this breaks the vertical slice pattern, because the pattern never depended on those tools. It only depends on grouping code by feature.

You have three friendly choices in 2026.

ChoiceWhat it isCost
Call handlers directlyInject the handler and call Handle yourselfFree, no library
Tiny dispatcherWrite a small class that finds and runs handlersFree, a few lines
WolverineA fast, MIT-licensed mediator and messaging libraryFree and open source

The direct-call example shown earlier needs no library at all. The handler is just a class you inject and call. For most slices, that is enough, and it keeps your project light.

CQRS: the natural partner

Vertical slices pair beautifully with CQRS, which simply means you split reads from writes.

  • A command changes data. Create an order, cancel an order, update an address.
  • A query reads data. Get an order, list orders, search orders.

Each slice is naturally one or the other. A command slice does work and saves. A query slice just reads and returns. This split keeps each slice tiny, because a query never carries the heavy validation a command needs, and a command never carries the shaping a list view needs.

Commands and queries as slices

Request
Command?
Write to DB
Read from DB

Steps

1

Request

comes in

2

Command?

decide write or read

3

Write to DB

command slice

4

Read from DB

query slice

Each slice is either a write (command) or a read (query).

You do not need a fancy framework for CQRS. The split is mostly a way of thinking. Name your slices CreateOrder, GetOrder, ListOrders, and the intent is already clear.

Common mistakes to avoid

Even a simple pattern can go wrong. Here are the traps to watch for.

Sharing too early. The biggest mistake is pulling code into a shared folder the moment two slices look alike. This couples your features together and kills the main benefit. Wait for the rule of three.

A giant shared service. Some teams keep one big OrderService that every slice calls. Now the slices are not independent; they all lean on one fat object. Keep the logic inside the slice instead.

Folders by file type. Do not create top-level folders called Handlers, Validators, and Endpoints with every feature's pieces scattered across them. That is the layered style wearing a slice costume. Group by feature first, by file type never.

Endpoints far from handlers. If the route lives in one project and the handler in another, you lost the tiffin box. Keep the door next to the kitchen.

When vertical slices shine, and when they do not

Vertical slices are a strong default, but they are not magic for every situation.

They shine when your app is a collection of features that each do one clear job: an orders API, a booking system, an admin panel. The more your work looks like "add a feature, change a feature, remove a feature," the better slices fit.

They struggle when a feature has deep, shared business rules that many slices must obey in exactly the same way. In that case you may add a small domain layer inside the slice, or borrow a few ideas from clean architecture for just that one complex feature. Mixing is allowed. Many teams in 2026 use slices on the outside and clean rules inside only where a feature truly earns the extra ceremony.

The honest summary is that vertical slices reduce the cost of change for most everyday features. You keep that benefit as long as each slice stays mostly self-contained.

Quick recap

  • Vertical slice architecture groups all the code for one feature together, like packing one tiffin box per child instead of running between shared pots.
  • The best starting structure is one folder per feature group, with each slice as a single file. Split a slice into many files only when it grows tall.
  • A slice usually holds four parts: request, validation, handler, endpoint. Keep them together so one feature reads top to bottom.
  • Use a small shared kernel for truly common code like the database context, and follow the rule of three before sharing anything.
  • You do not need MediatR. It went commercial on 2 July 2025, so call handlers directly, write a tiny dispatcher, or use the free Wolverine library.
  • Slices pair naturally with CQRS: each slice is either a command (write) or a query (read).
  • Avoid sharing too early, giant shared services, and folders organised by file type.

References and further reading

Related Posts