How do you implement Clean Architecture in a real .NET project?
In practice this translates to a solution with four separate projects:
Domain, Application, Infrastructure, and Presentation. The boundary between Application and Infrastructure is implemented through interfaces: Application declares what it needs, Infrastructure implements it.The result is code that is testable without real databases, business logic that does not change when the ORM changes, and an architecture that communicates its structure to whoever reads the code. The implementation details with real folder structure are in the article.

Marco opens the project on Monday morning. The folder structure is impeccable: Domain, Application, Infrastructure, Presentation.
The right names, the right namespaces, the right architecture.
Then he searches for where the business rules live. He finds them in the controller. Then in the service. Then in another service. Then in the repository — the piece of code that reads and writes data to the database.
They are everywhere and nowhere.
Marco has built a Clean Architecture without its one essential ingredient: business rules in the wrong place do not become correct simply because the folders have the right names.
The declared architecture and the actual one almost never match.
The folder structure is correct.
The code behaviour is not. And that difference, in this industry, is worth real money to those who can tell them apart.
Twenty years of working on real .NET systems have shown me this story more times than I can count. I lived it myself, before I understood where the real problem was.
Clean Architecture is easy to copy from any tutorial.
It is far harder to understand why, because tutorials show the structure and skip the reason. And the reason is the only thing that actually delivers real benefits.
I have worked with dozens of .NET teams across companies that have nothing in common except one problem: this one.
The symptom is always the same: declared architecture, code behaviour incompatible with that declaration.
The problem is not the developers. It is that the training ecosystem teaches answers without explaining the questions.
Architectural patterns exist because someone had a real problem to solve.
When only the patterns are passed on, without the problem that gave rise to them, you get developers who know what to do but not why.
Robert Cecil Martin (Uncle Bob) wrote Clean Architecture to solve real problems of maintainability and testability. The informal education system of the web turned it into a copy-and-paste template.
The result: everyone knows the structure, almost no one knows the reason. And the reason is the only thing that separates those who earn 40% more from those who stand still.
In this article you will not find an explanation of what Clean Architecture is.
You will find the 4-Dependency Mechanism: the conceptual framework that senior .NET developers use to decide what goes where, how to defend those choices with the compiler, how to test them in milliseconds instead of minutes, and when ignoring it is the most mature decision you can make.
With concrete examples, real code, and the four mistakes that almost no tutorial mentions — because those who write tutorials do not work on the legacy projects where they accumulate.
The code nobody dares to touch: how wrong dependencies paralyse .NET teams
The rule is one: the most important parts of the system must not depend on the most volatile ones. Business rules have no idea what Entity Framework is.
Application logic does not know ASP.NET exists. Technical details depend on the core, never the other way around.
Truly understanding this rule, not just reciting it, is what distinguishes a developer who applies a pattern from one who knows why that pattern exists.
You have studied the structure. You have applied the separation into layers. Your project has a Domain folder with entities.
And that is precisely why the right question is not "do you have the right layers?" but "are the layers doing the right thing?".
Because you can have the perfect structure and the wrong behaviour, and wrong behaviour costs exactly as much as no structure at all.
Think about the last project you worked on. Did you see business logic inside the controllers?
Did you see domain validation rules tangled together with database queries?
Did you see a 400-line service that retrieved data, applied discounts, sent emails, and updated the cache, all in one place?
This does not happen through laziness.
It happens because nobody ever explained the real cost of that choice, sprint after sprint, until the code becomes untouchable.
When business logic mixes with technical details, the code stops being testable in isolation. To verify that a discount is applied correctly, you have to start the database.
To check that an order confirmation follows the rules, you have to load the entire configuration. The feedback loop stretches from seconds to minutes.
Bugs surface in production instead of during development.
The fear of modifying existing code grows, release after release, until nobody wants to touch that module.
It is a trap that reveals itself fully only when it is too late to escape easily.
Architectural separation is not an aesthetic matter. It is the condition that makes it possible to verify business logic in milliseconds, without a database, without a framework, without system configuration.
The practical reason is straightforward.
Think of the electrical wiring in a building. If every appliance is connected to a socket with a custom-made cable, replacing the socket means dismantling the appliance.
If every appliance uses a standard plug, any compatible socket works.
Clean Architecture is that standard plug: the core declares the contract format, the infrastructure implements it with whatever technology it wants. Want to switch from SQL Server to PostgreSQL?
Rewrite only the data access layer.
Want to change email provider? You touch one file. The domain does not change by a single line.
The practical test is immediate: try writing a test for your core business logic without starting the database, without loading configuration, without complex mocks.
If you can, you are respecting the rule. If you cannot, you have coupling where there should be none, and every change to the system costs more than it should.
The salary difference between a senior developer and a software architect is not built on memorised patterns alone.
It is built on this ability: seeing where coupling erodes the value of software over time, and building structures that resist it.
If you already know the rule but business logic in your project is still mixed with technical details, the problem is not the theory: it is that nobody has ever shown you how to apply it on real code, under the pressure of a real sprint.
That is exactly what we do in the Software Architect AI Course.
Four modules, zero compromises: the structure the compiler enforces for you
The optimal structure in .NET is four modules with dependencies declared explicitly, verified by the compiler on every build.
This is not an aesthetic choice: it is a structural constraint that makes it impossible to violate architectural rules through distraction or deadline pressure.
When the compiler will not let you do something, there is no debate in code review. Sprint pressure changes nothing.
You know that moment when a colleague says "I did it this way because it was easier"? In a project where everything lives in the same module, or with folders instead of separate projects, this happens constantly.
Architectural conventions that depend solely on team discipline degrade one small compromise at a time.
After a year, there is no longer any architecture: there are only the decisions that seemed urgent at the time.
The problem is not lack of good intentions. It is that good intentions, under deadline pressure, always give way.
Separating the solution into modules solves this at the root.
If Domain.csproj attempts to reference Infrastructure.csproj, it will not compile.
The compiler becomes the guardian of architectural rules. And unlike good intentions, the compiler never gives way.
The hierarchy is precise and follows the same logic as the fundamental rule.
The Domain module has no external dependencies: pure language only, zero third-party libraries.
It contains entities, value objects, domain events, and domain errors.
The Application module depends only on Domain and contains use cases, contracts for external dependencies (the interfaces), and CQRS commands and queries.
The Infrastructure module depends on Application and contains the concrete implementations: EF Core repositories, email services, HTTP clients, and integrations with external services.
The Presentation module depends on Application and on Infrastructure only to connect contracts to implementations at application startup via the dependency injection container.
This structure has a side effect that is often undervalued: it is completely self-documenting.
When a new developer joins the team, the module structure immediately tells them where to find everything.
Contracts always live in Application.
Implementations always live in Infrastructure.
Business rules live in Domain. No additional documentation is needed, and documentation you do not have to maintain never becomes outdated.
A practical test to assess your current project: answer these questions without looking at the code:
- Where does the contract for fetching orders live?
- Where does the concrete implementation live?
- If I switch email provider tomorrow, which files do I touch?
- Where does the logic that decides whether an order can be confirmed live?
If the answers are not immediate, the structure is not clear enough.
In a well-structured project, each of these answers takes less than five seconds. If it takes longer, you are searching in a system that is not helping you work.
A practical detail that is often overlooked: the order in which modules are loaded at application startup does not necessarily mirror the dependency hierarchy.
The Presentation module is responsible for connecting everything via the DI container.
This means Presentation.csproj references both Application and Infrastructure, but only for initial configuration.
When the application is running, the Application pieces know nothing about Infrastructure: they only know the rules of the game.
The difference between "who knows whom" when the application is assembled and "who knows whom" when the application is running is one of the most misunderstood concepts in the entire architecture.
Entities with behaviour: the secret that tutorials never teach
A correct application core contains classes with behaviour, not just data.
The logic that decides whether an operation is valid lives inside the entity itself, not in the handler — the piece of code that executes a specific operation — that calls it.
If your entities have only properties and no meaningful methods, you are using Clean Architecture as decoration.
The external structure is there, but the main benefit is missing.
This is the most common mistake and the most costly in the long run. Entities become data containers, what Eric Evans — the domain modelling expert recognised worldwide — calls the Anemic Domain Model: entities containing only data and no rules.
All business logic migrates to handlers, services, and controllers.
The core exists on paper, but is empty of meaning.
It is like having a company legal department that does not know the law: it exists, it takes up space, but it solves no real problem.
The unmistakable signal is specific and easy to identify: if in a handler you find a check on the state of an entity followed by a direct modification of that state, you are writing business logic outside the place where it should live.
Every time you replicate that logic elsewhere, you create a risk: the two pieces can diverge over time.
When they diverge, you have subtle bugs that surface in production weeks later, difficult to reproduce and costly to fix.
The cost of this pattern is not immediate.
It accumulates over time and accelerates as the project grows.
Adding a new business rule requires finding every point in the code where that rule is applied, instead of modifying a single method in the right place.
Changes become risky because there is no single source of truth. Tests do not cover the behaviour of the core because the core has no behaviour to verify.
A well-designed entity exposes operations with names that represent real business intentions.
A well-designed entity does not expose fields for external modification: it exposes operations with names that represent real business intentions.
Not "update this value", but "confirm the order", "add a line item", "apply the discount".
The difference seems stylistic. It is not: in the first case, anyone can set any value at any moment.
In the second, the rules live inside the entity and cannot be bypassed.
The difference seems small in a single line of code, but it completely changes the maintainability of the system over time.
This approach delivers two concrete benefits that are felt every day.
Business rules are in one place: if a rule changes, the modification is in a single method, and a single automated check verifies the new behaviour.
And entities become testable in complete isolation: no database, no framework, no complex configuration.
You create the object, call the method, verify the result.
These tests run in milliseconds.
In a well-designed domain, handlers are short: they load the domain object, call a method, save the result.
All the complexity lives in the entities, where it is testable, understandable, and modifiable without risk.
The quickest way to assess the quality of a project's core: compare the size of the handlers with the size of the entities.
If the handlers are much larger, the logic is in the wrong place. In a well-designed core, a typical handler is short: it validates the input, loads the aggregate — the main entity with its rules — calls the appropriate domain method, and persists the result.
The visible complexity is in the entities, not in the handlers.
CQRS with MediatR: how to eliminate the untouchable classes nobody dares to modify

