Lesson 25 of 45 advanced

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?

Open interactive version (quiz + challenge)

Real-world analogy

Imagine a restaurant. The recipe book (the rules of the food) is written without knowing which stove, supplier or delivery app the restaurant uses. The kitchen follows the recipe and says "I need flour" without caring who delivers it. The delivery app, the stove brand and the menu printer can all change, and the recipe book stays the same. In code the recipe book is the domain, the kitchen is the application layer, and the suppliers are the outside layers that plug in.

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

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. 1. The comment tree shows four folders. Domain has no framework usings at all.
  2. 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. 3. PlaceOrderCommand and OrderLine are plain records. The API maps its request into them.
  4. 4. PlaceOrderHandler asks for the interface in its constructor and has no idea that EF Core exists.
  5. 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. 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?
Which layer is allowed to know that EF Core exists?
Show answer
The Domain entity references Microsoft.EntityFrameworkCore, so the innermost layer now depends on a framework from the outside. That breaks the dependency rule and ties the business rules to EF Core. Fix: remove the using and the attribute, and configure precision in Infrastructure with modelBuilder.Entity<Order>().Property(o => o.Total).HasPrecision(18, 2).

Explain like I'm 5

Put the most important rules in the middle of a box, and put the changeable things, like the database and the web page, around the outside. The middle never looks outward, so you can change the outside without breaking the middle.

Fun fact

Clean Architecture is close to older ideas called Hexagonal Architecture and Onion Architecture. They all draw the same rule in different shapes: keep the business core free of the outside world.

Hands-on challenge

Say it out loud in 30 seconds: "What is Clean Architecture and when would you not use it?" Then check: did you mention the dependency rule, the four layers, interfaces declared inside and implemented outside, and the cost of extra files for small apps?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook