When people first discover Clean Architecture, they often end up with a solution that looks like this:
src
├── Domain
├── Application
├── Infrastructure
├── Persistence
├── Authentication
├── WebApi
├── Shared
├── Contracts
├── Utilities
└── Presentation
Before writing actual business logic, half the day is already gone creating projects and wiring references.
The funny part? Most applications don’t actually need that complexity.
Projects vs Boundaries
A common misconception is that every layer must live inside its own class library.
The real goal is:
- controlling dependencies
- protecting business logic
- separating concerns
- keeping the code maintainable
You can achieve all of that perfectly fine inside a single project.
Multiple .csproj do separate projects physically preventing invalid references. That is excellent when many teams touch the same solution or when you publish libraries independently.
However, for many APIs, especially early in their life, that ceremony can feel heavy:
- more projects to navigate
- slower development
- more boilerplate
- more dependency management
- more complicated debugging
Clean Architecture is primarily about dependency direction and cohesion, not about how many DLLs you ship. One well-structured ASP.NET Core project with the same folders you already use can follow the exact mental model, although it requires a bit more discipline.
This is more than enough to follow the Clean Architecture principles:
src
└── WebApi
├── Features
│ ├── Users
│ ├── Orders
│ └── Payments
├── Domain
├── Persistence
├── Business
└── Infrastructure
It's still clean, maintainable, testable and scalable.
Physical Separation vs Logical Separation
Logical separation matters far more than physical separation.
The trick is discipline, without separate projects, nothing stops a teammate from adding using WebApi.Infrastructure inside a domain type, except conventions, code review or tooling.
A practical pattern is to mirror the multi-project layout with namespace roots, for example:
- WebApi.Domain… — entities, domain errors, value objects, enums, etc.
- WebApi.Business… — handlers, validators, abstractions, etc.
- WebApi.Persistence… — DbContext, EF configurations, migrations, etc.
- WebApi.Infrastructure… — Authorization, email providers, external APIs, etc.
- WebApi.Presentation… (or similar) — minimal endpoints, requests, etc.
The single-project approach in this article follows the same feature flow as that layered style, only packaged as one Web project so you can compare the two shapes of the same app without changing the core use cases.
Enforcing rules
To enforce the rules, you can use NsDepCop or architecture tests.
To learn more about architecture tests, you can read my blog post on Architecture Tests in .NET .
I plan to write a dedicated blog post on NsDepCop as well, in short, by adding a config.nsdepcop file in your project, you can define and enforce the rules at build time using a Roslyn analyzer.
It basically declares which namespace bands may talk to which.
When someone introduces a forbidden using, the build fails instead of waiting for review.
Conclusion
Start simple, a single project is enough. It will give you faster development, simpler onboarding, less ceremonial, easier navigation and refactoring.
When the project grows large, you can always migrate to a multi-project solution. Migration is usually simple if your boundaries are already well defined.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
