DEV Community

Cover image for N-tier, Clean Architecture and Vertical Slice: A Practical Comparison for .NET
Giorgi Mgebrishvili
Giorgi Mgebrishvili

Posted on Originally published at Medium

N-tier, Clean Architecture and Vertical Slice: A Practical Comparison for .NET

How each one organizes code, where they overlap, and what to consider when choosing between them.

Cover photo by Anirban Sengupta on Unsplash

Most .NET developers meet these three architectures in the same order: N-tier in their first projects, Clean Architecture when a team or a template introduces it, and Vertical Slice when someone mentions it in a code review or a conference talk. Each is presented as an improvement on the one before. In practice they answer slightly different questions, and the right choice depends more on the project than on which one is newest.

This article explains what each architecture is, shows how the same feature is laid out in all three, and compares their strengths and costs.

Why use an architecture at all

An architecture is a set of rules about three things: where code goes, which parts are allowed to depend on which, and how expensive it will be to change something later.

Not every project needs one. A console utility that reads a file, transforms it and writes the result can run top to bottom in a single class, and adding layers to it would only add ceremony. The same is true of many small background workers and scripts that share one common library.

Architecture starts to pay off when a codebase grows beyond what one person can hold in their head, when several developers work on it at the same time, or when the business logic is expected to outlive the framework, database or UI around it. At that point the rules prevent a familiar outcome: business logic spread across controllers, database calls inside views, and no clear place to put the next feature.

N-tier (layered) architecture

N-tier is the classic layered approach. The application is split into horizontal layers, usually three:

  • Presentation / API — controllers, request and response models
  • Business Logic Layer (BLL) — services that implement the rules of the domain
  • Data Access Layer (DAL) — the database context, entities and repositories

Each layer depends only on the one below it. The API calls the BLL, the BLL calls the DAL, and the DAL talks to the database.

MyApp.sln
├── MyApp.API
│   └── Controllers/
│       └── OrdersController.cs
├── MyApp.BLL
│   ├── Interfaces/
│   │   └── IOrderService.cs
│   └── Services/
│       └── OrderService.cs
└── MyApp.DAL
    ├── AppDbContext.cs
    ├── Entities/
    │   └── Order.cs
    ├── Interfaces/
    │   ├── IOrderRepository.cs
    │   └── IUnitOfWork.cs
    ├── Repositories/
    │   └── OrderRepository.cs
    └── UnitOfWork.cs
Enter fullscreen mode Exit fullscreen mode

In .NET this is commonly combined with Entity Framework, the Repository pattern and a Unit of Work class. Some developers argue that the last two are redundant, since DbContext already behaves as a Unit of Work and DbSet<T> as a repository. Teams often keep the wrappers anyway, for consistency across projects and to make the data layer easier to replace in tests.

A note on terminology: strictly speaking, a tier is a physical deployment boundary and a layer is a logical one. Microsoft's documentation uses "N-Layer" for this reason. In everyday .NET usage the two terms are used interchangeably, and this article follows that convention.

Clean Architecture

Clean Architecture keeps the idea of layers but reverses the direction of dependencies. Instead of business logic depending on data access, everything depends on the business logic.

  • Domain — entities and core business rules, with no external dependencies
  • Application — use cases, interfaces and DTOs; depends only on Domain
  • Infrastructure — database, file storage, external services; implements interfaces defined in Application
  • API / Presentation — the entry point; wires everything together through dependency injection

The key rule is that dependencies point inward. The Domain project knows nothing about Entity Framework, ASP.NET Core or any database.

The approach has gone by several names. Alistair Cockburn described Hexagonal Architecture, also called Ports and Adapters, in 2005. Jeffrey Palermo described Onion Architecture in 2008. Robert C. Martin popularized the name Clean Architecture in 2012. Microsoft's own architecture guide treats all of these as the same idea under different names, and the differences between them are mostly in how they are drawn.

