Q26 — Clean Architecture — Solution Structure, Dependency Rule, Layer Responsibilities
Level: Senior | Topic: Architecture
The Core Principle — Dependency Rule
In Clean Architecture, dependencies point inward only. The Domain layer has zero external dependencies. The Application layer depends only on Domain. Infrastructure and API layers depend on inner layers — but inner layers never know about outer ones. This makes the domain portable, testable in isolation, and independent of frameworks.
Layer Responsibilities
- Domain: Entities, Value Objects, Aggregates, Domain Events, Repository interfaces, Domain Exceptions. Zero external NuGet dependencies.
- Application: Use Cases / Handlers (CQRS Commands and Queries), DTOs, Validators, Pipeline Behaviours. Depends on Domain only.
- Infrastructure: EF Core, Redis, HTTP clients, file system, message bus implementations. Implements Domain repository interfaces. Depends on Domain.
- API: Controllers, Middleware, DI registration, Program.cs. Depends on Application and Infrastructure (for wiring only).
Solution Structure
src/
MyApp.Domain/ // Zero external dependencies
Entities/
Order.cs // Aggregate root with domain behaviour
OrderLine.cs
ValueObjects/
Money.cs
OrderId.cs
Events/
OrderCreatedEvent.cs
Repositories/
IOrderRepository.cs // Interface — domain defines the contract
Exceptions/
DomainException.cs
MyApp.Application/ // Depends on: Domain only
Orders/
Commands/PlaceOrderCommand.cs + PlaceOrderHandler.cs
Queries/GetOrderQuery.cs + GetOrderHandler.cs
Behaviours/
LoggingBehaviour.cs
ValidationBehaviour.cs
DTOs/OrderDto.cs
MyApp.Infrastructure/ // Depends on: Domain + EF Core + cloud SDKs
Persistence/
AppDbContext.cs
EfOrderRepository.cs // Implements IOrderRepository
Messaging/MassTransitEventPublisher.cs
Caching/RedisOrderCache.cs
MyApp.Api/ // Depends on: Application + Infrastructure (DI only)
Controllers/OrdersController.cs
Middleware/CorrelationIdMiddleware.cs
Program.cs
Domain Entity Example
// Pure C# — no EF, no ASP.NET, no framework references
public class Order : AggregateRoot<OrderId>
{
private readonly List<OrderLine> _lines = [];
public CustomerId CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public Money Total => Money.Sum(_lines.Select(l => l.LineTotal));
// Factory method — enforces invariants at creation
public static Order Create(CustomerId customerId, IEnumerable<OrderLine> lines)
{
var lineList = lines.ToList();
if (!lineList.Any())
throw new DomainException("Order must have at least one line");
var order = new Order
{
Id = OrderId.New(),
CustomerId = customerId,
Status = OrderStatus.Draft
};
order._lines.AddRange(lineList);
order.RaiseDomainEvent(new OrderCreatedEvent(order.Id, customerId));
return order;
}
// Domain behaviour — guards business rules
public void Submit()
{
if (Status != OrderStatus.Draft)
throw new DomainException($"Cannot submit order in {Status} status");
Status = OrderStatus.Submitted;
RaiseDomainEvent(new OrderSubmittedEvent(Id));
}
}
Senior Insight
Clean Architecture's biggest benefit is not structure — it's testability. Because the Domain has zero external dependencies, you can unit-test all business rules without spinning up a DB, a web server, or any infrastructure. The Application layer uses only interfaces (defined in Domain), so handlers are testable with NSubstitute fakes. Infrastructure is tested with Testcontainers at the integration level. Keep the domain rich — if all your business logic lives in the Application layer, you have an Anemic Domain Model.