What Is Hexagonal Architecture? Ports & Adapters Guide
Does changing your database affect your entire application? Are you locked into a framework? Hexagonal Architecture isolates your business logic from the outside world, making it testable and changeable.
What Is Hexagonal Architecture?
Hexagonal Architecture, defined by Alistair Cockburn in 2005, is also known as Ports & Adapters. The core idea: put business logic (domain) at the center, push external dependencies (DB, APIs, UI) to peripheral adapters.
┌──────────────────────┐
│ Adapters │
│ ┌────────────────┐ │
│ │ Ports │ │
│ │ ┌──────────┐ │ │
Input ──►│ │ │ Domain │ │ │──► Output
│ │ │ (Core) │ │ │
│ │ └──────────┘ │ │
│ └────────────────┘ │
└──────────────────────┘
Why Hexagonal?
Traditional layered architecture creates tight coupling:
Controller → Service → Repository → Database
Framework Mixed DB-bound
locked logic dependent
Hexagonal solution:
HTTP Adapter → [Port] → Domain Logic → [Port] → DB Adapter
REST/GraphQL PostgreSQL/MongoDB
- Domain has zero external dependencies
- Adapters are swappable (PostgreSQL → MongoDB, REST → GraphQL)
- Easy to test — test domain in isolation
Core Concepts
Domain (Core)
Your business rules. No framework, database, or HTTP concepts.
Port
An interface the domain defines for communicating with the outside world.
Adapter
A concrete implementation that fulfills a port interface.
Practical Example: Order System
Ports
// ports/OrderRepository.ts
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
// ports/PaymentGateway.ts
interface PaymentGateway {
charge(amount: number, token: string): Promise<PaymentResult>;
}
Domain
class OrderService {
constructor(
private orderRepo: OrderRepository,
private payment: PaymentGateway
) {}
async createOrder(items: Item[], paymentToken: string): Promise<Order> {
const order = Order.create(items);
const result = await this.payment.charge(order.total, paymentToken);
if (!result.success) throw new PaymentError(result.error);
order.markAsPaid(result.transactionId);
await this.orderRepo.save(order);
return order;
}
}
Adapters
// PostgresOrderRepository.ts
class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
await db.query('INSERT INTO orders ...', [order.id, order.total]);
}
}
// StripePaymentGateway.ts
class StripePaymentGateway implements PaymentGateway {
async charge(amount: number, token: string): Promise<PaymentResult> {
const result = await stripe.charges.create({ amount, source: token });
return { success: true, transactionId: result.id };
}
}
Hexagonal vs Other Architectures
| Feature | Layered | Hexagonal | Clean Architecture | |---------|---------|-----------|-------------------| | Domain isolation | Weak | Strong | Strong | | Testability | Medium | High | High | | Complexity | Low | Medium | High | | DB independence | Low | High | High |
Best Practices
- Dependency Rule — Dependencies always point inward
- No frameworks in domain — Pure TypeScript/Java/Python
- Keep ports small — Apply Interface Segregation
- Make adapters swappable — Use Dependency Injection
- Use domain events — Decouple side effects
Conclusion
Hexagonal Architecture isolates your business logic from technical details, delivering a sustainable, testable, and flexible architecture. It has a complexity cost, but for medium-to-large projects, the long-term payoff is substantial.
Learn Hexagonal Architecture with interactive missions on LabLudus.