In .NET, Clean Architecture is very often paired with CQRS (Command Query Responsibility Segregation) and the MediatR library. Each operation becomes either a command, which changes state, or a query, which reads it, and each has its own handler.

MyApp.sln
├── MyApp.Domain
│   └── Orders/
│       └── Order.cs
├── MyApp.Application
│   ├── Common/
│   │   └── Interfaces/
│   │       └── IAppDbContext.cs
│   └── Orders/
│       ├── Commands/
│       │   └── CreateOrder/
│       │       ├── CreateOrderCommand.cs
│       │       ├── CreateOrderCommandHandler.cs
│       │       └── CreateOrderCommandValidator.cs
│       └── Queries/
│           └── GetOrderById/
│               ├── GetOrderByIdQuery.cs
│               ├── GetOrderByIdQueryHandler.cs
│               └── OrderDto.cs
├── MyApp.Infrastructure
│   └── Persistence/
│       └── AppDbContext.cs
└── MyApp.API
    └── Controllers/
        └── OrdersController.cs
Enter fullscreen mode Exit fullscreen mode

A single command and its handler look like this:

public record CreateOrderCommand(int CustomerId, decimal Amount) : IRequest<int>;

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly IAppDbContext _context;

    public CreateOrderCommandHandler(IAppDbContext context) => _context = context;

    public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order(request.CustomerId, request.Amount);
        _context.Orders.Add(order);
        await _context.SaveChangesAsync(cancellationToken);
        return order.Id;
    }
}
Enter fullscreen mode Exit fullscreen mode

The controller then sends the command without knowing who handles it:

[HttpPost]
public async Task<int> Create(CreateOrderCommand command) => await _mediator.Send(command);
Enter fullscreen mode Exit fullscreen mode

One practical note: in 2025 MediatR moved to a commercial license for larger organizations. Some teams now use alternative libraries or simple handler interfaces of their own, without changing the architecture itself.

Vertical Slice architecture

Vertical Slice architecture, popularized by Jimmy Bogard, changes the axis of organization rather than the direction of dependencies.

In both N-tier and Clean Architecture, the top-level structure is a set of layers, and each feature is spread across them. Creating an order touches a controller in one project, a handler or service in another, and an entity in a third. Vertical Slice transposes this. The top-level structure is a set of features, and each feature contains everything it needs. For anyone with a database background, it is the same move as a PIVOT: the rows become columns.

MyApp.sln
└── MyApp
    ├── Features/
    │   └── Orders/
    │       ├── CreateOrder.cs
    │       └── GetOrderById.cs
    ├── Data/
    │   └── AppDbContext.cs
    └── Program.cs
Enter fullscreen mode Exit fullscreen mode

A slice often keeps the request, the handler, the validation and the endpoint in one file:

public static class CreateOrder
{
    public record Command(int CustomerId, decimal Amount);

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

    private static async Task<int> Handle(Command command, AppDbContext db)
    {
        var order = new Order(command.CustomerId, command.Amount);
        db.Orders.Add(order);
        await db.SaveChangesAsync();
        return order.Id;
    }
}
Enter fullscreen mode Exit fullscreen mode

The underlying principle is that code which changes together should live together. Slices may share infrastructure such as the database context, but they avoid sharing business logic, so changing one feature is less likely to affect another.

Vertical Slice is close in spirit to the way CQRS is usually applied inside Clean Architecture, where each command or query already sits in its own folder. The difference is that Vertical Slice makes the feature, rather than the layer, the primary unit of the whole project.

What they share and where they differ

All three separate concerns, all three work with dependency injection, and all three can be used with Entity Framework. The differences are in what they treat as the main unit of structure and how strictly they enforce boundaries.

Pros and cons

N-tier

Pros

  • Simple to understand and quick to start
  • Familiar to almost every .NET developer
  • Well suited to CRUD-heavy applications and projects with tight deadlines
  • Few projects and folders to navigate