The application layer contains the orchestration of use cases, not business logic.
It is a distinction that seems obvious on paper and nearly impossible to maintain in practice, because sprint pressure always pushes toward the shortcut: put it here, it works, let's move on.
You already know the result.
You have seen that service class with twenty methods. Perhaps you wrote it yourself, with the best intentions, on a project that seemed small at the time.
Every new requirement added a method. Dependencies grew because every use case had its own.
Tests became a nightmare because to test a single operation you had to configure the entire class.
And every cross-cutting behaviour — validation, logging, transactions — had to be added by hand to every method, with the very real risk that someone would forget.
Eventually, that class became untouchable: not because it was complex, but because nobody knew for certain what might break if they modified it.
The CQRS pattern solves this at the root, and it does so with a brutally simple idea: operations that change the state of the system are not the same as operations that only read, and they should not live together.
Commands declare an intention (ConfirmOrder, CreateOrder, AddProductToCart). Queries ask for something specific (GetOrderById, ListCustomerOrders).
Each has its own dedicated handler, with exactly the dependencies needed for that use case and nothing more.
The direct consequence is that adding a new use case means adding a new pair — operation plus handler — without touching anything that already exists.
Tests already written continue to pass. The risk of regression is structurally zero, not because the team is skilled, but because the existing code has not been modified.
This is the kind of structure that allows a project to grow to hundreds of features without development speed collapsing under its own weight.
MediatR is the standard tool in the .NET world for putting this pattern into practice, but its main advantage is not handler management itself.
It is the pipeline mechanism: a sequence of actions that fires automatically every time a registered operation is executed.
Input validation, logging, transaction management, caching. You write them once. You configure them once.
From that point on they apply to every existing operation and to any future operation, without any handler needing to know they exist.
The practical test is immediate: imagine needing to add logging to every write operation in a traditional service class with twenty methods.
Twenty changes, twenty points where you could make a mistake, twenty places where the next developer might forget to update the code.
With the pipeline: write one class, register it, and the behaviour applies everywhere automatically. Zero risk of forgetting anything.
There is a widespread misconception about CQRS worth addressing before it becomes an excuse not to adopt it.
The pattern does not require two separate databases, nor Event Sourcing, nor distributed architectures.
In its most useful form for the vast majority of real .NET projects, CQRS means only this: commands load aggregates, apply domain rules, and persist changes; queries go directly to the data source with optimised projections, bypassing the aggregates.
Same database, same infrastructure, separate models.
This version delivers ninety percent of the benefits without any of the operational complexity that frightens teams when they hear the word "CQRS" for the first time.
Some more advanced approaches — such as keeping multiple copies of data for faster reads, recording every change as a sequence of events, or accepting that data is not always immediately synchronised — do make sense in specific contexts.
But adopting them without need is pure over-engineering, and recognising that difference is exactly the kind of judgement that separates an architect from someone who applies patterns without asking why.
Reading best practices is the first step.
Turning them into concrete decisions for your system, knowing when to apply CQRS and when it is over-engineering, recognising whether your team is building or accumulating debt disguised as good architecture.
That requires guided practice, feedback on real cases, and someone who tells you when you are wrong before you find out in production.
That is what we do in our Software Architect AI Course, with direct mentorship on real codebases and decisions you already face every sprint.
The infrastructure layer that saves you when the client changes their mind
The infrastructure layer contains all the concrete implementations of external dependencies: the database, email, message queues, and integrations with third-party services.
When the client decides to change technology, or when an external service modifies its APIs, you touch only this layer. The domain and the application layer are not modified.
If you have respected the fundamental rule, the migration is a matter of hours, not weeks.
How many times have you seen Entity Framework used directly in controllers or in application services?
How many times have you seen LINQ queries scattered across different parts of the codebase, far from any separation of concerns?
These patterns create a direct coupling between business logic and technical details.
When the client decides to migrate from one database to another, the work is not changing the provider: it is finding and modifying every single point in the code that depends on the specific behaviour of the old technology.
In projects without architectural separation, this kind of migration typically takes weeks of work, with a high regression risk because the changes are spread everywhere.
In a project with a well-isolated infrastructure layer, the same migration takes hours: update the provider, verify the platform-specific configurations, re-run the automated tests.
No changes to the domain. No risk to business logic.
The separation works because Application defines contracts and Infrastructure implements them. Application does not know a specific database exists: it only knows it needs something that respects IOrderRepository.
The dependency injection system connects the concrete implementations to the contracts at application startup.
Changing an implementation means changing the registration in the DI container and the concrete class, without touching any of the code that uses the contract.
A real example of the value of this separation: a document management system with multiple approval stages and integration with three external services.
When one of those services changed its APIs, the update required changes only to the class that implemented the corresponding contract in the infrastructure layer.
Domain and Application were not touched. The existing tests confirmed that the contract was still being respected.
The deployment happened on the same day the change was announced. In an unstructured project, the same operation would have required days of impact analysis and weeks of changes.
It is worth mentioning a practical approach that is often undervalued: the separation between write and read operations within the same infrastructure layer.
For operations that modify the domain, loading the full data structure and applying business rules is correct: invariants are verified, consistency is guaranteed.
For optimised reads — dashboards, reports, and paginated lists — loading a complex data structure only to project a portion of it is costly without any architectural benefit.
Separate read queries that go directly to the data source with optimised projections are a pragmatic choice that does not violate the architecture and delivers concrete performance benefits for the most frequent operations.
The four mistakes that turn Clean Architecture into disguised technical debt
The systemic errors in Clean Architecture come down to four recurring patterns.
Recognising them is more than half the work of fixing them.
After years of code reviews on .NET projects that claim to use Clean Architecture, these four patterns emerge with almost comic regularity.
This is not a problem with less capable developers: it is a problem of superficial understanding of an architecture that is easy to copy but genuinely difficult to understand, requiring experience and someone who explains the why before the how.
The cost of these mistakes is not immediate: it manifests when the project reaches a certain size and adding a new feature requires touching seven files across four layers to do something that is logically simple:
- Mistake 1: a core without behaviour (Anemic Domain Model). Entities have only properties. Logic lives in handlers. The unmistakable signal: a handler that checks the state of an entity and modifies it directly, bypassing a method on the entity itself. The core exists on paper, but does nothing useful. It is a folder structure, not an architecture.
- Mistake 2: technological dependencies in the wrong place. Entity Framework attributes on Domain entities. References to Infrastructure libraries in Application handlers. Finding dependencies on infrastructure libraries in the core entities — even just as configuration attributes — is a direct violation of the fundamental rule. The core must depend on zero external technologies. Every exception to this rule, however small it seems, makes the core more fragile to technological change and harder to test in isolation.
- Mistake 3: interfaces everywhere without reason. Not every class needs a formal interface. Contracts in the application layer make sense for external dependencies you want to be able to substitute: data repositories, notification services, integrations with external systems. Creating an interface for every handler just for the sake of it makes no sense. The criterion is simple: create a contract only when you need to substitute that dependency in automated tests or for configuration reasons. If the answer is no, the contract is noise that increases complexity without delivering any benefit.
- Mistake 4: DTOs in the wrong layer. Data Transfer Objects used to communicate with the user interface belong in the application layer or the presentation layer, never in the domain core. Placing them in Domain creates a dependency between the central model and the structure the interface expects. Every change to the interface requires changes to Domain, which should instead be stable and independent from any presentation detail. The domain represents business reality, not the structure of a screen.
Developers who earn more do not know more patterns: they understand the patterns they use more deeply.
The mistake is not failing to know Clean Architecture. The mistake is continuing to apply it mechanically without understanding why each choice exists.
Structure without understanding produces exactly the same amount of technical debt as code without structure, with the added advantage of appearing correct during code review.
It is the most dangerous kind of debt because it stays hidden the longest.
500 tests in 5 seconds: the pyramid that protects every change without slowing the team
The speed of automated tests is a business metric, not just a technical one.
A team with fast tests releases more frequently, iterates faster, and reaches the product the market wants sooner.
Every minute the test suite takes is a minute taken away from actual development, for every team member, every day, for the entire life of the project.
Added up over a year, that is weeks of productivity.
The most common problem in teams that do not correctly apply layer separation is having almost only tests that require the entire system to be running.
Tests that need a database, network, and configuration run in seconds each. With two hundred tests like this, the entire suite takes fifteen minutes.
Nobody runs a fifteen-minute suite before every commit. The feedback loop stretches, bugs surface in production instead of development, and the cost of resolution grows sprint after sprint.
The structure that solves this problem is a pyramid with three distinct layers, each with a precise purpose.
At the base sit the domain tests.
They have no external dependencies: you create the entity, call the method, check the result.
Zero setup, zero configuration. They run in milliseconds, and this is only possible because domain entities depend on no external technology.
With four hundred tests of this kind, the entire base of the pyramid runs in under five seconds.
In the middle layer sit the application layer tests.
Here external dependencies are replaced with in-memory objects that simulate the expected behaviour: you verify that the handler calls the right operations with the right parameters, without touching any real system.
They run in tens of milliseconds, far faster than any test requiring an external system.
At the top of the pyramid sit the integration tests, and there are very few of them. They verify that the database configuration is correct and that queries return the expected results — not that business logic works, because that is what domain tests verify.
The modern solution for this layer is Testcontainers: a library that spins up an isolated container with the database during the tests, runs the real operations, and tears it down at the end.
Slower, but needed only at release time.
A realistic distribution for a medium-sized .NET application: four hundred domain tests in three seconds, one hundred and fifty application tests in eight seconds, forty integration tests in four minutes.
The first five hundred and fifty run on every push.
The forty integration tests run only at release time. Result: every change to business logic receives feedback within twelve seconds.
This concretely changes the team's pace and the quality of what reaches production.
A fifteen-minute suite that nobody runs protects nothing. Feedback in twelve seconds concretely changes how the team works every day.
In the Software Architect AI Course we build this structure together, on your codebase, not on an academic example.
When Clean Architecture is the wrong choice: the criterion of mature architects

