A practical guide to Clean Architecture folder structure, project organization, and dependency management in .NET—battle-tested patterns that help teams maintain large codebases.
Look, I've seen some things. I've opened solutions with 47 projects, each containing exactly one class (I am being dramatic!). I've navigated folder hierarchies so deep that Visual Studio needed a compass. I've traced project references that formed dependency graphs resembling spaghetti thrown at a wall.
And every time, someone said: "We'll refactor it later."
Spoiler: they didn't.
Table of Contents
- Common .NET Project Structure Problems
- The .NET Solution Structure That Actually Scales
- The Dependency Rule in Clean Architecture
- Folder Conventions Inside Each Project
- .NET Project Naming Conventions
- Project References and Dependency Graph
- The Shared Project Debate
- When to Split Into Multiple Solutions
- Common Architecture Mistakes to Avoid
- The Architecture Checklist
- Refactoring an Existing Codebase
- Frequently Asked Questions
- Final Thoughts
Common .NET Project Structure Problems
Here's what typically happens. You start a project. It's small. Everything lives in one project because why not? Then features grow. You add folders. More folders. Folders inside folders.
Six months later:
MyApp/
├── Controllers/
├── Services/
├── Repositories/
├── Models/
├── ViewModels/
├── DTOs/
├── Helpers/
├── Utilities/
├── Extensions/
├── Common/
├── Shared/
├── Core/
├── Infrastructure/
└── Misc/ ← This is where dreams go to die
Everything depends on everything. Your "Repository" calls your "Helper" which calls your "Service" which calls your "Repository" again. You've built a circular reference, but the compiler doesn't complain because it's all one project.
Then a new developer joins. They ask: "Where should I put this new feature?"
You stare blankly. "Uh... probably Services? Or maybe Helpers? What does it do exactly?"
The .NET Solution Structure That Actually Scales
After years of trial and error (mostly error), here's what works:
src/
├── MyApp.Domain/ # Zero dependencies. Just your business logic.
├── MyApp.Application/ # Use cases. References Domain only.
├── MyApp.Infrastructure/ # External concerns. Databases, APIs, files.
├── MyApp.Api/ # Your web host. Thin. Composition root.
└── MyApp.Shared.Kernel/ # Cross-cutting primitives (optional)
tests/
├── MyApp.Domain.Tests/
├── MyApp.Application.Tests/
├── MyApp.Infrastructure.Tests/
└── MyApp.Api.Tests/
Five projects. Clear boundaries. Dependencies flow one direction: inward.
"But that's just Clean Architecture!"
Yes. And there's a reason everyone keeps reinventing it. It works.
The Dependency Rule in Clean Architecture
Here's the one rule that matters:
Dependencies point inward. Always.
Api → Application → Domain
Infrastructure → Application → Domain
Domain knows nothing about databases. Application knows nothing about HTTP. Infrastructure implements the interfaces that Application defines.
Break this rule once, and you've broken it forever. I've watched teams add "just one small reference" from Domain to Infrastructure. Three months later, your business logic imports System.Data.SqlClient.
The compiler won't save you here. You need discipline. Or a tool like NetArchTest to enforce it:
[Fact]
public void Domain_Should_Not_Reference_Infrastructure()
{
var result = Types.InAssembly(typeof(Order).Assembly)
.ShouldNot()
.HaveReferenceTo("MyApp.Infrastructure")
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
Write this test on day one. Thank me later.
Folder Conventions Inside Each Project
Domain Project Structure
MyApp.Domain/
├── Entities/
│ ├── Order.cs
│ └── Customer.cs
├── ValueObjects/
│ ├── Money.cs
│ └── Address.cs
├── Enums/
│ └── OrderStatus.cs
├── Events/
│ └── OrderPlacedEvent.cs
├── Exceptions/
│ └── InsufficientStockException.cs
└── Interfaces/
└── IOrderRepository.cs # Just the interface. No implementation.
Notice what's not here: no Services folder. Domain services exist, but they're rare. If you're creating domain services for every entity, you're probably doing anemic domain modeling.
Application Project Structure
MyApp.Application/
├── Features/
│ ├── Orders/
│ │ ├── Commands/
│ │ │ ├── PlaceOrder/
│ │ │ │ ├── PlaceOrderCommand.cs
│ │ │ │ ├── PlaceOrderHandler.cs
│ │ │ │ └── PlaceOrderValidator.cs
│ │ │ └── CancelOrder/
│ │ │ └── ...
│ │ └── Queries/
│ │ └── GetOrderById/
│ │ ├── GetOrderByIdQuery.cs
│ │ ├── GetOrderByIdHandler.cs
│ │ └── OrderDto.cs
│ └── Customers/
│ └── ...
├── Common/
│ ├── Behaviors/
│ │ ├── ValidationBehavior.cs
│ │ └── LoggingBehavior.cs
│ └── Interfaces/
│ └── IApplicationDbContext.cs
└── DependencyInjection.cs
This is "Vertical Slice Architecture" meets "CQRS lite." Each feature is self-contained. Want to understand how placing an order works? Look in one folder.
The Misc folder from earlier? It doesn't exist because there's nowhere for random code to hide.
Infrastructure Project Structure
MyApp.Infrastructure/
├── Persistence/
│ ├── ApplicationDbContext.cs
│ ├── Configurations/
│ │ └── OrderConfiguration.cs
│ └── Repositories/
│ └── OrderRepository.cs
├── Services/
│ ├── EmailService.cs
│ └── PaymentGateway.cs
├── Identity/
│ └── IdentityService.cs
└── DependencyInjection.cs
Everything that touches the outside world lives here. Database? Here. Email? Here. Third-party APIs? Here.
When you need to swap your payment provider, you change one folder. The rest of your application doesn't care.
.NET Project Naming Conventions
After watching teams argue about this for years, here's what I've landed on:
Projects
{Company}.{Product}.{Layer}
Example: Contoso.Ordering.Domain
Don't get clever. Contoso.Ordering.SuperCore.Base.Abstractions.V2 helps nobody.
Folders
-
Plural for collections:
Entities/,Services/,Handlers/ -
Singular for features:
Orders/,Customers/(each is a single feature area)
Files
-
Suffix with role:
OrderService.cs,OrderRepository.cs,OrderController.cs -
Commands/Queries get full names:
PlaceOrderCommand.cs,GetOrderByIdQuery.cs
The suffix thing is controversial. Some folks hate IOrderRepository. I've tried dropping the I prefix. It's worse. Your IDE's autocomplete becomes useless.
Interface Naming and Placement
// In Domain or Application (whoever defines the contract)
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct);
Task AddAsync(Order order, CancellationToken ct);
}
// In Infrastructure (the implementation)
public class OrderRepository : IOrderRepository
{
// EF Core, Dapper, whatever
}
Interface lives with the code that uses it, not the code that implements it. This is Dependency Inversion 101, but I still see teams putting IOrderRepository in the Infrastructure project.
Project References and Dependency Graph
Here's the .csproj setup that enforces the dependency rule:
Domain.csproj - References nothing (maybe Shared.Kernel)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
Application.csproj - References Domain only
<ItemGroup>
<ProjectReference Include="..\MyApp.Domain\MyApp.Domain.csproj" />
</ItemGroup>
Infrastructure.csproj - References Application (and transitively Domain)
<ItemGroup>
<ProjectReference Include="..\MyApp.Application\MyApp.Application.csproj" />
</ItemGroup>
Api.csproj - References everything (it's the composition root)
<ItemGroup>
<ProjectReference Include="..\MyApp.Application\MyApp.Application.csproj" />
<ProjectReference Include="..\MyApp.Infrastructure\MyApp.Infrastructure.csproj" />
</ItemGroup>
The Api project wires everything together in Program.cs:
builder.Services
.AddApplication() // From Application's DependencyInjection.cs
.AddInfrastructure(); // From Infrastructure's DependencyInjection.cs
The Shared Project Debate
Every team eventually wants a "Shared" or "Common" project. Usually for:
- Extension methods
- Base classes
- Cross-cutting attributes
- Result types
My advice: resist as long as possible. When you can't resist anymore:
MyApp.Shared.Kernel/
├── Primitives/
│ ├── Entity.cs
│ ├── ValueObject.cs
│ └── DomainEvent.cs
├── Results/
│ └── Result.cs
└── Extensions/
└── StringExtensions.cs
Keep it small. The moment this project becomes a dumping ground, you've lost.
Rule of thumb: if you can't explain why something is in Shared in one sentence, it doesn't belong there.
When to Split Into Multiple Solutions
"Should we have one solution or multiple?"
Here's my heuristic:
| Team Size | Deployment | Recommendation |
|---|---|---|
| 1-5 devs | Monolith | One solution |
| 5-15 devs | Monolith | One solution, strict boundaries |
| 15+ devs | Services | Multiple solutions, shared packages |
Multiple solutions introduce real pain: package versioning, CI/CD complexity, integration testing overhead. Don't pay that cost until you have to.
When you do split:
solutions/
├── ordering/
│ └── Contoso.Ordering.sln
├── inventory/
│ └── Contoso.Inventory.sln
└── shared/
└── Contoso.Shared.sln # Published as NuGet packages
The Shared packages become internal NuGet packages. Each team owns their solution. Integration happens through APIs or messages, not project references. This is essentially a modular monolith approach.
Common Architecture Mistakes to Avoid
1. The "Everything Is A Service" Anti-Pattern
// No. Stop.
public class OrderService
{
public Order GetOrder(int id) { ... }
public void PlaceOrder(Order order) { ... }
public void CancelOrder(int id) { ... }
public void UpdateOrder(Order order) { ... }
public decimal CalculateTotal(Order order) { ... }
public bool ValidateOrder(Order order) { ... }
public void SendOrderEmail(Order order) { ... }
// 47 more methods
}
This is a God Class wearing a "Service" disguise. Break it up by use case.
2. Circular Project References (Fixed With a "Shared" Project)
If you need a Shared project just to break circular dependencies, your architecture is wrong. Step back and redraw the boundaries.
3. Infrastructure As a Junk Drawer
Infrastructure/
├── Database stuff
├── Email stuff
├── PDF generation
├── Image processing
├── Rate limiting
├── Background jobs
├── Feature flags
├── Analytics
└── That one weird integration with the legacy system nobody understands
When Infrastructure gets too big, split it:
MyApp.Infrastructure.Persistence/
MyApp.Infrastructure.Email/
MyApp.Infrastructure.BackgroundJobs/
4. Putting DTOs in the Wrong Layer
DTOs that leave your API boundary? They go in Application or a dedicated Contracts project.
DTOs for database mapping? They're not DTOs, they're entities. Put them in Domain.
DTOs shared between services? Publish them as a NuGet package from a Contracts project.
The Architecture Checklist
Before committing, ask yourself:
- [ ] Can a new dev find where to add a feature in under 2 minutes?
- [ ] Does each project have a single, clear responsibility?
- [ ] Do dependencies flow one direction (inward)?
- [ ] Is there exactly one place for each type of code?
- [ ] Can you explain the structure to a junior dev in 5 minutes?
If any answer is "no," you've got work to do.
Refactoring an Existing Codebase
Can't start fresh? Here's the incremental approach:
- Add architecture tests - Start enforcing rules before adding more violations
- Extract Domain - Pull out entities and business rules first
- Define interfaces - Create boundaries with interfaces before moving implementations
- Extract Infrastructure - Move external dependencies behind those interfaces
- Reorganize into features - Convert folder-by-type into folder-by-feature
Don't try to refactor everything at once. I've seen that movie. It ends with a half-migrated codebase and a demoralized team.
Frequently Asked Questions
How many projects should a .NET solution have?
A typical Clean Architecture solution has 4-5 projects: Domain, Application, Infrastructure, API, and optionally Shared.Kernel. Start minimal—you can always split later, but merging projects is painful. Each project should have a single, clear responsibility.
Should Domain reference Infrastructure in .NET?
Never. Dependencies should always flow inward. Domain should have zero external references beyond the .NET base libraries. If your Domain project references Infrastructure, you've violated the Dependency Rule and created tight coupling between business logic and implementation details.
What's the difference between Clean Architecture and Onion Architecture?
They're very similar. Both enforce inward-pointing dependencies with Domain at the center. Onion Architecture (Jeffrey Palermo, 2008) preceded Clean Architecture (Robert C. Martin, 2012). Clean Architecture adds more explicit layers and the concept of "Use Cases" in the Application layer. In practice, most .NET implementations blend both.
When should I split into multiple .NET solutions?
When your team exceeds 15 developers or you're deploying as separate services. Multiple solutions add complexity (package versioning, CI/CD, integration testing). A single solution with strict project boundaries works well for most teams.
Where should interfaces live in Clean Architecture?
Interfaces live with the code that uses them, not the code that implements them. IOrderRepository belongs in Domain or Application (wherever it's consumed). The implementation OrderRepository lives in Infrastructure. This is the Dependency Inversion Principle in action.
How do I enforce architecture rules in .NET?
Use NetArchTest to write unit tests that verify your dependency rules. Run these tests in CI to catch violations early. Example: test that Domain doesn't reference Infrastructure. Write these tests on day one—they're much harder to add later.
Final Thoughts
Good structure isn't about following a specific template. It's about making the easy path the correct path.
When adding new code is obvious, developers make good decisions. When it's confusing, they make expedient decisions. And expedient decisions compound into architectural debt.
The goal isn't to have the "perfect" structure on day one. It's to have a structure that guides good decisions as the team grows.
Start simple. Enforce boundaries. Refactor when pain demands it.
Now if you'll excuse me, I have a legacy solution with 73 projects to untangle. Wish me luck.
What does your solution structure look like? Drop your folder tree in the comments—I'd love to see what's working (or not working) for your team.
About the Author
I'm Mashrul Haque, a Systems Architect with over 15 years of experience building enterprise applications with .NET, Blazor, ASP.NET Core, and SQL Server. I specialize in Azure cloud architecture, AI integration, and performance optimization.
When production catches fire at 2 AM, I'm the one they call.
- LinkedIn: Connect with me
- GitHub: mashrulhaque
- Twitter/X: @mashrulthunder
Follow me here on dev.to for more .NET and SQL Server content
Top comments (2)
MyApp.Application/
├── Features/
│ ├── Orders/
│ │ ├── Commands/
│ │ │ ├── PlaceOrder/
│ │ │ │ ├── PlaceOrderCommand.cs
│ │ │ │ ├── PlaceOrderHandler.cs
│ │ │ │ └── PlaceOrderValidator.cs
│ │ │ └── CancelOrder/
│ │ │ └── ...
│ │ └── Queries/
│ │ └── GetOrderById/
│ │ ├── GetOrderByIdQuery.cs
│ │ ├── GetOrderByIdHandler.cs
│ │ └── OrderDto.cs
│ └── Customers/
│ └── ...
├── Common/
│ ├── Behaviors/
│ │ ├── ValidationBehavior.cs
│ │ └── LoggingBehavior.cs
│ └── Interfaces/
│ └── IApplicationDbContext.cs
└── DependencyInjection.cs
The structure shown in your article isn’t actually vertical slicing. It’s essentially Clean Architecture with feature folders, as it still uses horizontal layers (Infrastructure, Application, Domain, Presentation) as the top architecture layering.
In true Vertical Slice Architecture, the feature itself is the top-level organizing unit, and each slice contains its own horizontal 'clean code' layers. The layers live inside the feature, not the other way around.
Each slice should be independent from each other, , so that it can be developed, tested, and deployed in isolation. This independence reduces coupling, makes the codebase easier to maintain, and allows features to evolve without affecting unrelated parts of the system.
Your example is still grouping by architectural concern, not by business capability. That’s horizontal layering, so it’s clean architecture, not vertical sliced architecture.
This was such a refreshingly honest and painfully relatable read 😄.
I love how you explained Clean Architecture like someone who's personally fought in the “47-projects-one-class” war. The folder examples hit a little too close to home!! especially the part where Misc is where dreams go to die. Been there, buried a few dreams myself.
Also, huge respect for the line “Break this rule once, and you've broken it forever.”
Honestly, that should be printed on a poster and taped above every junior dev’s monitor.
Fantastic breakdown, clear structure, and just the right amount of therapy for traumatized .NET developers. Great job! 🚀