Skip to content

Clean Architecture

Clean Architecture separates concerns so that your business logic doesn’t care whether you’re using Entity Framework, SQL, or REST APIs. When frameworks change (and they do), your domain stays intact.


LayerResponsibilityExample in C#
Entities (Domain)Core business rules, no external dependenciesProduct entity with business validation logic
Use Cases (Application)Orchestrate data flow, application-level logicCreateProductUseCase implementing ICreateProductUseCase
Interface AdaptersMediators between use cases and external systemsProductController, ProductRepository (adapter to EF)
Frameworks & DriversDatabases, HTTP frameworks, external servicesEntity Framework, ASP.NET, third-party APIs

The rule is simple: never let outer layers leak into inner layers. A domain entity should never have an [Table] attribute. A use case should never know about your ORM.


Domain entities contain business rules. No EF attributes, no navigation properties from the database.

public class Product
{
public int Id { get; init; }
public string Name { get; init; }
public decimal Price { get; init; }
public bool IsDiscounted() => Price < OriginalPrice;
}

Use cases are application-level orchestrators. Define them as interfaces in the Application layer; concrete implementations follow the use case flow.

public interface ICreateProductUseCase
{
Task<ProductResponse> Execute(CreateProductRequest request);
}
public class CreateProductUseCase : ICreateProductUseCase
{
private readonly IProductRepository _repository;
public CreateProductUseCase(IProductRepository repository) => _repository = repository;
public async Task<ProductResponse> Execute(CreateProductRequest request)
{
var product = new Product { Name = request.Name, Price = request.Price };
var saved = await _repository.Save(product);
return MapToResponse(saved);
}
}

Repositories: Interface in Application, Implementation in Infrastructure

Section titled “Repositories: Interface in Application, Implementation in Infrastructure”

The interface lives in the Application layer (inner). The concrete implementation using EF lives in Infrastructure (outer).

// Application Layer
public interface IProductRepository
{
Task<Product> GetById(int id);
Task<Product> Save(Product product);
}
// Infrastructure Layer
public class EfProductRepository : IProductRepository
{
private readonly DbContext _context;
public async Task<Product> GetById(int id) =>
await _context.Products.FirstOrDefaultAsync(p => p.Id == id);
public async Task<Product> Save(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
return product;
}
}

DI wiring happens at the composition root (Program.cs):

services.AddScoped<IProductRepository, EfProductRepository>();
services.AddScoped<ICreateProductUseCase, CreateProductUseCase>();

A controller’s job is to translate HTTP into use case calls. Not the other way around.

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly ICreateProductUseCase _createUseCase;
public ProductsController(ICreateProductUseCase createUseCase) =>
_createUseCase = createUseCase;
[HttpPost]
public async Task<IActionResult> Create(CreateProductRequest request)
{
var result = await _createUseCase.Execute(request);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
}

The controller doesn’t orchestrate business logic — it delegates to the use case.


AspectMonolithMicroservices
DeploymentSingle unitEach service independent
ScalingScale entire app or nothingScale services that need it
Data ownershipSingle DB, shared schemaBounded context per service
ComplexitySimpler to startMore complex (network, distributed logic)
Team autonomyLess — teams share codebasesMore — teams own services end-to-end

Clean Architecture works for both. A monolith with clean layers is easier to split into microservices later — your domain layers become service boundaries, and your use cases become service contracts.


[Test]
public async Task CreateProduct_WithValidRequest_ShouldSave()
{
// Arrange
var mockRepository = new Mock<IProductRepository>();
var useCase = new CreateProductUseCase(mockRepository.Object);
var request = new CreateProductRequest { Name = "Chai", Price = 3.99m };
// Act
var result = await useCase.Execute(request);
// Assert
Assert.That(result.Name, Is.EqualTo("Chai"));
mockRepository.Verify(r => r.Save(It.IsAny<Product>()), Times.Once);
}

No database. No HTTP server. Just logic. Fast, deterministic, repeatable.


  • EF attributes in domain entities[Table], [Column], navigation properties — these are infrastructure concerns. Keep entities clean. Map at the boundary.
  • Fat controllers — Controllers should translate HTTP to use case calls, not orchestrate business logic. If your controller is more than 20 lines, you probably have logic that belongs in a use case.
  • Anemic domain model — If your entities have no business logic (just getters/setters), you’ve pushed all logic into use cases. Some balance is needed; the domain should encapsulate intent.
  • Leaky abstractionsIRepository.Where(x => x.Name == "Chai") exposes LINQ to the application layer. Repositories should expose domain queries: IRepository.GetByName(name).
  • Forgetting the dependency rule — It’s easy to “just add a reference” from a use case to a controller. Don’t. The inward dependency flow is the whole point.

“What is the dependency rule?” — Dependencies always point inward. Outer layers depend on inner; inner layers have no knowledge of outer. This keeps domain logic independent of delivery mechanisms.

“How does DIP (Dependency Inversion Principle) work in Clean Architecture?” — Define interfaces in inner layers (domain, application); implement them in outer layers (infrastructure). The inner layer depends on the abstraction, not the concrete implementation.

“Can you use Clean Architecture in a microservices setup?” — Yes. Each service has its own layered architecture. Bounded contexts in your domain become service boundaries. When you need to split a monolith, clean layers make the migration cheaper.

“Where do Entity Framework models live?” — In the Infrastructure layer. They’re infrastructure concerns. Map them to domain entities at the boundary; keep EF out of your domain.