The main virtue of vertical slice architecture is that it organizes code around business capabilities instead of technical layers, which buys you high cohesion and low coupling. That leaves one question open: how should a single feature be structured inside its own folder?

Start with the thing VSA is reacting against. In a traditional n-tier architecture, one feature gets split across a repository for data access, a service for business logic, and a controller for the API endpoint.

One feature spread across n-tier technical layersCreating an order touches three separate technical folders: a controller in the API layer, a service in the business layer, and a repository in the data layer, so the feature has no single home.

Create order feature

Controllers/OrdersController.cs

Services/OrderService.cs

Repositories/OrderRepository.cs

Three folders, three layers, one feature. Nothing tells you those files belong together except convention and memory. Microsoft’s own architecture guidance describes where that ends up: UI concerns “reside in multiple folders, which aren’t grouped together alphabetically,” business logic gets “scattered,” and there is “no clear indication of which classes in which folders should depend on which others”.

Vertical slice architecture drops the technical separation and keeps everything related to a use case in one place. The simplest version looks like this:

Features/
└── Orders/
    ├── CreateOrder.cs
    ├── GetOrders.cs
    └── CancelOrder.cs

Features evolve. They start validating requests, mapping between contracts, and integrating with third-party vendors. The folder gets crowded, and the next logical step is splitting it.

No universal folder structure fits every application. Two approaches show up again and again, so here is how both behave as a feature grows.

Shallow organization

Shallow organization keeps the hierarchy as flat as it can be. Use cases live directly inside the feature folder with minimal nesting.

Features/
└── Orders/
    ├── CreateOrder.cs
    ├── CreateOrderValidator.cs
    ├── GetOrders.cs
    └── CancelOrder.cs

Taken to its limit, a slice can be a single file: endpoint, handler, request, and response all in one. Minimal APIs make that practical, since endpoints do not have to live in Program.cs and MapGroup can register a whole feature’s routes from one place.

// Features/Orders/CreateOrder.cs
public static class CreateOrder
{
    public sealed record Request(Guid CustomerId, string Sku, int Quantity);
    public sealed record Response(Guid OrderId, decimal Total);

    public sealed class Validator : AbstractValidator<Request>
    {
        public Validator()
        {
            RuleFor(x => x.CustomerId).NotEmpty();
            RuleFor(x => x.Sku).NotEmpty().MaximumLength(32);
            RuleFor(x => x.Quantity).InclusiveBetween(1, 100);
        }
    }

    public static void MapEndpoint(IEndpointRouteBuilder app) =>
        app.MapPost("/orders", Handle)
           .WithTags("Orders")
           .RequireAuthorization();

    private static async Task<Results<Created<Response>, NotFound, ValidationProblem>>
        Handle(
            Request request,
            IValidator<Request> validator,
            OrderDbContext db,
            CancellationToken cancellationToken)
    {
        var validation = await validator.ValidateAsync(request, cancellationToken);
        if (!validation.IsValid)
        {
            return TypedResults.ValidationProblem(validation.ToDictionary());
        }

        var product = await db.Products
            .SingleOrDefaultAsync(p => p.Sku == request.Sku, cancellationToken);

        if (product is null)
        {
            return TypedResults.NotFound();
        }

        var order = Order.Place(request.CustomerId, product, request.Quantity);

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

        return TypedResults.Created(
            $"/orders/{order.Id}",
            new Response(order.Id, order.Total));
    }
}

Route, contracts, validation, lookup, and persistence all sit in one file you can read top to bottom. Note what the nested Validator is doing: it is the same class that would otherwise live in CreateOrderValidator.cs, just not promoted to its own file yet. You can also split that slice across several files and still keep the folders flat.

One file per slice:

Orders/
└── CreateOrder.cs

Split into several files, folders still flat:

Orders/
├── CreateOrder.cs
├── CreateOrderRequest.cs
├── CreateOrderValidator.cs
└── CreateOrderResponse.cs

Flatness stops paying off at some point.

  • Too many files in one folder
  • A single slice needing several supporting files
  • Related files no longer sitting next to each other visually
  • Naming conventions doing the work of telling you which files belong together

In practice it degrades into something like this:

Features/
└── Orders/
    ├── CancelOrder.cs
    ├── CancelOrderValidator.cs
    ├── CreateOrder.cs
    ├── CreateOrderMapper.cs
    ├── CreateOrderRequest.cs
    ├── CreateOrderResponse.cs
    ├── CreateOrderValidator.cs
    ├── GetOrders.cs
    ├── GetOrdersQuery.cs
    ├── OrderPaymentClient.cs
    └── RefundOrder.cs

The prefixes are load-bearing now. Sort that folder alphabetically and CancelOrderValidator.cs lands next to CreateOrder.cs, which has nothing to do with it. When the structure stops helping you navigate the feature, it is time to restructure.

Deep organization

Deep organization fixes the ambiguity by adding one more level: each use case gets its own folder.

