Levelwise
English
Code design

Vertical Slice architecture

Organize code by feature, not by technical layer. Each feature, like cancelling an order, has all its code in one place. Things that change together sit together, and features do not depend on each other.

Not reviewedWritten with AI helpReading time: 13 minExample of the cancel order feature in an online shopC# code on .NET 10

Author: bezzad

The problem: one small change, five folders

The sales team wants a new feature: the customer can cancel an order. Our project is organized in folders by layer. So the developer must:

  1. Add an action to OrdersController. This controller now has 15 actions.
  2. Add a method to OrderService and its interface. This service is 800 lines long.
  3. Add a method to OrderRepository.
  4. Create a new DTO.
  5. Register everything in DI and write tests.
Folders by layerControllers/OrdersController.csServices/OrderService.csInterfaces/IOrderService.csRepositories/OrderRepository.csDtos/CancelOrderDto.csFive files in five folders, mixed with other codeEach small change spreads across the projectFolders by featureFeatures/Orders/PlaceOrder.csCancelOrder.csGetMyOrders.csOne new file, next to related featuresWhat changes together sits together
In a layered structure, the code of one feature is spread across the whole project. In a vertical slice structure, it is in one place.

Now we have a few problems:

  • The code of one feature is spread out. To understand “how does cancelling an order work?”, you must open five files.
  • Shared files grow big. Each feature adds something to OrderService.
  • Changing one feature breaks another feature. They all use one service and one repository.
  • There are many conflicts (conflict) in Git. Two people who build two different features work on the same file.

The idea: each feature, one slice

Think of the layers like the layers of a cake. A layered structure cuts the cake horizontally: all controllers in one place, all services in one place. A vertical slice cuts the cake vertically: each slice is one feature and has a piece of every layer.

Web inputLogicDomainData accessPlaceOrderCancelOrderGetMyOrdersApplyCouponEndpointHandlerorder.Cancel()db.OrdersOne slice: everything for one feature
The layers still exist, but the folder structure and the main boundary of the code is the feature.

The main idea in one sentence: things that change together should sit together.

Why is this useful?

  1. Fast understanding. All the code for cancelling an order is in one file or one folder.
  2. Safe changes. A change in cancelling an order does not break placing an order. There is little shared code between them.
  3. There are no big shared classes. We no longer have an OrderService with 800 lines.
  4. Easy removal. A feature that is no longer needed? Delete its folder.

Each slice, its own approach

One important point of Vertical Slice is this: all slices do not need to look the same. Each slice picks the simplest approach that is enough for its own job.

PlaceOrderMany rulesUses the rich domain modelCancelOrderOne simple ruleCalls one domain methodGetMyOrdersRead onlyA light, direct querySlices do not call each otherSmall shared partDomain model and database access
Slices do not depend on each other. They only share a small common part.
  • Placing an order has many rules. So it uses the Order Aggregate in the domain, the one you saw in the DDD lesson.
  • Cancelling an order has one simple rule. It only calls one domain method.
  • My orders is read-only. A light query directly on the DbContext is enough. It does not need to go through the domain model.

This is the same idea as CQRS: each slice is either a command or a query.

Code

The folder structure

Shop.Api/
  Features/
    Orders/
      PlaceOrder.cs
      CancelOrder.cs
      GetMyOrders.cs
    Catalog/
      SearchProducts.cs
  Domain/
    Order.cs
  Data/
    ShopDbContext.cs
  Program.cs

A full slice: cancel order

Everything is in one file: the input, the endpoint and the logic.

namespace Shop.Api.Features.Orders;

public static class CancelOrder
{
    public sealed record Request(string Reason);

    public static void Map(IEndpointRouteBuilder app) =>
        app.MapPost("/orders/{id:guid}/cancel", HandleAsync);

    private static async Task<IResult> HandleAsync(
        Guid id, Request request, ShopDbContext db, CancellationToken ct)
    {
        var order = await db.Orders.FindAsync([id], ct);
        if (order is null)
            return TypedResults.NotFound();

        order.Cancel(request.Reason); // the business rule lives in the domain
        await db.SaveChangesAsync(ct);
        return TypedResults.NoContent();
    }
}

A read slice: my orders

namespace Shop.Api.Features.Orders;

public static class GetMyOrders
{
    public sealed record Item(Guid Id, DateTime PlacedAt, OrderStatus Status);

    public static void Map(IEndpointRouteBuilder app) =>
        app.MapGet("/customers/{customerId:guid}/orders", HandleAsync);

    private static Task<List<Item>> HandleAsync(
        Guid customerId, ShopDbContext db, CancellationToken ct) =>
        db.Orders
            .AsNoTracking()
            .Where(o => o.CustomerId == customerId)
            .Select(o => new Item(o.Id, o.PlacedAt, o.Status))
            .ToListAsync(ct);
}

Connecting the slices

var app = builder.Build();

PlaceOrder.Map(app);
CancelOrder.Map(app);
GetMyOrders.Map(app);

app.Run();

Notice a few things:

  1. We have no shared OrderService or OrderRepository. Each slice works directly with the DbContext.
  2. The business rule is still in the domain. The Cancel method in the Order class checks the rule “a shipped order cannot be cancelled”. The slice only coordinates.
  3. You do not need the MediatR library. Many teams build slices with MediatR, but Minimal API alone is enough.
One slice does not call another slice. If cancelling an order needs the logic of placing an order, move that logic to the domain or to a small shared class. If slices call each other, you build the same tangled dependencies of the layered structure again.

Repetition and shared code

The first worry of teams is: “The code gets repeated!” That is true. Vertical Slice accepts a little repetition, so that features do not get tied together.

  • Tolerate repetition that only looks the same. Two queries that look alike today may change tomorrow for different reasons.
  • Do not repeat business rules. If a rule is needed in two slices, its place is in the domain.
  • Write shared technical work once. Do logging, validation and error handling with middleware or an endpoint filter.
Vertical slices and Clean Architecture: These two are not enemies. You can keep the domain in a separate project with no dependencies, as described in the Clean Architecture lesson. Then you organize the application layer by feature instead of technical folders. Many teams use this exact mix.

Common mistakes

Mistake Why is it bad? Right way
Slices that call each other The features get tied together again. Shared logic in the domain or in a small shared class.
One big Shared folder for everything The same big OrderService, just with another name. Share only what is really the same everywhere.
A business rule inside a slice, copied into several slices The rule changes in one place and not in another. The rule in the domain, the slice only coordinates.
All slices with one heavy template Even a simple query gets a Command, a Validator and a Mapper. Each slice uses the simplest approach it needs.
Very big slices A “manage orders” slice with ten jobs is the same big service. Each slice is one clear user task.

When to use Vertical Slice?

Good fit

  • Many features that are fairly independent.
  • Features have different needs: some simple, some full of rules.
  • The team wants to add new features fast and with low risk.
  • Several people work on different features at the same time.

Bad fit or use with care

  • The team does not know the domain model yet, and rules get copied into slices.
  • The app is very small and has only a few endpoints. A simple structure is enough.
  • The features depend on each other a lot and have no clear boundaries.

Summary in six lines

  1. Organize the code in folders by feature, not by technical layer.
  2. Things that change together should sit together.
  3. Each slice picks the simplest approach it needs: the domain model for rules, a direct query for reading.
  4. Slices do not call each other. Only the domain and data access are shared.
  5. Accept a little repetition that only looks the same, but never repeat business rules.
  6. This approach can be combined with Clean Architecture and CQRS.