What Is Onion Architecture? Structuring Code from the Core Out

Summarize this article with:
Most applications start clean. Then the database leaks into your business logic, the UI framework infects your services, and suddenly a simple schema change takes three sprints to untangle.
That’s the exact problem onion architecture was built to solve. Jeffrey Palermo introduced this software design pattern in 2008 to give enterprise teams a way to keep their domain model completely independent from infrastructure concerns like databases, frameworks, and external APIs.
This article breaks down what onion architecture is, how its concentric layers work, where dependency injection fits in, and when this pattern actually makes sense for your project. You’ll also see how it compares to clean architecture and hexagonal architecture, plus the most common mistakes teams make when implementing it.
What is Onion Architecture

Onion architecture is a software design pattern that organizes code into concentric layers, with all dependencies pointing inward toward a central domain model. Jeffrey Palermo introduced it in 2008 as a response to the problems created by traditional layered architectures, where business logic ended up tightly coupled to databases and UI frameworks.
The core idea is simple. Your most valuable code (the business rules) sits at the center and depends on nothing external. Everything else, databases, web frameworks, third-party services, wraps around it in outer layers.
That inward dependency flow is what separates onion architecture from the standard N-tier approach most teams default to. In a typical three-tier setup, dependencies cascade downward from presentation to business logic to data access. Change the database, and your business logic breaks. Swap the UI framework, and you’re rewriting half the application.
Onion architecture flips this. The domain model has zero external dependencies, which means you can swap infrastructure components without touching the core logic that actually runs your business.
IcePanel’s 2025 State of Software Architecture survey found that microservices (60%) and event-driven (55%) remain the most adopted architecture patterns among practitioners. But the principles behind onion architecture, especially separation of concerns and dependency inversion, underpin how those patterns get implemented at the code level.
Jeffrey Palermo himself was clear about scope. This pattern is not for small websites or simple CRUD apps. It’s built for long-lived business applications where maintainability actually matters over time.
Core Layers of Onion Architecture

The layered structure is what gives onion architecture its name. Each ring wraps around the one inside it, and the rules about which direction dependencies flow are strict.
Get the layers wrong, and you’ve got a conventional layered architecture with extra folders. Get them right, and your codebase becomes significantly easier to test, change, and reason about.
Domain Model and Domain Services
The domain model sits at the absolute center. It contains entities, value objects, and the business rules that define how your organization actually works.
This layer has no dependencies on anything. Not on a database. Not on a framework. Not on the layer wrapping around it. Nothing.
Domain services live in the next ring out. They handle business rules that don’t belong to a single entity, like calculating shipping costs based on multiple product weights and customer locations.
A McKinsey study found that technical debt can account for up to 40% of a company’s technology estate. Keeping domain logic dependency-free is one of the most effective ways to prevent that debt from compounding, because the code that matters most stays clean regardless of what happens in outer layers.
Application Services Layer
Application services sit one ring further out. They orchestrate use cases by coordinating domain objects and services to accomplish specific tasks.
Think of a “place order” workflow. The application service calls the domain model to validate the order, checks inventory through an interface, and triggers payment processing. But it doesn’t know (or care) whether inventory lives in PostgreSQL or a RESTful API.
Key distinction: application services contain no business rules. They coordinate. If you find business logic creeping into this layer, something has gone wrong with your design.
Infrastructure and Presentation Layer
The outermost ring is where the real world lives. Databases, web frameworks, message queues, email services, API endpoints. All of it sits here.
| Layer | Contains | Depends On |
|---|---|---|
| Domain Model | Entities, value objects, core rules | Nothing |
| Domain Services | Cross-entity business logic | Domain Model only |
| Application Services | Use case orchestration | Domain Model + Domain Services |
| Infrastructure/UI | Databases, APIs, frameworks, UI | All inner layers |
This layer implements the interfaces defined by inner layers. A repository interface declared in the domain layer gets its concrete SQL or MongoDB implementation here. Controllers, API endpoints, and background job handlers also belong in this outer ring.
Expedia’s engineering team has written about how this separation makes their applications more portable and testable, noting that it helps with adopting new frameworks when old ones become outdated.
The Dependency Rule