Clean Architecture makes sense when business logic is complex, the project will last years, and the team is large enough to justify structural constraints.
For a predominantly CRUD system without complex logic, for a prototype, for a two-person team with an eighteen-month horizon, it is disproportionate: it adds structural weight without delivering real benefits.
This is a truth that tutorials never state, because tutorials do not have to deliver software to clients.
One of the most underestimated mistakes across the software development industry is applying sophisticated structures to simple problems.
The justification is always the same: let's do it properly from the start.
The result is a project with twelve modules, twenty contracts, zero complex business logic, and two developers spending three sprints to implement a feature that would have taken three days with something simpler.
This has a name: over-engineering.
It is as costly as technical debt, but less obvious because it appears to be doing things correctly.
The architecture is elegant. The structure is tidy. The code is three times as long as necessary, and the team delivers at a third of the speed it could have.
The right questions to ask before choosing an architecture concern the problem, not the technology. How much genuinely complex business logic is there?
If the answer is "mostly CRUD operations with some format validation", Clean Architecture adds structure without delivering real testability, because there is no complex domain behaviour to isolate and verify.
How long will the system last?
If the horizon is eighteen to twenty-four months, the cost of setting up and maintaining the structure may not be recovered before the project is retired or rewritten.
How many people will work on it?
The structural constraints of Clean Architecture are designed for larger teams where individual discipline is not sufficient.
A two-person team has more direct and effective coordination mechanisms.
Concrete alternatives to consider depending on context.
Vertical Slice Architecture organises code by feature instead of by layer: every feature is a self-contained unit that contains everything it needs, from the interface to the data store.
It is faster to develop, easier to understand, and scales well when features are relatively independent.
A simple layered architecture with a service layer directly over the data store is the right choice for predominantly CRUD systems with little logic.
The Modular Monolith is the intelligent middle ground for systems that grow but do not yet justify the operational complexity of microservices.
The decision criterion in summary:
| Architecture | Use it when | Avoid it when |
|---|---|---|
| Clean Architecture | Complex business logic, multi-year project, medium-to-large team | Predominantly CRUD system, prototype, small team with a short horizon |
| Vertical Slice Architecture | Independent features, development speed is the priority | Shared domain logic across many features, intertwined business rules |
| Simple layered architecture | CRUD with little logic, internal system, short expected lifespan | Complex logic that grows, scaling team, need for advanced testability |
| Modular Monolith | Growing system that does not yet justify microservices, team that wants clear boundaries without operational overhead | Single developer on a small project, or a system already ready for distribution |
A real example.
An internal management system for the advertising inventory of a small media platform: predominantly CRUD operations, no complex business logic, a two-developer team, an expected lifespan of eighteen months.
Clean Architecture introduced unjustified structural weight.
Restructuring toward something simpler reduced the codebase by 40% without losing a single functional requirement.
The team delivered two releases within a month. The client received more features in the same time.
The maturity of an architect is also measured by the ability to choose the simplest structure that solves the problem, not the most sophisticated.
The Result pattern in C#: handling errors without turning every method into a minefield
The explicit result pattern makes it clear in the signature of every operation that it can fail with expected domain errors.
Instead of using exceptions for the normal business flow, you return a type that can be success or failure.
The caller is forced by the code structure to handle both cases, eliminating an entire category of bugs caused by unhandled errors.
Exceptions in C# are designed for truly exceptional situations, not for the normal business flow.
The fact that a customer does not exist is not an exceptional event in a system that manages orders: it is an expected use case that must be handled cleanly.
The fact that an order cannot be confirmed because it is already in an advanced state is not a system failure: it is a business rule that the system must communicate clearly.
Using exceptions to handle these normal cases has real costs that accumulate over time. Using exceptions for the normal flow has specific drawbacks.
They are expensive from a performance standpoint because they require generating the full stack trace.
They hide error cases in method signatures: whoever reads the signature does not know that the method can fail in specific ways without reading the entire implementation.
And they make it difficult to handle multiple errors in a coordinated way, because the exception mechanism is designed to interrupt the flow, not to collect and present multiple errors together.
The result pattern solves this with a simple structure.
Every operation that can fail in a predictable way returns a Result that can be Success with the resulting value, or Failure with the error detail.
The method declares in its signature that it can fail.
The caller must handle both cases.
The compiler ensures that no case is ignored through distraction or haste.
The most common implementations in the .NET ecosystem come through libraries and packages.
Domain errors are centralised as predefined values with a code and a description.
Instead of constructing error messages at different points in the code, you use a catalogue of predefined errors accessible as static constants in the OrderErrors or CustomerErrors class in the domain.
This has a very useful side effect: automated tests can check that the result contains a specific error in a readable way, and changes to error messages happen in one place instead of at every point in the code that generates that error.
Input validation is positioned across three layers with different responsibilities. In the presentation layer you validate format: required field, maximum length, correct format.
In the application layer you validate the business rule that requires data access: does the customer exist in the system?
Is the product available in the requested quantity?
In the domain entity you verify pure invariants: an order cannot be confirmed if it has no line items.
Each type of validation in the right place.
Zero duplication. Zero ambiguity about where to look for a specific rule when something is not working.
The criterion applied to the three layers:
| Layer | Type of check | Example | Tool |
|---|---|---|---|
| Presentation | Format and required fields | Empty field, malformed email, string too long | Model validation (FluentValidation, DataAnnotations) |
| Application | Rules that require data | Does the customer exist? Is the product available? | Result.Failure with specific domain error |
| Domain (entity) | Pure invariants | An order without line items cannot be confirmed | Domain exception or Result internal to the entity |
| Infrastructure | Unexpected failures | Network timeout, database unreachable | Exception (propagated or handled in middleware) |
The practical rule is clear: use the Result pattern for expected domain errors, and leave exceptions for unexpected infrastructure failures.
A network timeout is an exception. A customer not found is a Result.Failure.
The two categories require completely different strategies, and mixing them makes the code harder to understand, test, and maintain over time.
ArchUnitNET: architectural tests that protect your design without depending on team discipline
ArchUnitNET lets you write tests that check architectural rules exactly as you write normal tests, and that run in the continuous integration system.
If a rule is violated, the build fails automatically. Violations are blocked before code review, without depending on anyone's manual vigilance.
Every team that adopts Clean Architecture faces sooner or later the same scenario: six months into the project, someone has added a reference to an infrastructure library in a domain class because it was more convenient.
It was urgent, we will fix it later. But later never comes.
The violation sets a precedent for the next one.
Within a year, the architectural boundaries exist only in the internal documentation and in presentations to stakeholders.
The actual code has already diverged from the declared architecture.
This is not a team discipline problem.
It is a structural problem that repeats in a predictable way, regardless of the team's experience level.
Architectural conventions that depend only on code review degrade under deadline pressure.
Code reviews lose effectiveness when the reviewers are themselves under pressure to deliver their own features. The result is inevitable without an automatic control mechanism.
ArchUnitNET analyses the compiled project files and lets you define rules such as: no class in the Domain layer may depend on classes in the Infrastructure layer.
These rules become tests in the architectural verification project, run in CI, and block the merge if violated. You do not need to remember to check it in code review: the machine checks it on every push to the repository.
The configuration cost is low compared to the benefit.
An additional test module in the project, a class with the main rules, and integration with the existing CI pipeline.
The return is high and permanent: the rules are respected automatically over time, regardless of deadline pressure or team turnover.
Code that violates the rules does not enter the main branch. No discussions, no exceptions.
The combination of separate modules, verified by the compiler, and architectural tests in CI creates two layers of protection.
The first blocks the most obvious violations at local build time.
The second blocks subtler violations before integration into the main branch.
Human code review can focus on logical correctness and design quality, not on the mechanical checks the machine already performs reliably.
The most useful architectural rules to implement immediately cover the main risks: that Domain depends on no other internal layer, that Application does not depend on Infrastructure, that concrete infrastructure classes are not used directly without going through the contracts defined in Application.
With four or five architectural tests you cover the vast majority of common violations.
This is not a complete solution, but it is an automatic safety net that is worth the initial configuration time and pays back every day for years.
Architectural rules that depend only on team discipline degrade under pressure. Those written as automated tests do not.
In our Software Architect AI Course we configure together the protection network that works for you on every push, regardless of who joins or leaves the team.
Clean Architecture done right: where to start concretely on a real project

