Software that works on day one often becomes painful by year two. Changing one thing breaks three others. Tests are slow because everything needs a database. Switching a tool means rewriting half the code. Clean Architecture is a way of organising code to avoid that pain. This article explains it for beginners, with .NET Core (the modern, cross-platform .NET) as the example. Each section gives you a plain picture first, then an Expert view with the precise terms.
The kitchen analogy
Picture a restaurant. The recipes are the heart of the business. The kitchen routine organises the work. The equipment and suppliers are the stove, fridge and delivery vans. The dining room is where customers meet the restaurant.
Expert viewWhere the idea comes from
The approach goes by several names that overlap strongly: Clean Architecture, Hexagonal Architecture (ports and adapters) and Onion Architecture. They share the same core principle: the business logic sits in the middle and does not depend on delivery mechanisms such as the web framework, the database or the user interface. Names and exact layer counts vary between authors; the dependency rule is what matters.
The layers
- DomainBusiness concepts and rules in plain C#, with no database, web or frameworks. Example: an Order, and the rule that it cannot be empty.
- ApplicationThe use cases, such as "place an order". It coordinates domain objects and describes what it needs from outside through interfaces.
- InfrastructureThe equipment: database, email, file storage, payment services. It implements the interfaces the application layer described.
- PresentationThe web API, website or mobile app. It turns requests into use-case calls and results into responses.
- 1Is it a business rule that would still be true with no database, no web and no screen?Yes Domain. Put it on the entity, such as Order.AddLine rejecting a zero quantity.No keep going
- 2Does it talk to a database, email, files or a payment service?Yes Infrastructure. It implements an interface that Application defined.No keep going
- 3Does it turn an HTTP request or a screen action into a call, or a result into a response?Yes Presentation, such as the controller or endpoint.No keep going
- 4OtherwiseApplication. It is a step of a use case: coordinate the domain objects and call interfaces.
Takeaway. Ask what the code needs to know about, and put it in the innermost layer that can do without the rest.
The one rule that matters
Source code dependencies point inward only. Outer layers know about inner layers, never the reverse.
How can the application use a database without knowing about it? Through an interface, a promise written in code. The application layer says: "I need something that can save an order." The infrastructure layer provides it.
- Presentation The request arrives and the controller turns it into a PlaceOrder command.
- Application The controller calls PlaceOrderHandler. Presentation depends on Application.
- Domain The handler builds an Order and calls AddLine. The domain enforces its own rules.
- Application The handler asks IOrderRepository to save the order. It only knows the interface.
- Infrastructure The EF Core repository, which implements that interface, writes to the database.
- Presentation The total goes back up and the controller returns the response.
Presentation and Infrastructure both point at Application, Application points at Domain, and Domain points at nothing. The request goes in and out, but code references only point inward.
Takeaway. The request travels through every layer, but the code references only ever point inward.
Expert viewThe dependency rule, precisely
Dependency inversion means the high-level policy (application and domain) owns the abstraction, and the low-level detail (database, web) implements it. In ports-and-adapters vocabulary, IOrderRepository is a port and its Entity Framework Core implementation is an adapter. In .NET, the built-in dependency injection container in ASP.NET Core connects them at start-up, usually in the composition root of the outermost project.
A compile-time guarantee helps: project references enforce the rule, so Domain references nothing, Application references Domain, and Infrastructure and the web project reference Application. If someone calls the database from the domain project, it will not compile.
What it looks like in a .NET solution
Shop.sln
src/
Shop.Domain // entities, value objects, business rules
Shop.Application // use cases, interfaces (IOrderRepository)
Shop.Infrastructure // EF Core, email, payment adapters
Shop.WebApi // ASP.NET Core controllers or endpoints
tests/
Shop.Domain.Tests
Shop.Application.Tests- 4source projects, one per layer
- 2test projects, no database needed
- 1rule: dependencies point inward
A small example
The domain protects its own rules:
// Shop.Domain
public class Order
{
private readonly List<OrderLine> _lines = new();
public IReadOnlyList<OrderLine> Lines => _lines;
public void AddLine(string product, int quantity, decimal unitPrice)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive.");
_lines.Add(new OrderLine(product, quantity, unitPrice));
}
public decimal Total() => _lines.Sum(l => l.Quantity * l.UnitPrice);
}The application layer defines what it needs and runs the use case:
// Shop.Application
public interface IOrderRepository
{
Task SaveAsync(Order order);
}
public class PlaceOrderHandler
{
private readonly IOrderRepository _orders;
public PlaceOrderHandler(IOrderRepository orders) => _orders = orders;
public async Task<decimal> HandleAsync(PlaceOrder command)
{
var order = new Order();
foreach (var l in command.Lines)
order.AddLine(l.Product, l.Quantity, l.UnitPrice);
await _orders.SaveAsync(order);
return order.Total();
}
}Infrastructure then provides the real IOrderRepository, for example with Entity Framework Core and a SQL database. The names above are illustrative, not a framework you must install.
- Controller to Use case The controller passes the PlaceOrder command to PlaceOrderHandler.
- Use case (to itself) The handler creates the Order and calls AddLine, so the domain rules run.
- Use case to Repository The handler calls IOrderRepository. At start-up, dependency injection supplied the EF Core implementation, so the handler never names it.
- Repository to Database The adapter translates the Order into the database's own format.
- Database to Repository (reply) The database confirms.
- Repository to Use case (reply) The task completes. Nothing database-specific comes back.
- Use case to Controller (reply) The handler returns the order total.
Takeaway. In tests, swap the Repository lane for a fake and nothing else changes.
Expert viewDesign notes an experienced reviewer would raise
- Rich domain, not anaemic. Rules live on the entity (
AddLinevalidates) instead of in service classes that manipulate bags of properties. - Mapping lives at the boundaries. Convert between persistence models, domain models and API contracts in the outer layers so inner layers never depend on serialisation or ORM attributes.
- Use cases as the unit of change. Whether you use plain handler classes or a mediator library is a style choice, not part of the architecture itself.
- Money and units. Using
decimalfor currency, or a dedicated value object, avoids floating-point surprises; the domain is the right home for such rules. - Transactions at the use-case edge. A use case usually corresponds to one unit of work; keep transaction handling out of the domain.
Why it helps with testing
Because the domain and application layers do not touch a database, you can test them in milliseconds.
Without the pattern
- Needs a database, set-up and clean-up
- Slow, so tests are run less often
With the pattern
- Give the handler a fake repository that remembers what it was given
- Runs in milliseconds, so it runs constantly
Expert viewA test in practice
A unit test of PlaceOrderHandler passes a fake IOrderRepository (an in-memory list or a mocking library), calls HandleAsync, and asserts on the returned total and on what the fake received. Domain tests need no fake at all: construct an Order, call AddLine with a zero quantity and assert that it throws. Reserve a smaller number of integration tests for the infrastructure adapters, run against a real or containerised database, to prove the mapping works.
Common mistakes
More layers always means better design
Each layer must earn its place. A three-screen app does not need five layers and a mapper for every field.
Interfaces for every class
Create interfaces where a layer boundary needs one, not everywhere.
- Leaking the database into the domain. ORM attributes or database types in domain classes tie the heart to the equipment.
- An anaemic domain. Entities that are bags of properties while all rules live in services.
- Returning database entities from the API. Return purpose-built response objects.
- Skipping tests. The structure makes testing easy. Use it.
When not to use it
- Start simpleOne project, clear folders
- Watch for painRules growing, several developers, a long life ahead
- Then add layersMove the rules to the centre
Expert viewEvolving into it
A pragmatic path is a modular monolith: begin with feature folders in one project and extract the domain and application code into their own projects once rules and team size justify it. Keep the dependency direction right from the start (no domain code referencing the database), and the later split is mostly moving files. Avoid copying a template of many projects into a project that has only simple data entry.
Frequently asked questions
Is Clean Architecture a framework I install?
No. It is a way of arranging your own code. You can do it with plain .NET projects.
Does it only work with .NET?
No. The idea is language-neutral. This article uses C# as the example.
Is it slower at run time?
The extra layers cost almost nothing at run time. The real cost is development effort and complexity.
How many layers must I have?
Three or four is typical. What matters is the dependency rule, not the count.