Everything in onion architecture stands or falls on one principle. Dependencies must always point inward.
Outer layers depend on inner layers. Inner layers know absolutely nothing about what’s outside them. Break this rule, and the entire architecture collapses back into a conventional tightly coupled mess.
How does this actually work in practice? Through interfaces. The domain layer defines a contract (say, IOrderRepository) that describes what it needs. The infrastructure layer provides the concrete implementation (SqlOrderRepository). The domain layer never references the infrastructure layer directly.
This is the dependency inversion principle from SOLID, applied at the architectural level. Robert C. Martin described the same concept in his clean architecture work, and Alistair Cockburn’s hexagonal architecture (ports and adapters) follows a similar pattern.
According to TekLume research, developers spend roughly 42% of their working week (about 13.5 hours) dealing with technical debt and poor code quality. When inner layers reference outer layer implementations directly, every infrastructure change ripples through the entire system. That’s exactly the kind of coupling that generates most of that maintenance burden.
A practical test: can you unit test your domain logic without starting a database, spinning up a web server, or mocking HTTP calls? If yes, you’re probably following the dependency rule. If not, something in your inner layers is reaching outward.
Onion Architecture vs Clean Architecture vs Hexagonal Architecture

These three get confused constantly. And honestly? The confusion makes sense. They’re solving the same fundamental problem with slightly different vocabulary.
All three patterns isolate business logic from infrastructure. All three enforce inward dependency flow. All three want you to be able to swap databases or frameworks without rewriting your core application.
The differences are mostly structural and terminological.
| Pattern | Introduced By | Key Concept | Structural Emphasis |
|---|---|---|---|
| Onion Architecture | Jeffrey Palermo (2008) | Concentric layers, inward dependencies | Explicit layer rings |
| Hexagonal Architecture | Alistair Cockburn (2005) | Ports and adapters | Symmetrical ports |
| Clean Architecture | Robert C. Martin | Use case interactors, boundary objects | Flexible layer count |
Hexagonal architecture talks about “ports” (interfaces your application exposes) and “adapters” (implementations that connect those ports to the outside world). It treats all external interactions symmetrically, whether they come from a user or a database.
Clean architecture adds explicit use case interactors between the domain and the outside world. It also provides more flexibility in how many layers you define, which is both a strength and a source of team disagreements.
Onion architecture’s specific contribution is the concentric ring visualization and a more structured approach to code layout. The Allegro engineering blog noted that onion architecture’s explicit layer definitions provide stronger structural guidance compared to hexagonal architecture’s more conceptual approach.
In practice, most teams blend elements from all three without strict adherence to any single pattern. What matters is the shared core principle: your business logic should never depend on your infrastructure choices.
Why Onion Architecture Exists

