Vertical Slice Architecture: Organizing Code by Feature for Clean Maintainable Systems

Vertical Slice Architecture for Clean Code

Vertical Slice Architecture clean code practices represent a fundamental shift in how we organize software systems. Instead of the traditional layered architecture (controllers, services, repositories) where each layer spans the entire application, vertical slices organize code by feature — each slice contains everything needed for a single use case, from the API endpoint down to the database query.

This approach was popularized by Jimmy Bogard and has gained significant traction in 2025-2026 as teams discovered that layered architectures often lead to shotgun surgery — changing a single feature requires modifications across multiple layers. Moreover, vertical slices align naturally with how teams think about and deliver features.

The Problem with Horizontal Layers

Traditional layered architecture groups code by technical concern. Your controllers folder has 50 controllers, your services folder has 80 services, and your repositories folder has 60 repositories. When you need to add a new feature like “cancel order,” you create a method in OrderController, a method in OrderService, and a method in OrderRepository.

Furthermore, these layers become a dumping ground. OrderService starts with 3 methods and grows to 30 methods over two years. The class handles order creation, cancellation, fulfillment, refunds, reporting, and notifications. Testing requires mocking dozens of dependencies, and understanding any single feature means reading across multiple files in different directories.

Software architecture planning and design
Comparing horizontal layers versus vertical slice organization

Vertical Slice Architecture: Core Concept

A vertical slice encapsulates a single feature or use case in a self-contained unit. Each slice handles its own request/response, validation, business logic, and data access. Therefore, adding a new feature means adding a new slice — not modifying existing code across multiple layers.

Traditional Layered Architecture:
├── Controllers/
│   ├── OrderController.cs        (handles 15 endpoints)
│   ├── ProductController.cs
│   └── CustomerController.cs
├── Services/
│   ├── OrderService.cs           (30 methods, 800 lines)
│   ├── ProductService.cs
│   └── CustomerService.cs
├── Repositories/
│   ├── OrderRepository.cs
│   ├── ProductRepository.cs
│   └── CustomerRepository.cs

Vertical Slice Architecture:
├── Features/
│   ├── Orders/
│   │   ├── CreateOrder/
│   │   │   ├── CreateOrderCommand.cs
│   │   │   ├── CreateOrderHandler.cs
│   │   │   ├── CreateOrderValidator.cs
│   │   │   └── CreateOrderEndpoint.cs
│   │   ├── CancelOrder/
│   │   │   ├── CancelOrderCommand.cs
│   │   │   ├── CancelOrderHandler.cs
│   │   │   └── CancelOrderEndpoint.cs
│   │   └── GetOrderDetails/
│   │       ├── GetOrderDetailsQuery.cs
│   │       ├── GetOrderDetailsHandler.cs
│   │       └── GetOrderDetailsEndpoint.cs
│   └── Products/
│       ├── SearchProducts/
│       └── UpdateInventory/

Implementing Vertical Slices with MediatR

// Features/Orders/CreateOrder/CreateOrderCommand.cs
public record CreateOrderCommand(
    string CustomerId,
    List<OrderItemDto> Items,
    string ShippingAddress
) : IRequest<CreateOrderResult>;

public record CreateOrderResult(
    Guid OrderId,
    decimal TotalAmount,
    string Status
);

// Features/Orders/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, CreateOrderResult>
{
    private readonly AppDbContext _db;
    private readonly IEventBus _eventBus;
    private readonly IPricingService _pricing;

    public CreateOrderHandler(AppDbContext db, IEventBus eventBus, IPricingService pricing)
    {
        _db = db;
        _eventBus = eventBus;
        _pricing = pricing;
    }

    public async Task<CreateOrderResult> Handle(
        CreateOrderCommand request, CancellationToken ct)
    {
        // Validate inventory
        var items = await _db.Products
            .Where(p => request.Items.Select(i => i.ProductId).Contains(p.Id))
            .ToListAsync(ct);

        if (items.Count != request.Items.Count)
            throw new ValidationException("Some products not found");

        // Calculate pricing
        var total = await _pricing.CalculateTotal(request.Items);

        // Create order
        var order = new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = request.CustomerId,
            TotalAmount = total,
            Status = OrderStatus.Created,
            ShippingAddress = request.ShippingAddress,
            Items = request.Items.Select(i => new OrderItem
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                UnitPrice = items.First(p => p.Id == i.ProductId).Price
            }).ToList()
        };

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

        // Publish domain event
        await _eventBus.Publish(new OrderCreatedEvent(order.Id, order.TotalAmount));

        return new CreateOrderResult(order.Id, order.TotalAmount, "Created");
    }
}

// Features/Orders/CreateOrder/CreateOrderEndpoint.cs
public static class CreateOrderEndpoint
{
    public static void Map(IEndpointRouteBuilder app)
    {
        app.MapPost("/api/orders", async (
            CreateOrderCommand command,
            IMediator mediator) =>
        {
            var result = await mediator.Send(command);
            return Results.Created($"/api/orders/{result.OrderId}", result);
        })
        .WithName("CreateOrder")
        .WithTags("Orders")
        .Produces<CreateOrderResult>(StatusCodes.Status201Created);
    }
}
Clean code architecture patterns
Each vertical slice contains everything needed for a single feature

Vertical Slice Architecture: Handling Cross-Cutting Concerns

One concern with vertical slices is handling cross-cutting behavior like logging, validation, and authorization. Additionally, pipeline behaviors in MediatR solve this elegantly by wrapping every request handler with shared logic.

// Shared pipeline behavior for validation
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
        => _validators = validators;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        var context = new ValidationContext<TRequest>(request);
        var failures = (await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, ct))))
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

// Shared pipeline behavior for logging and performance
public class PerformanceBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<TRequest> _logger;
    private readonly Stopwatch _timer = new();

    public PerformanceBehavior(ILogger<TRequest> logger) => _logger = logger;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        _timer.Start();
        var response = await next();
        _timer.Stop();

        if (_timer.ElapsedMilliseconds > 500)
            _logger.LogWarning("Long running request: {Name} ({Ms}ms)",
                typeof(TRequest).Name, _timer.ElapsedMilliseconds);

        return response;
    }
}

When NOT to Use Vertical Slice Architecture

Vertical slices can lead to code duplication when multiple features share significant logic. If your application has heavy domain logic that genuinely belongs in shared services, forcing everything into slices creates maintenance overhead. As a result, CRUD-heavy applications with minimal business logic may not benefit — the overhead of separate command/handler/endpoint files for simple operations is not justified.

Small teams working on small applications may find that vertical slices add organizational complexity without proportional benefit. Start with simple layered architecture and refactor to slices when your services grow beyond comfortable sizes.

Team collaboration on software architecture
Deciding when vertical slices add genuine organizational value

Key Takeaways

Vertical Slice Architecture clean code organization aligns your codebase with how features are actually delivered. Each slice is self-contained, independently testable, and easy to understand. Furthermore, new team members can contribute faster because they only need to understand one slice at a time rather than tracing logic across multiple layers.

Begin by refactoring your most complex controller into vertical slices and measure the impact on development velocity and code clarity. For more architectural patterns, explore the original Vertical Slice Architecture post by Jimmy Bogard and the Microsoft Architecture Guide. Additionally, our articles on saga patterns for distributed transactions and data mesh implementation complement this architectural approach.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top