Strip away the diagrams and the folder structures, and Clean Architecture reduces to a single rule: source code dependencies point inward. Everything else (the layers, the interfaces, the DI wiring) exists to serve that rule. Here is what it means in practice, the part about control flow that confuses everyone at first, and how to make the compiler enforce it for you.
What Is the Dependency Rule?
Robert C. Martin defines it simply:
Source code dependencies must only point inward.
In Clean Architecture, the application is organized in concentric circles:
- Domain (innermost) - entities, value objects, domain events
- Application - use cases, commands, queries, interfaces
- Infrastructure (outermost) - database access, external services, frameworks
The Dependency Rule says: nothing in an inner circle can know anything about an outer circle.
- Domain doesn't reference Application
- Application doesn't reference Infrastructure
- Infrastructure references Application and Domain
Arrows always point inward. Never outward.
Why the Dependency Rule Matters
Without it, your domain logic gets polluted with infrastructure concerns:
// WRONG - Domain depends on Infrastructure
public class Order
{
public void Confirm()
{
// Domain class knows about EF Core
using var context = new AppDbContext();
context.Orders.Update(this);
context.SaveChanges();
// Domain class knows about email service
var emailService = new SendGridEmailService();
emailService.Send(this.CustomerEmail, "Order confirmed!");
}
}
This code can't be tested without a database and an email service. It can't be reused in a different context. And changing your email provider means changing your domain.
With the Dependency Rule:
// RIGHT - Domain knows nothing about infrastructure
public class Order
{
public void Confirm()
{
// Pure business logic
Status = OrderStatus.Confirmed;
ConfirmedAt = DateTime.UtcNow;
RaiseDomainEvent(new OrderConfirmedDomainEvent(Id));
}
}
The domain is pure. How to persist the order and send the email is handled by outer layers.
How to Enforce It in .NET
Project References
Set up your project references to match the Dependency Rule:
<!-- Domain - references NOTHING -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
<!-- Application - references Domain only -->
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
</Project>
<!-- Infrastructure - references Application (and Domain transitively) -->
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
</ItemGroup>
</Project>
<!-- API - references Infrastructure and Application -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project>
The compiler enforces this. If Domain tries to reference an Infrastructure class, it won't compile.
Dependency Inversion
The Application layer defines interfaces. The Infrastructure layer implements them:
// Application layer - defines what it needs
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct);
void Add(Order order);
}
public interface IEmailService
{
Task SendOrderConfirmationAsync(string email, Guid orderId, CancellationToken ct);
}
// Infrastructure layer - implements using specific technology
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _dbContext;
public OrderRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
{
return await _dbContext.Orders
.Include(o => o.LineItems)
.FirstOrDefaultAsync(o => o.Id == id, ct);
}
public void Add(Order order) => _dbContext.Orders.Add(order);
}
The Application layer calls IOrderRepository. It doesn't know (or care) that Entity Framework Core is behind it.
This is the Dependency Inversion Principle at work - high-level modules (Application) don't depend on low-level modules (Infrastructure). Both depend on abstractions.
Flow of Control vs Source Dependencies
Here's the part that confuses most people when they first meet the Dependency Rule.
At runtime, control flows outward: the Application layer calls the database, sends emails, publishes messages. So how can dependencies point inward?
Because a source dependency is about what your code references at compile time, not who calls whom at runtime.
Walk through a single request:
- The API controller (outer) calls a command handler (inner). Control flows inward, dependency points inward. No conflict.
- The handler calls
IOrderRepository.GetByIdAsync(...). Control is about to flow outward into EF Core, but the handler only references an interface it owns. - The DI container resolved
IOrderRepositorytoOrderRepositoryfrom Infrastructure at startup. The call lands in the outer layer without the inner layer ever naming it.
The interface is the trick. It lets control flow outward while the compile-time arrow keeps pointing inward. When you see a diagram of Clean Architecture, the arrows show source dependencies, not call direction.
The Rule Applies to NuGet Packages Too
Project references are only half the story.
A Domain project with zero project references but a Microsoft.EntityFrameworkCore package reference violates the Dependency Rule just the same.
My guidelines:
- Domain: no packages. Entities, value objects, and domain events are plain C#.
- Application: contracts-only packages at most (MediatR contracts, FluentValidation). No EF Core, no HTTP clients, no cloud SDKs.
- Infrastructure: this is where the package weight belongs - EF Core, message brokers, external SDKs.
The gray zone is logging.
Microsoft.Extensions.Logging.Abstractions in the Application layer is a pragmatic exception most teams accept: it's an abstraction package with no implementation baggage.
Where Does the DI Container Fit?
Someone has to know about all the layers to wire them together.
That's the composition root: the API project's Program.cs.
The outermost layer referencing everything isn't a violation. It's the design: the most volatile, framework-heavy project depends on everything, and nothing depends on it.
builder.Services
.AddApplication() // handlers, validators, behaviors
.AddInfrastructure(builder.Configuration); // EF Core, email, auth
If you find yourself wanting to resolve services inside the Domain layer (service locator style), that's the Dependency Rule being violated at runtime even though the compiler is happy. Domain objects receive what they need as method parameters; they don't ask a container.
What Breaks the Dependency Rule
1. Framework Attributes in Domain
// WRONG - Domain depends on EF Core
public class Order
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(100)]
public string CustomerName { get; set; }
}
Fix: Use EF Core's Fluent API configuration in the Infrastructure layer:
// Infrastructure layer
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasKey(o => o.Id);
builder.Property(o => o.CustomerName).IsRequired().HasMaxLength(100);
}
}
2. Using Concrete Services in Application
// WRONG - Application depends on Infrastructure
public class PlaceOrderCommandHandler
{
private readonly AppDbContext _dbContext; // Infrastructure class
}
Fix: Depend on abstractions defined in the Application layer:
// RIGHT - Application depends on its own interfaces
public class PlaceOrderCommandHandler
{
private readonly IOrderRepository _repository;
private readonly IUnitOfWork _unitOfWork;
}
3. Leaking Infrastructure Types
// WRONG - Application returns infrastructure types
public class GetOrderQueryHandler
{
public async Task<DbSet<Order>> Handle(GetOrderQuery query)
{
return _dbContext.Orders; // Leaking DbSet<T>
}
}
Fix: Map to DTOs or domain objects:
public async Task<OrderResponse?> Handle(GetOrderQuery query)
{
return await _dbContext.Orders
.Where(o => o.Id == query.OrderId)
.Select(o => new OrderResponse(
o.Id,
o.Customer.Name,
o.TotalAmount.Amount,
o.Status.Name,
o.CreatedAt))
.FirstOrDefaultAsync();
}
Architecture Tests
Enforce the Dependency Rule automatically with architecture tests:
[Fact]
public void Domain_Should_Not_Reference_Application()
{
var result = Types
.InAssembly(typeof(Order).Assembly)
.ShouldNot()
.HaveDependencyOn(typeof(PlaceOrderCommand).Assembly.GetName().Name)
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
[Fact]
public void Domain_Should_Not_Reference_Infrastructure()
{
var result = Types
.InAssembly(typeof(Order).Assembly)
.ShouldNot()
.HaveDependencyOn(typeof(AppDbContext).Assembly.GetName().Name)
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
[Fact]
public void Application_Should_Not_Reference_Infrastructure()
{
var result = Types
.InAssembly(typeof(PlaceOrderCommand).Assembly)
.ShouldNot()
.HaveDependencyOn(typeof(AppDbContext).Assembly.GetName().Name)
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
Run these in CI. If someone adds a wrong dependency, the build fails.
I wrote more about this approach in enforcing software architecture with architecture tests, including rules that go beyond layer references (naming conventions, sealed handlers, interface placement).
Violating the Dependency Rule is also the most common of the Clean Architecture anti-patterns I see in real codebases - usually starting with a single EF Core attribute on a domain entity.
Summary
The Dependency Rule is the foundation of Clean Architecture. It keeps your domain and application logic independent of frameworks, databases, and external services.
Enforce it through:
- Project references - the compiler prevents invalid dependencies
- Dependency Inversion - interfaces in Application, implementations in Infrastructure
- Architecture tests - automated verification in CI
Get the Dependency Rule right, and everything else in Clean Architecture falls into place.
Thanks for reading, and stay awesome!
Frequently Asked Questions
What is the Dependency Rule in Clean Architecture?
Source code dependencies must only point inward, toward higher-level policies. The domain references nothing, the application layer references only the domain, and infrastructure references the application layer. Nothing in an inner circle knows about an outer circle.
How is the Dependency Rule different from the Dependency Inversion Principle?
The Dependency Rule is an architectural constraint about the direction of dependencies between layers. The Dependency Inversion Principle is the mechanism that makes it possible: inner layers define abstractions, and outer layers implement them.
Does the Dependency Rule apply to NuGet packages?
Yes. A domain project that references EF Core violates the rule just as much as one referencing an Infrastructure project. Keep the domain free of packages, and limit the application layer to contracts-only packages.
How does the application layer call the database if it cannot reference infrastructure?
Through interfaces it defines itself, like IOrderRepository. At runtime the DI container injects the infrastructure implementation, so control flows outward while the source dependency still points inward.
How do you enforce the Dependency Rule in .NET?
Three layers of defense: project references so the compiler blocks invalid dependencies, dependency inversion so implementations stay in outer layers, and architecture tests in CI that fail the build when someone introduces a violation.



