Levelwise
English
Code design

Clean Architecture

Put the business rules in the center of the app, and keep technical details like the database and the web outside. All dependencies point only toward the center. With this, the important logic of the app is tested without a database, and changing tools does not hurt it.

Not reviewedWritten with AI helpReading time: 16 minExample of placing an order in an online shopC# and .NET 10 code

Author: bezzad

The problem: business logic everywhere

Our online shop has been running for three years. Where in the code is the rule “the order total is at most 20 million toman”? A new developer searches and finds this:

  • Once in the order controller. It is checked before saving.
  • Once in a Stored Procedure. Someone added it last year.
  • Once in a class that works with EF Core. With a slightly different number.

Now we have several problems:

  1. Testing the rule without the database and the web is not possible. The rule is stuck to the controller and the DbContext.
  2. Changing a tool changes the logic. If we move from EF Core to Dapper, the rules must move too.
  3. Nobody knows the right place. Each person writes a new rule wherever it is easier.

Clean Architecture gives a simple answer to this question: where should each thing be, and what should it depend on?

The idea: the onion and the dependency rule

See the app as an onion with several rings. The most important and most stable code is in the center. Details that change often are outside.

DomainDomainApplicationApplicationInfrastructureInfrastructurePresentationWeb APICenterBusiness rulesDepends on no other layerApplication layerUser tasks, like orderingDefines the interfacesOuter ringDatabase, email, webDetails that changeDependency ruleArrows point only inward
An arrow means "depends on this". No arrow goes from inside to outside.

One main rule holds everything together. We call it the Dependency Rule:

The Dependency Rule: The code of each ring can only know the rings further inside. The domain knows nothing about the application. The application knows nothing about EF Core or ASP.NET Core.

Why is this rule useful?

  1. The center is stable. The “20 million limit” rule does not change for years, but the EF Core version changes every year.
  2. Outer changes do not reach the inside. When the database changes, only the outer ring changes.
  3. Testing becomes simple. You test the domain and the application without a database and without a web server.

Four layers, with an example

Layer What is inside it? In our shop
Domain Entities, Value Objects and business rules The Order class and the 20 million limit rule
Application The tasks the user does, and the interfaces it needs Placing an order, and the IOrderRepository interface
Infrastructure Implementations of the interfaces with real tools The EfOrderRepository class with EF Core, sending email
Presentation The input and output of the app Minimal API endpoints

The domain layer is where the ideas of the DDD lesson live: Entity, Value Object and Aggregate. Clean Architecture says where this code should be. DDD says how this code should be written. These two work well together, but each one can also be used without the other.

How does the domain not need the database?

The important question is this: the application layer must save the order. But it is not allowed to know EF Core. So how?

The answer is the same dependency inversion from the SOLID lesson:

  1. The application layer defines an interface. This interface says “I need something that loads and saves an order”.
  2. The infrastructure layer implements it. The EfOrderRepository class works with EF Core.
  3. The presentation layer connects these two in DI. Only this layer knows both of them.
Shop.ApiPresentation layerShop.InfrastructureEfOrderRepositoryOnly to register servicesShop.ApplicationIOrderRepositoryShop.DomainImplementsAll arrows point to the domainThe domain has no referencesThe compiler catches mistakes
The infrastructure project references the application project, not the other way around. If someone uses EF Core in the domain, the code does not compile.

Here is an interesting point: the run path and the dependency direction are different. At run time, the request goes from the API to the database. But in the code, the infrastructure depends on the application interface.

POST /ordersPresentationPlaceOrderHandlerApplicationorder.Place()DomainIOrderRepositoryInterface in application layerEfOrderRepositoryInfrastructureRun pathCode dependency

Code

Project structure

Each layer is a separate project. The references enforce the dependency rule with the compiler:

<!-- Shop.Application.csproj -->
<ItemGroup>
  <ProjectReference Include="..\Shop.Domain\Shop.Domain.csproj" />
</ItemGroup>

<!-- Shop.Infrastructure.csproj -->
<ItemGroup>
  <ProjectReference Include="..\Shop.Application\Shop.Application.csproj" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
</ItemGroup>

The Shop.Domain project has no references. Not to EF Core, and not to any other project.

Domain

namespace Shop.Domain;

public sealed class Order
{
    private const decimal MaxTotal = 20_000_000;
    private readonly List<OrderLine> _lines = [];

    public Guid Id { get; } = Guid.NewGuid();
    public IReadOnlyList<OrderLine> Lines => _lines;
    public decimal Total => _lines.Sum(line => line.Price * line.Quantity);

