
Vertical Slice Architecture in Practice: How I Structure a Real ASP.NET Core API
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.csEverything 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.csEach file has one responsibility. No generic "OrderService". No cross-feature helpers.
Endpoints, Not Controllers
I prefer minimal endpoints over MVC controllers.
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
1public sealed record CreateOrderRequest(
2 Guid CustomerId,
3 IReadOnlyList<OrderItemDto> Items
4);Validation lives next to the request, not in a global folder:
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.
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
/LoggingFeatures 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
/GetByIdIf 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.






