Back to blog
Backend Systemsbeginner

Vertical Slice vs Clean Architecture — the Trade-offs

Understand what vertical slice architecture is, how it differs from Clean Architecture, when to choose it, and what the real trade-offs look like in a .NET codebase.

Asma HafeezApril 17, 20263 min read
architecturevertical-sliceclean-architecturedotnetcqrs
Share:š•

Vertical Slice Architecture

Vertical Slice organizes code around features rather than technical layers. Instead of Controllers → Services → Repositories, each feature owns its full stack: request, handler, and data access in one place.


The Problem with Layered Architecture

In Clean Architecture, adding "Create Order" touches:

  • OrderController.cs (Presentation)
  • CreateOrderCommand.cs (Application)
  • IOrderService.cs (Application interface)
  • OrderService.cs (Application implementation)
  • IOrderRepository.cs (Domain interface)
  • OrderRepository.cs (Infrastructure)

Six files across four layers — for one feature.


Vertical Slice Structure

Features/
ā”œā”€ā”€ Orders/
│   ā”œā”€ā”€ CreateOrder/
│   │   ā”œā”€ā”€ CreateOrderCommand.cs    — request model
│   │   ā”œā”€ā”€ CreateOrderHandler.cs   — all logic here
│   │   └── CreateOrderEndpoint.cs  — HTTP binding
│   ā”œā”€ā”€ GetOrder/
│   │   ā”œā”€ā”€ GetOrderQuery.cs
│   │   └── GetOrderHandler.cs
│   └── ListOrders/
│       ā”œā”€ā”€ ListOrdersQuery.cs
│       └── ListOrdersHandler.cs
ā”œā”€ā”€ Products/
│   └── ...

Everything for one feature is in one folder.


A Vertical Slice in Practice

C#
// Features/Orders/CreateOrder/CreateOrderCommand.cs
public record CreateOrderCommand(int CustomerId, List<OrderLineDto> Lines);
public record CreateOrderResponse(int OrderId, decimal Total);

// Features/Orders/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler(AppDbContext db) : IRequestHandler<CreateOrderCommand, CreateOrderResponse>
{
    public async Task<CreateOrderResponse> Handle(CreateOrderCommand cmd, CancellationToken ct)
    {
        // Validation, business logic, persistence — all here
        var order = new Order
        {
            CustomerId = cmd.CustomerId,
            Lines = cmd.Lines.Select(l => new OrderLine
            {
                ProductId = l.ProductId,
                Quantity  = l.Quantity,
                UnitPrice = l.UnitPrice,
            }).ToList()
        };

        order.Total = order.Lines.Sum(l => l.Quantity * l.UnitPrice);

        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);

        return new CreateOrderResponse(order.Id, order.Total);
    }
}

// Features/Orders/CreateOrder/CreateOrderEndpoint.cs
public static class CreateOrderEndpoint
{
    public static void Map(IEndpointRouteBuilder app)
    {
        app.MapPost("/orders", async (CreateOrderCommand cmd, IMediator mediator) =>
        {
            var result = await mediator.Send(cmd);
            return Results.Created($"/orders/{result.OrderId}", result);
        }).RequireAuthorization();
    }
}

Vertical Slice vs Clean Architecture

| | Vertical Slice | Clean Architecture | |---|---|---| | Organization | By feature | By layer | | Coupling | Feature-isolated | Layer-isolated | | Adding a feature | One folder | Multiple layers | | Shared code | Shared/ folder | Domain layer | | Abstraction | Per-feature | Cross-cutting | | Best for | Feature-heavy apps | Domain-heavy apps |


When to Choose Vertical Slice

Good fit:

  • CRUD-heavy APIs where features are mostly independent
  • Teams working on different features in parallel
  • Apps where the domain model is thin
  • When you want to avoid premature abstraction

Less suited:

  • Complex domain logic that's shared across many features
  • When you need strict layer enforcement for compliance
  • Very small apps — the overhead isn't worth it

Shared Code

Not everything belongs in a feature. Use a Shared/ folder:

Shared/
ā”œā”€ā”€ Abstractions/    — interfaces used across features
ā”œā”€ā”€ Behaviors/       — MediatR pipeline behaviors (logging, validation)
ā”œā”€ā”€ Errors/          — common error types
ā”œā”€ā”€ Persistence/     — DbContext, migrations
└── Extensions/      — extension methods

Key Takeaways

  1. Vertical Slice organizes by what (feature) not where (layer)
  2. Each slice owns its full stack — request, logic, and data access in one place
  3. MediatR is the glue — commands/queries decouple the endpoint from the handler
  4. Shared code goes in Shared/ — resist the urge to add cross-cutting abstractions too early
  5. Neither architecture is objectively better — choose based on your team size, domain complexity, and how features relate to each other

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.