Pavan Rangani

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

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

By Pavan Rangani · March 22, 2026 · Architecture

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. Consequently, the unit of change becomes the feature itself, not a horizontal tier that touches every feature at once.

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. When a product manager asks for “cancel order,” the engineer creates one folder, not five scattered edits.

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. As a result, a single conceptual change is smeared across three directories that may be hundreds of lines apart.

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. In practice, teams report that the blast radius of a “small” change keeps growing because the shared service has become an implicit god object.

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. Because each slice owns its own contract, two engineers can build “Create Order” and “Cancel Order” simultaneously without stepping on shared service files or fighting over merge conflicts.

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

How Vertical Slice Architecture Clean Coupling Improves Cohesion

The deepest argument for slices is not folder aesthetics — it is the coupling/cohesion trade-off. In a layered design, code that changes together (one feature) lives apart, while code that rarely changes together (unrelated features sharing a service) lives in the same file. Vertical slices invert this: high cohesion within a slice, low coupling between slices. Therefore, when you delete a deprecated feature, you delete one folder with confidence instead of carefully extracting methods from three shared classes.

Importantly, slices do not forbid sharing. Genuinely stable domain rules — money math, tax calculation, an Order aggregate that enforces invariants — still belong in a shared Domain project. The discipline is that slices share downward into stable abstractions, not sideways into each other. A common heuristic from teams adopting this style is the “rule of three”: duplicate freely until the same logic appears in three slices, then extract it deliberately rather than speculatively.

Testing a Slice End to End

Because a slice owns its full path, the most valuable test is a request-level test that exercises the handler directly. Rather than mocking a repository, many teams run the handler against an in-memory or containerized database so the SQL is real. As a result, the test asserts behavior a user cares about, and refactoring the internals does not break it.

public class CreateOrderTests : IClassFixture<WebAppFactory>
{
    private readonly IMediator _mediator;

    public CreateOrderTests(WebAppFactory factory)
        => _mediator = factory.Services.GetRequiredService<IMediator>();

    [Fact]
    public async Task Rejects_order_with_unknown_product()
    {
        var command = new CreateOrderCommand(
            CustomerId: "cust_1",
            Items: new() { new OrderItemDto(ProductId: 999, Quantity: 1) },
            ShippingAddress: "1 Test St");

        // Edge case: product 999 does not exist
        var act = () => _mediator.Send(command);

        await Assert.ThrowsAsync<ValidationException>(act);
    }
}

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. Instead of repeating a validation block in twelve handlers, you register one behavior and every command flows through it — keeping each slice focused purely on its use case.

Key Takeaways

  • Start with a solid foundation and build incrementally based on your requirements
  • Test thoroughly in staging before deploying to production environments
  • Monitor performance metrics and iterate based on real-world data
  • Follow security best practices and keep dependencies up to date
  • Document architectural decisions for future team members
// 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;
    }
}

Vertical Slices Versus Clean Architecture and Ports-and-Adapters

It helps to position this pattern against its neighbors. Clean Architecture and Hexagonal (ports-and-adapters) organize primarily around dependency direction — the domain sits at the center and infrastructure points inward. Vertical slices organize primarily around feature boundaries. The two are not mutually exclusive; a common pattern is to keep a clean domain core while structuring the application layer as slices. In contrast, pure layered MVC optimizes for technical familiarity at the cost of feature cohesion.

For teams already on modular monoliths, slices are often the natural internal structure of each module, and they make a future extraction to microservices cheaper because a feature’s code is already colocated. However, the pattern is a tactic, not a religion — mixing a thin layered CRUD section with rich slices for complex workflows is a legitimate, pragmatic outcome rather than an inconsistency to be ashamed of.

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, and the indirection through a mediator can obscure what is otherwise a five-line database read.

Small teams working on small applications may find that vertical slices add organizational complexity without proportional benefit. Likewise, a junior-heavy team without strong refactoring discipline can let duplicated logic drift out of sync across slices, producing subtle bugs that a shared method would have prevented. Start with simple layered architecture and refactor to slices when your services grow beyond comfortable sizes, or when feature ownership and parallel work become the real bottleneck.

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

Key Takeaways

Vertical slice 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. The payoff compounds: smaller blast radius on change, simpler tests, and code that you can confidently delete.

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.

In conclusion, Vertical Slice Architecture Clean is an essential discipline for modern software development. By applying the patterns and practices covered in this guide, you can build more robust, scalable, and maintainable systems. Start with the fundamentals, iterate on your implementation, and continuously measure results to ensure you are getting the most value from these approaches.

← Back to all articles