Skip to main content
Article complete

Get one like this every Tuesday at 7 PM IST.

codewithmukesh
← Back to blog
dotnet architecture 15 min read Lesson 117/152 New

Vertical Slice vs Clean Architecture in .NET 10 - What I Use

Vertical Slice vs Clean Architecture in .NET 10: real file counts, a decision matrix, and the pragmatic hybrid I recommend, with architecture tests.

Vertical Slice vs Clean Architecture in .NET 10: real file counts, a decision matrix, and the pragmatic hybrid I recommend, with architecture tests.

dotnet architecture webapi-course

vertical slice architecture clean architecture vertical slice vs clean architecture clean architecture dotnet dotnet 10 asp.net-core minimal-api feature folders software architecture project structure ef core 10 architecture tests netarchtest fluentvalidation domain model problem details modular monolith web-api dotnet-webapi-zero-to-hero-course

Mukesh Murugan
Mukesh Murugan
Solutions Architect · Microsoft MVP
Chapter 117 of 152
View course →

.NET Web API Zero to Hero Course

From dotnet new to docker push - REST, EF Core 10, auth, caching, Clean Architecture, observability. 152 hands-on lessons, source on GitHub.

Vertical Slice Architecture groups your code by feature, while Clean Architecture groups it by layer. For most .NET 10 Web APIs, I do not use either of them in its pure form. I keep the Domain and Infrastructure projects from Clean Architecture, remove the Application project, and place every feature (its endpoint, request, validator, and handler) in a single file inside the API project.

In this article, I will walk you through this hybrid approach. I will build the same Product API in all three layouts, count the files that each one makes you touch for a new feature, and go through the rules and tests that keep the hybrid maintainable.

You can find the complete source code in the GitHub repo. It has three solutions named CleanArchitecture, VerticalSlice, and Hybrid. I built and tested the hybrid on .NET SDK 10.0.401 with EF Core 10.0.12. Let’s get started.

Prerequisites

As usual, here is what you need to follow along.

  • .NET 10 SDK
  • PostgreSQL running on localhost:5432, with the connection string set in appsettings.json
  • A basic understanding of Minimal APIs and EF Core

What Is the Difference Between Clean Architecture and Vertical Slice Architecture?

Clean Architecture splits an application into layers (usually Domain, Application, Infrastructure, and Presentation), where the dependencies only point inward toward the business rules. Robert C. Martin described it in 2012, and Microsoft’s common web application architectures guide shows the same idea for ASP.NET Core.

Vertical Slice Architecture organizes the code around features instead of layers. Everything that one use case needs, like the endpoint, the request, the handler, and the response, sits together in one folder or even in one file.

Clean ArchitectureVertical Slice Architecture
Code is grouped byTechnical layerFeature / use case
Boundaries enforced byProject references (the compiler)Team discipline
Adding one feature touches3-4 projects1 folder
Business rules liveIn the Domain projectWherever the slice author puts them
Best atComplex domains, long-lived systemsCRUD-heavy APIs, fast-moving products
Main riskCeremony and indirectionDuplicated or scattered rules

I would not call either of them the correct one, since each of them runs into a different kind of problem as the application grows.

What Does Clean Architecture Look Like in the Sample?

The CleanArchitecture solution has four projects, and the dependencies between them follow the usual Clean Architecture direction.

CleanArchitecture/ 5 dirs · 0 files
CleanArchitecture/
CleanArchitecture.Domain/ # Product entity
CleanArchitecture.Application/ # commands, queries, handlers, IProductRepository
CleanArchitecture.Infrastructure/ # AppDbContext, ProductRepository
CleanArchitecture.Api/ # ProductEndpoints.cs, Program.cs

A handler in the Application project talks to an IProductRepository interface, and the actual implementation lives in the Infrastructure project. The good part about this setup is that the Application project references only the Domain project, so its handlers cannot use the DbContext or anything else from the Infrastructure and Api projects. If someone tries to do that, the code does not compile.

The downside is the number of places you have to visit to follow a single request. You would open the endpoint in Api, the command, the handler, and the interface in Application, and finally the repository in Infrastructure. I have covered the complete four-layer build in my Clean Architecture guide.

What Does Vertical Slice Architecture Look Like in the Sample?

The VerticalSlice solution has just one project, and each feature in it is a single file inside Features/Products. If you open CreateProduct.cs, you will see that the request, the response, the handler, and the endpoint mapping all live together, and that the handler uses the DbContext directly. To read a feature, you only have to open this one file. There is no repository interface with just one implementation, and you do not have to switch between projects.

However, the Product entity in this sample has a public setter on every property and no behavior of its own. This means that a rule like “price must be greater than zero” has no specific place to live, so it gets copied into CreateProduct, UpdateProduct, and every other handler that touches the price. This is the main problem with pure vertical slices.

