Clean Architecture in .NET: Layers and the Dependency Rule
Dependency Inversion, from lesson 081, scaled up from one constructor to an entire application's file structure.
Go back to lesson 081. OrderFulfillmentService depended on IInventoryRepository, never on SqlInventoryRepository — the high-level policy never mentioned the low-level, technology-specific detail. That was Dependency Inversion applied to one class's constructor. Now ask a bigger question: what if that same rule — dependencies point toward abstractions, never toward concrete, swappable detail — governed not just one class, but the layout of an entire application? Every folder, every project reference, every "can this file import that file" decision?
That's Clean Architecture: the same idea from lesson 081, applied at the scale of an entire codebase's structure, with one precise, non-negotiable rule about which direction dependencies are allowed to point.
In this lesson, you'll learn Clean Architecture's concentric-circles model, the Dependency Rule that makes the whole thing work, and how to build a small, concrete slice of it — a domain entity, an interface it defines, and an infrastructure implementation that the domain layer never sees.
What Is It?
The Simple Explanation
Clean Architecture is a way of organizing an application into layers, arranged as concentric circles, where the innermost layer holds your most important, most stable code — your actual business rules — and every layer further out holds progressively more replaceable, technology-specific detail. The rule that makes it work: code in an inner circle is never allowed to know that an outer circle exists.
The Technical Definition — the Four Rings
- Domain / Entities — the center. Core business objects and the rules that govern them (an Order, a Product, the rule that an order can't ship with zero line items). No dependency on anything outside — no EF Core, no ASP.NET Core, no framework of any kind, not even as a NuGet package reference.
- Application / Use-Cases — the next ring out. Orchestrates domain objects to fulfill a specific application operation ("place this order," "cancel this shipment") — this is where the workflow steps live, calling into the domain and into abstractions the domain/application layer itself defines.
- Infrastructure — an outer ring. The technology-specific "detail" — EF Core's DbContext, an HTTP client calling a payment provider, an SMTP client. This is where SqlOrderRepository and similar concrete classes actually live.
- Presentation / UI — the outermost ring. ASP.NET Core controllers, minimal API endpoints, a CLI, a mobile app's networking layer — whatever drives the application from the outside world.
Why Does It Exist?
- Without an explicit, enforced structure, business logic drifts into wherever is convenient — a controller action, a piece of code that also calls EF Core directly, a class that also happens to send emails — until the actual business rules are inseparable from ASP.NET Core, EF Core, and whichever vendor SDKs the team picked
- An explicit, structural guarantee — not just a convention developers are supposed to remember — that the business rules stay independent of any specific framework, database, or external service, so they can be understood, tested, and changed without dragging infrastructure along
- Arrange the application in layers, and enforce one rule strictly across all of them: dependencies only ever point inward, toward the domain — this is Clean Architecture's Dependency Rule
Big Picture — the Dependency Rule
Dependencies only ever point inward. An outer layer may depend on an inner layer. An inner layer must never depend on an outer one. The Domain layer doesn't know EF Core exists. It doesn't reference DbContext, HttpClient, or any framework type at all — not even as a compiled reference, let alone actual code calling into it.
This is stated precisely because it's the single fact everything else in this lesson depends on. It is not "try to keep the domain layer mostly clean" — it is an absolute, structural rule: if Order.cs in the Domain project has a using Microsoft.EntityFrameworkCore; anywhere in it, or a method signature that takes a DbContext, the rule has been broken, full stop, regardless of how small or convenient that reference seemed at the time.
- Infrastructure → references → Domain/Application
- Presentation → references → Application
- Application → references → Domain
- Domain → references → EF Core, ASP.NET Core, anything
- Application → references → Infrastructure directly
- Domain → references → Application
How It Works — the Same Trick as Lesson 081, at Larger Scale
- IProductRepository is declared inside the Domain/Application layer itself — the abstraction belongs conceptually to the inside, exactly as lesson 081 taught for a single class's dependencies.
- A use-case class takes IProductRepository through its constructor — never EfProductRepository, never DbContext.
- EfProductRepository : IProductRepository lives in the Infrastructure project, which references the Domain project — the arrow points inward, from detail toward abstraction, not the reverse.
- In Program.cs — the composition root — services.AddScoped<IProductRepository, EfProductRepository>() is the one line in the entire solution that mentions both the abstraction and its concrete Infrastructure implementation together. Nowhere inside the Domain or Application project does that pairing ever appear.
Simple Example — a Product, Across Three Layers
// ═══ DOMAIN PROJECT — references NOTHING but the base class library ═══
public class Product
{
public int Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
public Product(string name, decimal price)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Product name is required."); // a real domain rule, enforced here
if (price < 0)
throw new ArgumentException("Price cannot be negative.");
Name = name;
Price = price;
}
}
// The interface lives in the Domain/Application layer too — it's owned by the inside
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task AddAsync(Product product);
}
// ═══ APPLICATION PROJECT — references ONLY Domain ═══
public class CreateProductUseCase(IProductRepository repository)
{
public async Task<int> ExecuteAsync(string name, decimal price)
{
var product = new Product(name, price); // domain rule enforced inside the constructor
await repository.AddAsync(product);
return product.Id;
}
}
// ═══ INFRASTRUCTURE PROJECT — references Domain/Application, AND EF Core ═══
public class EfProductRepository(ShopDbContext context) : IProductRepository
{
public async Task<Product?> GetByIdAsync(int id) => await context.Products.FindAsync(id);
public async Task AddAsync(Product product) => await context.Products.AddAsync(product);
}
// ═══ PRESENTATION PROJECT (ASP.NET Core) — the composition root ═══
// Program.cs — the ONLY place EfProductRepository and IProductRepository are mentioned together
builder.Services.AddScoped<IProductRepository, EfProductRepository>();
builder.Services.AddScoped<CreateProductUseCase>();Code → Meaning → Result: Product.cs has no using Microsoft.EntityFrameworkCore; anywhere — you could delete the entire Infrastructure project and Product.cs would still compile. That's the Dependency Rule made concrete: the most important code in the application — what a valid product actually is — survives untouched even if the database technology underneath it is torn out and replaced entirely.
Real-World Example — Order Placement, End to End
A checkout flow, walked through every layer, showing exactly where each piece of code is allowed to live:
| Layer | Contains | References |
|---|---|---|
| Domain | Order, OrderLine, the rule "an order needs at least one line," IOrderRepository | nothing (base class library only) |
| Application | PlaceOrderUseCase — checks stock via IInventoryRepository, charges via IPaymentGateway, saves via IOrderRepository | Domain |
| Infrastructure | EfOrderRepository, StripePaymentGateway, SmtpOrderNotifier | Application, Domain |
| Presentation | OrdersController, calling PlaceOrderUseCase | Application |
Notice the controller in Presentation depends on PlaceOrderUseCase in Application — not directly on EfOrderRepository or StripePaymentGateway in Infrastructure. And PlaceOrderUseCase itself depends only on interfaces defined in Domain. If the team switches payment providers from Stripe to a new vendor, exactly one new Infrastructure class is written, and exactly one line changes in the composition root (238) — Order, PlaceOrderUseCase, and OrdersController are never touched.
Analogy
Think of the Domain layer as the actual law being interpreted inside a courthouse — the rules that decide a case's outcome. Those rules don't change because the building's electrical wiring changed, or because the courthouse switched from paper filing to a digital records system, or because a different security company now runs the metal detectors. The law is written in terms of legal principle, not in terms of any specific vendor's equipment.
The building's electricians, IT staff, and security contractors (Infrastructure) all have to work within a structure the law defines — they don't get to rewrite legal principle to suit whichever equipment is easiest to install. And the public entrance, the clerk's counter, the courtroom layout (Presentation) is how people interact with the whole system — but it, too, doesn't determine what the law says. Swap out the entire records system, or the entire security vendor, and the law itself is untouched — because it was never written in terms of them in the first place.
Under the Hood — How .NET Actually Enforces (or Fails to Enforce) This
- Split the solution into separate .csproj projects — MyApp.Domain, MyApp.Application, MyApp.Infrastructure, MyApp.Web — and simply never add a project reference from Domain to Infrastructure. If Product.cs tries to using Microsoft.EntityFrameworkCore;, the build fails outright, because the Domain project has no reference to the EF Core package at all. This is the single most effective, and most commonly used, way to make the Dependency Rule a compiler error instead of a code-review hope.
- A single-project application organized only by folders (/Domain, /Infrastructure) can still have a file inside /Domain that adds a using for EF Core — nothing stops the compiler from allowing it. Architecture tests (automated checks that fail a build if a forbidden reference appears) are one common way teams enforce the rule even within one project.
- Just like lesson 081's "under the hood" section explained for one class's dependencies, something still has to construct the real, concrete EfProductRepository and wire it to IProductRepository at runtime — the Dependency Rule doesn't eliminate that need, it relocates it entirely to the composition root (238), so it happens in exactly one place instead of scattered throughout the domain.
Common Confusion
1. "This is a brand-new idea" — it isn't, you already learned its core principle
Clean Architecture didn't invent a new rule — it took the Dependency Inversion Principle from lesson 081 and applied it at the scale of an entire application's structure instead of one class. "High-level modules should not depend on low-level modules; both should depend on abstractions" is, word for word, the same idea as "the Domain layer should not depend on the Infrastructure layer; both depend on interfaces the Domain layer defines." If you understood 081, you already understand the core of this lesson — the new part is applying it consistently across every layer of a whole codebase, not just one constructor.
2. "The Application layer and the Domain layer are the same thing" — they're related but distinct
Domain holds the objects and rules themselves — what a valid Order is. Application holds the orchestration of those objects to fulfill a specific operation — the steps involved in placing an order, in what sequence, calling which abstractions. Many smaller applications combine these into one project pragmatically; larger ones separate them because the rate of change differs — core domain rules tend to be very stable, while specific application workflows change more often as requirements evolve.
Common Mistakes
Mistake 1 — Putting an EF Core attribute or navigation-property shape directly on a domain entity, "just this once"
Adding [Table("Products")] or shaping Product's constructor around what EF Core's change tracker prefers (a public parameterless constructor, mutable setters everywhere) rather than around what the domain rules actually require — a small, seemingly harmless leak of Infrastructure concerns into Domain.
Keep the entity's shape driven entirely by domain rules; use EF Core's Fluent API configuration (in the Infrastructure layer) to map that entity to a table, rather than decorating the entity itself with persistence-specific attributes.
Mistake 2 — Letting the Application layer reference Infrastructure directly, "just for this one repository"
CreateProductUseCase taking EfProductRepository as a constructor parameter instead of IProductRepository — a single exception that quietly breaks the Dependency Rule and welds the use-case to EF Core specifically.
Every dependency the Application layer takes must be an abstraction defined inward, in Domain or Application itself — with zero exceptions, the same discipline lesson 081 taught for any single class.
Mistake 3 — Applying full Clean Architecture ceremony to a genuinely small application
Splitting a five-screen internal tool into four separate .csproj projects, with the accompanying interfaces, composition-root wiring, and layer discipline — for an application that will likely never swap its database or its framework, maintained by one developer.
See "When Should I Use It?" below — this level of structural rigor earns its cost on applications with real, ongoing complexity and a real, multi-person team; it's genuine overhead on a small, simple one.
When Should I Use It?
| Situation | Leans toward |
|---|---|
| A large, long-lived application with real, non-trivial business rules and a multi-person team | Full Clean Architecture — separate projects, strict Dependency Rule enforcement |
| Business logic that genuinely needs to outlive a specific framework or database choice | Full Clean Architecture — this is exactly the scenario it's built for |
| A small internal tool, a prototype, or a project with one developer and no real complexity to isolate | Skip the multi-project ceremony — a well-organized single project with clear folders is honest and sufficient |
| "Every professional application should be Clean Architecture" as the only justification | Reconsider — this is exactly the same blanket-rule trap lesson 081 and 148 both warned against, applied to a bigger decision |
Mental Model
Application = orchestrates the domain, depends only on Domain and its own abstractions.
Infrastructure & Presentation = the detail — swappable, technology-specific, depends inward.
The Dependency Rule = arrows only point inward; an inner layer never knows an outer one exists.
Remember: this is lesson 081's Dependency Inversion Principle, applied to a whole application's file structure instead of one class's constructor.
Key Takeaway
- Clean Architecture arranges an application into concentric layers — Domain/Entities at the center, then Application/Use-Cases, then Infrastructure and Presentation/UI at the edges.
- The Dependency Rule: dependencies only ever point inward. An outer layer may depend on an inner one; an inner layer must never depend on an outer one. The Domain layer never references EF Core, ASP.NET Core, or any framework type.
- This is Dependency Inversion (081) applied at the scale of an entire application's architecture, not just one class's constructor — the same "depend on abstractions the high-level side owns" idea, scaled up.
- Separate .csproj projects, with references only allowed inward, are the standard way to make the Dependency Rule a compile-time guarantee rather than a hopeful convention.
- It earns its real cost on large, long-lived applications with genuine complexity and multi-person teams — it's real, avoidable overhead on a small, simple project.
Check Your Understanding
You've seen the Dependency Rule applied to a real Product example across four layers. Let's confirm you can spot it holding — and breaking — in new scenarios.
1. According to the Dependency Rule, which of these is allowed?
Show answer
Correct: B
Why B is correct: This is precisely the allowed direction — Infrastructure, an outer layer, depends inward on Domain, an inner layer, in order to implement an interface Domain defines. Arrows pointing inward are exactly what the Dependency Rule permits.
Why A is incorrect: This is the Domain layer depending outward on Infrastructure — precisely forbidden by the Dependency Rule, which the lesson states as an absolute rule with no exceptions.
Why C is incorrect: The Domain layer must have zero dependency on any framework, including ASP.NET Core — this describes exactly the kind of leak Mistake 1 and the rule's precise statement warn against.
Why D is incorrect: This is Mistake 2 directly — the Application layer bypassing an abstraction to reference Infrastructure concretely breaks the rule just as much as Domain depending on Infrastructure would.
Reinforcement: Only inward-pointing references are allowed — an outer layer may know about an inner one; an inner layer must never know an outer one exists.
2. Why does this lesson describe Clean Architecture as "Dependency Inversion (081), applied at the scale of an entire application's architecture"?
Show answer
Correct: B
Why B is correct: Lesson 081's DIP states that high-level modules should depend on abstractions, not low-level details — Clean Architecture applies that identical structural rule (Domain/Application depending only on abstractions they own, never on Infrastructure directly) at the scale of an entire application's project structure.
Why A is incorrect: The lesson explicitly and directly connects the two — this is not a superficial comparison, it's the same principle at a different scale.
Why C is incorrect: As lesson 081 established, DIP itself requires no DI container — it's a compile-time reference structure. The same is true for Clean Architecture's Dependency Rule, which is fundamentally about project references, not runtime tooling.
Why D is incorrect: Neither is a C# language feature at all — both are design principles/architectural patterns independent of any specific C# version.
Reinforcement: Recognizing the same underlying principle recurring at different scales — one constructor, then an entire codebase — is the core insight this lesson builds on.
3. A small internal tool with one developer, five screens, and no realistic plan to ever change database or framework is split into four separate Clean Architecture projects with full layer separation. What does this lesson say about that decision?
Show answer
Correct: B
Why B is correct: This is Common Mistake 3 and the "When Should I Use It?" table directly — full Clean Architecture's structural overhead is worth paying for genuine complexity and long-lived, multi-person projects; applying it reflexively to a small, simple tool is unnecessary ceremony.
Why A is incorrect: This is exactly the blanket-rule trap the lesson explicitly warns against — "every professional application should be Clean Architecture" is called out as a weak justification on its own.
Why C is incorrect: EF Core works perfectly well referenced from an Infrastructure project in a multi-project solution — this isn't a technical limitation at all.
Why D is incorrect: The lesson names no specific screen count as a threshold — the deciding factors are complexity, longevity, and team size, not a fixed number of screens.
Reinforcement: Weigh the real complexity and lifespan of the specific project, not a blanket rule, when deciding how much of Clean Architecture's structural ceremony to adopt.
You've now scaled Dependency Inversion from one class up to an entire application's structure. Next: Hexagonal Architecture — a closely related idea, described through a different vocabulary, pursuing the exact same goal.