Clean Architecture
By the end of this lesson you'll be able to split a .NET app into layers where the business rules sit at the centre, the database and email and web framework sit at the edges, and every dependency points inward — giving you a core you can unit-test in milliseconds and swap technologies without rewriting your logic.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of your app as an onion . The very centre is the part that can never change just because the world around it does — your business rules. Around it you wrap layers: use cases, then frameworks, then the database and the web. The outer layers are the skin: they get peeled off and replaced all the time (you switch from SQL Server to Postgres, from REST to gRPC, from email to push notifications). But the core stays put. The rule that keeps the onion healthy is simple — an inner layer must never know about an outer one . The heart of the onion doesn't depend on its skin.
The Dependency Rule reads down this list: each layer may only point at layers above it (toward the centre). Domain points at nothing; Presentation points inward through everything. If an arrow ever points outward — Domain referencing Infrastructure — the architecture is broken.
1. The Layers & the Dependency Rule
A layer is just a group of classes with the same job, kept in its own project. The Domain is the heart — your entities and the rules they enforce, written in plain C# with no framework in sight. The Application layer holds use cases : thin orchestrators that drive the Domain to get something done. Infrastructure is the messy outside world — databases, email, HTTP. Presentation is the entry point — a controller or console Main . The one law tying them together is the Dependency Rule : source-code references only ever point inward. Read this worked example and run it.
2. Ports & Adapters (Dependency Inversion)
Here's the puzzle: a use case needs to send email, but email is an outer concern — and inner layers can't depend on outer ones. The fix is the port & adapter pattern. A port is an interface defined in the inner layer ("someone must be able to notify a user"). An adapter is a class in Infrastructure that implements it (the real SendGrid call). The use case depends on the port , never the adapter — so even though data flows outward at runtime, the source-code dependency still points inward. That's dependency inversion , and it's the trick that makes the whole thing work.
Your turn. The program below is almost complete — define a Domain entity that validates its input, and declare an Application port. Fill in the blanks marked ___ using the hints, then run it.
3. Adapters & Use Cases That Point Inward
Now build the other half: an adapter that implements the port, and a use case that holds the interface type — not the concrete adapter. The adapter gets handed in from outside (this is dependency injection), so the use case stays blissfully ignorant of which implementation it's talking to. Get this right and you can drop in a fake adapter in your tests and a real one in production, changing nothing in between. Fill in the two blanks.
If your Order entity carries EF Core attributes like [Key] or inherits DbContext , your business rules are now welded to a specific database technology. Change ORMs and you rewrite your core. Worse, you can't unit-test the rules without spinning up a database.
Keeping the Domain pure flips that around. The rules are just C#, so a test runs in microseconds with no setup. And because the Domain defines the ports (interfaces) it needs, Infrastructure depends on the Domain — never the reverse. The arrows stay pointing inward.
Because the use case depends on a port, your test can supply a tiny fake adapter instead of a real database — no mocking framework, no I/O. This is the same WelcomeUser use case from section 2, now driven by a fake that records calls so the test can assert on them.
Nothing in the core changed to make this testable — the inward-pointing dependency is the test seam. That's the whole return on the architecture's investment.
Q: Where do interfaces like IOrderRepository actually live?
In the inner layer that needs them — Application or Domain — not in Infrastructure. Infrastructure implements them. That's what inverts the dependency so the arrow points inward.
Q: Isn't this just a lot of extra interfaces and projects?
For a throwaway CRUD app, yes — skip it. For a system with real business rules that must survive database and framework changes, the layers pay for themselves in testability and flexibility.
Q: How is this different from the classic three-tier (UI → Business → Data) layering?
In three-tier, Business depends on Data, so the core still knows about the database. Clean Architecture inverts that bottom arrow with a port, so Data depends on Business — the core depends on nothing.
DTOs belong to the Application layer (the shape data crosses the boundary in). Mapping between a Domain entity and a database row lives in Infrastructure, keeping the Domain ignorant of persistence.
Q: Can a controller call the Domain directly?
It shouldn't. Controllers call use cases in the Application layer, which orchestrate the Domain. Keeping that path consistent is what lets you reuse a use case from a web API, a CLI, or a background job.
No blanks this time — just a brief and an outline. Wire a use case, an entity, and an in-memory adapter so the core depends only on the port. Build it, run it, and check your output against the example in the comments. This is the smallest complete Clean Architecture slice there is.
Practice quiz
What does the Dependency Rule state in Clean Architecture?
- Dependencies point outward, toward the database
- Every layer depends on every other layer
- Source-code dependencies only ever point inward, toward the Domain
- The Domain depends on Infrastructure
Answer: Source-code dependencies only ever point inward, toward the Domain. The Dependency Rule says source-code references only point inward, toward the centre. The Domain depends on nothing.
Which are the four layers, from centre outward?
- Domain, Application, Infrastructure, Presentation
- Database, Service, Controller, View
- Model, View, Controller, Router
- Entity, Repository, Service, API
Answer: Domain, Application, Infrastructure, Presentation. The four layers are Domain (rules), Application (use cases), Infrastructure (db/email/http), and Presentation (entry point).
What belongs in the Domain layer?
- Entity Framework DbContext and SQL
- ASP.NET controllers
- The composition root
- Entities, value objects, and business rules in pure C# with no framework references
Answer: Entities, value objects, and business rules in pure C# with no framework references. The Domain holds entities, value objects, and business rules as pure C# — it never references EF Core, ASP.NET, or any other layer.
In ports and adapters, what is a 'port'?
- A concrete class in Infrastructure
- An interface defined and owned by an inner layer
- A network socket number
- A database connection string
Answer: An interface defined and owned by an inner layer. A port is an interface the inner layer owns ('someone must be able to notify a user'); the adapter implements it in Infrastructure.
What is an 'adapter'?
- An outer-layer class (e.g. in Infrastructure) that implements a port
- An interface owned by the Domain
- A use case in the Application layer
- The composition root
Answer: An outer-layer class (e.g. in Infrastructure) that implements a port. An adapter is the real implementation of a port, living in an outer layer such as Infrastructure (e.g. a SendGrid email adapter).
A use case should depend on which of these?
- The concrete adapter class
- The database driver directly
- The port (interface), never the adapter
- The web framework
Answer: The port (interface), never the adapter. The use case depends on the port, so the dependency arrow points inward even though data flows outward at runtime — that's dependency inversion.
Why must the Domain have no framework or database references?
- To make it run faster on the GPU
- So the business rules aren't welded to a technology and can be unit-tested in microseconds with no database
- Because frameworks are not allowed in C#
- To reduce the binary size only
Answer: So the business rules aren't welded to a technology and can be unit-tested in microseconds with no database. A pure Domain isn't coupled to any ORM or framework, so its rules are testable in microseconds and survive technology changes.
Where is the composition root, where adapters are wired to ports?
- In the Domain layer
- Inside each entity
- In the database
- Usually Program.cs in the Presentation layer — the only place allowed to know every concrete type
Answer: Usually Program.cs in the Presentation layer — the only place allowed to know every concrete type. The composition root (typically Program.cs) is the single place that wires concrete adapters to the ports via dependency injection.
How does Clean Architecture differ from classic three-tier (UI to Business to Data) layering?
- It removes the Business layer
- It inverts the Business-to-Data arrow with a port, so Data depends on Business and the core depends on nothing
- It makes the UI depend on the database directly
- There is no difference
Answer: It inverts the Business-to-Data arrow with a port, so Data depends on Business and the core depends on nothing. In three-tier, Business depends on Data so the core knows the database. Clean Architecture inverts that with a port, so Data depends on Business.
What makes the inward-pointing dependency a useful 'test seam'?
- It requires a mocking framework
- It forces all tests to hit the database
- A test can inject a tiny fake adapter for the port instead of a real database, with no I/O
- It disables the Domain during tests
Answer: A test can inject a tiny fake adapter for the port instead of a real database, with no I/O. Because the use case depends on a port, a test supplies a fake adapter implementing it — no database, no mocking framework, runs in microseconds.