Skip to main content
SEMastery
Architecturebeginner

Vertical Slice Architecture Is Easier Than You Think (.NET)

A gentle, beginner-friendly guide to Vertical Slice Architecture in .NET. Learn to organise code by feature with simple examples, diagrams, and no scary tools.

13 min readUpdated September 15, 2025

A lunchbox, not a buffet line

Think about how lunch works at a busy school.

In one school, the food is served buffet style. There is a long counter. One person serves only rice. The next serves only dal. The next serves only curry. To get a full meal, you walk down the whole line and collect one spoon of each thing from a different person. If the rice runs out, the whole line slows down.

In another school, every child gets a lunchbox. Inside one box is everything for that child's meal — rice, dal, curry, and a sweet, all packed together. You open one box and your full meal is right there. You do not walk anywhere.

Code can be arranged in these same two ways.

The buffet line is how most older .NET apps are built. All the controllers sit in one folder. All the services sit in another. All the database code sits in a third. To build one feature, you walk down the whole line, picking up a piece from each folder.

The lunchbox is Vertical Slice Architecture (VSA). Everything one feature needs is packed into one folder — the endpoint, the logic, the validation, and the data access. You open one box and the whole feature is there.

People hear the word "architecture" and feel scared. But this idea is gentle. By the end of this post you will see why it is easier than you think.

What a "slice" actually is

A slice is one feature, packed in one place.

Not a layer. Not a service class. One thing your app does.

Here are some real slices:

  • "Add a product to the cart"
  • "Place an order"
  • "Get my order history"
  • "Cancel a booking"

Each of these is a vertical cut through your whole app. It touches the web part, the rules part, and the database part. Instead of spreading those pieces across the building, you put them in one drawer labelled with the feature name.

Figure 1: A slice cuts top to bottom through the app. One feature owns its endpoint, its rules, and its data access together.

The key idea: a slice is self-contained. To understand "Place an order", you open one folder and read top to bottom. You do not hunt across five other folders.

The problem slices solve

Let us look at the buffet-line way more closely, because seeing the pain helps the cure make sense.

In a classic layered app, adding one small feature like "Create Order" makes you touch many files in many folders:

  • a controller in the API folder
  • a service in the Application folder
  • an interface for that service
  • a repository in the Infrastructure folder
  • a DTO, a validator, and a mapping profile in yet more folders
Figure 2: In layered code, one feature is scattered. You jump between many folders to finish a single job.

Each of these files lives next to files from other features. The OrdersController sits beside the UsersController and the PaymentsController. So when you want to change order code, you keep bumping into payment code and user code that has nothing to do with you.

This causes three everyday problems:

  1. Lots of jumping. To read one feature, you open many folders. Your eyes get tired and you lose the thread.
  2. Scary changes. Many features share the same big service and repository classes. Touch one method and you might break a feature you forgot existed.
  3. Slow new starters. A new teammate must learn the whole layer map before they can ship a small change.

Vertical slices remove all three. One feature, one folder. Change it without fear.

Buffet line vs lunchbox, side by side

Here is the same idea as a quick table.

QuestionLayered (buffet line)Vertical Slice (lunchbox)
Where is one feature's code?Spread across many foldersAll in one folder
To add a feature, I touch...A controller, service, repo, and moreUsually one slice file
Risk of breaking other featuresHigher (shared big classes)Lower (isolated slices)
Easy for a new teammate?Must learn the whole mapRead one folder
Folder names tell you...The tech (controllers, services)The features (Orders, Cart)

Notice the last row. With slices, your folder names scream what the app does. This is sometimes called "screaming architecture". You open the project and immediately see Orders, Cart, and Booking — not a wall of generic tech folders.

What a project looks like

Let us make it real. Imagine a small shop API. With vertical slices, the folder tree is organised by feature.

src/
  Features/
    Orders/
      PlaceOrder.cs
      GetOrderHistory.cs
      CancelOrder.cs
    Cart/
      AddItemToCart.cs
      RemoveItemFromCart.cs
    Products/
      SearchProducts.cs
  Shared/
    Database/
    Results/
  Program.cs

Look at Features/Orders/. Every order action is one file. To work on placing an order, you open PlaceOrder.cs and everything is there. No tour of the building needed.

The Shared folder holds the few things that truly belong to everyone, like the database connection. We will talk more about shared code soon.

Building one slice

Name it
One file
Endpoint
Logic
Data
Done

Steps

1

Name it

Pick a clear feature name

2

One file

Create one slice file

3

Endpoint

Map the route

4

Logic

Add the rules

5

Data

Read or write the DB

6

Done

Ship without fear

The small, friendly steps to add a feature with vertical slices.

A complete slice in plain .NET

Here is a full slice with no special library. Just a minimal API endpoint and a small handler class. This is all you need to start.

namespace Shop.Features.Cart;
 
// The data coming in from the request.
public record AddItemRequest(int ProductId, int Quantity);
 
// The endpoint: this is the "front door" of the slice.
public static class AddItemToCart
{
    public static void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPost("/cart/items", Handle);
    }
 
    // The logic lives right next to the endpoint.
    private static async Task<IResult> Handle(
        AddItemRequest request,
        ShopDbContext db)
    {
        if (request.Quantity <= 0)
        {
            return Results.BadRequest("Quantity must be at least 1.");
        }
 
        var item = new CartItem
        {
            ProductId = request.ProductId,
            Quantity = request.Quantity
        };
 
        db.CartItems.Add(item);
        await db.SaveChangesAsync();
 
        return Results.Ok(new { item.Id });
    }
}

Read it top to bottom. The request shape, the route, the rule (quantity must be positive), and the database write all sit in one place. A new teammate can understand this feature in under a minute.