Traditional layered architectures create a specific, predictable failure mode. Your business logic ends up depending on your database layer. Then someone decides to switch from SQL Server to PostgreSQL, or migrate from on-premise to a cloud-based setup, and suddenly everything breaks.
That was the pain point Jeffrey Palermo was addressing.
In a conventional three-tier system, the dependency chain runs top to bottom: presentation depends on business logic, business logic depends on data access. The problem? Your most stable, valuable code (business rules) ends up chained to your most volatile code (infrastructure).
A 2024 survey of technology executives found that for more than 50% of companies, technical debt eats up greater than a quarter of their total IT budget (vFunction). Much of that debt traces back to architectural decisions that couple business logic tightly to specific technology choices.
Onion architecture inverts the dependency direction. Instead of business logic calling database code directly, it defines interfaces that infrastructure code implements. The result is a system where:
- Domain logic can be tested without any infrastructure running
- Databases can be swapped without rewriting business rules
- UI frameworks can change without touching application services
- Teams can work on different layers independently
This became especially relevant as dependency injection containers matured in the .NET and Java ecosystems. Frameworks like Spring (used by 60% of Java developers for production applications, according to Snyk research) and ASP.NET Core made wiring up interface implementations at startup trivial. Without those DI containers, onion architecture would have remained a nice diagram on a whiteboard.
Allegro’s engineering team put it well: software architecture is often defined as the things that are hard to change. Getting the dependency direction right at the start prevents the kind of structural debt that becomes nearly impossible to fix later.
How Dependency Injection Works in Onion Architecture
Dependency injection is the mechanism that makes the dependency rule enforceable in real code. Without it, you’re stuck with inner layers creating their own infrastructure dependencies, which defeats the entire purpose.
The pattern works like this. Inner layers define interfaces (abstractions). Outer layers provide implementations (concrete classes). A DI container wires them together at application startup.
Interface Definition in Inner Layers
The domain or application layer declares what it needs, not how it gets fulfilled.
An IUserRepository interface lives in the domain layer. It says “I need a way to find users by ID and save updated user records.” That’s it. No mention of SQL, MongoDB, or any specific storage technology.
The back-end infrastructure layer then provides SqlUserRepository or MongoUserRepository, each implementing that same interface with technology-specific code.
This is where the pattern directly supports test-driven development. During testing, you inject a fake or mock implementation. During production, the DI container injects the real one. Your domain logic never knows the difference.
The Composition Root
The composition root is where all interface-to-implementation mappings get registered. It lives in the outermost layer, typically your application’s entry point.
In an ASP.NET Core project, that’s Program.cs or Startup.cs. In a Spring Boot application, it’s the configuration classes annotated with @Configuration.
Common containers by ecosystem:
- .NET: Microsoft.Extensions.DependencyInjection, Autofac
- Java: Spring IoC container, Google Guice, CDI
New Relic’s 2024 State of the Java Ecosystem report found that 35% of Java applications now run on Java 17, with rapid adoption of newer LTS versions. These modern runtime versions include improved support for dependency injection patterns that onion architecture relies on.
What Happens Without Dependency Injection
Without DI, the inner layers must instantiate their own dependencies. An application service that needs a user repository would have to call new SqlUserRepository() directly.
That single line of code creates a hard dependency from the application layer to the infrastructure layer. It violates the dependency rule. And it means you can’t test that application service without a running SQL database.
Jeffrey Palermo himself described dependency injection as a “necessary evil” in onion architecture. It adds complexity to setup, but the scalability and testability gains make it non-negotiable for the kind of enterprise applications this pattern was built for.
When to Use Onion Architecture

Onion architecture is not a universal solution. Took me a while to accept that, actually. It adds structural overhead that only pays off when your application reaches a certain level of complexity.
Jeffrey Palermo was explicit about this from the start: the pattern is built for long-lived enterprise applications, not for simple websites or throwaway prototypes.
Projects That Benefit from Onion Architecture
Complex business rules are the clearest signal. If your application has domain logic that goes beyond basic CRUD operations, onion architecture gives that logic a protected home at the center of your system.
According to a Khan et al. survey, roughly 63% of organizations have already implemented or are migrating to distributed architectures like microservices, with scalability as the primary driver for 78% of respondents. Onion architecture fits naturally into this world because each microservice can maintain its own clean domain core.
- Enterprise resource planning (ERP) and CRM platforms
- Financial systems where regulatory compliance demands isolated, auditable business rules
- E-commerce platforms with complex pricing, inventory, and order workflows
- SaaS products that need to support multiple front-end interfaces against a shared domain
Teams that expect infrastructure changes over the application’s lifetime (database migrations, cloud provider switches, framework upgrades) also get the most value from this pattern.
Projects That Don’t Need It
A vFunction 2024 survey found that for more than 50% of companies, technical debt already consumes over a quarter of their IT budget. Adding unnecessary architectural layers to a simple app just creates a different kind of debt.
Skip onion architecture when:
- The app is mostly passing data between a UI and a database
- You’re building an MVP or prototype that might not survive past validation
Victor Rentea, a staff engineer at Artium, has written about the overengineering trap with onion and hexagonal architectures. His point: introducing interfaces “just in case” before they’re actually needed is the definition of speculative generality, a code smell, not good architecture.
Onion Architecture in Practice with .NET and Java