How Many Files Does Adding One Feature Touch?

This is where you will feel the difference in your day-to-day work. I counted the files that you have to create or edit to add one new endpoint in each sample, assuming that the endpoint needs a new data access method (like “get product by id”).

Clean ArchitectureVertical SliceHybrid
Projects in the solution413
.cs files in the sample16817
Files to add one endpoint5 (query, handler, repository interface, repository, endpoints file)2 (feature file, Program.cs)1 (feature file)
Projects touched311 (2 if a new business rule is needed)

The hybrid has one file more than the Clean Architecture sample, but six of its files belong to the one-time setup in Shared. The last two rows are the ones that matter the most, because that is the cost you pay every time you add a feature.

When I first built these samples, I also timed myself adding the same CRUD entity to both. It took me about 25 minutes in the Clean Architecture version and about 8 minutes in the vertical slice version. To be fair, a part of that speed came from the fact that the vertical slice version skipped business rules entirely. The hybrid keeps most of this speed, and it still gives the business rules a proper place to live.

The Hybrid I Recommend: Domain Project Plus Feature Folders

The layout that I recommend for new .NET 10 APIs keeps the entities in a Domain project, the EF Core code in an Infrastructure project, and the endpoints in feature folders, without an Application project.

Hybrid/ 10 dirs · 6 files
Hybrid/
src/
Hybrid.Domain/ # entities and business rules, zero dependencies
Hybrid.Infrastructure/ # AppDbContext, EF Core configurations
Hybrid.Api/
Features/
Products/
CreateProduct.cs
GetProducts.cs
GetProductById.cs
ChangeProductPrice.cs
RemoveStock.cs
Shared/ # technical code only
Program.cs
tests/
Hybrid.ArchitectureTests/

Why Drop the Application Project?

Once the endpoint and the handler live in the same feature file, there are only two things that can happen to the Application project. It either becomes an empty project that only passes calls through, or the feature folders move into it and it starts depending on ASP.NET Core, which breaks the very dependency rule that the project was created to protect. So, in the hybrid, the feature folders live in the Api project, and the handlers use AppDbContext directly.

I would keep the Application project when more than one app runs the same use cases. For example, if an API and a background worker both need the “place order” logic, that logic has to live in a project that both of them can reference. This is the setup that my Clean Architecture template is built for.

What Goes in the Domain Project?

The Domain project is the part of Clean Architecture that I think is worth keeping. It has no NuGet packages and no project references, and the business rules live on the entity itself.

public sealed class Product
{
private Product()
{
}
public Guid Id { get; private set; }
public string Name { get; private set; } = string.Empty;
public string Description { get; private set; } = string.Empty;
public decimal Price { get; private set; }
public int Stock { get; private set; }
public DateTime CreatedAt { get; private set; }
public uint Version { get; private set; }
public static Product Create(string name, string description, decimal price, int stock)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new DomainException("Name is required.");
}
if (price <= 0)
{
throw new DomainException("Price must be greater than zero.");
}
if (stock < 0)
{
throw new DomainException("Stock cannot be negative.");
}
return new Product
{
Id = Guid.CreateVersion7(),
Name = name,
Description = description,
Price = price,
Stock = stock,
CreatedAt = DateTime.UtcNow
};
}
public void RemoveStock(int quantity)
{
if (quantity <= 0)
{
throw new DomainException("Quantity must be greater than zero.");
}
if (quantity > Stock)
{
throw new DomainException($"Cannot remove {quantity} items. Only {Stock} left in stock.");
}
Stock -= quantity;
}
}

Since the setters are private, no handler can set Price = -5 on a product. And since every feature that changes the stock has to go through RemoveStock, the rule for it lives in just one place.

The Version property is a concurrency token. Without it, two requests can read the same stock value at the same time, both can pass the check, and the second save overwrites the first one. Before I added the token, six parallel requests to remove one item each from a product with 3 in stock gave me four successful responses in one of my runs. The property is configured with IsRowVersion(), which the Npgsql provider maps to the xmin system column of PostgreSQL. Now EF Core throws a DbUpdateConcurrencyException when the row has changed since it was read, and an exception handler in Shared returns a 409 Conflict so that the client can retry. I have explained this in detail in my article on optimistic concurrency in EF Core.

What Does a Feature Slice Look Like?

Here is the CreateProduct.cs file from the hybrid. The request, the response, the validator, the endpoint, and the handler sit together, and the handler calls the domain method instead of setting the properties by itself.

