DEV Community

Cover image for Clean Architecture in .NET — A Practical, Real-World Guide
Libin Tom Baby
Libin Tom Baby

Posted on

Clean Architecture in .NET — A Practical, Real-World Guide

Layers (Domain/Application/Infrastructure/Presentation), dependency rules, folder structure, real project setup

Clean Architecture is one of the most talked-about patterns in enterprise .NET development.

But the theory is often dense and the examples either too simple or too complex to apply directly.

This guide breaks it down with a practical folder structure, real code examples, and clear rules for each layer.


The Core Idea

Clean Architecture organises your code into layers with one golden rule:

Dependencies only point inward.

The inner layers know nothing about the outer layers.

[Presentation]   → [Application] → [Domain]
[Infrastructure] → [Application] → [Domain]
Enter fullscreen mode Exit fullscreen mode

Your business logic (Domain) has zero knowledge of databases, HTTP, or UI frameworks.


The Four Layers

1. Domain — the core

Contains your business entities, value objects, domain events, and interfaces.

No dependencies on any other layer. No NuGet packages for databases or HTTP.

// Domain/Entities/Order.cs
public class Order
{
    public Guid Id { get; private set; }
    public string CustomerId { get; private set; }
    public List<OrderLine> Lines { get; private set; } = new();
    public OrderStatus Status { get; private set; }

    public void AddLine(Product product, int quantity)
    {
        // Pure business logic — no EF, no HTTP
        if (quantity <= 0) throw new DomainException("Quantity must be positive");
        Lines.Add(new OrderLine(product, quantity));
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Application — use cases

Contains your use cases (CQRS commands and queries), interfaces for infrastructure, and application-level business logic.

Depends on Domain only.

// Application/Orders/Commands/CreateOrderCommand.cs
public record CreateOrderCommand(string CustomerId, List<OrderLineDto> Lines)
    : IRequest<Guid>;

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly IOrderRepository _orders;
    private readonly IEmailService _email;

    public CreateOrderCommandHandler(IOrderRepository orders, IEmailService email)
    {
        _orders = orders;
        _email = email;
    }

    public async Task<Guid> Handle(CreateOrderCommand command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Lines);
        await _orders.AddAsync(order, ct);
        await _email.SendOrderConfirmationAsync(order);
        return order.Id;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Infrastructure — the outside world

Implements the interfaces defined in Application. Contains EF Core, HTTP clients, email services, file storage.

// Infrastructure/Persistence/OrderRepository.cs
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context) => _context = context;

    public async Task AddAsync(Order order, CancellationToken ct)
    {
        await _context.Orders.AddAsync(order, ct);
        await _context.SaveChangesAsync(ct);
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Presentation — the entry point

Controllers, minimal API endpoints, SignalR hubs. Receives HTTP requests and delegates to the Application layer.

// Presentation/Controllers/OrdersController.cs
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly ISender _mediator;

    public OrdersController(ISender mediator) => _mediator = mediator;

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateOrderCommand command)
    {
        var id = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetById), new { id }, null);
    }
}
Enter fullscreen mode Exit fullscreen mode

Folder Structure

src/
├── MyApp.Domain/
│   ├── Entities/
│   ├── ValueObjects/
│   ├── Enums/
│   └── Exceptions/
├── MyApp.Application/
│   ├── Orders/
│   │   ├── Commands/
│   │   └── Queries/
│   ├── Interfaces/
│   └── Common/
├── MyApp.Infrastructure/
│   ├── Persistence/
│   ├── ExternalApis/
│   └── Services/
└── MyApp.Presentation/
    ├── Controllers/
    └── Program.cs
Enter fullscreen mode Exit fullscreen mode

The Dependency Rule in Practice

  • Domain has no project references
  • Application references Domain only
  • Infrastructure references Application (to implement interfaces) and Domain
  • Presentation references Application — never Infrastructure directly

When to Use Clean Architecture

Use it when:

  • The system will grow over time
  • Multiple developers work on the codebase
  • Business logic is complex and must be tested independently
  • You expect to swap out infrastructure (database, external APIs)

Do not use it for small CRUD applications or prototypes — the overhead is not justified.


Interview-Ready Summary

  • Dependencies point inward — Domain knows nothing about Infrastructure
  • Domain = entities and pure business rules, no external dependencies
  • Application = use cases (CQRS commands/queries), defines interfaces
  • Infrastructure = implements interfaces, owns EF Core and external services
  • Presentation = controllers that delegate to Application via MediatR
  • Clean Architecture makes business logic testable without a database or HTTP stack

A strong interview answer:

"Clean Architecture separates the system into layers where dependencies only point inward. The Domain layer contains pure business logic with no external dependencies. Application defines use cases and interfaces. Infrastructure implements them. Presentation handles HTTP. This makes the business logic fully testable in isolation, and allows you to swap infrastructure — like switching databases — without touching your business rules."

Top comments (0)