The theory is clean. The implementation is where things get interesting (and where teams stumble).
Both .NET and Java ecosystems have strong conventions for onion architecture project structures, though the naming varies a bit between communities.
| Layer | .NET Project | Java/Spring Package |
|---|---|---|
| Domain Model | MyApp.Domain | com.myapp.domain |
| Application Services | MyApp.Application | com.myapp.application |
| Infrastructure | MyApp.Infrastructure | com.myapp.infrastructure |
| Presentation/API | MyApp.Web | com.myapp.presentation |
In .NET, the typical setup is a Visual Studio solution with separate class library projects for each layer, plus a web project (ASP.NET Core) as the entry point. Microsoft’s own eShopOnWeb reference application follows this exact approach, using the Ardalis Clean Architecture template.
Snyk research shows that 60% of Java developers rely on the Spring Framework for production applications. In Spring Boot projects, the layers map to packages or Gradle/Maven modules. Entity Framework, Dapper, or NHibernate implementations in .NET (and JPA or Hibernate in Java) live strictly in the infrastructure project.
Controllers and API endpoints sit in the outermost layer and call application services. They never touch domain entities directly. At least, that’s the rule. Enforcing it across a team of 15 developers over two years is a different story. Tools like NDepend for .NET or ArchUnit for Java can automatically validate that your layer boundaries aren’t being violated.
The software development process for onion architecture requires more upfront structure than a simple layered approach. But Code Maze and other .NET tutorial sites have documented that the long-term payoff in testability and reliability makes the initial investment worthwhile for mid-to-large applications.
Common Mistakes When Implementing Onion Architecture

Getting the diagram right is the easy part. Keeping it intact over months of feature development, that’s where most teams fail.
These are the mistakes I see again and again, and they’re almost always the same ones.
Leaking Infrastructure into the Domain
This is the single most common violation. It happens when ORM attributes (like Entity Framework’s [Table] or [Column] annotations) show up on domain entities.
The NDepend blog put it simply: your domain models should be plain objects. They shouldn’t know what framework you’re using. Once your domain class imports HttpClient or references a database-specific exception, the dependency rule is broken.
The fix is straightforward. Keep domain entities as POCOs (plain old CLR objects in .NET) or POJOs (plain old Java objects). Mapping between domain objects and persistence entities belongs in the infrastructure layer.
Pass-Through Layers
Developers sometimes create application services that just forward calls to repositories without adding logic. Every method looks like return _repository.GetById(id) with nothing else happening.
The problem: you’ve added complexity without adding value. If a layer doesn’t do anything meaningful, it shouldn’t exist yet.
A better approach: start without the application service layer for simple operations and introduce it when orchestration actually becomes necessary. Modern IDEs make extracting interfaces and adding layers a matter of seconds, so there’s no real cost to doing it later.
Over-Abstracting Everything
| Mistake | What It Looks Like | Better Approach |
|---|---|---|
| ORM in domain | EF attributes on domain entities | Separate persistence models in infrastructure |
| Pass-through layers | Services that only call repositories | Add the layer only when orchestration is needed |
| Interface per class | IFoo for every Foo, even with one implementation | Extract interfaces only when polymorphism is needed |
| Controllers calling repos | Skipping application services entirely | Always route through the application layer |
Victor Rentea calls the “interface for everything” approach speculative generality. Creating IUserService when there will only ever be one UserService adds noise to your codebase without providing actual flexibility.
Developers spend about 42% of their working week on technical debt and maintenance (TekLume). Unnecessary abstractions contribute to that burden just as much as missing abstractions do. The goal is the right level of indirection, not the maximum amount of it.
Onion Architecture and Domain-Driven Design