public static class CreateProduct
{
public sealed record Request(string Name, string Description, decimal Price, int Stock);
public sealed record Response(Guid Id, string Name, string Description, decimal Price, int Stock, DateTime CreatedAt);
internal sealed class Validator : AbstractValidator<Request>
{
public Validator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Description).MaximumLength(1000);
}
}
internal sealed class Endpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app) =>
app.MapPost("/api/products", Handle)
.AddEndpointFilter<ValidationFilter<Request>>()
.ProducesValidationProblem()
.WithName(nameof(CreateProduct))
.WithTags("Products");
}
internal static async Task<Created<Response>> Handle(Request request, AppDbContext db, CancellationToken cancellationToken)
{
var product = Product.Create(request.Name, request.Description, request.Price, request.Stock);
db.Products.Add(product);
await db.SaveChangesAsync(cancellationToken);
var response = new Response(product.Id, product.Name, product.Description, product.Price, product.Stock, product.CreatedAt);
return TypedResults.Created($"/api/products/{product.Id}", response);
}
}

The validator uses FluentValidation 12.1.1, and it is connected to the endpoint through ValidationFilter<TRequest>, which is a generic endpoint filter in Shared that returns a validation problem response if anything fails.

You might have noticed that there is no mediator in this slice. The handler is a static method that is passed straight to MapPost, and Minimal APIs already take care of dependency injection and model binding for it. If you want pipeline behaviors, I have covered a small dispatcher in CQRS without MediatR.

How Do Slices Get Registered?

Each slice has a nested Endpoint class that implements a small interface from the Shared folder.

public interface IEndpoint
{
void MapEndpoint(IEndpointRouteBuilder app);
}

At startup, an AddEndpoints extension method scans the assembly and registers every IEndpoint implementation, and MapEndpoints calls MapEndpoint on each of them. This is the reason why adding a feature touches only one file in the hybrid.

Where Does Validation Stop and the Domain Start?

The hybrid has two kinds of checks, and it works well only when you keep them separate from each other.

  • Input validation checks the shape of the request, like required fields and maximum lengths. It is done by the validator of the slice, and it runs before your handler is called. An empty name returns a 400 with the title “One or more validation errors occurred.”
  • Business rules check what the business allows, like a price above zero or enough stock. They live on the entity.

A price of zero passes the input validation, and then it is rejected by the domain rule. The DomainExceptionHandler in Shared converts this exception into a Problem Details response, which is the approach I have covered in my guide on global exception handling.

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Business rule violated",
"status": 400,
"detail": "Price must be greater than zero."
}

A note on exceptions. I have used exceptions for the business rules only to keep the sample small and easy to follow. A broken business rule is an expected outcome, so the better way is to return it as a value using the Result pattern. I have explained this in my article on the Result pattern in .NET 10, and I would recommend that you use it in a real application.

Where Does Shared Logic Go?

Most articles on vertical slices tell you to put the shared code in a Shared folder, and they do not go any further than that. Without some rules around it, this folder slowly turns into a new Services layer, and you end up with a layered application again without really planning for it. Here are the rules that I recommend.

  1. Business rules never go in Shared. If the code has an if condition about what the business allows, it belongs on a Domain entity.
  2. Shared holds technical code only. In the sample, that is IEndpoint, the endpoint discovery, ValidationFilter, the two exception handlers, and a paging helper.
  3. Features never call other features. If GetProducts and a future GetLowStockProducts need a similar query, I would simply copy it, and extract it only when a third feature needs the same query.
  4. No Services/ folder in Shared. If you see a ProductService inside Shared, it usually means that the feature folders are slowly turning back into layers.
  5. Enforce as much of this as you can with tests. It is easy to miss a violation during a code review, but it is very hard to ignore a failing test run in your pipeline.

I know that the duplication in rule 3 bothers a lot of developers, but I think it is the better choice here. When two slices have a similar five-line query, each of them can change on its own. When one shared query is used by six features, you cannot change it without checking all six of them.

How Do You Keep the Hybrid Maintainable Over Time?

Clean Architecture enforces its boundaries with project references. In the hybrid, you get that protection only for the Domain and Infrastructure projects. Inside the Api project, there is nothing that stops Features/Orders from referencing something in Features/Products.

This is where architecture tests help. The sample has four of them, written with NetArchTest 1.3.2 and xUnit v3. They check that Domain does not depend on any other project or on EF Core and ASP.NET Core, that Infrastructure does not depend on Api, that Shared does not depend on any feature, and that one feature folder does not depend on another. To confirm that the last rule really works, I added a temporary Features/Orders/PlaceOrder.cs file that referenced GetProductById.Response. The project still compiled, but the test run failed as expected.

Hybrid.ArchitectureTests.ArchitectureTests.Features_Should_Not_Depend_On_Other_Features [FAIL]
Violations: Hybrid.Api.Features.Orders.PlaceOrder
Hybrid.ArchitectureTests Total: 4, Errors: 0, Failed: 1, Skipped: 0, Not Run: 0

