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
| Concept | Definition |
|---|---|
| Core Idea | Organize by feature, not by layer |
| Slice | All code for one feature in one place |
| Coupling | High cohesion within slice, low coupling between slices |
| Trade-off | Some 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
| Benefit | Trade-off |
|---|---|
| Feature cohesion | Some code duplication |
| Independence | Less reuse between features |
| Easy navigation | Unfamiliar to layer-focused devs |
| Simple changes | Need 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
- Modular Monolith - Combining slices into modules
- CQRS Deep Dive - Command/Query separation