To turn it on, you call the slice's own map method in Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<ShopDbContext>();
 
var app = builder.Build();
 
// Each slice registers its own endpoint.
AddItemToCart.MapEndpoint(app);
PlaceOrder.MapEndpoint(app);
GetOrderHistory.MapEndpoint(app);
 
app.Run();

That is the whole pattern. No magic. No framework you must master first. You can grow into nicer helpers later, but you never have to.

Commands and queries: two kinds of slices

Most slices fall into one of two simple groups.

  • A command changes something. "Place an order" creates an order. "Cancel a booking" removes one.
  • A query just reads. "Get my order history" reads and returns a list. It changes nothing.

This split is called CQRS (Command Query Responsibility Segregation). The name sounds big, but the idea is small: writing and reading are different jobs, so give them different slices.

Figure 3: Requests split into two kinds of slices. Commands change data; queries only read it.

Why does this help? Because a query can be tuned for speed without worrying about write rules, and a command can be careful about rules without slowing reads. Each slice does one job well.

Here is a tiny query slice. Notice it only reads.

namespace Shop.Features.Orders;
 
public static class GetOrderHistory
{
    public static void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapGet("/orders/history", Handle);
    }
 
    private static async Task<IResult> Handle(int customerId, ShopDbContext db)
    {
        var orders = await db.Orders
            .Where(o => o.CustomerId == customerId)
            .Select(o => new { o.Id, o.Total, o.PlacedAt })
            .ToListAsync();
 
        return Results.Ok(orders);
    }
}

The route for this is GET /orders/history. The slice owns its own query shape. If you later need a faster read, you change this one file and nothing else.

A note on MediatR

You will see many older tutorials use a library called MediatR for slices. It turns each slice into a "command" or "query" object with a matching handler, and a central dispatcher routes them.

Two honest points for 2026:

  1. You do not need it. Everything above works with plain classes and minimal API endpoints. The slice pattern is about where your files live, not about any tool.
  2. It is now commercially licensed. Since mid-2025, newer MediatR versions ship under a license that needs a paid tier for many business uses. The same is true for MassTransit. So do not feel you must reach for them.

If you do want a mediator-style helper, there are free, open-source options, or you can write a tiny dispatcher yourself in about a hundred lines. But for learning, plain handlers are clearer and easier to debug. Start there.

ApproachFree to use?Good for beginners?
Plain handlers + minimal APIYesYes, start here
Hand-written tiny dispatcherYesLater, when you want it
MediatR (newer versions)Paid tier for businessNot needed to learn
Open-source mediator librariesOften yesOptional

Handling shared code without copying

There is one fair worry about slices. If every feature is on its own, do we copy the same code into every folder?

The honest answer: a little repetition is fine, but real duplication needs a home.

Use a simple rule. Share things that are stable and truly common. Keep things that change per feature inside the slice.

  • A database context, a money type, or a "Result" wrapper are stable and common. Put them in Shared.
  • A request shape, a validation rule, or a special query is specific to one feature. Keep it in the slice.

Where does this code go?

New code
Used by many?
Stable?
Shared
In slice

Steps

1

New code

You wrote something

2

Used by many?

More than one feature?

3

Stable?

Rarely changes?

4

Shared

Move to Shared folder

5

In slice

Keep it local

A quick decision path for shared versus slice-only code.

If two slices start to share a real chunk of logic, that is a signal. Maybe they belong to the same small module. You can group related slices into a feature module and let them share a private helper. This is how slices grow naturally into a modular monolith without a big redesign.

How slices help your team

Picture three teammates. One works on Cart, one on Orders, one on Products.

With layered code, all three keep editing the same giant service and repository files. They bump into each other. Merge conflicts pile up. A change in one corner breaks another.

With slices, each person mostly stays inside their own feature folder. Their files rarely overlap. They move fast and step on each other far less.

Figure 4: Three teammates work in separate slice folders. Their work rarely overlaps, so changes stay safe.

Testing gets simpler too. To test "Place an order", you test that one slice. You give it a request and check the result. You do not wire up five layers first. Small, focused tests are easier to write and faster to run.

When slices are not the best fit

Slices are friendly, but no tool fits every job. Be honest about the limits.

  • Very tiny apps. If your app has only two or three endpoints, the extra folders may feel heavier than the gain. Plain layering is fine.
  • Heavy shared logic. If almost every feature runs the same complex rules, slices can lead to copying unless you pull that logic into a shared core with care.
  • No team discipline. Slices need a shared rule for what goes in Shared. Without it, code can sprawl. A short team agreement fixes this.

For most growing apps with many distinct features, though, slices keep the code calm and clear as it grows.

Putting it together

Here is the whole flow of a request through a slice, start to finish.

Figure 5: The life of a request in one slice, from the route to the response.

One route. One handler. One trip to the database. One response. All in one file you can read in a minute. That is the whole promise of vertical slices, and that is why it really is easier than you think.

Quick recap

  • A slice is one feature packed in one folder: its endpoint, rules, and data access together.
  • Layered code is a buffet line (walk many folders for one meal). Slices are a lunchbox (one box, full meal).
  • Slices cut down on jumping, lower the risk of breaking other features, and help new teammates start fast.
  • Most slices are either a command (writes) or a query (reads). That split is CQRS, and it is simpler than its name.
  • You do not need MediatR. It is now commercially licensed. Plain handlers and minimal APIs work great and are easier to debug.
  • Put stable, common code in Shared. Keep feature-specific code in the slice.
  • Slices help teams work side by side and make tests small and focused.
  • For tiny apps, plain layering is fine. For growing apps with many features, slices keep things calm.

References and further reading

Related Posts