The Hexagonal Architecture

Introduction

Hexagonal Architecture (also known as Ports and Adapters pattern) organizes code to enforce separation of concerns and dependency inversion, making applications more maintainable, testable, and adaptable.

Core Concepts

1. Three Main Layers

  • Domain Core: Business logic and entities

  • Ports: Interface definitions

  • Adapters: Implementation of interfaces

2. Key Principles

  • Domain core has no external dependencies

  • All dependencies point inward

  • External systems interact through ports

  • Business logic is technology-agnostic

Implementation Example

Domain Core

// Domain Entity
class Order {
    constructor(
        private id: string,
        private items: OrderItem[],
        private status: OrderStatus
    ) {}

    calculateTotal(): number {
        return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    }

    confirm(): void {
        if (this.status !== OrderStatus.PENDING) {
            throw new Error('Order cannot be confirmed');
        }
        this.status = OrderStatus.CONFIRMED;
    }
}

// Port (Primary/Driving)
interface OrderService {
    createOrder(items: OrderItem[]): Promise<Order>;
    confirmOrder(orderId: string): Promise<void>;
}

// Port (Secondary/Driven)
interface OrderRepository {
    save(order: Order): Promise<void>;
    findById(id: string): Promise<Order>;
}

Adapters

// Primary Adapter (REST Controller)
class OrderController {
    constructor(private orderService: OrderService) {}

    @Post('/orders')
    async createOrder(@Body() items: OrderItem[]): Promise<Order> {
        return this.orderService.createOrder(items);
    }
}

// Secondary Adapter (Database Implementation)
class PostgresOrderRepository implements OrderRepository {
    constructor(private db: Database) {}

    async save(order: Order): Promise<void> {
        await this.db.query('INSERT INTO orders...', [order.toJSON()]);
    }

    async findById(id: string): Promise<Order> {
        const data = await this.db.query('SELECT * FROM orders WHERE id = $1', [id]);
        return Order.fromJSON(data);
    }
}

Application Service

class OrderApplicationService implements OrderService {
    constructor(private orderRepository: OrderRepository) {}

    async createOrder(items: OrderItem[]): Promise<Order> {
        const order = new Order(uuid(), items, OrderStatus.PENDING);
        await this.orderRepository.save(order);
        return order;
    }

    async confirmOrder(orderId: string): Promise<void> {
        const order = await this.orderRepository.findById(orderId);
        order.confirm();
        await this.orderRepository.save(order);
    }
}

Testing Approach

describe('OrderService', () => {
    let orderService: OrderService;
    let mockRepository: MockOrderRepository;

    beforeEach(() => {
        mockRepository = new MockOrderRepository();
        orderService = new OrderApplicationService(mockRepository);
    });

    it('should create order', async () => {
        const items = [{ productId: '1', quantity: 2, price: 10 }];
        const order = await orderService.createOrder(items);

        expect(order.calculateTotal()).toBe(20);
        expect(mockRepository.savedOrder).toBeDefined();
    });
});

Common Patterns

Dependency Injection

// Configuration
class AppConfig {
    @Inject()
    static configureServices(container: Container): void {
        container.register<OrderService>(OrderApplicationService);
        container.register<OrderRepository>(PostgresOrderRepository);
    }
}

Error Handling

// Domain Exception
class DomainException extends Error {
    constructor(message: string, public readonly code: string) {
        super(message);
    }
}

// Adapter Exception Handler
@Catch(DomainException)
class DomainExceptionFilter implements ExceptionFilter {
    catch(exception: DomainException, host: ArgumentsHost): void {
        const response = host.switchToHttp().getResponse();
        response.status(400).json({
            code: exception.code,
            message: exception.message
        });
    }
}

Best Practices

  1. Domain Logic Isolation

    • Keep business rules in domain layer

    • Use value objects for domain concepts

    • Implement domain events for side effects

  2. Port Design

    • Define clear interface contracts

    • Use dependency injection

    • Keep ports focused and cohesive

  3. Adapter Implementation

    • Handle technology-specific concerns

    • Implement error mapping

    • Maintain separation between adapters

Anti-patterns to Avoid

  1. Domain logic leaking into adapters

  2. Direct adapter-to-adapter communication

  3. Technology-specific dependencies in domain

  4. Bypassing ports for external communication

Practical Considerations

Performance Optimization

// Caching Adapter
class CachedOrderRepository implements OrderRepository {
    constructor(
        private repository: OrderRepository,
        private cache: Cache
    ) {}

    async findById(id: string): Promise<Order> {
        const cached = await this.cache.get(id);
        if (cached) return Order.fromJSON(cached);

        const order = await this.repository.findById(id);
        await this.cache.set(id, order.toJSON());
        return order;
    }
}

Scalability

// Event-Driven Adapter
class EventDrivenOrderRepository implements OrderRepository {
    constructor(private eventBus: EventBus) {}

    async save(order: Order): Promise<void> {
        await this.eventBus.publish('order.saved', order);
    }
}

Migration Strategies

  1. Incremental Approach

    • Identify domain boundaries

    • Create ports for existing interfaces

    • Gradually refactor adapters

    • Move business logic to domain

  2. Testing Strategy

    • Write tests against ports

    • Mock external dependencies

    • Ensure domain logic coverage

    • Test adapters in isolation

Conclusion

Hexagonal Architecture provides:

  • Clear separation of concerns

  • Technology independence

  • Improved testability

  • Flexible adaptation to change

Success depends on careful boundary definition, strict adherence to dependency rules, and consistent refactoring toward the pattern's principles.

💡
Generated Using Claude.ai by Anthropic