Learn .NET  

Building Clean Architecture in .NET 8 — A Modern Guide for Developers

As .NET applications grow in size and complexity, organizing your codebase becomes critical. Clean Architecture has become one of the most popular and effective ways to structure enterprise applications in .NET.

In this article, we’ll break down Clean Architecture in simple terms, explore its building blocks, and provide practical examples using .NET 8, EF Core, and Dependency Injection.

What Is Clean Architecture?

Clean Architecture is a software design approach proposed by Robert C. Martin (“Uncle Bob”). The main idea is simple:

Your business logic should not depend on infrastructure. Infrastructure should depend on your business logic.

This leads to:

  • Better scalability

  • Easier maintenance

  • High testability

  • Separation of concerns

  • Independence from frameworks and UI layers

Core Principles

1. Separation of Concerns

Each layer has one responsibility.

2. Dependency Rule

The inner layers should not depend on external layers.

3. Independent Business Logic

Your core domain should work even without:

  • Database

  • UI

  • Third-party services

  • Frameworks

Typical Clean Architecture Layers

Presentation Layer (API, MVC, Blazor)
    ↓
Application Layer (Use Cases)
    ↓
Domain Layer (Entities, Rules)
    ↓
Infrastructure Layer (EF Core, Files, External APIs, Services)

✔ Domain Layer — The Heart of the System

The Domain is the most important layer. It contains:

  • Entities

  • Value Objects

  • Interfaces

  • Domain Events

Example: Product Entity

public class Product
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public decimal Price { get; private set; }

    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }

    public void UpdatePrice(decimal newPrice)
    {
        Price = newPrice;
    }
}

Notice there is no reference to EF Core.

✔ Application Layer — Use Cases

Contains:

  • Commands

  • Queries

  • Handlers

  • Interfaces for repositories

Example: IProductRepository

public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
    Task AddAsync(Product product);
    Task SaveChangesAsync();
}

Example: CreateProductCommand

public class CreateProductCommand
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Command Handler

public class CreateProductHandler
{
    private readonly IProductRepository _repo;

    public CreateProductHandler(IProductRepository repo)
    {
        _repo = repo;
    }

    public async Task<int> HandleAsync(CreateProductCommand command)
    {
        var product = new Product(command.Name, command.Price);
        await _repo.AddAsync(product);
        await _repo.SaveChangesAsync();

        return product.Id;
    }
}

Notice: No EF Core here.
The application layer depends only on the Domain.

Infrastructure Layer — Implementation Details

This is where you add:

  • EF Core DbContext

  • Repository implementations

  • External API clients

  • File storage

  • Email services

Example: EF Core Repository

public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _db;

    public ProductRepository(AppDbContext db)
    {
        _db = db;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        return await _db.Products.FindAsync(id);
    }

    public async Task AddAsync(Product product)
    {
        await _db.Products.AddAsync(product);
    }

    public async Task SaveChangesAsync()
    {
        await _db.SaveChangesAsync();
    }
}

✔ Presentation Layer — API Controllers

The thin layer that exposes endpoints.

Example: ASP.NET Core Controller

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly CreateProductHandler _handler;

    public ProductsController(CreateProductHandler handler)
    {
        _handler = handler;
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductCommand command)
    {
        var id = await _handler.HandleAsync(command);
        return Ok(new { ProductId = id });
    }
}

Controller → Application Layer → Domain → Infrastructure

Exactly how clean architecture flows.

Dependency Injection Setup

In .NET 8, register the dependencies like this:

builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<CreateProductHandler>();

Why Clean Architecture Works So Well

✔ Testability

You can test domain and application logic without database or API calls.

✔ Maintainability

Each responsibility lives in its correct layer.

✔ Flexibility

Change EF Core → Dapper → MongoDB without touching business logic.

✔ Perfect for Microservices

Each service becomes clean, maintainable, and scalable.

Common Mistakes to Avoid

  • ❌ Putting EF Core code in Domain layer

  • ❌ Putting business logic in Controllers

  • ❌ Letting Infrastructure leak into Application layer

  • ❌ Making handlers too big (violate SRP)

  • ❌ Not separating Commands & Queries

Conclusion

Clean Architecture continues to be one of the best ways to structure enterprise-level .NET applications in 2025. By separating domain logic, application logic, and infrastructure details, you create a robust system that is easier to test, maintain, and scale.