Features/
└── Orders/
    ├── CreateOrder/
    │   ├── CreateOrder.cs
    │   ├── CreateOrderRequest.cs
    │   ├── CreateOrderResponse.cs
    │   ├── CreateOrderValidator.cs
    │   └── CreateOrderMapper.cs
    ├── CancelOrder/
    │   ├── CancelOrder.cs
    │   └── CancelOrderValidator.cs
    ├── GetOrders/
    │   ├── GetOrders.cs
    │   └── GetOrdersQuery.cs
    ├── RefundOrder/
    │   └── RefundOrder.cs
    └── Shared/
        └── OrderPaymentClient.cs

Same eleven files as the crowded folder above, no deletions and no rewrites. The only thing that changed is which folder each one sits in.

The goal is not more folders. It is preserving clear grouping while the feature grows. A slice folder draws an explicit boundary: everything belonging to that use case lives inside it, and anything outside it belongs to something else.

A slice earns its own folder when the extra structure makes it easier to understand and navigate, not when it crosses some number.

When a slice earns its own folderIf a slice has supporting files and the flat folder no longer makes it obvious which files belong together, give that slice its own folder. Otherwise keep it flat.

No

Yes

Yes

No

Slice grows

Supporting files

beyond the handler?

Keep it flat

Still obvious which

files belong together?

Give the slice its own folder

One rule survives the move: the deeper structure still follows use cases, not technical responsibilities.

Shallow vs deep

Neither one is the answer. Here is how they trade off.

AspectShallowDeep
Folder depthMinimal nestingMore nested folders
Slice representationFiles directly in the feature folderEach slice gets a folder
NavigationFast while the feature is smallEasier once it is large
File groupingLeans on naming conventionsGrouped by folder
Best fitSimple or small slicesComplex slices with supporting code
CeremonyLowHigher
ScalabilityGets crowded over timeHandles growth better
DiscoverabilityGood while the folder stays smallBetter when many files share a slice
RiskLarge, noisy feature foldersOver-structuring and excessive nesting
Main benefitSimplicityClearer slice boundaries
Main drawbackLoses clarity as complexity growsAdds navigation overhead

Forced symmetry vs pragmatic consistency

On every project where I have used VSA, the plan was to pick one folder organization. Features evolve independently, so I ended up running both in the same codebase.

The Event feature needed depth. It held CreateEvent, GetEvents, and UpdateEvent, and each of those grew its own validators and mappers while solving a different problem. HealthChecks had a handful of files and no reason for extra folders, so it stayed flat.

Features/
├── Event/
│   ├── CreateEvent/
│   ├── GetEvents/
│   └── UpdateEvent/
└── HealthChecks/
    ├── DatabaseHealthCheck.cs
    └── HealthCheckEndpoint.cs

Structure should reflect actual complexity. Do not buy consistency by giving up pragmatism.

Avoid over-organization

The mistake I have seen, and made, is treating extra folders as the point of deep organization. Every new folder is another level to navigate through.

Over-organized feature folder with technical sub-layersThe same Orders feature split into Commands and Queries folders, then CreateOrder and GetOrders folders, then separate Handlers, Validators, and Mappers folders, which rebuilds technical layering inside the slice.

Orders/

Commands/

Queries/

CreateOrder/

Handlers/

Validators/

Mappers/

GetOrders/

Four levels deep to reach one handler. At that point you are thinking about folder conventions instead of the business problem, and Handlers, Validators, and Mappers have quietly rebuilt the technical layering VSA removed, just nested one level lower.

Deep organization already gives you a pragmatic CQRS: commands and queries are separated by use case, without needing Commands and Queries folders to say so.

Shared code follows a similar rule. If something is reused only inside one feature, keep it next to that feature instead of promoting it straight to a global Common folder. Cross-cutting concerns that genuinely span features, such as filters and middleware or global error handling, are a different case and belong outside the slice.

Features/
└── Orders/
    ├── CreateOrder/
    ├── CancelOrder/
    ├── GetOrders/
    ├── RefundOrder/
    └── Shared/
        └── OrderPaymentClient.cs

OrderPaymentClient is used by CreateOrder and RefundOrder but by nothing outside the feature, so it stays in the feature’s own Shared folder rather than moving to a project-wide one.

Before adding a folder inside a feature, answer one question: does this solve a real organization problem? If it does, add it. If it does not, the file belongs somewhere else.

How my VSA folders evolve

This has been my approach for years now, and you are welcome to steal it:

  • Start shallow
  • Let complexity create the need for depth
  • Restructure only the slice that needs it
  • Keep simple slices simple
  • Avoid designing the final hierarchy upfront

That last one matters most. Designing the hierarchy before the feature exists means guessing which slices will get complicated, and that guess is usually wrong.

Summary

There is no universal folder structure for vertical slice architecture. Start shallow, add depth when it improves clarity, and let each feature evolve according to its own complexity. Keep the deeper levels organized around use cases, because the moment they turn into Handlers and Validators folders, you have rebuilt n-tier inside your slice.