A Detailed Introduction to Clean Architecture - NDepend Blog (original) (raw)

May 25, 2026 9 minutes read

It seems to me that the topic of software architecture has attracted a lot of interest in the last few years. And among many different flavors and styles of software architecture, there’s one that attracts even more interest than the others.

It’s the Clean Architecture, Not a Clean Architecture

I’m talking about the clean architecture, proposed and evangelized by Robert C. Martin, a.k.a. Uncle Bob. (And for the rest of this post, it’s simply referred to as “clean architecture.”)

By employing clean architecture, you can design applications with very low coupling and independent of technical implementation details, such as databases and frameworks. That way, the application becomes easy to maintain and flexible to change. It also becomes intrinsically testable.

If you want the one-sentence version before we go any further: clean architecture is a way of organizing a codebase so that the business rules sit at the center, and every other concern – the database, the web framework, the UI, the message bus – depends on those rules through interfaces, never the other way around. That is the whole pattern. Everything else in this article is a set of techniques to keep it true once a real .NET solution starts to grow.

So here’s what we’re going to do in today’s post:

“Desperation” of Concerns

How does one free oneself from this mess?If you have at least a couple years of software development experience under your belt, then I can almost guarantee you’ve heard the term “separation of concerns.” It’s a principle that shows up quite often when talking about software architecture.

One of its incarnations is in the form of advice:

“Separate presentation from business logic.”

It’s very sound advice and I agree with it wholeheartedly, but the problem is that it’s not so easy or clear how to put it into practice, especially for beginners.

In practice, what ends up happening is that business logic frequently leaks to the application’s UI. And the same can go the other way around: it’s not so rare to find business logic code concerning itself with UI concepts, such as colors, markup languages, font sizes, and what have you.

And things can get even worse when you add data access to the mix. How does one free oneself from this mess?

Enter the Layered Architecture

The solution people often found for the mixing of concerns is to break up the application into layers, following an architectural pattern. There are generally three:

Nothing groundbreaking here, right? This stuff is pretty standard. You can bet there are no shortages of applications running around the world right now that follow an architecture like the one I’ve just described.

Maybe the names aren’t exactly the same or there are more (or maybe just two) layers, but the general principles still apply.

The Problems With the Not So Clean Architecture

In order to see clean architecture’s benefits, we really need to understand the flaws in the current alternative. And for that, we need to talk about dependencies.

Quick question about the architecture described in the previous section: which layer depends on which? Well, it’s trivial to answer this if we organize the layers by using a diagram:

UL to BLL to DAL flow chart

We can see that the presentation layer depends on (or references) the business logic layer, which is entirely reasonable. We wouldn’t want aesthetic tweaks to cause changes to our underlying business logic, right?

On the other hand, it’s perfectly acceptable that changes to the business rules cause changes in the UI.

The Traditional Layered Architecture Is Very Database-Centric

By looking at the diagram, we see that the business logic layer references the data access layer. If you rearranged the layers as concentric circles – like the common depiction of clean architecture, as we’ll see soon – then the data access layer would be at the center.

At first sight, it doesn’t look like a problem. After all, you often have to persist changes in the database after some interaction with the user. It seems like the natural workflow:

Well, of course that’s pretty much what happens at execution time. What doesn’t follow is that the dependencies should flow in the same direction at compile time also. They shouldn’t. This will harm you. Let’s understand why.

The first problem is that you couple yourself to a specific database vendor. I already hear you saying how rare it is for this to happen in practice.

It happened to me on my first job after college. I had to adapt an application that up until then only ran on SQL Server and make it support Oracle Database also. ORMs were out of the question, for…reasons. It was a tough job, but it could’ve been easier if the application wasn’t so heavily coupled to SQL Server.

The second problem is subtler and shows up later. Once the business code knows about SqlConnection, SqlCommand, and the shape of a particular schema, it starts to drift toward those concepts. Domain methods grow parameters that exist only because a table column exists. Validation rules end up where the SQL is. A few releases down the line, the “business” layer is really a thin wrapper around the database, and the actual rules of the business live nowhere in particular.

Dirty Architecture Hurts Testability

NDepend can show you a dirty architecture, in all of its gory detail.

The next point is about testability.

It’s not uncommon to have data access code mixed up with code that performs calculations or other tasks that don’t rely on the database at all. You really wish you could test the logic of the code. But that would mean hitting the database, which would make the test not only slow but also harder to set up and less predictable.

Coupling to the database does not only harm automated testing. Even manual exploratory testing can suffer. As put by Alistair Cockburn:

“When the database server goes down or undergoes significant rework or replacement, the programmers can’t work because their work is tied to the presence of the database. This causes delay costs and often bad feelings between the people.”

