🧠 What “clean and maintainable” really means
A clean .NET solution is not the one with the most patterns or the most projects, because complexity does not become maintainability just because it is organized, and teams often confuse architectural ceremony with architectural quality.
A maintainable solution is the one where responsibilities are obvious, dependencies flow in one direction, business logic can evolve without rewriting infrastructure, and developers can change code confidently because the boundaries are clear and tests can cover the right things without mocking the universe.
If you keep those goals in mind, the rest becomes a set of practical tradeoffs rather than a religious argument about architecture diagrams.
![Clean Architecture .NET]()
🎯 The default blueprint that works for most teams
For most real-world .NET applications, the cleanest long-term structure is a layered approach that separates the application into distinct projects with strict dependency rules, while still keeping the solution small enough that developers do not spend their lives navigating folders.
The “clean” part comes from boundaries and dependency direction, not from whether you call it Clean Architecture, Onion Architecture, Hexagonal Architecture, or simply “a well-structured codebase.”
The dependency rule you should not break
Your business rules should not depend on infrastructure details, which means your domain and application logic should not reference things like Entity Framework Core, ASP.NET Core controllers, message brokers, cloud SDKs, or third-party clients directly.
When the dependency direction is wrong, teams end up unable to change databases, swap messaging systems, or refactor APIs without rewriting business logic, which is the exact opposite of maintainability.
🧱 Recommended solution structure
A pragmatic structure that scales with team size and complexity usually looks like this.
Solution layout
| Project | Responsibility | Depends on |
|---|
Company.Product.Api | HTTP endpoints, authentication, request shaping | Application, Infrastructure |
Company.Product.Application | Use cases, orchestration, validation | Domain |
Company.Product.Domain | Core business rules, entities, value objects | nothing |
Company.Product.Infrastructure | EF Core, external services, messaging, file storage | Application, Domain |
Company.Product.Contracts (optional) | DTOs and contracts shared across boundaries | nothing or Domain if careful |
Company.Product.Tests.* | Unit, integration, and API tests | the projects being tested |
This layout works because it forces clarity: the API layer handles transport concerns, the Application layer handles orchestration, the Domain layer holds business meaning, and Infrastructure implements technical details behind interfaces.
🧠 What belongs in each layer
Domain
Domain should contain the business model and rules that remain true even if you delete ASP.NET Core, EF Core, and your cloud provider.
This typically includes entities, value objects, domain services, domain events, and business invariants, and it should remain free of persistence or HTTP concepts, because those are implementation details that change far more frequently than business rules.
Application
Application is where use cases live, meaning the code that coordinates work, validates input in a business sense, enforces authorization decisions at the business boundary, and calls into abstractions for persistence, messaging, and external services.
This layer is usually expressed as commands and queries, handlers, workflows, or services, and its job is to describe what the system does rather than how the system does it.
Infrastructure
Infrastructure is where you implement the “how,” which includes EF Core DbContext, repositories, external API clients, message bus adapters, caching, file storage, and platform-specific behavior.
The maintainability win comes from ensuring infrastructure depends on the application and domain contracts, not the other way around, because this keeps business logic stable while infrastructure can evolve.
API
API is responsible for transport level concerns such as routing, authentication setup, request validation at the boundary, response formatting, and mapping between external models and internal use-case models.
A clean API layer is intentionally boring, because controllers or endpoints should be thin and predictable, and you should avoid putting business logic there because it becomes hard to test and hard to reuse.
🧩 Organizing within a project without creating chaos
Inside each project, structure by capability rather than by technical type once the system reaches a certain size, because grouping everything by Controllers, Services, and Repositories often becomes a dumping ground where features are scattered across many folders.
A feature-based structure inside the Application layer often scales better.
Example inside Application
Customers/CreateCustomer
Customers/GetCustomer
Orders/PlaceOrder
Orders/GetOrder
Each feature folder can include its command or query model, handler, validator, and mapping, which keeps change sets small and makes the codebase easier to navigate.
🧱 Avoiding the “too many projects” trap
While separation is good, creating dozens of projects too early is a common way to slow a team down, especially when build times increase and everything becomes ceremony.
A practical guideline is to start with Domain, Application, Infrastructure, and one host project such as Api, and only introduce additional hosts or modules when you feel real pressure from complexity, runtime requirements, or team ownership boundaries.
🔁 Handling cross-cutting concerns cleanly
Cross-cutting concerns should be centralized rather than repeated, but they should also not leak infrastructure into your application logic.
Validation should live at the boundary and in application workflows when needed, logging should be structured and consistent, and authorization should be enforced where business operations happen, not just at the controller level.
If you use MediatR or pipeline behaviors, you can keep cross-cutting concerns consistent, but you should use such tooling to reduce duplication rather than to hide complexity.
🗄️ Data access in a maintainable structure
A clean structure does not mean you must hide EF Core behind layers of abstraction that make queries harder to write and debug, because the goal is maintainability, not indirection.
A pragmatic approach is to keep EF Core in Infrastructure, expose intent-based interfaces to Application, and allow query optimization through explicit query services when needed, rather than forcing every query through a generic repository abstraction that returns IQueryable everywhere.
🧪 Testing strategy that matches the architecture
A clean solution structure becomes valuable when it improves testability, but you should still be realistic about what kinds of tests catch real bugs.
Unit tests should cover domain rules and application workflows that do not depend on infrastructure, while integration tests should validate the data access layer and external dependencies using real infrastructure when practical.
API tests should validate the system end to end, because many production failures happen at boundaries, configuration, serialization, and integration points rather than in pure business logic.
🧾 Naming conventions that reduce confusion
Consistency matters more than personal preference.
Use predictable suffixes such as Api, Application, Domain, Infrastructure, and Tests.
Use company or product namespace prefixes consistently.
Avoid dumping “Common” or “Helpers” folders everywhere, because those become trash bins.
Prefer explicit names that express business intent over generic names like Manager or Processor.
🚀 Modularity without microservices
Many teams try to solve maintainability by jumping to microservices, but a modular monolith is often the better step because it gives you strong boundaries and independent features without operational explosion.
You can structure modules by feature areas, enforce internal visibility, and keep deployment simple while still giving teams ownership and isolation.
If you later decide to split into microservices, a well-structured modular monolith makes that migration easier because boundaries already exist.
✅ A practical maintainability checklist
| Area | What to enforce | Why it stays maintainable |
|---|
| Boundaries | Domain does not depend on Infrastructure | Business rules stay stable |
| Direction | API is thin, Application orchestrates | Less duplication and fewer side effects |
| Structure | Organize by feature where it helps | Developers find code quickly |
| Abstractions | Avoid generic repositories that leak IQueryable | Queries stay explicit and debuggable |
| Testing | Unit plus integration plus API tests | Real confidence, fewer regressions |
| Conventions | Predictable project and namespace names | Less cognitive load |
❓ Top 5 FAQs
1. Should I use Clean Architecture for every .NET app
Not for every app, but the dependency rule and separation of concerns apply universally, so even small applications benefit from a lightweight version that keeps business logic separate from infrastructure.
2. Should my Domain project contain DTOs
Usually no, because DTOs are transport models and change more frequently than domain concepts, although some teams create shared Contracts projects for stable interfaces that are not tied to HTTP.
3. Where should I put EF Core DbContext
In Infrastructure, because persistence is an implementation detail, even when it is a very important one.
4. Where should I enforce authorization
You should enforce authorization both at the API boundary for basic access control and inside application workflows for business critical operations, because controllers change over time and business rules must remain protected.
5. When should I split into microservices
Only when organizational scaling or runtime requirements justify the operational cost, because poor modularity is usually the real issue, and you can fix that with a modular monolith long before you need microservices.