Skip to content
SP StackPractices
intermediate By StackPractices

Vertical Slice Architecture — Feature-First Organization

A practical guide to Vertical Slice Architecture: organizing code by feature instead of technical concern, reducing cross-layer navigation and improving cohesion.

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

Vertical Slice Architecture, popularized by Jimmy Bogard, flips the traditional layered approach. Instead of organizing code by technical concern (Controllers, Services, Repositories), you organize by feature. All code for a single feature — controller, service, queries, DTOs, validation — lives together in one place. When you need to change “Create Order,” all the relevant code is in one folder. This dramatically reduces the cognitive load of navigating a codebase.

When to Use

  • Your application has many features that evolve independently
  • Team members frequently ask “where is the code for X?”
  • Cross-layer changes require touching 5+ files in 3+ directories
  • You want to minimize merge conflicts between feature teams
  • Features have varying complexity — some are simple CRUD, others complex workflows

Horizontal vs Vertical Organization

Horizontal (Layered)          Vertical (Feature Slices)
├── Controllers               ├── Features
│   ├── OrderController.cs    │   ├── CreateOrder
│   └── ProductController.cs  │   │   ├── CreateOrderCommand.cs
├── Services                  │   │   ├── CreateOrderHandler.cs
│   ├── OrderService.cs       │   │   ├── CreateOrderValidator.cs
│   └── ProductService.cs     │   │   └── CreateOrderEndpoint.cs
├── Repositories              │   ├── GetOrderById
│   ├── OrderRepository.cs    │   │   ├── GetOrderByIdQuery.cs
│   └── ProductRepository.cs  │   │   └── GetOrderByIdHandler.cs
                              │   └── UpdateOrderStatus

Feature Structure

Each feature is self-contained and typically includes:

ComponentPurpose
Command/QueryInput model (DTO)
HandlerBusiness logic for the feature
ValidatorInput validation rules
Endpoint/ControllerHTTP or messaging entry point
ResponseOutput model (DTO)

Example: Create Order Feature

// Features/Orders/CreateOrder/CreateOrderCommand.cs
public record CreateOrderCommand(
    int ProductId,
    int Quantity,
    string CustomerEmail
) : IRequest<OrderDto>;
// Features/Orders/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderDto>
{
    private readonly AppDbContext _dbContext;

    public CreateOrderHandler(AppDbContext dbContext) => _dbContext = dbContext;

    public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var product = await _dbContext.Products.FindAsync(request.ProductId);
        if (product == null) throw new NotFoundException("Product not found");
        if (product.Stock < request.Quantity)
            throw new ValidationException("Insufficient stock");

        var order = new Order
        {
            ProductId = request.ProductId,
            Quantity = request.Quantity,
            CustomerEmail = request.CustomerEmail,
            Total = product.Price * request.Quantity,
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.Orders.Add(order);
        product.Stock -= request.Quantity;
        await _dbContext.SaveChangesAsync(cancellationToken);

        return new OrderDto(order);
    }
}
// Features/Orders/CreateOrder/CreateOrderValidator.cs
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderValidator()
    {
        RuleFor(x => x.ProductId).GreaterThan(0);
        RuleFor(x => x.Quantity).GreaterThan(0).LessThanOrEqualTo(100);
        RuleFor(x => x.CustomerEmail).NotEmpty().EmailAddress();
    }
}
// Features/Orders/CreateOrder/CreateOrderEndpoint.cs
public class CreateOrderEndpoint : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPost("/orders", async (CreateOrderCommand command, ISender sender) =>
        {
            var result = await sender.Send(command);
            return Results.Created($"/orders/{result.Id}", result);
        });
    }
}

Sharing Cross-Cutting Concerns

Not everything belongs in a feature slice. Shared infrastructure lives in a common folder:

├── Features/           # Vertical slices
├── Common/
│   ├── Behaviors/      # MediatR pipelines (logging, validation, transactions)
│   ├── Exceptions/     # Domain and application exceptions
│   ├── Interfaces/     # Shared abstractions
│   └── Infrastructure/ # DbContext, DI configuration

Common Mistakes

  • No shared abstractions — duplicating DbContext access or validation pipelines in every feature
  • Features too granular — creating a slice for every CRUD operation instead of grouping related operations
  • Business logic in endpoints — handlers should contain the logic, endpoints just delegate
  • Ignoring cross-cutting concerns — logging, caching, and transactions still need centralized handling
  • Mixing horizontal and vertical — picking one approach per application, not both arbitrarily

FAQ

Does Vertical Slice replace Clean Architecture? No, they address different concerns. Vertical Slice is about code organization (folder structure). Clean Architecture is about dependency direction. You can combine them: vertically organized features with inward-pointing dependencies.

What framework works best with Vertical Slice? Any framework that supports a mediator pattern. ASP.NET Core with MediatR, FastAPI with dependency injection, or Spring Boot with CQRS libraries all work well.

How do I handle features that share logic? Extract shared logic into domain services or common behaviors. The goal is cohesion within a feature, not absolute isolation at all costs.