Here he says “programmers,” but replace that with “testers” or “QA engineers” and the effect is pretty much the same.

This issue also applies to the UI layer. Sometimes you really wish you could test the user interface, just for the sake of testing the presentation logic itself (e.g., when the user clicks on a radio button, then the field x should be enabled) but without actually exercising the application logic.

This would allow, for instance, two completely separate teams to handle the UI and business logic development.

Clean Architecture to the Rescue

Now we know that the problem with the traditional approach, in short, is that it creates unnecessary coupling.

It couples the UI to the other layers; worse, it couples the domain (or business logic) layer to implementation details such as storage, which can have dire consequences for the testability of the application.

So, what’s the solution? Well, I’ve almost spelled it out in the previous paragraph: restore the domain to its rightful location, at the center of the architectural diagram.

Clean Architecture: A Bit of History

NDepend can also show you when your architecture looks pleasingly simple.

To the best of my knowledge, the first mention of clean architecture is from a blog post by Robert C. Martin, published back in 2011.

He then proceeded to write another, more famous post on the subject the next year. In this newer post, Robert provides a more formal definition of clean architecture, complete with diagrams.

Right at the beginning, Robert explains that the architecture he’s proposing isn’t necessarily new. Actually, it’s an attempt to integrate and create a shared vocabulary about several architectural ideas that had appeared in the years prior, such as onion architecture and hexagonal architecture.

Since then, he’s written more posts and given talks on the subject, and it started gaining momentum. Other developers were inspired enough to create their own sample projects and examples based on clean architecture.

Characteristics of Clean Architecture

So, a quick recap. Up until now we’ve seen

Finally, now it’s time to actually see what this whole clean architecture thing is about.

Layers and Organization

The common depiction of clean architecture is a diagram consisting of concentric circular layers, very reminiscent of the onion architecture, which is not a surprise. The idea here is that the inner layers are high-level, abstract policies; the outer layers are technical implementation details.

The proposed layers are

The Dependency Rule

The aforementioned layer organization isn’t set in stone, though. There’s nothing really stopping you from using more than that if you need to.

What you really have to remember is to follow the dependency rule: all dependencies should point inwards.

The entities layer shouldn’t be aware of any other layer in the application. There should be no change in the outer layers that causes a change to it. The opposite holds, though: a change in the entities layer can and probably will cause changes in next layers.

The technical name for this trick is the Dependency Inversion Principle, the “D” in SOLID. Without it, the rest of clean architecture collapses into a pretty diagram with no teeth. With it, you get a codebase you can unit-test in milliseconds and a database you can swap without rewriting your business logic.

A small example helps. Suppose the use cases layer needs to persist an order. Without dependency inversion, the use case would call a concrete class such as SqlOrderRepository, and the use cases layer would then have to reference the data-access assembly. The arrow points outward, and the architecture is broken.

With dependency inversion, the use cases layer declares an interface it needs and never knows who implements it:

| | // Use cases layer - declares what it needs.public interface IOrderRepository{ Task AddAsync(Order order, CancellationToken ct); Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct);} | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

The infrastructure layer references the use cases layer in order to implement the interface. The arrow now points inward, exactly as the rule demands:

| | // Infrastructure layer - implements what the inner layer declared.internal sealed class EfOrderRepository : IOrderRepository{ private readonly AppDbContext _db; public EfOrderRepository(AppDbContext db) => _db = db; public Task AddAsync(Order order, CancellationToken ct) => _db.Orders.AddAsync(order, ct).AsTask(); public Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct) => _db.Orders.Include(o => o.Lines) .FirstOrDefaultAsync(o => o.Id == id, ct);} | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

That single move – declaring the abstraction where it is needed, not where it is implemented – is the difference between clean architecture and a layered diagram that does not survive contact with a real project.

What Clean Architecture Looks Like in a .NET Solution

The four conceptual layers translate into four projects in a typical C# solution. The names vary from team to team, but the references between them do not:

