Loading learning content...
If you've studied Hexagonal Architecture (Ports and Adapters) and Onion Architecture, you've probably noticed striking similarities. Both place the domain at the center. Both isolate business logic from infrastructure. Both use interfaces to invert dependencies. So what's the difference? Are they just the same idea with different names?
The answer is nuanced. These architectures share a common philosophy but differ in organizational emphasis and conceptual vocabulary. Understanding both—and their relationship—makes you a more effective architect, capable of choosing the right approach for each situation and communicating clearly with teams using either style.
By the end of this page, you will understand the philosophical and structural relationships between Onion and Hexagonal Architecture, identify their key differences in layer organization and terminology, and know how to choose between them or combine their concepts effectively.
Both Onion Architecture and Hexagonal Architecture are responses to the same problem: traditional layered architectures that couple business logic to infrastructure. They both solve this problem through the same fundamental mechanism: Dependency Inversion.
The shared insight:
Traditional architecture:
Business Logic → Database (business depends on infrastructure)
Inverted architecture:
Database → Interface ← Business Logic (infrastructure depends on business)
This inversion—achieved by placing interfaces in the domain and implementations in infrastructure—is the core technique both architectures use. Their shared ancestry traces back to Robert Martin's Dependency Inversion Principle (1996) and ultimately to the broader principles of modular design.
Hexagonal Architecture (Alistair Cockburn, 2005) came first, focusing on the idea of ports and adapters. Onion Architecture (Jeffrey Palermo, 2008) built on similar principles with explicit emphasis on concentric layers. Clean Architecture (Robert Martin, 2012) later synthesized these ideas. All three share the same DNA.
Hexagonal Architecture visualizes the application as a hexagon with the domain at the center. External actors (users, databases, services) interact with the domain through ports (interfaces) and adapters (implementations).
┌──────────────────────────────────────┐
│ PRIMARY ADAPTERS │
│ (Web Controller, CLI, Message Consumer)
└─────────────────┬────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ PRIMARY PORTS │
│ (Use Case Interfaces) │
└─────────────────┬────────────────────┘
│
┌─────────────────▼────────────────────┐
│ │
│ APPLICATION CORE │
│ (Domain Model + Use Cases) │
│ │
└─────────────────┬────────────────────┘
│
┌─────────────────▼────────────────────┐
│ SECONDARY PORTS │
│ (Repository Interfaces, │
│ External Service Interfaces) │
└─────────────────┬────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ SECONDARY ADAPTERS │
│ (Database, APIs, File System) │
└──────────────────────────────────────┘
Key concepts:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// ============================================// HEXAGONAL ARCHITECTURE STRUCTURE// ============================================ // PRIMARY PORT: Use case interface (what the application offers)interface SubmitOrderUseCase { execute(command: SubmitOrderCommand): Promise<SubmitOrderResult>;} // SECONDARY PORT: Repository interface (what the application needs)interface OrderRepository { findById(id: OrderId): Promise<Order | null>; save(order: Order): Promise<void>;} // APPLICATION CORE: Domain + Use Case Implementationclass SubmitOrderService implements SubmitOrderUseCase { constructor(private readonly orderRepository: OrderRepository) {} async execute(command: SubmitOrderCommand): Promise<SubmitOrderResult> { const order = await this.orderRepository.findById( new OrderId(command.orderId) ); if (!order) throw new OrderNotFoundError(command.orderId); order.submit(); // Domain logic await this.orderRepository.save(order); return { orderId: order.id.value, status: 'submitted' }; }} // PRIMARY ADAPTER: REST Controller (drives the application)class OrderRestAdapter { constructor(private readonly submitOrder: SubmitOrderUseCase) {} async handleSubmit(req: Request, res: Response): Promise<void> { const result = await this.submitOrder.execute({ orderId: req.params.id }); res.json(result); }} // SECONDARY ADAPTER: PostgreSQL Repository (driven by the application)class PostgresOrderAdapter implements OrderRepository { async findById(id: OrderId): Promise<Order | null> { // Database-specific implementation } async save(order: Order): Promise<void> { // Database-specific implementation }}Onion Architecture visualizes the application as concentric circles (like an onion) with the domain model at the innermost core and infrastructure at the outermost edge.
┌─────────────────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ (Controllers, Repositories, External Services, IoC Configuration) │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ APPLICATION SERVICES LAYER │ │
│ │ (Use Cases, DTOs, Transaction Management) │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ DOMAIN SERVICES LAYER │ │ │
│ │ │ (Cross-Entity Logic, Domain Policies) │ │ │
│ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ DOMAIN MODEL LAYER │ │ │ │
│ │ │ │ (Entities, Value Objects, Aggregates, Events, │ │ │ │
│ │ │ │ Repository Interfaces, Domain Exceptions) │ │ │ │
│ │ │ └───────────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Key concepts:
Onion Architecture explicitly defines multiple layers within the domain (Domain Model, Domain Services) and provides a dedicated Application Services layer. This granularity helps teams understand where different types of code belong.
While Hexagonal and Onion Architecture share core principles, they differ in emphasis, vocabulary, and organizational structure. Understanding these differences helps you communicate with teams using either style and choose the right mental model for your context.
| Aspect | Hexagonal Architecture | Onion Architecture |
|---|---|---|
| Primary Metaphor | Hexagon with ports/adapters | Concentric circles (onion layers) |
| Core Abstraction | Ports (interfaces) and Adapters (implementations) | Layers with dependency rules |
| Domain Subdivision | Single 'Application Core' (often undivided) | Explicit Domain Model + Domain Services layers |
| Layer Count | Typically 2-3 (Core, Adapters, sometimes Ports layer) | Typically 4-5 (Domain Model, Domain Services, Application, Infrastructure) |
| Vocabulary | Driving/Driven, Primary/Secondary, Ports/Adapters | Layers, Dependencies, Core, Edge |
| Adapter Direction | Explicitly distinguishes Primary (driving) and Secondary (driven) adapters | All infrastructure is 'outer layer' without directional distinction |
| Use Case Location | Part of Application Core | Dedicated Application Services layer |
| Emphasis | External interaction symmetry (all external actors access uniformly) | Internal layer structure and dependency direction |
The fundamental difference in perspective:
Hexagonal focuses on the boundary between the application and the outside world. It emphasizes how external actors (users, databases, services) interact with the application through ports and adapters. The internal structure of the 'application core' is not prescribed.
Onion focuses on the internal structure of the application. It prescribes how to organize the domain into layers with explicit dependency rules. The external interaction mechanism (adapters) is less emphasized.
When working with teams using different architectural vocabularies, it helps to have a concept mapping. Most concepts have direct equivalents—it's largely a matter of terminology.
| Hexagonal Concept | Onion Equivalent | Notes |
|---|---|---|
| Application Core | Domain Model + Domain Services + Application Services | Onion subdivides what Hexagonal treats as one unit |
| Primary Port | Application Service Interface | Entry point for use cases |
| Secondary Port | Repository Interface / Service Interface | Interface for infrastructure needs |
| Primary Adapter | Controller / CLI / Consumer (Infrastructure) | Drives the application |
| Secondary Adapter | Repository Implementation (Infrastructure) | Implements domain-defined interfaces |
| Driving Side | Outer Infrastructure (input handlers) | Where requests originate |
| Driven Side | Outer Infrastructure (output handlers) | Where persistence/APIs are called |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// ============================================// THE SAME CODE THROUGH DIFFERENT LENSES// ============================================ // HEXAGONAL VIEW:// - SubmitOrderUseCase is a PRIMARY PORT// - SubmitOrderService is APPLICATION CORE// - OrderRepository is a SECONDARY PORT// - PostgresOrderRepository is a SECONDARY ADAPTER// - OrderController is a PRIMARY ADAPTER // ONION VIEW:// - SubmitOrderService is APPLICATIONS SERVICES LAYER// - Order (entity) is DOMAIN MODEL LAYER// - OrderRepository interface is DOMAIN MODEL LAYER// - PostgresOrderRepository is INFRASTRUCTURE LAYER// - OrderController is INFRASTRUCTURE LAYER // The code itself is identical. Only the labeling differs: interface OrderRepository { // Hexagonal: Secondary Port | Onion: Domain Layer Interface findById(id: OrderId): Promise<Order | null>; save(order: Order): Promise<void>;} class Order { // Hexagonal: Application Core | Onion: Domain Model Layer submit(): void { if (this.items.length === 0) throw new EmptyOrderError(); this.status = OrderStatus.SUBMITTED; }} class SubmitOrderService { // Hexagonal: Application Core | Onion: Application Services Layer constructor(private orderRepository: OrderRepository) {} async execute(command: SubmitOrderCommand): Promise<void> { const order = await this.orderRepository.findById(new OrderId(command.orderId)); order.submit(); await this.orderRepository.save(order); }} class PostgresOrderRepository implements OrderRepository { // Both: Infrastructure / Adapter async findById(id: OrderId): Promise<Order | null> { /* ... */ } async save(order: Order): Promise<void> { /* ... */ }} class OrderController { // Both: Infrastructure / Adapter (Primary/Outer) constructor(private submitOrderService: SubmitOrderService) {} async handleSubmit(req: Request, res: Response): Promise<void> { await this.submitOrderService.execute({ orderId: req.params.id }); res.json({ status: 'submitted' }); }}A well-designed codebase often follows both architectures simultaneously because they prescribe compatible structures. The difference is in how you think about and explain the organization. Use Hexagonal vocabulary when discussing integration boundaries; use Onion vocabulary when discussing internal layering.
While both architectures can apply to most applications, certain situations favor one mental model over the other. The choice often depends on your team's background, the application's characteristics, and what aspects of the architecture you need to emphasize.
In practice, many teams combine vocabularies. You might describe your overall structure using Onion layers, but use 'adapter' terminology when discussing specific infrastructure implementations. The architectures are complementary views of the same underlying principles.
The most effective architects don't rigidly adhere to one architecture—they synthesize concepts from both to create an approach that fits their context. Here's how to combine the strengths of Hexagonal and Onion:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
src/├── domain/ # ONION: Domain layer(s)│ ├── model/ # Domain Model Layer│ │ ├── entities/│ │ │ ├── Order.ts│ │ │ └── Customer.ts│ │ ├── value-objects/│ │ │ ├── Money.ts│ │ │ └── OrderId.ts│ │ ├── events/│ │ │ └── OrderSubmittedEvent.ts│ │ └── ports/ # HEXAGONAL: Secondary Ports (interfaces needed)│ │ ├── OrderRepository.ts│ │ └── PaymentGateway.ts│ ││ └── services/ # Domain Services Layer│ ├── ShippingCalculator.ts│ └── PricingPolicy.ts│├── application/ # ONION: Application Services Layer│ ├── commands/ # HEXAGONAL: Partial Primary Ports│ │ └── SubmitOrderCommand.ts│ ├── queries/│ │ └── GetOrderQuery.ts│ └── services/│ ├── SubmitOrderService.ts # Use Case implementation│ └── OrderQueryService.ts│└── infrastructure/ # ONION: Infrastructure | HEXAGONAL: Adapters ├── adapters/ │ ├── primary/ # HEXAGONAL: Driving Adapters │ │ ├── rest/ │ │ │ └── OrderController.ts │ │ ├── cli/ │ │ │ └── OrderCLI.ts │ │ └── messaging/ │ │ └── OrderEventConsumer.ts │ │ │ └── secondary/ # HEXAGONAL: Driven Adapters │ ├── persistence/ │ │ └── PostgresOrderRepository.ts │ ├── payment/ │ │ └── StripePaymentGateway.ts │ └── notification/ │ └── SendGridEmailService.ts │ └── config/ └── DependencyInjection.tsThis structure:
The result is a coherent architecture that leverages the strengths of both approaches.
We've compared Hexagonal and Onion Architecture, revealing them as two perspectives on the same underlying principles. Let's consolidate the key insights:
What's next:
Theory is necessary but insufficient. In the next page, we'll see Onion Architecture in practice: real-world implementation patterns, common pitfalls to avoid, and strategies for introducing Onion Architecture to existing codebases.
You now understand the relationship between Hexagonal and Onion Architecture—their shared principles, distinct emphases, and how to combine their concepts. This knowledge makes you a more versatile architect, capable of communicating effectively with teams using either approach and selecting the right mental model for each situation.