Cons

  • Business logic depends on data access details
  • Unit testing business logic can require a database or extensive mocking
  • As the project grows, the BLL and DAL tend to become tightly coupled
  • Large service classes accumulate many unrelated methods over time

Clean Architecture

Pros

  • The domain is independent of frameworks and infrastructure
  • Business logic is easy to unit test
  • Infrastructure can be replaced with limited impact on the rest of the system
  • Clear, enforceable rules that scale to large teams
  • The most widely recognized structure in current .NET job descriptions

Cons

  • Significantly more files and projects per feature
  • Higher initial setup cost
  • More navigation required to follow a single request through the code
  • Can be heavier than necessary for small applications or rapidly changing requirements

Vertical Slice

Pros

  • All code for a feature is in one place
  • Adding or removing a feature rarely affects others
  • Little ceremony for simple features, with room for more structure in complex ones
  • Fits well with Minimal APIs

Cons

  • Less prescriptive, so consistency depends on team discipline
  • Shared business rules can end up duplicated across slices
  • Fewer established templates and conventions than Clean Architecture
  • Less familiar to many developers and interviewers

Projects that fit none of them

Not every application has a domain worth protecting. An integration service that receives results from an external API, maps them and passes them on is closer to the Adapter pattern than to any of these architectures. Its main concern is translation between two formats, and splitting that translation across three or four projects adds little.

The same applies to console tools, scheduled jobs and small internal utilities. A sensible default for these is a simple structure, with an architecture introduced only when the project grows into one.

Practical notes

A few observations that rarely appear in architecture diagrams but matter in day-to-day work:

Navigation cost is real. In a Clean Architecture solution, one feature can be spread across four projects and several nested folders. Files are easy to find with search, but moving between them is slower. In Visual Studio, enabling Track Active Item in Solution Explorer (Tools → Options → Projects and Solutions → General) makes the Solution Explorer follow whichever file is open in the editor. For a one-off jump, Sync with Active Document (Ctrl + [, S) does the same on demand.

Boilerplate is cheaper than it used to be. The most common practical objection to CQRS with Clean Architecture was the amount of repetitive code: a command class, then a handler, then a validator, then a DTO, often created by copying an existing set and renaming it. AI coding assistants now generate most of this reliably, which removes much of the cost that made the approach feel slow a few years ago.

Testing benefits are uneven. Because features and business rules are isolated, unit tests are easier to write in Clean Architecture and Vertical Slice than in a tightly coupled N-tier project. Integration tests are roughly the same effort in all three, since they go through the API regardless of how the code behind it is organized.

Change frequency matters. Projects with tight deadlines and requirements that change often feel the extra structure of Clean Architecture most, because each change touches more files. Projects with a stable, complex domain and a long expected lifetime benefit most from it.

The market

Whatever the technical trade-offs, Clean Architecture is currently the default in .NET job descriptions, interview questions and project templates. Developers entering the field today often learn it first, and many teams use it as their standard structure for new services.

This is similar to the situation in frontend development, where React is not the only good option but is the one most often required by employers. Understanding Clean Architecture well is useful for a .NET developer's career regardless of which structure a particular project ends up using.

Choosing

There is no universally correct answer, but a few questions narrow it down:

  • How long will this code live? Short-lived or experimental projects favor N-tier or Vertical Slice. Long-lived systems with complex rules favor Clean Architecture.
  • How often will requirements change? Frequent change rewards less ceremony per feature.
  • How large is the team? Larger teams benefit from Clean Architecture's strict, well-known boundaries.
  • What does the team already know? A familiar architecture applied consistently is usually better than an unfamiliar one applied partially.

All three are valid ways to build a .NET application. The most important decision is to choose one deliberately and apply it consistently.


Which architecture does your team use, and was it a deliberate choice or the template that came with the project? I'd be interested to hear how it has worked out in practice. 👇

Originally published on Medium.

Top comments (0)