.NET  

How Do I Structure a Clean, Maintainable .NET Solution?

🧠 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

ProjectResponsibilityDepends on
Company.Product.ApiHTTP endpoints, authentication, request shapingApplication, Infrastructure
Company.Product.ApplicationUse cases, orchestration, validationDomain
Company.Product.DomainCore business rules, entities, value objectsnothing
Company.Product.InfrastructureEF Core, external services, messaging, file storageApplication, Domain
Company.Product.Contracts (optional)DTOs and contracts shared across boundariesnothing or Domain if careful
Company.Product.Tests.*Unit, integration, and API teststhe 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

AreaWhat to enforceWhy it stays maintainable
BoundariesDomain does not depend on InfrastructureBusiness rules stay stable
DirectionAPI is thin, Application orchestratesLess duplication and fewer side effects
StructureOrganize by feature where it helpsDevelopers find code quickly
AbstractionsAvoid generic repositories that leak IQueryableQueries stay explicit and debuggable
TestingUnit plus integration plus API testsReal confidence, fewer regressions
ConventionsPredictable project and namespace namesLess 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.