Skip to main content

Vertical Slice Architecture: Feature-First Organization

Vertical Slice Architecture organizes code by feature rather than by technical layer. Each "slice" contains everything needed for a single feature, from UI to database.

TL;DR

ConceptDefinition
Core IdeaOrganize by feature, not by layer
SliceAll code for one feature in one place
CouplingHigh cohesion within slice, low coupling between slices
Trade-offSome duplication for independence

Layers vs Slices

The Problem with Layers

Change Amplification

Adding a single field requires changes across all layers:

Adding "discount" to orders:

Layered Architecture:
├── Controllers/OrdersController.cs ← Change
├── ViewModels/OrderViewModel.cs ← Change
├── Services/OrderService.cs ← Change
├── Models/Order.cs ← Change
├── Repositories/OrderRepository.cs ← Change
├── Entities/OrderEntity.cs ← Change
└── Migrations/AddDiscountToOrders.cs ← Change

7 files across 7 folders!

Vertical Slice Solution

Adding "discount" to orders:

Vertical Slice:
└── Features/Orders/CreateOrder/
├── CreateOrderCommand.cs ← Change
├── CreateOrderHandler.cs ← Change
└── CreateOrderValidator.cs ← Change

3 files in 1 folder!

Structure

Folder Organization

src/
├── Features/
│ ├── Orders/
│ │ ├── CreateOrder/
│ │ │ ├── CreateOrderCommand.cs
│ │ │ ├── CreateOrderHandler.cs
│ │ │ ├── CreateOrderValidator.cs
│ │ │ └── CreateOrderEndpoint.cs
│ │ ├── GetOrder/
│ │ │ ├── GetOrderQuery.cs
│ │ │ ├── GetOrderHandler.cs
│ │ │ └── GetOrderEndpoint.cs
│ │ ├── CancelOrder/
│ │ │ ├── CancelOrderCommand.cs
│ │ │ ├── CancelOrderHandler.cs
│ │ │ └── CancelOrderEndpoint.cs
│ │ └── Shared/ # Shared within Orders feature
│ │ ├── Order.cs
│ │ └── OrderDto.cs
│ ├── Customers/
│ │ ├── RegisterCustomer/
│ │ └── GetCustomer/
│ └── Products/
│ └── ...
├── Common/ # Truly shared infrastructure
│ ├── Database/
│ │ └── AppDbContext.cs
│ └── Behaviors/
│ └── ValidationBehavior.cs
└── Program.cs

Implementation with MediatR

Command/Query

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

public record CreateOrderResult(
bool IsSuccess,
Guid? OrderId,
string? Error);

public record OrderItemDto(Guid ProductId, int Quantity, decimal Price);

Handler

// Features/Orders/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, CreateOrderResult>
{
private readonly AppDbContext _context;

public CreateOrderHandler(AppDbContext context)
{
_context = context;
}

public async Task<CreateOrderResult> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
// Validation
var customer = await _context.Customers.FindAsync(request.CustomerId);
if (customer == null)
return new CreateOrderResult(false, null, "Customer not found");

// Create order
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = request.CustomerId,
Status = OrderStatus.Pending,
CreatedAt = DateTime.UtcNow,
Lines = request.Items.Select(i => new OrderLine
{
ProductId = i.ProductId,
Quantity = i.Quantity,
Price = i.Price
}).ToList()
};

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

_context.Orders.Add(order);
await _context.SaveChangesAsync(cancellationToken);

return new CreateOrderResult(true, order.Id, null);
}
}

Endpoint

// Features/Orders/CreateOrder/CreateOrderEndpoint.cs
public static class CreateOrderEndpoint
{
public static void Map(WebApplication app)
{
app.MapPost("/api/orders", async (
CreateOrderCommand command,
IMediator mediator) =>
{
var result = await mediator.Send(command);

return result.IsSuccess
? Results.Created($"/api/orders/{result.OrderId}", result)
: Results.BadRequest(result.Error);
});
}
}

// Or with controllers
[ApiController]
[Route("api/orders")]
public class CreateOrderController : ControllerBase
{
private readonly IMediator _mediator;

[HttpPost]
public async Task<ActionResult<CreateOrderResult>> CreateOrder(
CreateOrderCommand command)
{
var result = await _mediator.Send(command);

if (!result.IsSuccess)
return BadRequest(result.Error);

return CreatedAtAction(
nameof(GetOrderController.GetOrder),
new { id = result.OrderId },
result);
}
}

