Vertical Slice Architecture in Practice cover image

Vertical Slice Architecture in Practice

By Julian Krause · Wednesday, August 20, 2025 · ~3 min read

Vertical slice architecture inverts the traditional layered approach. Instead of organizing code by technical concern (controllers, services, repositories), you organize by feature. Each feature is a self-contained vertical slice through the application stack. After using this approach on two large projects, I am convinced it is a better default than layered architecture for most applications.

The Problem with Layers

In a layered architecture, adding a single feature requires touching files across multiple directories: a controller, a service, a repository, a DTO, a mapper, a validator. These files are scattered across the project, connected only by naming conventions. When you need to understand a feature, you are chasing references across the codebase.

// Layered architecture — one feature touches many folders
/Controllers/OrdersController.cs
/Services/IOrderService.cs
/Services/OrderService.cs
/Repositories/IOrderRepository.cs
/Repositories/OrderRepository.cs
/DTOs/CreateOrderDto.cs
/Validators/CreateOrderValidator.cs
/Mappers/OrderProfile.cs

A vertical slice groups everything related to a single use case in one place. When you open the folder, you see everything that feature does.

// Vertical slice architecture — one folder per feature
/Features/Orders/CreateOrder/
    CreateOrder.cs           // Request + Response types
    CreateOrderHandler.cs    // Business logic
    CreateOrderValidator.cs  // Validation rules
    CreateOrderEndpoint.cs   // HTTP endpoint mapping

The handler contains the business logic and data access for this specific use case. It does not go through a generic repository or service layer.

public record CreateOrderCommand(
    Guid CustomerId,
    List<OrderLineDto> Lines) : IRequest<CreateOrderResponse>;

public record CreateOrderResponse(Guid OrderId);

public class CreateOrderHandler(AppDbContext db) : IRequestHandler<CreateOrderCommand, CreateOrderResponse>
{
    public async Task<CreateOrderResponse> Handle(
        CreateOrderCommand request,
        CancellationToken ct)
    {
        var customer = await db.Customers
            .FirstOrDefaultAsync(c => c.PublicId == request.CustomerId, ct)
            ?? throw new NotFoundException("Customer not found");

        var order = new Order(customer.Id);

        foreach (var line in request.Lines)
        {
            var product = await db.Products
                .FirstOrDefaultAsync(p => p.PublicId == line.ProductId, ct)
                ?? throw new NotFoundException($"Product {line.ProductId} not found");

            order.AddLine(product, line.Quantity);
        }

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

        return new CreateOrderResponse(order.PublicId);
    }
}

You Do Not Need a Repository for Every Slice

This is the most controversial aspect: vertical slices often use the DbContext directly instead of going through a repository abstraction. The reasoning is pragmatic — a repository that wraps a single EF Core query adds a layer of indirection without adding value. The DbContext is already a unit of work with change tracking.

When the same data access pattern is genuinely reused across multiple slices, extract it into a shared query object or extension method. But do not pre-emptively abstract everything behind repositories.

Mixing Approaches Is Fine

Not every feature warrants a full vertical slice. Simple CRUD endpoints can stay in traditional controllers. Complex features with rich business logic benefit most from the vertical slice approach. The architecture should serve the code, not the other way around.

// Simple CRUD — traditional controller is fine
app.MapGet("/api/products", async (AppDbContext db) =>
    await db.Products.AsNoTracking().Select(p => new ProductDto(p.PublicId, p.Name)).ToListAsync());

// Complex feature — vertical slice with handler, validator, etc.
app.MapPost("/api/orders", async (CreateOrderCommand command, ISender sender) =>
    Results.Created($"/api/orders/{(await sender.Send(command)).OrderId}", null));

The key insight is that vertical slice architecture is not about eliminating abstraction — it is about placing abstraction where it provides value, not where convention dictates.


Comments (2)

Anna Sunday, February 15, 2026 5:08 PM

Thanks for sharing — very helpful.

Hannah Saturday, March 14, 2026 5:08 PM

I have a question: does this also apply to older versions?

Ben Tuesday, March 10, 2026 8:08 PM

Thanks for your comment — glad it helped!

Clara Wednesday, March 11, 2026 8:08 PM

I had the same experience, can confirm.

Leave a Comment