| | MyApp.sln |-- src | | -- MyApp.Domain (no project references) | | -- MyApp.Application (references Domain only) | | -- MyApp.Infrastructure (references Application) | `-- MyApp.Web (references Application, plus Infrastructure at startup only) `-- tests | -- MyApp.Domain.Tests | -- MyApp.Application.Tests `-- MyApp.Infrastructure.Tests | | ---------------------- | | ---------------------------------------- | | ---------------------------------------------- | | ------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------ |

Domain is the innermost project. Plain C# classes that model the business: Order, Invoice, Customer, value objects like Money or EmailAddress, domain events, and the invariants that make those types valid. No EF Core attributes, no [Table], no [JsonPropertyName], nothing from ASP.NET Core. If your Domain compiles against the BCL with zero third-party references, you are on the right track.

12345678910111213141516171819202122232425262728 // MyApp.Domain/Orders/Order.cspublic sealed class Order{ private readonly List<OrderLine> _lines = new(); public OrderId Id { get; } public CustomerId CustomerId { get; } public OrderStatus Status { get; private set; } public IReadOnlyList<OrderLine> Lines => _lines; private Order(OrderId id, CustomerId customerId) { Id = id; CustomerId = customerId; Status = OrderStatus.Draft; } public static Order Place(CustomerId customerId, IEnumerable<OrderLine> lines) { var order = new Order(OrderId.New(), customerId); foreach (var line in lines) order._lines.Add(line); if (order._lines.Count == 0) throw new DomainException("An order needs at least one line."); order.Status = OrderStatus.Placed; return order; }}

Notice the constructor is private, the collection is exposed as read-only, and the factory method enforces an invariant. That is what a non-anaemic domain object looks like in practice. Compare it with the alternative – a record with public setters and an Id – and you can see why so many “clean” codebases end up with the rules scattered across services: the entities did not pull their weight.

Application sits one ring out. It orchestrates the domain through use cases, and it declares the interfaces it needs from the outside world. In a CQRS-flavored solution these become command and query handlers; without CQRS they are just application services.

1234567891011121314151617181920212223 // MyApp.Application/Orders/PlaceOrder/PlaceOrderHandler.cspublic sealed class PlaceOrderHandler{ private readonly IOrderRepository _orders; private readonly IUnitOfWork _uow; private readonly IClock _clock; public PlaceOrderHandler(IOrderRepository orders, IUnitOfWork uow, IClock clock) { _orders = orders; _uow = uow; _clock = clock; } public async Task<OrderId> Handle(PlaceOrderCommand cmd, CancellationToken ct) { var order = Order.Place(cmd.CustomerId, cmd.Lines); await _orders.AddAsync(order, ct); await _uow.SaveChangesAsync(ct); return order.Id; }}

The handler depends on three abstractions, all of which live in Application. None of them carry a single line of EF Core, ASP.NET, or anything else that would tie this code to a particular technology. Unit-testing it is a matter of supplying fakes for the three interfaces.

Infrastructure is where the messy reality lives: Entity Framework Core, Dapper, Azure Service Bus, SendGrid, Stripe, the file system, HttpClient, a Redis cache. Each adapter implements an interface declared by Application or Domain. A small habit that pays back over time is to mark these implementations internal; nothing outside Infrastructure ever needs to know they exist.

Presentation (often called Web in ASP.NET Core solutions) is where controllers, minimal-API endpoints, Razor Pages, Blazor components, gRPC services, and SignalR hubs live. The composition root – Program.cs – is the only place that knows about both Application and Infrastructure, because that is where the DI container wires interfaces to their concrete implementations.

123456789101112131415161718192021 // MyApp.Web/Program.cs (composition root)var builder = WebApplication.CreateBuilder(args);builder.Services.AddScoped<PlaceOrderHandler>();builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();builder.Services.AddScoped<IUnitOfWork, EfUnitOfWork>();builder.Services.AddSingleton<IClock, SystemClock>();var app = builder.Build();app.MapPost("/orders", async ( PlaceOrderRequest req, PlaceOrderHandler handler, CancellationToken ct) =>{ var id = await handler.Handle(req.ToCommand(), ct); return Results.Created($"/orders/{id}", new { id });});app.Run();

The endpoint is three lines of glue. All the real work, including the domain invariants, has been pushed inward where it can be tested without an HTTP context, a database, or a clock.

Common Pitfalls That Quietly Break the Architecture

Most failing implementations I have reviewed share the same handful of mistakes. None of them are subtle once you know what to look for.

Keeping It Honest with NDepend

An architecture document on a wiki is worth nothing if the code drifts from it on Wednesday. The point of running static analysis on a clean-architecture solution is to encode the dependency rule and let CI fail when someone breaks it.

You can refer to the default rule Enforcing Clean Architecture whose core is:

| 123456789101112131415161718192021 | // Define layers let layers = new[] { new { Name = "Domain", Types = getLayerTypes("Domain|Entities") }, new { Name = "Application", Types = getLayerTypes("Application|UseCases") }, new { Name = "Infrastructure", Types = getLayerTypes("Infrastructure|Persistence") }, new { Name = "Presentation", Types = getLayerTypes("Presentation|UI|API|Web") }}// Define forbidden dependencies as anonymous typeslet forbiddenDeps = new[] { new { From = "Domain", To = "Application" }, new { From = "Domain", To = "Infrastructure" }, new { From = "Domain", To = "Presentation" }, new { From = "Application", To = "Infrastructure" }, new { From = "Application", To = "Presentation" }, new { From = "Infrastructure", To = "Presentation" }} | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

The Dependency Structure Matrix and the Dependency Graph make the layering visible at a glance, and the trend charts on the dashboard tell you whether the architecture is decaying release by release. If you can see a slow climb in “Application depends on Infrastructure” edges over three sprints, you have a problem to fix before it ossifies.

Clean Architecture vs N-Tier vs Vertical Slice

Clean architecture is not the only sensible way to organize a .NET codebase, and pretending it is would be doing you a disservice. The two patterns that come up most often in the same conversation are classic N-tier and Vertical Slice Architecture, championed in the .NET world by Jimmy Bogard.

Aspect N-Tier (UI / BLL / DAL) Clean Architecture Vertical Slice
Organizing principle Technical layer Technical layer plus dependency inversion Feature
Dependency direction UI to BLL to DAL Inward (Infrastructure to Application to Domain) Per slice, often direct to DB
Test friction High (needs a database) Very low for Domain and Application Low to medium
Feature-level cohesion Low Medium High
Best for Small CRUD apps Domain-rich systems, long-lived products Microservices, focused APIs
Risk if overdone Tight coupling, untestable logic Ceremony, over-abstraction Duplication, drift across slices

In practice the answer is rarely pure. A modular monolith with clean architecture inside each module, and vertical slices inside each Application project, is a perfectly reasonable shape for a serious .NET system. Pick the boundary that buys you the most isolation, and stop adding ceremony past that point.

When Clean Architecture Is Overkill

Honest answer: most weekend projects, prototypes, throwaway internal tools, and small CRUD admin pages do not benefit. If the system is genuinely a thin wrapper over a database table, a four-project solution is friction without payback. Ship a single ASP.NET Core project, get the feedback, and refactor outward only when complexity demands it.

Clean architecture earns its keep on systems that will be touched by more than one team, live for more than five years, have non-trivial business rules, or need to swap a major piece of infrastructure at some point. Those are exactly the systems where shortcuts compound the fastest, which is why the discipline pays.

Frequently Asked Questions

Is clean architecture the same as onion or hexagonal architecture?

They are members of the same family. Hexagonal (Alistair Cockburn, 2005), onion (Jeffrey Palermo, 2008), and clean architecture (Robert C. Martin, 2012) all push the same dependency-inversion idea. Clean architecture is the most prescriptive about the four-ring layout; hexagonal is the most abstract about ports. In a .NET project the implementation looks essentially identical.

Do I need MediatR or CQRS to do clean architecture?

No. Neither is part of the pattern. MediatR is a convenient in-process dispatcher for commands and queries inside the Application layer, and CQRS is a useful split when read and write models diverge. Plenty of clean codebases use plain handler classes injected directly into endpoints. Adopt MediatR when its pipeline behaviors solve a real problem in your codebase.

Where do DTOs and view models live?

Request and response models live in Presentation. Internal command and query models live in Application. Domain types never travel across the wire. Mapping happens at the edge, ideally with a source-generated mapper to keep it allocation-free.

Should the Domain project reference NuGet packages?

As few as possible. The BCL is fair game. A small immutable-collections package or a value-object helper library is defensible. Anything that ties you to a framework (EF Core, ASP.NET, MediatR, AutoMapper) does not belong here.

Where do cross-cutting concerns like logging, authorization, and validation go?

The interfaces (IAppLogger<T>, ICurrentUser) live in Application. The implementations live in Infrastructure or Presentation. Pipeline behaviors wrap your handlers and apply validation, logging, and authorization uniformly without leaking into the use cases themselves.

How big should a use case be?

One verb. PlaceOrder, CancelOrder, GetOrderById, ListPendingInvoices. If a handler is doing two things, split it. A use-case file that grows past 150 lines is almost always doing too much.

Does clean architecture work with microservices?

Yes, and it works particularly well: each service is small enough that the four-project overhead is manageable, and the architecture protects the service’s bounded context from leaking. A modular monolith with one clean-architecture stack per module is often a better starting point than a microservice fleet on day one, mind.

Your Journey to a Clean and Safer Architecture Is Just Beginning

Today we’ve just touched the tip of the iceberg in regards to clean architecture.

There’s a lot more to know about it, of course. My intention with this post was to present what might be a novel idea, to excite your curiosity and leave you wanting more.

In the next posts, we’ll continue to explore the subject here on the NDepend blog. We’ll build a sample application in order to put these guidelines into practice. And we’ll test these ideas against the most realistic scenarios we can create in a blog post.

Want to learn more? Here is the next post in our clean architecture series.

This article is brought to you by the team behind NDepend — a proven .NET static analysis tool for improving code maintainability, security, and overall quality. Whether you’re modernizing a legacy .NET application or starting fresh in C#, get started with your free full-featured trial today!