Clean Architecture
Dependencies point inward, so the business rules do not know about the database or HTTP
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
What does the dependency rule say?
Real-world analogy
What is it?
Clean Architecture is a way to arrange a solution so that business rules do not depend on frameworks. It is built from rings: Domain in the middle, then Application, then Infrastructure and the API on the outside. The rule is that dependencies point inward. When the Application needs the database, it declares an interface it owns, and Infrastructure implements it. The result is a core you can test without a database and an outer layer you can swap.
Real-world relevance
A team starts with controllers that call DbContext directly. Two years later the rules for discounts are scattered across controllers and EF queries, and nobody can test a rule without a database. Moving the rules into a Domain and Application layer makes them testable with plain unit tests. The price is more ceremony per feature, so teams often pick this for long-lived systems with rich rules and use a simpler design for small admin tools.
Key points
- The dependency rule — Source code references point inward only. Inner layers do not mention outer ones, so the core can be built and tested alone.
- Domain: the business rules — Entities and value objects live here, with no using for EF Core, ASP.NET or any database. It is plain C# that describes what the business means.
- Application: the use cases — Each use case, such as placing an order, is a handler class with one entry point. It coordinates the domain and talks to the outside through interfaces.
- Infrastructure: the details — EF Core, email, files and external APIs live here. Classes implement interfaces that the Application layer declared.
- API: the delivery layer — Controllers or minimal API endpoints read the request, call one handler and return a response. They hold almost no business logic.
- Interfaces inside, code outside — The interface sits in the inner layer, and the implementation sits in the outer one. That reversal lets the core call the database without referencing it.
- Where mapping happens — The API maps request DTOs into commands, and handlers return results or response DTOs. Entities usually stay inside, so a database change does not change the public contract.
- The cost is real — You get more projects, more files and more indirection. For a small CRUD app a simple layered design is usually enough, and you can split later when rules grow.
Quick check
Answer from memory — scrolling up first makes it stick less.
Where should the IOrderRepository interface be declared, and where should its EF Core implementation live?
Which is the most honest downside of Clean Architecture?
Code example
// Solution layout (each folder is usually its own project)
// Domain/ Order.cs plain C#, no framework usings
// Application/ PlaceOrderHandler.cs, IOrderRepository.cs
// Infrastructure/ EfOrderRepository.cs, ShopContext.cs
// Api/ OrdersController.cs, Program.cs
// ---- Application ----
public interface IOrderRepository
{
void Add(Order order);
Task SaveChangesAsync(CancellationToken ct);
}
public record OrderLine(int ProductId, int Quantity);
public record PlaceOrderCommand(int CustomerId, List<OrderLine> Lines);
public class PlaceOrderHandler(IOrderRepository orders)
{
public async Task<int> HandleAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId,
cmd.Lines.Select(l => (l.ProductId, l.Quantity))); // domain rules run here
orders.Add(order);
await orders.SaveChangesAsync(ct);
return order.Id;
}
}
// ---- Infrastructure ----
public class EfOrderRepository(ShopContext db) : IOrderRepository
{
public void Add(Order order) => db.Orders.Add(order);
public Task SaveChangesAsync(CancellationToken ct) => db.SaveChangesAsync(ct);
}Line-by-line walkthrough
- 1. The comment tree shows four folders. Domain has no framework usings at all.
- 2. IOrderRepository is declared in Application, because Application is the layer that needs it. Some teams put it in Domain instead; either way it sits inside. To keep the sample short it also exposes SaveChangesAsync, acting as the unit of work from lesson 24.
- 3. PlaceOrderCommand and OrderLine are plain records. The API maps its request into them.
- 4. PlaceOrderHandler asks for the interface in its constructor and has no idea that EF Core exists.
- 5. Order.Create is a static factory method on the Domain entity, not shown here. It holds the business rules, so the handler only coordinates: create, add, save once, return the id.
- 6. EfOrderRepository sits in Infrastructure and implements the interface with the DbContext.
Spot the bug
// Domain/Order.cs
using Microsoft.EntityFrameworkCore;
public class Order
{
public int Id { get; set; }
[Precision(18, 2)]
public decimal Total { get; set; }
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Common web application architectures (Microsoft Learn)
- Architectural principles (Microsoft Learn)
- Dependency injection in ASP.NET Core (Microsoft Learn)
Lock it in