Vertical Slice Architecture · Lesson 1 of 1
Vertical Slice vs Clean Architecture — the Trade-offs
Vertical Slice Architecture
Vertical Slice organizes code around features rather than technical layers. Instead of Controllers → Services → Repositories, each feature owns its full stack: request, handler, and data access in one place.
The Problem with Layered Architecture
In Clean Architecture, adding "Create Order" touches:
OrderController.cs(Presentation)CreateOrderCommand.cs(Application)IOrderService.cs(Application interface)OrderService.cs(Application implementation)IOrderRepository.cs(Domain interface)OrderRepository.cs(Infrastructure)
Six files across four layers — for one feature.
Vertical Slice Structure
Features/
├── Orders/
│ ├── CreateOrder/
│ │ ├── CreateOrderCommand.cs — request model
│ │ ├── CreateOrderHandler.cs — all logic here
│ │ └── CreateOrderEndpoint.cs — HTTP binding
│ ├── GetOrder/
│ │ ├── GetOrderQuery.cs
│ │ └── GetOrderHandler.cs
│ └── ListOrders/
│ ├── ListOrdersQuery.cs
│ └── ListOrdersHandler.cs
├── Products/
│ └── ...Everything for one feature is in one folder.
A Vertical Slice in Practice
// Features/Orders/CreateOrder/CreateOrderCommand.cs
public record CreateOrderCommand(int CustomerId, List<OrderLineDto> Lines);
public record CreateOrderResponse(int OrderId, decimal Total);
// Features/Orders/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler(AppDbContext db) : IRequestHandler<CreateOrderCommand, CreateOrderResponse>
{
public async Task<CreateOrderResponse> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
// Validation, business logic, persistence — all here
var order = new Order
{
CustomerId = cmd.CustomerId,
Lines = cmd.Lines.Select(l => new OrderLine
{
ProductId = l.ProductId,
Quantity = l.Quantity,
UnitPrice = l.UnitPrice,
}).ToList()
};
order.Total = order.Lines.Sum(l => l.Quantity * l.UnitPrice);
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return new CreateOrderResponse(order.Id, order.Total);
}
}
// Features/Orders/CreateOrder/CreateOrderEndpoint.cs
public static class CreateOrderEndpoint
{
public static void Map(IEndpointRouteBuilder app)
{
app.MapPost("/orders", async (CreateOrderCommand cmd, IMediator mediator) =>
{
var result = await mediator.Send(cmd);
return Results.Created($"/orders/{result.OrderId}", result);
}).RequireAuthorization();
}
}Vertical Slice vs Clean Architecture
| | Vertical Slice | Clean Architecture | |---|---|---| | Organization | By feature | By layer | | Coupling | Feature-isolated | Layer-isolated | | Adding a feature | One folder | Multiple layers | | Shared code | Shared/ folder | Domain layer | | Abstraction | Per-feature | Cross-cutting | | Best for | Feature-heavy apps | Domain-heavy apps |
When to Choose Vertical Slice
Good fit:
- CRUD-heavy APIs where features are mostly independent
- Teams working on different features in parallel
- Apps where the domain model is thin
- When you want to avoid premature abstraction
Less suited:
- Complex domain logic that's shared across many features
- When you need strict layer enforcement for compliance
- Very small apps — the overhead isn't worth it
Shared Code
Not everything belongs in a feature. Use a Shared/ folder:
Shared/
├── Abstractions/ — interfaces used across features
├── Behaviors/ — MediatR pipeline behaviors (logging, validation)
├── Errors/ — common error types
├── Persistence/ — DbContext, migrations
└── Extensions/ — extension methodsKey Takeaways
- Vertical Slice organizes by what (feature) not where (layer)
- Each slice owns its full stack — request, logic, and data access in one place
- MediatR is the glue — commands/queries decouple the endpoint from the handler
- Shared code goes in
Shared/— resist the urge to add cross-cutting abstractions too early - Neither architecture is objectively better — choose based on your team size, domain complexity, and how features relate to each other