Vertical Slice Architecture in Practice: How I Structure a Real ASP.NET Core API

Vertical Slice Architecture in Practice: How I Structure a Real ASP.NET Core API

Barış TanerBarış Taner
December 31, 2025
3 min read
.NETBackendSoftware Architecture

Vertical Slice in Practice: How I Structure a Real ASP.NET Core API

Vertical Slice Architecture is often explained with clean diagrams and ideal examples. In practice, things are rarely that neat.

This post is not about theory. It's about how I actually structure a real ASP.NET Core API using vertical slices, what I keep strict, what I bend, and why.


Why Vertical Slice?

I moved to vertical slicing after working on APIs where:

  • Controllers became thin but meaningless
  • Business logic leaked into random services
  • A single change required touching 6 different folders

Vertical Slice doesn't magically fix complexity, but it contains it.

The goal is simple:

A feature should live in one place, or very close to it.


High-Level Folder Structure

At the top level, my API usually looks like this:

/Features
  /Orders
  /Users
  /Authentication
/Infrastructure
/Shared
Program.cs

Everything interesting lives under Features.


A Single Feature Slice

Let's take a simple example: creating an order.

/Features/Orders/Create
  CreateOrderEndpoint.cs
  CreateOrderRequest.cs
  CreateOrderValidator.cs
  CreateOrderHandler.cs

Each file has one responsibility. No generic "OrderService". No cross-feature helpers.


Endpoints, Not Controllers

I prefer minimal endpoints over MVC controllers.

csharp
1public static class CreateOrderEndpoint
2{
3    public static void Map(IEndpointRouteBuilder app)
4    {
5        app.MapPost("/orders", async (
6            CreateOrderRequest request,
7            CreateOrderHandler handler,
8            CancellationToken ct) =>
9        {
10            await handler.Handle(request, ct);
11            return Results.Created("/orders", null);
12        });
13    }
14}

Why?

  • Easier to read
  • No inheritance
  • No magic filters
  • Dependencies are explicit

Request and Validation Stay Together

csharp
1public sealed record CreateOrderRequest(
2    Guid CustomerId,
3    IReadOnlyList<OrderItemDto> Items
4);

Validation lives next to the request, not in a global folder:

csharp
1public sealed class CreateOrderValidator
2    : AbstractValidator<CreateOrderRequest>
3{
4    public CreateOrderValidator()
5    {
6        RuleFor(x => x.CustomerId).NotEmpty();
7        RuleFor(x => x.Items).NotEmpty();
8    }
9}

This avoids the classic problem:

"Where is the validation for this request?"


The Handler Is the Use Case

The handler is where the actual work happens.

csharp
1public sealed class CreateOrderHandler
2{
3    private readonly AppDbContext _db;
4
5    public CreateOrderHandler(AppDbContext db)
6    {
7        _db = db;
8    }
9
10    public async Task Handle(CreateOrderRequest request, CancellationToken ct)
11    {
12        var order = new Order(
13            request.CustomerId,
14            request.Items.Select(...)
15        );
16
17        _db.Orders.Add(order);
18        await _db.SaveChangesAsync(ct);
19    }
20}

No interfaces. No base classes. No "ApplicationService".

Just a use case.


What About Shared Logic?

This is where people overthink Vertical Slice.

My rules are simple:

  • Domain logic → stays in domain entities
  • Cross-cutting concerns → middleware or infrastructure
  • Feature-specific helpers → stay inside the feature

Only extract shared code when:

  • You've duplicated it at least twice
  • The abstraction is obvious

Infrastructure Is Boring (and That's Good)

Infrastructure stays boring and predictable:

/Infrastructure
  /Persistence
  /Auth
  /Logging

Features depend on infrastructure. Infrastructure does not depend on features.


Things I Don't Do (On Purpose)

  • No "Application" layer with 200 services
  • No MediatR everywhere "because CQRS"
  • No premature abstractions

Vertical Slice works best when you resist over-engineering.


When a Slice Gets Too Big

Slices can grow. That's fine.

When it hurts, I split by use case, not by type:

/Orders
  /Create
  /Cancel
  /GetById

If two use cases diverge, they deserve separate slices.


Final Thoughts

Vertical Slice is not about folder structure. It's about ownership.

When I open a feature folder, I want to see:

  • the request
  • the validation
  • the behavior

In one place.

If that's true, the architecture is doing its job.

Share: