Hexagonal Architecture
Architectural pattern isolating business logic from external dependencies through ports and adapters for optimal maintainability.
Updated on February 24, 2026
Hexagonal Architecture, also known as Ports & Adapters, is an architectural pattern created by Alistair Cockburn that aims to completely isolate an application's business logic from its external technical dependencies. This approach enables building highly testable, scalable systems that are independent of frameworks, databases, or user interfaces.
Fundamentals
- The business core (domain) has no dependencies on external technologies and contains only business rules
- Ports define interface contracts between the domain and the outside world (abstraction)
- Adapters implement these ports to connect concrete technologies (database, API, UI)
- Dependency direction always points inward, toward the domain (Dependency Inversion Principle)
Benefits
- Maximum testability: domain can be tested without any real external dependencies
- Technology independence: change frameworks, databases, or APIs without modifying business logic
- Enhanced scalability: add new input/output channels by simply creating adapters
- Improved maintainability: clear separation of concerns and reduced coupling
- Business code reusability: domain can be used across different application contexts
Practical Example
Consider an e-commerce order management system. The domain contains validation and order processing logic, while adapters handle interactions with PostgreSQL, Stripe, and a REST API.
// DOMAIN - Pure business logic
class Order {
constructor(
public readonly id: string,
public readonly items: OrderItem[],
public status: OrderStatus
) {}
validate(): void {
if (this.items.length === 0) {
throw new DomainError('Order must contain at least one item');
}
}
calculateTotal(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.price.multiply(item.quantity)),
Money.zero()
);
}
}
// PORT - Outbound interface
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
interface PaymentGateway {
processPayment(amount: Money): Promise<PaymentResult>;
}
// USE CASE - Application service
class PlaceOrderUseCase {
constructor(
private orderRepo: OrderRepository,
private paymentGateway: PaymentGateway
) {}
async execute(command: PlaceOrderCommand): Promise<Order> {
const order = new Order(uuid(), command.items, 'pending');
order.validate();
const paymentResult = await this.paymentGateway.processPayment(
order.calculateTotal()
);
if (paymentResult.isSuccess()) {
order.status = 'confirmed';
await this.orderRepo.save(order);
}
return order;
}
}
// ADAPTER - Concrete implementation
class PostgresOrderRepository implements OrderRepository {
constructor(private db: DatabaseClient) {}
async save(order: Order): Promise<void> {
await this.db.query(
'INSERT INTO orders (id, items, status) VALUES ($1, $2, $3)',
[order.id, JSON.stringify(order.items), order.status]
);
}
async findById(id: string): Promise<Order | null> {
const row = await this.db.queryOne('SELECT * FROM orders WHERE id = $1', [id]);
return row ? this.mapToDomain(row) : null;
}
}
class StripePaymentGateway implements PaymentGateway {
async processPayment(amount: Money): Promise<PaymentResult> {
const stripe = new Stripe(process.env.STRIPE_KEY);
const charge = await stripe.charges.create({
amount: amount.cents,
currency: amount.currency
});
return PaymentResult.fromStripe(charge);
}
}Implementation
- Identify and model the business domain with its entities, value objects, and business rules
- Define primary ports (use cases) exposing business functionalities
- Define secondary ports (interfaces) for external dependencies (persistence, messaging, etc.)
- Implement primary adapters (REST API, GraphQL, CLI) that invoke use cases
- Implement secondary adapters (repositories, gateways) respecting domain interfaces
- Configure dependency injection to wire adapters and domain together
- Test the domain in isolation using adapter mocks
Pro Tip
Always start by modeling your business domain while completely ignoring technical aspects. Ask yourself: 'How would my business work if I had no database, no API, no graphical interface?' This approach forces genuine reflection on business value and prevents technical contamination of the domain.
Related Tools
- NestJS: Node.js framework with native dependency injection support facilitating hexagonal architecture
- TypeDI / InversifyJS: dependency injection containers for TypeScript
- Jest / Vitest: essential testing frameworks for testing the domain in isolation
- Class-validator: declarative validation for value objects and entities
- TypeORM / Prisma: ORMs adaptable as persistence adapters
Hexagonal architecture represents an upfront investment in structuring that quickly pays off on medium to long-term projects. By isolating business logic from technical details, it allows teams to focus on business value while ensuring an evolvable, testable, and resilient codebase against technological changes. It's a strategic choice for critical systems requiring both stability and agility.

