After years of refactoring .NET backends, one architecture pattern consistently holds up under pressure: true layered separation. Here's exactly how I structure it and why every layer boundary is a deliberate decision.

Lorem ipsum dolor sit amet, consectetur adipiscing elit lobortis arcu enim urna adipiscing praesent velit viverra sit semper lorem eu cursus vel hendrerit elementum morbi curabitur etiam nibh justo, lorem aliquet donec sed sit mi dignissim at ante massa mattis.
Vitae congue eu consequat ac felis placerat vestibulum lectus mauris ultrices cursus sit amet dictum sit amet justo donec enim diam porttitor lacus luctus accumsan tortor posuere praesent tristique magna sit amet purus gravida quis blandit turpis.

At risus viverra adipiscing at in tellus integer feugiat nisl pretium fusce id velit ut tortor sagittis orci a scelerisque purus semper eget at lectus urna duis convallis. Porta nibh venenatis cras sed felis eget neque laoreet suspendisse interdum consectetur libero id faucibus nisl donec pretium vulputate sapien nec sagittis aliquam nunc lobortis mattis aliquam faucibus purus in.
Nisi quis eleifend quam adipiscing vitae aliquet bibendum enim facilisis gravida neque. Velit euismod in pellentesque massa placerat volutpat lacus laoreet non curabitur gravida odio aenean sed adipiscing diam donec adipiscing tristique risus. amet est placerat in egestas erat imperdiet sed euismod nisi.
“Nisi quis eleifend quam adipiscing vitae aliquet bibendum enim facilisis gravida neque velit euismod in pellentesque massa placerat”
Eget lorem dolor sed viverra ipsum nunc aliquet bibendum felis donec et odio pellentesque diam volutpat commodo sed egestas aliquam sem fringilla ut morbi tincidunt augue interdum velit euismod eu tincidunt tortor aliquam nulla facilisi aenean sed adipiscing diam donec adipiscing ut lectus arcu bibendum at varius vel pharetra nibh venenatis cras sed felis eget.
Every project starts simple. A controller calls a service. A service calls the database. It works until it doesn't. Six months in, you have controllers with business logic, services reaching directly into the database, and a test suite that requires a live SQL Server connection to run a single assertion.
I've refactored this pattern more times than I can count. The fix is always the same: real layer separation, enforced through project structure not just naming conventions.
My standard structure maps directly to the Onion Architecture model, adapted for the realities of enterprise .NET development.
No dependencies. None. This layer contains your entities, value objects, domain events, and repository interfaces. It defines what the system models contracts without implementations. If this project references an ORM, a logger, or an HTTP client, something has gone wrong.
Use cases, commands, queries (CQRS-style with MediatR), validators, and DTOs live here. The Application layer depends only on Domain. It orchestrates work — calling domain methods, dispatching events but it never talks to a database directly. It trusts the interfaces defined in Domain.
EF Core repositories, Azure Blob clients, SMTP services, external API wrappers — all here. Infrastructure implements the interfaces declared in Domain. This is the only layer that should ever import a NuGet package for I/O. Swapping a SQL Server repository for Cosmos DB should require changes in exactly one project.
Controllers, middleware, request validators, response mappers. The Presentation layer translates HTTP into Application commands. It knows nothing about your database or domain rules — it maps and dispatches.
At ADENES, we inherited a system where insurance claim processing logic was embedded directly in API controllers. Extracting that into a proper Domain + Application structure took three months — but slashed the time to test a new claim rule from hours to minutes. Unit tests run without a database. New developers onboard in days, not weeks.
"The goal isn't clean architecture for its own sake. The goal is a codebase where the cost of change stays low as the system grows."
Dependencies always point inward. Domain knows nothing. Application knows Domain. Infrastructure knows Domain and Application. Presentation knows Application. Enforce this with .NET project references — not naming conventions. If a reference creates a circular dependency or points the wrong direction, the build fails. That's your architecture enforcing itself.
Don't try to refactor everything at once. Start with one domain concept. Extract the interface. Move the implementation. Write one unit test that runs without the database. That first test is proof of concept — and it's usually enough to convince the rest of the team.