Clean Architecture: The Dependency Rule
Clean Architecture, popularized by Robert C. Martin (Uncle Bob), builds on hexagonal architecture with explicit layers and a strict dependency rule. It's widely adopted in enterprise applications.
TL;DR
| Concept | Definition |
|---|---|
| Core Idea | Dependencies point inward toward business rules |
| Layers | Entities → Use Cases → Interface Adapters → Frameworks |
| Dependency Rule | Inner layers know nothing about outer layers |
| Goal | Framework-independent, testable, UI-independent |
The Concentric Circles
The Layers
Layer 1: Entities (Enterprise Business Rules)
// Entities - pure business rules, no dependencies
public class Order
{
public OrderId Id { get; }
public CustomerId CustomerId { get; }
public Money Total { get; private set; }
public OrderStatus Status { get; private set; }
public void AddLine(ProductId productId, int quantity, Money price)
{
if (Status != OrderStatus.Draft)
throw new OrderNotEditableException();
_lines.Add(new OrderLine(productId, quantity, price));
RecalculateTotal();
}
}
Layer 2: Use Cases (Application Business Rules)
public class SubmitOrderUseCase : ISubmitOrderUseCase
{
private readonly IOrderRepository _orderRepository;
private readonly IPaymentGateway _paymentGateway;
public async Task<SubmitOrderResponse> Execute(SubmitOrderRequest request)
{
var order = await _orderRepository.GetById(new OrderId(request.OrderId));
order.Submit();
var paymentResult = await _paymentGateway.Charge(order.Total);
if (!paymentResult.IsSuccessful)
return SubmitOrderResponse.PaymentFailed(paymentResult.Error);
await _orderRepository.Save(order);
return SubmitOrderResponse.Success(order.Id);
}
}
Layer 3: Interface Adapters
// Controller - adapts HTTP to use case
[ApiController]
public class OrdersController : ControllerBase
{
private readonly ISubmitOrderUseCase _submitOrder;
[HttpPost("{orderId}/submit")]
public async Task<ActionResult<OrderDto>> SubmitOrder(Guid orderId)
{
var response = await _submitOrder.Execute(new SubmitOrderRequest(orderId));
if (!response.IsSuccess) return BadRequest(response.Error);
return Ok(new OrderDto { Id = response.OrderId!.Value });
}
}
The Dependency Rule
┌─────────────────────────────────────────────────────────┐
│ THE DEPENDENCY RULE │
├─────────────────────────────────────────────────────────┤
│ │
│ ✅ ALLOWED │
│ • Controller → Use Case │
│ • Use Case → Entity │
│ • Repository Implementation → Repository Interface │
│ │
│ ❌ NOT ALLOWED │
│ • Entity → Use Case │
│ • Use Case → Controller │
│ • Entity → Repository │
│ │
└─────────────────────────────────────────────────────────┘
Project Structure
src/
├── Domain/ # Entities (innermost)
│ ├── Entities/
│ └── ValueObjects/
├── Application/ # Use Cases
│ ├── UseCases/
│ └── Interfaces/
├── Infrastructure/ # Interface Adapters + Frameworks
│ ├── Persistence/
│ └── ExternalServices/
└── WebApi/ # Frameworks & Drivers
└── Controllers/
Common Mistakes
Mistake 1: Entities Know About Persistence
// ❌ Entity with EF Core attributes
public class Order
{
[Key] public Guid Id { get; set; }
[ForeignKey("CustomerId")] public Customer Customer { get; set; }
}
// ✅ Pure entity
public class Order
{
public OrderId Id { get; }
public CustomerId CustomerId { get; }
}
Mistake 2: Use Cases Return Entities
// ❌ Leaking domain
public interface IGetOrderUseCase
{
Task<Order> Execute(Guid orderId); // Returns entity!
}
// ✅ Return DTOs
public interface IGetOrderUseCase
{
Task<OrderDto> Execute(Guid orderId);
}