The Best Way to Structure .NET Projects: Clean Architecture + Vertical Slices
A friendly guide to structuring .NET projects by mixing Clean Architecture with Vertical Slices, with diagrams, code, tables, and simple, beginner-ready advice.
A tiffin dabbawala and a thali kitchen
Imagine the famous dabbawalas of Mumbai. Every day they carry thousands of lunch boxes across the city and almost never make a mistake. How? They follow two simple ideas at the same time.
First, there is a strict, protected rule at the center: the right box must reach the right person. Nothing is allowed to break that rule. The trains may change, the bicycles may change, the streets may change, but the promise in the middle never changes.
Second, each dabbawala owns one full route from start to finish. He picks up, sorts, carries, and delivers his own boxes. He does not wait in a long queue behind other workers. He handles his slice of the city on his own.
Good .NET project structure works the same way. Clean Architecture gives you the strict, protected rule in the center, so your important business logic never depends on the database or the web framework. Vertical Slice Architecture lets each feature own its full path from start to finish, like one dabbawala owning one route.
In this article, you will learn how to combine both so your code stays safe in the middle and easy to change at the edges.
Two ideas, quickly
Before we mix them, let us be clear about what each idea means on its own.
Clean Architecture organises code by technical concern and points all dependencies inward. The pure business rules sit in the center. Databases, web frameworks, and email services sit on the outside. The outside knows about the inside, but the inside never knows about the outside. This protects your rules from changes in tools.
Vertical Slice Architecture (VSA) organises code by feature. Instead of one big folder of controllers, another of services, and another of repositories, you put everything for one feature, such as "Cancel Order", into a single slice: its endpoint, its request, its handler, and its data access, all in one place.
Notice the arrows. The Domain in the center has zero arrows leaving it. It depends on nothing. That is the heart of Clean Architecture.
Why combine them?
People often argue about which is better. The honest answer is that they solve different problems, so you do not have to choose.
Clean Architecture is great at protecting complex rules, but on its own it can spread one feature across many folders. To add a single "Cancel Order" feature, you might edit a controller here, a service there, an interface somewhere else, and a repository far away. New developers get lost.
Vertical Slices fix that by keeping a feature together. But pure VSA, with no center, can let business rules leak into every slice and get copied around.
So we combine them: keep the protected core from Clean Architecture, and slice the outer feature code by feature using VSA. The core stays pure and tested. The features stay together and easy to find.
| Concern | Clean Architecture alone | Vertical Slices alone | Combined |
|---|---|---|---|
| Protect business rules | Strong | Weak | Strong |
| Find all code for a feature | Hard | Easy | Easy |
| Onboarding new developers | Slower | Faster | Faster |
| Risk of duplicated logic | Low | Higher | Low to medium |
| Good for complex domains | Yes | Sometimes | Yes |
The project layout
A clean, friendly starting point uses four projects. Each one has a clear job.
The four-project solution
Steps
Web
Hosts feature slices and endpoints
Application
Use cases and slice handlers
Domain
Pure entities and rules, no dependencies
Infrastructure
EF Core, email, files, external APIs
Here is how the folders look on disk. The big change from classic Clean Architecture is the Features folder, which holds one slice per feature instead of separate Services and Controllers folders.
src/
Domain/ // pure core: entities, value objects, rules
Orders/
Order.cs
OrderStatus.cs
Application/ // use cases, grouped as vertical slices
Features/
Orders/
CancelOrder/
CancelOrderCommand.cs
CancelOrderHandler.cs
CancelOrderValidator.cs
PlaceOrder/
PlaceOrderCommand.cs
PlaceOrderHandler.cs
Abstractions/
IOrderRepository.cs // interface lives here, inside
Infrastructure/ // EF Core, email, the outside world
Persistence/
AppDbContext.cs
OrderRepository.cs // implements the interface above
Web/ // ASP.NET Core host
Endpoints/
OrderEndpoints.cs
Program.csThe key rule never changes: Domain depends on nothing, and Application depends only on Domain. The Infrastructure and Web projects sit on the outside and depend inward.
A feature as one slice
Let us build the "Cancel Order" feature as a single slice. Everything you need lives close together, but the pure rule still lives in the Domain.
First, the Domain entity. It holds the real rule: you cannot cancel an order that is already shipped. This rule has no idea a database or web request exists.
namespace Domain.Orders;
public sealed class Order
{
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
public void Cancel()
{
if (Status == OrderStatus.Shipped)
{
throw new InvalidOperationException(
"A shipped order cannot be cancelled.");
}
Status = OrderStatus.Cancelled;
}
}Next, the slice in the Application layer. It is a small command plus a handler. Notice it depends on an interface, IOrderRepository, not on EF Core. The interface lives inside the Application layer, so the dependency still points inward.
namespace Application.Features.Orders.CancelOrder;
public sealed record CancelOrderCommand(Guid OrderId);
public sealed class CancelOrderHandler
{
private readonly IOrderRepository _orders;
public CancelOrderHandler(IOrderRepository orders) => _orders = orders;
public async Task HandleAsync(CancelOrderCommand command, CancellationToken ct)
{
var order = await _orders.GetByIdAsync(command.OrderId, ct)
?? throw new KeyNotFoundException("Order not found.");
order.Cancel(); // the pure rule runs here
await _orders.SaveAsync(order, ct);
}
}Finally, the endpoint in the Web layer. With minimal APIs you do not even need a controller. This keeps the slice thin.
app.MapPost("/orders/{id}/cancel", async (
Guid id, CancelOrderHandler handler, CancellationToken ct) =>
{
await handler.HandleAsync(new CancelOrderCommand(id), ct);
return Results.NoContent();
});Read that route carefully: POST /orders/{id}/cancel. All three pieces, the command, the handler, and the endpoint, belong to the same feature. A new teammate can open the CancelOrder folder and understand the whole thing in minutes.
How a request flows
Let us follow one request all the way through. A user clicks "Cancel" in the app. Here is the journey.
The important detail is the direction. The web edge calls inward. The pure Order.Cancel() rule sits at the deepest point and knows nothing about HTTP or EF Core. If you swap the database tomorrow, this rule does not change at all.
Where do the layers actually point?
A common worry is, "If the slice has an endpoint, a handler, and a repository, does it break the dependency rule?" No, and the next diagram shows why. The interface sits inside, and the database class on the outside implements it.
This is called dependency inversion, and it is the glue that lets Clean Architecture and Vertical Slices live together. The slice feels self-contained, but the actual database code stays in Infrastructure, on the outside, where it belongs.
Wiring it up in Program.cs
The Web project connects everything at startup. This is the one place that knows about all the layers, because its whole job is to assemble them.
var builder = WebApplication.CreateBuilder(args);
// Infrastructure: the real database implementation
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
// Application: register slice handlers
builder.Services.AddScoped<CancelOrderHandler>();
builder.Services.AddScoped<PlaceOrderHandler>();
var app = builder.Build();
app.MapOrderEndpoints(); // extension method that maps each slice
app.Run();Notice the line AddScoped<IOrderRepository, OrderRepository>(). The inside asked for an interface; the outside provides the real class. This single line is where dependency inversion becomes real at runtime.
A note on MediatR and tools
For years, the popular way to build slice handlers was a library called MediatR. In 2025 it moved to a commercial license, with only a free Community edition for smaller companies, non-profits, and learning. The same happened to MassTransit and AutoMapper.
This is not a problem. The patterns here do not need any of them. A "handler" is just a small class with a method, exactly like the CancelOrderHandler above. If you want a mediator-style dispatch, you can write a tiny one yourself, use minimal API endpoints directly, or pick an open-source library such as Paramore Brighter.
| Tool | License in 2026 | Do you need it? |
|---|---|---|
| Plain handler classes | Free, built in | This is enough for most apps |
| MediatR | Commercial, free Community tier | Optional, not required |
| MassTransit | Commercial (v8 last free) | Only for advanced messaging |
| Minimal API endpoints | Free, built in | Great for thin slices |
The lesson: structure is about how you organise code, not about which paid library you install.
When to add the deep core
You do not always need all four projects on day one. Structure should grow with the problem.
Grow your structure
Steps
Start
One project, simple CRUD
Slices
Group code by feature in folders
Add core
Move pure rules into Domain
Split projects
Separate Application and Infrastructure
A simple guide: if your app is mostly reading and writing data with few rules, stay light. As real business rules appear, like "a shipped order cannot be cancelled" or "a refund needs manager approval", give those rules a protected home in the Domain. That is the moment Clean Architecture starts to earn its keep.
Common mistakes to avoid
A few traps catch many teams. Watch out for these.
- Putting EF Core in the Domain. The moment your pure entity uses a database attribute, the rule is no longer pure. Keep persistence in Infrastructure.
- Sharing one giant handler base class with lots of logic. Slices should be independent. Shared logic belongs in the Domain or in small, clear helpers, not in a heavy base class.
- Slicing too early. For a tiny app, ten slice folders can feel like more ceremony than value. Start simple.
- Letting one slice call another slice's handler. If two features need the same rule, that rule belongs in the Domain, not copied between slices.
Keep the center pure, keep the slices independent, and most other choices become easy.
References and further reading
- Common web application architectures — Microsoft Learn
- Vertical Slice Architecture — Jimmy Bogard
- N-Layered vs Clean vs Vertical Slice Architecture — Anton Dev Tips
- MediatR and MassTransit Going Commercial — Milan Jovanović
- Vertical Slice Architecture in ASP.NET Core — NDepend Blog
Quick recap
- Clean Architecture points all dependencies inward to a pure business core that has no database or framework code.
- Vertical Slice Architecture groups all the code for one feature into a single slice, so features are easy to find and change.
- You can combine them: keep the protected core, and slice the outer feature code by feature.
- A sensible layout is four projects: Domain, Application, Infrastructure, and Web, with the
Featuresfolder holding one slice each. - Interfaces live inside the Application layer; the database class implements them from Infrastructure, so dependencies still point inward.
- You do not need MediatR or any paid tool. A handler is just a small class. Use plain classes or minimal APIs.
- Grow your structure with the problem. Start light, and add the deep core only when real business rules appear.
Related Posts
N-Layered vs Clean vs Vertical Slice Architecture in .NET
A simple, friendly guide comparing N-layered, Clean, and Vertical Slice architecture in .NET, with diagrams, code, tables, and clear advice on when to pick each one.
Clean Architecture Folder Structure in .NET: A Simple Guide
Learn how to organise a .NET solution with Clean Architecture. Understand the Domain, Application, Infrastructure, and Presentation layers, the dependency rule, and the exact folders, with diagrams and examples.
Building a Modular Monolith With Vertical Slice Architecture in .NET
Learn to build a modular monolith using vertical slice architecture in .NET. Simple words, real-life analogy, diagrams, tables, and clean C# code examples.
Vertical Slice Architecture in .NET: The Easy Guide
Learn Vertical Slice Architecture in .NET in simple words. Organise code by feature instead of by layer, with diagrams, real examples, a comparison with Clean Architecture, and when to use each.
Understanding Microservices: Core Concepts and Benefits for .NET
A beginner-friendly guide to microservices in .NET: what they are, the core ideas behind them, their real benefits and trade-offs, and when to use them.
Where Vertical Slices Fit Inside the Modular Monolith
A simple guide to how vertical slices live inside the modules of a modular monolith in .NET, with diagrams, code, tables, and everyday examples.