These two show up together so often that developers sometimes assume they’re the same thing. They’re not. But they work together extremely well.
Domain-driven design, introduced by Eric Evans in 2003, is a set of principles for modeling complex business domains. Onion architecture provides the structural foundation that makes DDD tactical patterns (aggregates, repositories, domain events) work cleanly in a real codebase.
A systematic literature review published in 2025 found that DDD has gained significant attention for its ability to facilitate system decomposition, especially in microservices contexts. The most commonly applied patterns across the studied projects were entities and value objects, bounded contexts, and ubiquitous language.
How the mapping works:
- Aggregate roots: live in the domain layer as the entry points for modifying groups of related entities
- Repository interfaces: defined in the domain, implemented in infrastructure
- Domain events: raised by domain objects, handled by application or infrastructure services
- Bounded contexts: each context can have its own onion, with clear boundaries between them
The DDD in Banking Solutions market was valued at $2.4 billion in 2024 and is expected to grow to $8.7 billion by 2033, according to Market Intelo. That growth is driven by financial institutions modernizing legacy systems using modular architectures where domain logic stays isolated from infrastructure concerns.
Not all onion architecture projects use DDD. But most serious DDD implementations use some form of this layering. Without the structural protection onion architecture provides, domain models tend to accumulate infrastructure dependencies over time and eventually become what Eric Evans called an anemic domain model: entities that hold data but contain no real business behavior.
The software architect’s job is to make sure those boundaries hold. A code review process that specifically checks for layer violations is one of the most effective ways to keep the architecture intact as the team grows.
FAQ on What Is Onion Architecture
What is onion architecture in simple terms?
Onion architecture is a software design pattern that organizes code into concentric layers. The domain model sits at the center with zero external dependencies. All outer layers (databases, UI, APIs) depend inward, never the reverse.
Who created onion architecture?
Jeffrey Palermo introduced the pattern in 2008. He designed it specifically for enterprise applications where business logic needed protection from infrastructure changes like database swaps or framework upgrades.
What are the main layers of onion architecture?
Four primary layers: domain model (innermost), domain services, application services, and infrastructure/presentation (outermost). Each layer can only depend on layers closer to the center. Dependencies always point inward.
How is onion architecture different from clean architecture?
Both enforce inward dependency flow. Clean architecture, by Robert C. Martin, adds explicit use case interactors and allows more flexible layering. Onion architecture provides stricter concentric ring structure with more defined layer boundaries.
What is the dependency rule in onion architecture?
Inner layers never reference outer layers. The domain model knows nothing about databases or frameworks. This rule is enforced through interfaces defined in inner layers and implemented by outer layers using dependency injection.
When should you use onion architecture?
Use it for medium-to-large applications with complex business rules and long expected lifespans. Skip it for simple CRUD apps, prototypes, or small microservices where the structural overhead adds unnecessary complexity.
Does onion architecture require domain-driven design?
No. They complement each other well, but onion architecture works without DDD. That said, most serious domain-driven design implementations use onion or similar layering to protect domain logic from infrastructure concerns.
What programming languages support onion architecture?
Any object-oriented language works. It’s most common in .NET (ASP.NET Core) and Java (Spring Boot) ecosystems because both have mature dependency injection containers that make enforcing the dependency rule straightforward.
What is the biggest mistake teams make with onion architecture?
Leaking infrastructure into the domain layer. Putting ORM attributes on domain entities or importing framework-specific types into business logic breaks the entire pattern. Domain objects should be plain objects with no external dependencies.
How does onion architecture improve testability?
Because the domain model has zero infrastructure dependencies, you can unit test business logic without running databases or web servers. Inner layer interfaces are easily replaced with mocks or fakes during testing.
Conclusion
Understanding what is onion architecture comes down to one principle: your business logic should never depend on your technology choices. The concentric layer model that Jeffrey Palermo formalized gives development teams a proven structure for keeping domain entities isolated from databases, frameworks, and external services.
The pattern works best for enterprise applications built with .NET or Spring where long-term code maintainability matters more than shipping speed. Smaller projects can safely skip it.
Whether you pair it with domain-driven design or use it alongside CQRS and the repository pattern, the core benefit stays the same. Your most valuable code, the business rules, remains testable and portable regardless of what happens in the outer layers.
Start with the dependency rule. Get that right, and the rest follows.