There is one limit that you should be aware of. The feature test compares folders like Features.Products and Features.Orders, so it does not catch two slices inside the same folder referencing each other, and no test can tell whether a class in Shared contains a business rule. I confirmed both by adding such code to the sample, and all four tests still passed. So, those cases still have to be checked during code reviews.

When Does the Hybrid Stop Working?

I have not reached the limits of this layout yet, so please treat these as warning signs to watch for.

  • Shared keeps growing. If it has more files than your biggest feature folder, there is probably some business logic sitting in there.
  • Two apps need the same use case. This is when I would bring back an Application project for those use cases.
  • Teams start getting in each other’s way. The next step is a modular monolith, where each module gets its own Domain, Infrastructure, and feature folders.

I started fullstackhero, my open-source .NET starter kit, on Clean Architecture back in 2021, and today it runs on .NET 10 as a modular monolith with vertical slices inside each module.

Decision Matrix: Which Architecture Should You Pick?

Your situationPick
Small CRUD API, few business rules, 1-3 developersVertical slices (single project)
Typical product API with real business rulesHybrid
Same use cases called from an API and a worker or gRPC serviceClean Architecture (keep the Application project)
Complex domain, many invariants, long-lived system, large teamClean Architecture, or hybrid modules inside a modular monolith
Several teams owning separate business areasModular monolith with slices inside each module

My Take

I have run a mix of Clean Architecture and feature folders in 3 production projects, and the layout in this article is what I settled on after that experience. I still agree with Clean Architecture that the business rules need a protected place of their own. What I no longer agree with is the number of projects that a single feature has to pass through because of it.

My default for a new .NET 10 Web API is three projects: Domain, Infrastructure, and Api with feature folders. I bring back the Application project only when a second app needs the same use cases, and I add the architecture tests on the very first day, because it is much easier to enforce the rules before anyone has had a chance to break them.

Key Takeaways

  • Clean Architecture groups code by layer and uses the compiler to enforce the boundaries. Vertical Slice Architecture groups code by feature and relies on the discipline of the team.
  • In the sample, adding one endpoint touches 5 files across 3 projects in Clean Architecture, 2 files in vertical slices, and 1 file in the hybrid.
  • The hybrid keeps a zero-dependency Domain project, an Infrastructure project, and feature folders in Api, without an Application project.
  • With architecture tests, a violation of the project or feature folder boundaries fails the test run in CI.

FAQ

Can you combine Clean Architecture and Vertical Slice Architecture?

Yes, and for most .NET Web APIs it is the option I would recommend. You can keep a Domain project for the business rules and an Infrastructure project for EF Core, and organize the API project into feature folders.

Is Vertical Slice Architecture better than Clean Architecture?

Neither of them is better in general. Vertical slices are faster to work with because a feature lives in one place, but the business rules tend to get duplicated across handlers. Clean Architecture protects the business rules with project boundaries, but it adds ceremony to every feature.

Where does shared logic go in Vertical Slice Architecture?

Business rules should go on the Domain entities. The Shared folder should only hold technical code such as endpoint registration, validation filters, exception handlers, and paging helpers.

Does Vertical Slice Architecture need MediatR?

No. In .NET 10, a Minimal API endpoint can call a static handler method directly, and the framework takes care of dependency injection and model binding. A mediator is worth adding only if you want pipeline behaviors such as logging or transactions.

When should I keep the Application project in a hybrid architecture?

You should keep it when more than one application runs the same use cases, for example a Web API and a background worker that both place orders. If only the API uses them, the Application project adds work to every feature without a real benefit.

How do I enforce architecture rules in a .NET project?

You can use architecture tests with a library such as NetArchTest. Assert that the Domain project has no dependency on EF Core or ASP.NET Core and that the feature namespaces do not reference each other, and run the tests in CI.

Summary

Clean Architecture protects your business rules, but it makes every feature pass through several projects. Vertical slices keep a feature together, but they do not give the business rules a place of their own. The hybrid takes the useful part from each of them.

Which layout does your team use today, and what made you pick it? Let me know in the comments below.

Happy Coding :)

Source code Open on GitHub

Grab the source code.

Get the full implementation. Drop your email for instant access, or skip straight to GitHub.

Skip - go straight to GitHub
View all articles

What's your take?

Push back, share a war story, or ask the obvious question someone else is wondering. I read every comment.

View on GitHub

Weekly .NET tips · free

Newsletter

stay ahead in .NET

One email every Tuesday at 7 PM IST. One topic, deep. The week's articles. No filler.

Tutorials Architecture DevOps AI
Join 9,735 developers · Delivered every Tuesday
Privacy notice 30s read

Cookies, but only the useful ones.

I use cookies to understand which articles get read and which CTAs actually work. No third-party advertising trackers, ever. Read the privacy policy →