Skip to content

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.
  • MiddlewareHTTP-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

Controllers/   Services/   Repositories/   Models/   Validators/

Five folders for 4 endpoints. Every change touches multiple.

✅ Correct: VSA

Features/Orders/
  PlaceOrder/    GetOrderById/   CancelOrder/   ListOrders/

❌ 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

public class CancelOrder.Handler(PlaceOrder.Handler placeHandler) { /* ... */ }

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.

Further reading