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.
The 4 Layers
Section titled “The 4 Layers”| Layer | Responsibility | Example in C# |
|---|---|---|
| Entities (Domain) | Core business rules, no external dependencies | Product entity with business validation logic |
| Use Cases (Application) | Orchestrate data flow, application-level logic | CreateProductUseCase implementing ICreateProductUseCase |
| Interface Adapters | Mediators between use cases and external systems | ProductController, ProductRepository (adapter to EF) |
| Frameworks & Drivers | Databases, HTTP frameworks, external services | Entity 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.
Implementation Patterns
Section titled “Implementation Patterns”Domain Entities
Section titled “Domain Entities”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 via Interfaces
Section titled “Use Cases via Interfaces”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 Layerpublic interface IProductRepository{ Task<Product> GetById(int id); Task<Product> Save(Product product);}
// Infrastructure Layerpublic 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>();Controllers: Don’t Mix Concerns
Section titled “Controllers: Don’t Mix Concerns”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.
Monolith vs Microservices
Section titled “Monolith vs Microservices”| Aspect | Monolith | Microservices |
|---|---|---|
| Deployment | Single unit | Each service independent |
| Scaling | Scale entire app or nothing | Scale services that need it |
| Data ownership | Single DB, shared schema | Bounded context per service |
| Complexity | Simpler to start | More complex (network, distributed logic) |
| Team autonomy | Less — teams share codebases | More — 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.
Testing Strategy
Section titled “Testing Strategy”Unit Test: Use Case + Mocked Repository
Section titled “Unit Test: Use Case + Mocked Repository”[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.
Common Gotchas
Section titled “Common Gotchas”- 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 abstractions —
IRepository.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.
Key Interview Questions
Section titled “Key Interview Questions”“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.