Applying Clean Architecture to an existing project does not require a complete rewrite.
It requires an incremental strategy that delivers real benefits at every step, without blocking the team on the features the client is waiting for.
The key is to work by strangler fig: isolate a boundary, migrate that boundary, verify the result, then advance.
- The first step is always the same: identify where the most critical business logic lives in the current code. Not the most elegant. The most critical for the business, the one that changes most often, the one that has caused the most bugs in production over the past year. Start there. Extract that logic into entities with real behaviour. Write the domain tests that cover it. This alone delivers a tangible benefit: that logic becomes testable, visible, and modifiable without fear.
- The second step is to separate the modules where the natural boundaries still exist. The entire project is not migrated at once: you work on one well-defined piece at a time. Every separate module that compiles with the correct dependencies is a permanent win. The compiler starts working for you, not against you.
- The third step is to add the architectural tests before completing the migration, not after. This creates the safety net that prevents backsliding under pressure. Every architectural rule that becomes an automated test is a rule that nobody needs to remember any more.
A team that follows this sequence, even on a complex legacy project, typically sees the first concrete benefits in two or three releases: faster tests on the migrated area, fewer regressions, and more confidence in making changes.
The benefits grow with the coverage of the migration. It is not a linear path and it is not without friction. It is an investment with compounding returns: every part migrated makes it easier to migrate the next.
Clean Architecture done right is not an academic exercise.
It is the tool that separates developers who execute from those who design the structure in which others work. And in 2026, that distinction means a fundamentally different career trajectory and a fundamentally different compensation level.
Every month that passes with an architecture that appears correct but is not is a month of debt accumulating in silence.
Bugs surface sprints away from whoever introduced them.
Changes take twice as long as estimated.
Onboarding new developers stretches from weeks to months.
And all of this materialises at the worst possible moment: when the most critical project is underway, when the deadline cannot move, when the client is waiting.
The cost of a wrong architecture is not measured in individual bugs: it is measured in lost sprints, in developers who lose motivation, and in commercial opportunities that slip away because the system cannot sustain the innovation it demands.
Two years from now, the teams that have already made this journey will be delivering features in half the time.
The teams that are waiting will still be debating which service to touch without breaking everything else.
Marco closes the project on Friday afternoon.
The folder structure is identical to Monday's. But now he knows where every business rule lives, why every structural choice exists, and how to defend it with the compiler and architectural tests.
The code he delivers is testable in milliseconds.
The team that uses it will know where to find everything. Future changes will not require understanding the entire system. This is the difference between applying a pattern and understanding why that pattern exists.
If you are here because you lead a team or you are responsible for technical decisions, what you have read is not theory: it is the map of the mistakes that cost the most in production.
I have seen teams of 6 to 10 developers recover months of slowdown in three sprints, simply by reorganising the code.
Not by adding tools. Not by rewriting everything from scratch.
By understanding where the structural error was and fixing it before the cost became unsustainable.
In one case out of three, the correct separation of the domain layer alone reduced the average onboarding time from three months to three weeks.
If you recognise this situation in your team, the Software Architect AI Course by BestDeveloper is built exactly for this: not to explain the theory you have already read, but to support you on concrete decisions, on real codebases, in situations you recognise because you have already lived them.
It is the difference between knowing how it should work and knowing how to make it work on the project in front of you right now.
If you want your team to stop accumulating disguised technical debt and start building systems that last, this is the right place.
Book your call now.
Frequently asked questions
Clean Architecture has one fundamental rule: the most important parts of the system must not depend on the most volatile ones. If business rules live in controllers, services, or handlers instead of domain entities, the folder structure is correct but the code behavior is not. This produces the Anemic Domain Model: entities containing only data and no rules. The result is technical debt disguised as correct architecture, which surfaces only when the project grows and every change starts requiring modifications across four layers to do something logically simple.
The optimal structure in .NET uses four distinct compiler-verified modules: Domain (zero external dependencies, only entities, value objects, domain events, and business rules), Application (depends only on Domain, contains use cases, interfaces, and CQRS commands), Infrastructure (depends on Application, implements EF Core repositories, email services, and HTTP clients), and Presentation (depends on Application and connects contracts to implementations via the dependency injection container). If Domain.csproj tries to reference Infrastructure.csproj, it won't compile: the compiler becomes the guardian of architectural rules, eliminating violations caused by distraction or deadline pressure.
The four systemic errors that emerge in code reviews on real .NET projects are: (1) core without behavior (Anemic Domain Model), where entities have only properties and all logic moves to handlers; (2) technological dependencies in the wrong place, such as Entity Framework attributes directly on Domain entities; (3) interfaces everywhere without reason, created even where not needed for testing or substitution, increasing complexity without benefit; (4) DTOs in the wrong layer, placed in Domain instead of the Application or Presentation layer, creating a dependency between the core model and the UI structure.
CQRS separates state-changing operations (commands, such as ConfirmOrder or CreateOrder) from read-only ones (queries, such as GetOrderById), each with its own dedicated handler. With MediatR the main advantage is not handler management itself, but the pipeline: validation, logging, transactions, and caching are written once and automatically applied to every present and future operation, without any handler needing to know they exist. In its practical form for most .NET projects, CQRS does not require two separate databases or Event Sourcing: same database, same infrastructure, separate models.
The optimal structure has three layers: domain tests at the base (no external dependencies, run in milliseconds, 400 tests in under 5 seconds because domain entities have no external technology dependencies), application layer tests in the middle (external dependencies simulated in memory, tens of milliseconds), and integration tests at the top (very few, verify database configuration and queries, use Testcontainers for an isolated container). A realistic distribution on a medium-sized .NET application: 400 domain tests in 3 seconds, 150 application tests in 8 seconds, 40 integration tests only at release time. Result: feedback on business logic within 12 seconds on every push.
Clean Architecture makes sense when business logic is complex, the project will last years, and the team is large enough to justify structural constraints. It doesn't make sense for predominantly CRUD systems without complex logic, prototypes, or two-person teams with an 18-month horizon: it adds structural weight without delivering real benefits. Concrete alternatives are Vertical Slice Architecture (for independent features), simple layered architecture (for CRUD with little logic), and Modular Monolith (for growing systems that don't yet justify microservices). An architect's maturity is also measured by the ability to choose the simplest structure that solves the problem.
The Result pattern returns an explicit type that can be success or failure instead of using exceptions for normal business flow. If a customer doesn't exist or an order can't be confirmed, you don't throw an exception: you return a Result.Failure with the specific domain error. The compiler forces the caller to handle both cases. Exceptions remain for unexpected infrastructure failures, such as network timeouts. Domain errors are centralized as predefined values with code and description, making the code more readable, testable, and free of bugs caused by unhandled errors.
The correct approach is incremental, using strangler fig: identify where the most critical business logic lives in the current code, extract it into domain entities with covering tests, separate modules where natural boundaries exist, and add architectural tests with ArchUnitNET before completing the migration. ArchUnitNET analyzes compiled project files and lets you define rules such as 'no Domain class may depend on Infrastructure classes', running them in CI and blocking merges if violated. A team following this sequence sees the first concrete benefits in two or three releases: faster tests on the migrated area, fewer regressions, and onboarding time reduced from three months to three weeks.
