Loading...

Layered Architecture and Clean Architecture in C#

Learn layered architecture and Clean Architecture in C# to build maintainable, testable, and well-structured applications.

As software projects grow, maintaining organized and sustainable code becomes increasingly important. For this purpose, the Layered Architecture and its modern evolution, the Clean Architecture approach, are used. Both models provide a solid foundation for managing dependencies, separating responsibilities, and building testable, extensible systems.


What Is Layered Architecture?

Layered architecture is a classic software architecture pattern that divides an application into layers with different responsibilities. The most common form consists of 3 or 4 layers:

In terms of dependency direction, layers are dependent from top to bottom. That is, the UI can access the Business layer, and Business can access the Data Access layer, but not the other way around.


Classic Layered Architecture Flow

The following diagram illustrates a simple example:


graph TD;
  A[UI Layer] --> B[Business Layer];
  B --> C[Data Access Layer];
  C --> D[Database];

This structure is sufficient for simple CRUD-based systems, but as layers become tightly coupled, it tends to grow complex over time.


What Is Clean Architecture?

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), is an architectural approach that directs dependencies toward the center (the domain). Its goal is to keep the application’s business rules completely isolated from the outside world.

In this architecture, layers are dependent from the outside in:


graph LR;
  A[UI / API] --> B[Application];
  B --> C[Domain];
  C --> D[Infrastructure (External)];

The key difference here: The Domain layer is never dependent on any external layer.


Layers and Responsibilities in Clean Architecture


Dependency Rule

The most fundamental principle of Clean Architecture: Dependencies always point inward. That is, outer layers depend on inner layers, but inner layers are unaware of the outer ones.


// Domain Layer (depends on nothing)
public class Order
{
    public int Id { get; set; }
    public decimal Total { get; set; }
    public bool IsApproved { get; private set; }

    public void Approve()
    {
        if (Total <= 0)
            throw new InvalidOperationException("Total must be greater than 0.");
        IsApproved = true;
    }
}

// Application Layer
public interface IOrderRepository
{
    Task<Order> GetByIdAsync(int id);
    Task SaveAsync(Order order);
}

public class ApproveOrderUseCase
{
    private readonly IOrderRepository _repository;
    public ApproveOrderUseCase(IOrderRepository repository)
        => _repository = repository;

    public async Task ExecuteAsync(int id)
    {
        var order = await _repository.GetByIdAsync(id);
        order.Approve();
        await _repository.SaveAsync(order);
    }
}

// Infrastructure Layer
public class EfOrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;
    public EfOrderRepository(AppDbContext context) => _context = context;

    public Task<Order> GetByIdAsync(int id) =>
        _context.Orders.FirstOrDefaultAsync(o => o.Id == id);

    public Task SaveAsync(Order order)
    {
        _context.Update(order);
        return _context.SaveChangesAsync();
    }
}

// Presentation Layer (API)
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly ApproveOrderUseCase _useCase;
    public OrdersController(ApproveOrderUseCase useCase) => _useCase = useCase;

    [HttpPost("{id}/approve")]
    public async Task Approve(int id)
    {
        await _useCase.ExecuteAsync(id);
        return Ok();
    }
}

As you can see, the Domain layer knows nothing about infrastructure code; the Application layer works only with interfaces. This allows dependencies to be swapped easily (for example, replacing EF Core with Dapper).


Dependency Management Between Layers

Dependencies are typically resolved through Dependency Injection (DI) in the Infrastructure or Composition Root layer:


// Program.cs or Startup.cs
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddScoped<ApproveOrderUseCase>();

This way, the Controller only depends on ApproveOrderUseCase and doesn’t need to know which repository implementation is being used.


Differences Between Layered and Clean Architecture

Feature Classic Layered Architecture Clean Architecture
Dependency Direction Top to bottom Outside to inside (toward the core)
Database Dependency Business logic depends on the database Domain is independent of data access
Testability Difficult Easy (mock interfaces)
Modifiability Tightly coupled layers Dependencies managed through abstractions
Long-term Maintenance Becomes difficult over time Easy to maintain, modular structure

Real-World Example: E-Commerce Application

Below is a simplified example of how Clean Architecture might be structured in an e-commerce system:

With this design, even if the UI changes (e.g., from API to WPF), the business rules (Domain & Application) remain unaffected.


Best Practices


Example Project Structure


MyApp/
├── MyApp.Domain/
│   ├── Entities/
│   ├── Interfaces/
│   └── Exceptions/
├── MyApp.Application/
│   ├── UseCases/
│   ├── Services/
│   └── DTOs/
├── MyApp.Infrastructure/
│   ├── Persistence/
│   └── Repositories/
└── MyApp.API/
    ├── Controllers/
    └── Program.cs

This structure represents a Clean Architecture–compliant, modular .NET solution layout.


TL;DR

  • Layered Architecture: A traditional model flowing from UI → Business → Data.
  • Clean Architecture: Reverses dependencies — the core is the Domain.
  • Domain is independent, Application manages flow, and Infrastructure handles external systems.
  • Dependency Injection is the key to managing dependencies.
  • Each layer should have a clear responsibility, be testable, and replaceable.
  • Clean Architecture is ideal for long-term, maintainable, and scalable systems.

Related Articles