Vertical Slice Architecture: A Practical Alternative to Clean Architecture


Clean Architecture and Domain-Driven Design have dominated architectural discussions for years, prescribing careful layer separation and technical abstractions. Vertical Slice Architecture takes a different approach: organize code by feature rather than technical concern, minimizing coupling between features rather than between layers.

The Core Concept

In traditional layered architecture, you organize code by technical responsibility: controllers, services, repositories, models. Every feature touches every layer, creating dependencies that spread horizontally across the codebase.

Vertical Slice Architecture organizes by feature instead. Each feature is a vertical slice through all technical layers, containing everything needed to implement that specific behavior. The “Add Product” feature contains its endpoint, validation, business logic, and data access—all together, isolated from other features.

This isn’t just folder organization. It’s a fundamental shift in how you think about dependencies and coupling. Instead of trying to share code across features, you accept duplication and optimize for feature independence.

Why This Matters

The primary benefit is reduced coupling. When features are independent slices, changes to one feature don’t ripple through the codebase affecting others. You can modify, extend, or remove a feature with confidence that you’re not breaking something unrelated.

Development velocity increases because developers can work on features without navigating complex architectural abstractions. You don’t need to understand the entire domain model or service layer to add a new feature. You just need to understand that one vertical slice.

Testing becomes simpler. Each feature can be tested in isolation without mocking layers of abstraction. Integration tests for a feature only need to exercise that feature’s specific slice, not the entire application.

Practical Implementation

Here’s what this looks like in a typical web application. Instead of organizing like this:

/Controllers
  ProductController.cs
  OrderController.cs
/Services
  ProductService.cs
  OrderService.cs
/Repositories
  ProductRepository.cs
  OrderRepository.cs

You organize by feature:

/Features
  /AddProduct
    Endpoint.cs
    Validator.cs
    Command.cs
    Handler.cs
  /GetProductDetails
    Endpoint.cs
    Query.cs
    Handler.cs
  /PlaceOrder
    Endpoint.cs
    Command.cs
    Handler.cs

Each feature folder is self-contained. Everything needed to implement “AddProduct” lives in that folder. There’s no service layer, no repository abstraction to find elsewhere in the codebase.

The MediatR Pattern

Jimmy Bogard popularized Vertical Slice Architecture in the .NET world using MediatR. Each feature is implemented as a command or query with a corresponding handler.

The AddProduct feature might look like this:

namespace Features.AddProduct;

public record Command(string Name, decimal Price) : IRequest<int>;

public class Validator : AbstractValidator<Command>
{
    public Validator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Price).GreaterThan(0);
    }
}

public class Handler : IRequestHandler<Command, int>
{
    private readonly AppDbContext _db;

    public Handler(AppDbContext db) => _db = db;

    public async Task<int> Handle(Command request, CancellationToken ct)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price
        };

        _db.Products.Add(product);
        await _db.SaveChangesAsync(ct);

        return product.Id;
    }
}

The endpoint is minimal:

public class Endpoint : EndpointBase
{
    public override void ConfigureEndpoint()
    {
        Post("/products");
    }

    public override async Task HandleAsync(Command req, CancellationToken ct)
    {
        var id = await Mediator.Send(req, ct);
        await SendAsync(new { id }, 201, ct);
    }
}

Everything related to adding a product is visible in one place. There’s no hunting through layers to understand the implementation.

Handling Shared Concerns

The obvious question is: what about code that genuinely needs to be shared? Database context, authentication, logging, common utilities?

These live outside features as infrastructure concerns. The difference is recognizing what’s genuinely shared infrastructure versus what’s attempted code reuse across features that aren’t actually related.

Database context is shared infrastructure. A “ProductService” used by multiple features is attempted abstraction that Vertical Slice Architecture rejects. If multiple features need product-related logic, duplicate that logic in each feature slice.

This seems wasteful, but duplication is vastly cheaper than coupling. Duplicated code can diverge independently as features evolve. Shared abstractions require coordination and create brittleness.

When Features Actually Need to Interact

Sometimes features legitimately depend on each other. Placing an order might need to check product inventory. How do you handle this without creating coupling?

One approach is domain events. The “UpdateInventory” feature publishes an event when inventory changes. The “PlaceOrder” feature subscribes to those events. The features communicate through events rather than direct dependencies.

Another approach is to query infrastructure directly. The PlaceOrder handler queries the database for current inventory rather than calling through an inventory service. This couples to database schema rather than to another feature’s implementation, which is often acceptable.