Validator

// Features/Orders/CreateOrder/CreateOrderValidator.cs
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty()
.WithMessage("Customer ID is required");

RuleFor(x => x.Items)
.NotEmpty()
.WithMessage("Order must have at least one item");

RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.Quantity)
.GreaterThan(0)
.WithMessage("Quantity must be positive");

item.RuleFor(x => x.Price)
.GreaterThan(0)
.WithMessage("Price must be positive");
});
}
}

Query Example

// Features/Orders/GetOrder/GetOrderQuery.cs
public record GetOrderQuery(Guid OrderId) : IRequest<OrderDto?>;

// Features/Orders/GetOrder/GetOrderHandler.cs
public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto?>
{
private readonly AppDbContext _context;

public async Task<OrderDto?> Handle(
GetOrderQuery request,
CancellationToken cancellationToken)
{
return await _context.Orders
.Where(o => o.Id == request.OrderId)
.Select(o => new OrderDto
{
Id = o.Id,
CustomerId = o.CustomerId,
Status = o.Status.ToString(),
Total = o.Total,
CreatedAt = o.CreatedAt,
Lines = o.Lines.Select(l => new OrderLineDto
{
ProductId = l.ProductId,
Quantity = l.Quantity,
Price = l.Price
}).ToList()
})
.FirstOrDefaultAsync(cancellationToken);
}
}

Benefits

1. High Cohesion

Everything for a feature is together:

Features/Orders/CreateOrder/
├── CreateOrderCommand.cs # What comes in
├── CreateOrderHandler.cs # What happens
├── CreateOrderValidator.cs # Validation rules
├── CreateOrderEndpoint.cs # How it's exposed
└── CreateOrderTests.cs # Tests for this feature

2. Low Coupling

Features are independent:

// CreateOrder doesn't need to know about GetOrder
// Changes to CreateOrder don't affect GetOrder

3. Easy to Find Code

"Where's the code for creating orders?"
→ Features/Orders/CreateOrder/

"Where's the code for canceling orders?"
→ Features/Orders/CancelOrder/

4. Easy to Delete

# Removing a feature is deleting a folder
rm -rf Features/Orders/CancelOrder/

Trade-offs

BenefitTrade-off
Feature cohesionSome code duplication
IndependenceLess reuse between features
Easy navigationUnfamiliar to layer-focused devs
Simple changesNeed discipline to avoid coupling

When to Use

Good Fit

  • CRUD-heavy applications - Features map to operations
  • Microservices - Natural service boundaries
  • Teams organized by feature - Conway's Law alignment
  • Rapid iteration - Easy to add/remove features

Less Ideal

  • Complex domain logic - Consider DDD instead
  • Highly shared behavior - Lots of duplication
  • Small applications - Over-engineering

Combining with DDD

Vertical slices can contain DDD patterns:

Features/Orders/CreateOrder/
├── CreateOrderCommand.cs
├── CreateOrderHandler.cs
│ └── Uses Order aggregate from Domain/
└── CreateOrderEndpoint.cs

Domain/
├── Orders/
│ ├── Order.cs # Aggregate root
│ ├── OrderLine.cs # Entity
│ └── OrderStatus.cs # Value object
└── Shared/
└── Money.cs # Shared value object

Quick Reference Card

┌─────────────────────────────────────────────────────────┐
│ VERTICAL SLICE ARCHITECTURE QUICK REFERENCE │
├─────────────────────────────────────────────────────────┤
│ │
│ STRUCTURE │
│ Features/ │
│ └── [Domain]/ │
│ └── [Feature]/ │
│ ├── Command/Query │
│ ├── Handler │
│ ├── Validator │
│ └── Endpoint │
│ │
│ PRINCIPLES │
│ • Organize by feature, not layer │
│ • High cohesion within slice │
│ • Low coupling between slices │
│ • Accept some duplication │
│ │
│ GOOD FOR │
│ • CRUD applications │
│ • Microservices │
│ • Feature teams │
│ │
│ TOOLS │
│ • MediatR for handlers │
│ • FluentValidation for validation │
│ • Minimal APIs or Controllers │
│ │
└─────────────────────────────────────────────────────────┘

Next Steps