Onion Architecture
Onion Architecture
"The diagram at the left shows the Onion Architecture. The fundamental rule is that all code can depend on layers more central, but code cannot depend on layers further out from the core." — Jeffrey Palermo, 2008
Intent
Onion Architecture is Jeffrey Palermo's 2008 formulation of the dependency inversion principle applied at the application level. Palermo introduced Onion Architecture as an evolution of Hexagonal Architecture, explicitly specifying the internal structure of the application core that Hexagonal deliberately left unspecified.
Onion's unique contribution: the explicit separation of Domain Services (domain logic that does not belong to one entity — e.g., PricingService, ShippingCalculator) from Application Services (use case orchestration — e.g., PlaceOrderService). Clean Architecture collapses both into the "Use Cases" layer. Hexagonal Architecture prescribes no internal layering at all. Onion added DDD-style internals to what Hexagonal left unspecified.
Clean Architecture, Hexagonal Architecture, and Onion Architecture express the same foundational principle — dependency inversion toward the domain — with different vocabularies. Three separate notes exist because the vocabulary differences matter in practice: teams encounter these patterns by name in codebases and must work in the specific terminology already in use.
When NOT to Use
- Small applications or CRUD systems without complex domain logic — the Domain Services / Application Services distinction creates overhead with no benefit
- Teams without familiarity with DDD tactical patterns (Aggregate, Domain-Events, Repository)
- Prototypes or scripts
When to Use
- Systems where the domain model is complex and must be protected from infrastructure concerns
- Teams already using DDD patterns — Aggregate, Domain-Events, Repository — where Onion's explicit ring model aligns with the team's existing vocabulary
- When persistence ignorance is a hard requirement — repository interfaces must live in the domain layer, with no domain object knowing about databases
How It Works
Onion Architecture organises code into four concentric rings (inner to outer):
- Domain Model (innermost) — entities, value objects, domain events; no external dependencies whatsoever. This ring owns the repository interfaces — e.g.,
OrderRepositoryis defined here, with its entity. - Domain Services — domain logic that operates on the domain model but does not belong to one entity (e.g.,
PricingServicecalculating totals across multipleOrderItemobjects,ShippingCalculatorapplying business rules to anOrder). No infrastructure dependencies. - Application Services — use case orchestration; the outermost domain-facing layer. An application service calls domain services and repositories, coordinates transactions, and translates between the domain model and the outside world.
- Infrastructure / UI / Tests (outermost) — all implementations of interfaces defined in inner rings:
JpaOrderRepositoryimplementingOrderRepository, HTTP controllers, message consumers, test doubles.
Repository interface placement (Palermo's rule): Repository interfaces belong in the Domain Model ring (inner). Implementations belong in Infrastructure (outer ring). This is the structural enforcement of persistence ignorance — the domain model knows what it needs (an OrderRepository) but has zero knowledge of how it is implemented. See Repository for the detailed pattern.
+------------------------------------------------+
| Infrastructure / UI / Tests (outer) |
| +------------------------------------------+ |
| | Application Services (use case layer) | |
| | +------------------------------------+ | |
| | | Domain Services | | |
| | | +------------------------------+ | | |
| | | | Domain Model | | | |
| | | | (entities, value objects, | | | |
| | | | domain events) | | | |
| | | +------------------------------+ | | |
| | +------------------------------------+ | |
| +------------------------------------------+ |
+------------------------------------------------+
All dependencies point inward -->
All three architectures express the same foundational principle — dependency inversion toward the domain — with different vocabularies.
| Dimension | Hexagonal (Cockburn, 2005) | Onion (Palermo, 2008) | Clean (Martin, 2012) |
|---|---|---|---|
| Layer Names | Port / Adapter / Application Core | Domain Model / Domain Services / Application Services / Infrastructure | Entities / Use Cases / Interface Adapters / Frameworks & Drivers |
| Dependency Rule Statement | "Outside actors connect via ports; the application core has no outward dependencies" | "All coupling toward the centre; outer layers depend on inner layers, never the reverse" | "Source code dependencies can only point inward. Nothing in an inner circle can know anything about something in an outer circle." |
| What It Prescribes Inside the Core | Nothing — Hexagonal does not specify the internal structure of the application core | Explicit separation of Domain Services from Application Services | Explicit separation of Entities (enterprise-wide rules) from Use Cases (application-specific rules) |
Architecture Diagram
Onion-Architecture-diagram.excalidraw
TypeScript Example
Folder layout:
src/
domain/
model/ # Domain Model (entities, value objects, repository interfaces)
Order.ts
OrderRepository.ts # Repository interface DEFINED HERE
services/ # Domain Services
PricingService.ts
application/
services/ # Application Services (use cases)
PlaceOrderService.ts
infrastructure/ # Outer ring — implementations
persistence/
PostgresOrderRepository.ts # Implements domain/model/OrderRepository.ts
http/
Domain Services vs Application Services pattern:
// Onion Architecture — TypeScript
// src/domain/services/PricingService.ts
// DOMAIN SERVICE — pure domain logic, no orchestration, no infrastructure
export class PricingService {
calculateTotal(items: OrderItem[]): Money {
return items.reduce((sum, item) => sum.add(item.price), Money.zero());
}
}
// src/application/services/PlaceOrderService.ts
// APPLICATION SERVICE — orchestrates domain objects, calls repository port
import { PricingService } from '../../domain/services/PricingService';
import { OrderRepository } from '../../domain/model/OrderRepository';
export class PlaceOrderService {
constructor(
private readonly pricing: PricingService,
private readonly orders: OrderRepository
) {}
async placeOrder(command: PlaceOrderCommand): Promise<void> {
const total = this.pricing.calculateTotal(command.items);
const order = new Order(command.customerId, command.items, total);
await this.orders.save(order);
}
}Java Example
Package layout:
com.app/
domain/
model/
Order.java
OrderRepository.java # Interface DEFINED HERE (not in application/ or infrastructure/)
services/
PricingService.java
application/
services/
PlaceOrderService.java
infrastructure/
persistence/
JpaOrderRepository.java # Implements domain.model.OrderRepository
web/
Domain Services vs Application Services pattern:
// Onion Architecture — Java
// domain/services/PricingService.java — pure domain logic
public class PricingService {
public Money calculateTotal(List<OrderItem> items) {
return items.stream()
.map(OrderItem::getPrice)
.reduce(Money.zero(), Money::add);
}
}
// application/services/PlaceOrderService.java — use case orchestration
public class PlaceOrderService {
private final PricingService pricingService;
private final OrderRepository orderRepository;
public PlaceOrderService(PricingService pricingService, OrderRepository orderRepository) {
this.pricingService = pricingService;
this.orderRepository = orderRepository;
}
public void placeOrder(PlaceOrderCommand command) {
Money total = pricingService.calculateTotal(command.items());
Order order = new Order(command.customerId(), command.items(), total);
orderRepository.save(order);
}
}Enforcement Tooling
Install:
# TypeScript — dependency-cruiser
npm install --save-dev dependency-cruiser
# Java — ArchUnit (Maven)
# com.tngtech.archunit:archunit-junit5:1.3.0TypeScript — dependency-cruiser ring boundary rule:
// .dependency-cruiser.js
{
forbidden: [{
name: "no-domain-to-application",
from: { path: "^src/domain" },
to: { path: "^src/application" }
}, {
name: "no-inner-to-infrastructure",
from: { path: "^src/(domain|application)" },
to: { path: "^src/infrastructure" }
}]
}Java — ArchUnit:
@AnalyzeClasses(packages = "com.app")
class OnionArchitectureTest {
@ArchTest
static final ArchRule domainMustNotDependOnApplication =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..application..");
@ArchTest
static final ArchRule noInnerLayersDependOnInfrastructure =
noClasses().that().resideInAnyPackage("..domain..", "..application..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..");
}In Onion Architecture, the repository interface (e.g., OrderRepository) must be defined in the Domain Model ring (innermost), NOT in the Application Services ring. Placing it in application/ instead of domain/model/ breaks persistence ignorance — the domain model is not truly isolated.
Warning sign: A domain/ folder that imports from application/ or infrastructure/, or an application/ folder that defines repository interfaces that belong with the entities they serve in domain/model/.
Related Concepts
- Clean-Architecture — Robert C. Martin's 2012 formulation; collapses Domain Services and Application Services into a single "Use Cases" layer; same dependency rule with different vocabulary
- Hexagonal-Architecture — Alistair Cockburn's 2005 formulation; provides Port/Adapter vocabulary but leaves the internal structure of the core unspecified — Onion fills that gap
- Adapter-Pattern — GoF structural pattern; Onion's infrastructure implementations are Adapters between inner-ring interfaces and external systems
- Repository — the canonical pattern for the repository interface that lives in the Domain Model ring; implementation lives in Infrastructure
- Bounded-Context — each Onion application naturally maps to a bounded context; domain model ring holds the bounded context's core entities
- Domain-Events — live in the Domain Model ring (innermost); raised by entities and aggregates, consumed by application services or infrastructure
- Aggregate — lives in the Domain Model ring; the consistency boundary that owns invariants and raises domain events
- Value-Object — lives in the Domain Model ring; immutable descriptors used by entities and aggregates
Sources
- Jeffrey Palermo, "The Onion Architecture: part 1" (2008) — https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/
- Jeffrey Palermo, "The Onion Architecture: part 3" (2008) — https://jeffreypalermo.com/2008/08/the-onion-architecture-part-3/
- Herberto Graca, "Onion Architecture" (2017) — https://herbertograca.com/2017/09/21/onion-architecture/
- Herberto Graca, "DDD, Hexagonal, Onion, Clean, CQRS — How I Put It All Together" (2017) — https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/