The key insight is that feature independence is the optimization target, not code reuse. If features need to interact, prefer loose coupling through events or shared data over tight coupling through shared code.

Comparing to Clean Architecture

Clean Architecture optimizes for separation of concerns and testability through abstraction. Business logic lives in a core domain layer, completely isolated from infrastructure concerns through dependency inversion.

This creates beautiful architecture diagrams but often results in significant complexity. Every feature requires touching multiple layers, coordinating across abstractions, and maintaining mappings between layer-specific types.

Vertical Slice Architecture accepts infrastructure dependencies in favor of feature independence. A feature handler might directly use Entity Framework, depend on a specific database schema, and reference infrastructure libraries. This violates Clean Architecture principles but dramatically simplifies implementation.

For small to medium applications, the complexity cost of Clean Architecture often exceeds its benefits. Vertical Slice Architecture provides a simpler path to maintainability through feature independence rather than layer isolation.

The Duplication Trade-off

Vertical Slice Architecture permits and even encourages duplication. If two features both need to validate email addresses, they each implement that validation independently rather than sharing a validator.

This feels wrong to developers trained to eliminate duplication. But consider the actual cost. Duplicated validation logic is simple and stable. The overhead of maintaining two copies is minimal.

Contrast this with shared validation abstraction. Now changes to validation logic require considering all consumers. The abstraction becomes a coordination point, slowing development and creating fragility.

Duplication is not inherently bad. Inappropriate coupling is bad. Vertical Slice Architecture trades duplication for independence, and that trade is often favorable.

When This Approach Works Best

Vertical Slice Architecture shines for applications with many independent features. Business applications, APIs, and microservices are excellent candidates.

The approach works less well for applications where features share complex domain logic that genuinely needs consistency. If your application is primarily implementing one large complex domain model, Clean Architecture’s emphasis on that domain layer might be more appropriate.

The pattern also assumes features can be relatively independent. If every feature interacts heavily with every other feature, vertical slices become difficult to maintain. At that point, explicit domain modeling might be necessary.

Migration Strategy

You don’t need to rewrite everything to adopt Vertical Slice Architecture. Start implementing new features as vertical slices while leaving existing layered code intact.

Over time, as features need modification, gradually refactor them into vertical slices. This incremental migration lets you evaluate the approach without committing to wholesale architectural change.

Teams I’ve worked with using this migration strategy typically find that once they implement a few features as vertical slices, developers strongly prefer the approach and push to accelerate migration.

Tooling and Frameworks

MediatR in .NET provides excellent support for this pattern. FastEndpoints extends this with minimal endpoint definitions that integrate cleanly with MediatR handlers.

In other ecosystems, the pattern translates well even without specific libraries. Organize features into folders, use whatever request/response pattern makes sense for your framework, and maintain the key principle: everything for one feature lives together.

The Cognitive Load Advantage

Perhaps the biggest advantage is reduced cognitive load. Developers can work on a feature without loading the entire application architecture into their heads.

Need to understand how “AddProduct” works? Open that folder. Everything you need is there. Need to modify “AddProduct”? Make changes in that folder with confidence you’re not affecting other features.

This localization of understanding accelerates onboarding and reduces the expertise required to make changes safely. Junior developers can contribute meaningfully to features without understanding the entire system.

Practical Limitations

Vertical Slice Architecture isn’t perfect. Very complex features might still benefit from internal layering within the slice. The pattern doesn’t eliminate architectural thinking; it shifts where that thinking happens.

Database schema remains a shared concern that requires coordination across features. Migrations affect all features, and schema changes can impact multiple slices. This is inherent to shared data persistence and not something Vertical Slice Architecture solves.

The pattern also requires discipline to maintain feature independence. It’s easy to start creating dependencies between features, especially when under time pressure. This defeats the primary benefit of the approach.

Conclusion

Vertical Slice Architecture offers a pragmatic alternative to layered architectures and Clean Architecture. By organizing code around features rather than technical layers, it reduces coupling, accelerates development, and simplifies maintenance.

The approach trades code reuse for feature independence. That trade feels uncomfortable to developers trained to eliminate duplication, but the benefits typically outweigh the costs for applications with many independent features.

Consider trying this pattern for your next project or the next few features you build. You might find, as many teams have, that the simplicity and velocity gains are substantial enough to make this your default architectural approach.