Vertical Slice Architecture
Key Points
- Vertical Slice Architecture (VSA) organizes by feature (a single use case = a single folder) rather than by technical layer (Controllers/Services/Repositories).
- Each "slice" is end-to-end: request, handler, validator, response — one folder, one concern.
- Often paired with CQRS via MediatR or hand-rolled handler classes; the slice owns its full pipeline.
- REPR pattern (Request → Endpoint → Response): each endpoint a self-contained unit.
- Pros: features change in one folder; coupling minimized. Cons: less reuse via shared layers; some duplication.
- Excellent fit for Minimal APIs + handlers in modern .NET.
Concepts (deep dive)
The shape
Traditional layered structure:
src/MyApp/
├── Controllers/
│ └── OrdersController.cs
├── Services/
│ └── OrderService.cs
├── Repositories/
│ └── OrderRepository.cs
├── Models/
│ └── Order.cs
└── Validators/
└── PlaceOrderValidator.cs
A change to "place order" touches Controllers, Services, Repositories, Validators — four files in four folders.
Vertical slice structure:
src/MyApp/
├── Features/
│ ├── Orders/
│ │ ├── PlaceOrder/
│ │ │ ├── PlaceOrder.cs ← request, handler, validator
│ │ │ └── PlaceOrderEndpoint.cs ← HTTP endpoint
│ │ ├── GetOrderById/
│ │ │ ├── GetOrderById.cs
│ │ │ └── GetOrderByIdEndpoint.cs
│ │ └── CancelOrder/
│ │ ├── CancelOrder.cs
│ │ └── CancelOrderEndpoint.cs
│ └── Customers/
│ └── ...
└── Domain/
└── Order.cs (shared aggregate)
A change to "place order" touches one folder. A change to "cancel order" touches a different folder — independent slices.
Example slice
// Features/Orders/PlaceOrder/PlaceOrder.cs
public static class PlaceOrder
{
public record Command(Guid CustomerId, List<Line> Lines) : IRequest<Result<Guid>>;
public record Line(Guid ProductId, int Quantity, decimal UnitPrice);
public class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(c => c.CustomerId).NotEmpty();
RuleFor(c => c.Lines).NotEmpty();
}
}
public class Handler(AppDb db) : IRequestHandler<Command, Result<Guid>>
{
public async Task<Result<Guid>> Handle(Command cmd, CancellationToken ct)
{
var order = Order.Place(new CustomerId(cmd.CustomerId),
cmd.Lines.Select(l => new OrderLine(/* ... */)).ToList());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result.Ok(order.Id.Value);
}
}
}
// Features/Orders/PlaceOrder/PlaceOrderEndpoint.cs
public static class PlaceOrderEndpoint
{
public static IEndpointRouteBuilder MapPlaceOrder(this IEndpointRouteBuilder app)
{
app.MapPost("/orders", async (PlaceOrder.Command cmd, IMediator m) =>
{
var r = await m.Send(cmd);
return r.Match(
ok => Results.Created($"/orders/{ok}", new { id = ok }),
err => Results.ValidationProblem(/* ... */));
});
return app;
}
}
// Program.cs
app.MapPlaceOrder().MapGetOrderById().MapCancelOrder();
Everything for "place order" is in one folder. Modify in one place, test in one place, delete in one place.
REPR — Request, Endpoint, Response
Steve Smith's REPR template:
public class PlaceOrderEndpoint : Endpoint<PlaceOrderRequest, PlaceOrderResponse>
{
public override void Configure()
{
Post("/orders");
}
public override async Task HandleAsync(PlaceOrderRequest req, CancellationToken ct)
{
// ...
}
}
Used heavily by FastEndpoints library — each endpoint is a class. Combines naturally with vertical slicing.
When VSA shines
- CRUD-heavy services with many independent features.
- Microservices with clear feature boundaries.
- Teams large enough that different people work on different features simultaneously.
- Greenfield projects where you can establish convention.
When traditional layering wins
- Heavy shared business logic that touches many features.
- Strong DDD with rich aggregates that span many use cases.
- Existing layered codebases — refactoring to VSA is a big undertaking.
Mixing VSA with DDD
VSA and DDD aren't exclusive:
src/MyApp/
├── Features/ ← vertical slices (use cases)
│ └── Orders/
│ ├── PlaceOrder/
│ └── CancelOrder/
└── Domain/ ← shared domain model
└── Orders/
├── Order.cs ← aggregate root
└── OrderStatus.cs
Slices coordinate; aggregates encapsulate. Slice handler loads aggregate, invokes operation, saves.
Cross-cutting concerns
VSA doesn't eliminate cross-cutting (logging, validation, transactions). Place them in:
- Pipeline behaviors (MediatR) — wrap handlers globally.
- Endpoint filters (Minimal APIs) — wrap endpoints.
- Middleware — HTTP-pipeline level.
Slices stay focused on the feature; cross-cutting lives in shared infrastructure.
Code duplication is OK (sometimes)
VSA accepts that some code duplicates across slices — that's a feature, not a bug:
- Two slices with similar query → don't necessarily share a method.
- Premature reuse couples slices.
- Once you find genuine reuse, then extract.
Rule of three: see the same logic in three slices → extract. Two slices: tolerate.
Folder vs project boundary
For small VSA, one project, many feature folders. For large, project-per-bounded-context, with feature folders inside. The pattern doesn't dictate the boundary.
Code: correct vs wrong
❌ Wrong: layered for a small CRUD service
Five folders for 4 endpoints. Every change touches multiple.
✅ Correct: VSA
❌ Wrong: prematurely extracting "shared" service
// Features/Orders/Helpers/OrderQueryService.cs
public class OrderQueryService { /* used by 1 slice */ }
If only one slice uses it, leave it in the slice.
❌ Wrong: cross-slice direct dependency
Don't reuse handlers from other slices. Extract to domain or a shared service if needed.
Design patterns for this topic
Pattern 1 — "One folder per feature; static class per slice"
- Intent: all related code in one place.
Pattern 2 — "REPR / FastEndpoints style"
- Intent: endpoint as a self-contained class.
Pattern 3 — "Shared domain; sliced application"
- Intent: DDD aggregate + vertical slice handlers.
Pattern 4 — "Pipeline behaviors for cross-cutting"
- Intent: keep slices feature-focused.
Pattern 5 — "Tolerate duplication; extract on rule of three"
- Intent: avoid premature coupling.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Vertical slice | Feature locality | Some duplication |
| Layered | Reuse via shared layers | Changes ripple across folders |
| FastEndpoints/REPR | Endpoint = class | Less standard |
| MediatR + slices | Convention for pipelines | Reflection-based mediator deps |
When to use / when to avoid
- Use VSA for new CRUD-heavy services.
- Use VSA when feature teams work in parallel.
- Avoid VSA for heavily shared-logic apps.
- Avoid premature shared services within VSA.
Interview Q&A
Q1. What's vertical slice architecture? Organizing code by feature (use case) rather than by technical layer. Each slice is end-to-end: request, handler, validator, response.
Q2. How does VSA differ from layered architecture? Layered: organize by technical role (controllers, services, repos). VSA: organize by feature (each use case in one folder).
Q3. Does VSA preclude DDD? No. Slices coordinate; DDD aggregates encapsulate domain logic. Use both.
Q4. What's the REPR pattern? Request, Endpoint, Response — endpoint as a self-contained class with explicit input/output types. Used by FastEndpoints.
Q5. When does VSA cause too much duplication? When many slices share complex logic. Rule of three before extracting.
Q6. Where do cross-cutting concerns go in VSA? Pipeline behaviors (MediatR), endpoint filters, or middleware. Shared infrastructure, not in each slice.
Q7. Can you mix VSA and traditional layering? Yes — most apps end up doing some of both. VSA at the application layer; layered at the infrastructure layer.
Q8. What's the main benefit of VSA? Change locality. A feature change touches one folder. New devs onboard faster — you read the slice, you understand the feature.
Q9. How does this interact with CQRS? Naturally. Each slice is a command or query handler. CQRS provides the request shape; VSA provides the file organization.
Q10. Should you always use a mediator with VSA? No. Mediator is one option. You can inject handler classes directly into endpoints — simpler.
Gotchas / common mistakes
- ⚠️ Premature shared services — couple slices.
- ⚠️ Cross-slice dependencies — defeats the boundary.
- ⚠️ No domain layer — slices duplicate domain logic.
- ⚠️ Slice that's too big — split into smaller slices.