    public void AddLine(Guid productId, decimal price, int quantity)
    {
        if (Total + price * quantity > MaxTotal)
            throw new DomainException("Order total is over the limit.");

        _lines.Add(new OrderLine(productId, price, quantity));
    }
}

Application

namespace Shop.Application.Orders;

public interface IOrderRepository
{
    void Add(Order order);
    Task SaveChangesAsync(CancellationToken ct);
}

public sealed record PlaceOrderCommand(IReadOnlyList<CartItem> Items);

public sealed class PlaceOrderHandler(IOrderRepository orders)
{
    public async Task<Guid> HandleAsync(PlaceOrderCommand command, CancellationToken ct)
    {
        var order = new Order();
        foreach (var item in command.Items)
            order.AddLine(item.ProductId, item.Price, item.Quantity);

        orders.Add(order);
        await orders.SaveChangesAsync(ct);
        return order.Id;
    }
}

The application layer does not check any rule itself. It only coordinates the work: create, hand it to the domain, save.

Infrastructure

namespace Shop.Infrastructure;

internal sealed class EfOrderRepository(ShopDbContext db) : IOrderRepository
{
    public void Add(Order order) => db.Orders.Add(order);

    public Task SaveChangesAsync(CancellationToken ct) => db.SaveChangesAsync(ct);
}

public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services, string connectionString)
    {
        services.AddDbContext<ShopDbContext>(options => options.UseSqlServer(connectionString));
        services.AddScoped<IOrderRepository, EfOrderRepository>();
        return services;
    }
}

The EfOrderRepository class is internal. No other project can use it directly.

Presentation

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<PlaceOrderHandler>();
builder.Services.AddInfrastructure(builder.Configuration.GetConnectionString("Shop")!);

var app = builder.Build();

app.MapPost("/orders", async (PlaceOrderCommand command, PlaceOrderHandler handler, CancellationToken ct) =>
{
    var id = await handler.HandleAsync(command, ct);
    return TypedResults.Created($"/orders/{id}", id);
});

app.Run();
A common compromise: Instead of a Repository, some teams make an interface for the DbContext itself in the application layer, which exposes the DbSets. This makes the code shorter and keeps all EF Core features available. But the application layer then depends on the EF Core package. Both ways are common. What matters is that the team chooses with awareness.

Important rules

  1. The domain has no references. No EF Core, no ASP.NET Core, no infrastructure library.
  2. The interface belongs to the one who uses it. The repository interface is in the application, not in the infrastructure.
  3. Business rules are in the domain. Not in the controller, not in the Handler, not in the database.
  4. Only the presentation layer knows everything. This layer is where everything is connected together in DI.
  5. Check the boundary with tools. Project references catch most mistakes. For more exact rules, write architecture tests with libraries like NetArchTest or ArchUnitNET.
Other names, the same idea: Hexagonal architecture (Hexagonal, or Ports and Adapters), Onion architecture and Clean Architecture have different details. But the main idea of all three is the same: business logic in the center, and dependencies only toward the center.

Common mistakes

Mistake Why is it bad? The right way
EF Core attributes on domain classes The domain depends on database details. Set up mapping with configuration classes in the infrastructure.
Business rules in the controller or Handler The domain becomes anemic, and the rules are repeated. The rule inside a method of the Entity itself.
The repository interface next to the implementation, in the infrastructure The dependency direction is not inverted. The interface in the application layer.
One generic Repository for all tables on top of EF Core An extra layer that hides the EF Core features. Either a repository for each Aggregate, or the DbContext itself behind an interface.
DTOs and Mappers between every two layers Five files change for every new field. Make separate models only at real boundaries, like API input and output.
Four projects for a small CRUD service A heavy structure, with no business logic worth protecting. One simple project, or folders by feature (Vertical Slice).

When to use Clean Architecture?

Good fit

  • There is a lot of important business logic.
  • The system lives for years, and the tools change during that time.
  • You want to test the rules quickly and without a database.
  • Several teams or several developers work on one service.

Poor fit

  • A small service that mostly reads data and returns it.
  • A prototype or a short-term project.
  • The team is small, and the layers only slow the work down.
One important weakness: In a layered architecture, the code of one feature is spread over several projects. To add “cancel order”, you must visit four projects. The Vertical Slice lesson shows a way to reduce this problem. These two methods can be combined.

Summary in six lines

  1. Business logic is in the center. Technical details like the database and the web are outside.
  2. Dependencies point only toward the center. The domain knows nothing.
  3. The application layer defines interfaces. The infrastructure implements them.
  4. The run path goes outward, but the code dependency points inward.
  5. Project references and architecture tests check the dependency rule automatically.
  6. For a small CRUD service, this structure is heavy. Start simple.