Arquitetura hexagonal: ports and adapters

1. Introdução à Arquitetura Hexagonal

A Arquitetura Hexagonal, também conhecida como Ports and Adapters, foi proposta por Alistair Cockburn em 2005 como resposta a um problema recorrente em sistemas de software: o alto acoplamento entre o núcleo do negócio e os detalhes de infraestrutura. Em sistemas tradicionais, bibliotecas de banco de dados, frameworks web e serviços externos frequentemente "vazam" para dentro do código de domínio, tornando a lógica de negócios frágil e difícil de testar.

O padrão propõe um isolamento radical: o núcleo da aplicação — contendo as regras de negócio e entidades — deve ser completamente independente de tecnologias externas. Esse núcleo se comunica com o mundo exterior através de portas (interfaces) e adaptadores (implementações concretas). O nome "hexagonal" vem da representação visual do padrão, onde o núcleo é um hexágono cercado por adaptadores que o conectam a diferentes sistemas.

2. Estrutura Fundamental: O Hexágono e Suas Camadas

A arquitetura hexagonal organiza o sistema em três zonas principais:

Núcleo (Core/Application): Contém as entidades de domínio, regras de negócio e casos de uso. Esta camada não importa nenhuma biblioteca de infraestrutura (banco, HTTP, etc.).

Portas (Ports): Interfaces Java (ou contratos em outras linguagens) que definem como o núcleo interage com o exterior. Dividem-se em portas de entrada (inbound) e portas de saída (outbound).

Adaptadores (Adapters): Implementações concretas das portas. Adaptadores de entrada recebem requisições externas (REST, CLI, filas) e as traduzem para chamadas às portas de entrada. Adaptadores de saída implementam portas de saída para interagir com bancos, APIs externas, sistemas de arquivos, etc.

           ┌─────────────┐
           │  Adaptador   │
           │   REST API   │
           └──────┬──────┘
                  │
           ┌──────▼──────┐
           │  Porta de   │
           │   Entrada   │
           └──────┬──────┘
                  │
        ┌─────────▼─────────┐
        │    NÚCLEO (Core)   │
        │  Entidades + Casos │
        │     de Uso        │
        └─────────┬─────────┘
                  │
           ┌──────▼──────┐
           │  Porta de   │
           │   Saída     │
           └──────┬──────┘
                  │
           ┌──────▼──────┐
           │  Adaptador  │
           │  JPA/BD     │
           └─────────────┘

3. Portas de Entrada (Inbound Ports)

As portas de entrada são interfaces expostas pelo núcleo que definem os casos de uso disponíveis. Elas representam as operações que o sistema pode realizar, independentemente de como são invocadas.

public interface CreateOrderUseCase {
    Order createOrder(CreateOrderCommand command);
    Order createOrder(CreateOrderCommand command, PaymentMethod payment);
}

public record CreateOrderCommand(
    String customerId,
    List<OrderItem> items,
    Address shippingAddress
) {}

Observe que a interface não faz referência a HTTP, filas ou qualquer tecnologia específica. Ela simplesmente define o contrato do caso de uso. Um controlador REST, um listener de fila ou uma CLI podem todos invocar a mesma porta de entrada.

4. Portas de Saída (Outbound Ports)

As portas de saída são interfaces que o núcleo define para interagir com sistemas externos. O princípio da Inversão de Dependência (DIP) é aplicado: o núcleo declara o contrato, e a infraestrutura implementa.

public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(String orderId);
    List<Order> findByCustomerId(String customerId);
    void delete(String orderId);
}

public interface PaymentGateway {
    PaymentResult charge(PaymentRequest request);
    RefundResult refund(String transactionId);
}

O núcleo chama orderRepository.save(order) sem saber se a implementação usa JPA, JDBC, MongoDB ou um arquivo em memória. Isso permite trocar a tecnologia de persistência sem alterar uma linha do código de negócio.

5. Adaptadores de Entrada (Inbound Adapters)

Os adaptadores de entrada são responsáveis por receber requisições do mundo externo e traduzi-las para chamadas às portas de entrada. Um adaptador REST típico:

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final CreateOrderUseCase createOrderUseCase;

    public OrderController(CreateOrderUseCase createOrderUseCase) {
        this.createOrderUseCase = createOrderUseCase;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @RequestBody @Valid CreateOrderRequest request) {
        CreateOrderCommand command = request.toCommand();
        Order order = createOrderUseCase.createOrder(command);
        return ResponseEntity.status(201).body(OrderResponse.from(order));
    }
}

O adaptador converte o DTO HTTP (CreateOrderRequest) no objeto de domínio (CreateOrderCommand), chama o caso de uso e converte o resultado de volta para o formato de resposta. O núcleo permanece puro, sem anotações Spring ou referências a HTTP.

6. Adaptadores de Saída (Outbound Adapters)

Os adaptadores de saída implementam as portas definidas pelo núcleo. Um adaptador JPA para OrderRepository:

@Component
public class JpaOrderRepository implements OrderRepository {
    private final SpringDataOrderRepository springRepo;
    private final OrderMapper mapper;

    public JpaOrderRepository(SpringDataOrderRepository springRepo, 
                               OrderMapper mapper) {
        this.springRepo = springRepo;
        this.mapper = mapper;
    }

    @Override
    public Order save(Order order) {
        OrderEntity entity = mapper.toEntity(order);
        OrderEntity saved = springRepo.save(entity);
        return mapper.toDomain(saved);
    }

    @Override
    public Optional<Order> findById(String orderId) {
        return springRepo.findById(orderId)
                .map(mapper::toDomain);
    }

    @Override
    public List<Order> findByCustomerId(String customerId) {
        return springRepo.findByCustomerId(customerId)
                .stream()
                .map(mapper::toDomain)
                .toList();
    }

    @Override
    public void delete(String orderId) {
        springRepo.deleteById(orderId);
    }
}

O adaptador gerencia toda a complexidade de ORM, mapeamento entre entidades JPA e objetos de domínio, consultas SQL e transações. O núcleo nunca vê @Entity, @Table ou EntityManager.

7. Relação com Clean Architecture e Arquitetura em Camadas

A arquitetura hexagonal compartilha princípios fundamentais com a Clean Architecture de Robert C. Martin e com a Arquitetura em Camadas tradicional, mas com diferenças importantes:

Arquitetura em Camadas Tradicional:
- Camadas: Presentation → Business → Data
- Dependências fluem de cima para baixo
- Risco: vazamento de infraestrutura para a camada de negócio

Arquitetura Hexagonal:
- Núcleo isolado no centro
- Adaptadores na periferia
- Dependências sempre apontam para dentro (Regra de Dependência)

Clean Architecture:
- Usa círculos concêntricos em vez de hexágono
- Entidades e Casos de Uso no centro
- Adaptadores de interface e infraestrutura nos círculos externos

A correspondência é direta: as "Entities" e "Use Cases" da Clean Architecture equivalem ao núcleo hexagonal. Os "Interface Adapters" e "Frameworks & Drivers" equivalem aos adaptadores. Ambas garantem que o código de domínio nunca dependa de frameworks, bancos de dados ou interfaces de usuário.

8. Vantagens, Desvantagens e Quando Aplicar

Vantagens:
- Testabilidade: O núcleo pode ser testado com mocks das portas, sem infraestrutura real
- Independência de tecnologia: Trocar de banco de dados ou framework web não afeta o domínio
- Substituição de adaptadores: Múltiplos adaptadores para a mesma porta (REST + GraphQL + gRPC)
- Manutenibilidade: Regras de negócio concentradas e isoladas de detalhes técnicos

Desvantagens:
- Complexidade inicial: Mais interfaces, classes de mapeamento e configuração
- Overhead de interfaces: Para sistemas simples, a indireção pode ser excessiva
- Curva de aprendizado: Desenvolvedores precisam entender o padrão e a inversão de dependência

Cenários recomendados:
- Sistemas com múltiplas interfaces de entrada (REST, filas, CLI)
- Microsserviços que precisam trocar tecnologias ao longo do tempo
- Domínios complexos onde a pureza do código de negócio é crítica
- Projetos com vida útil longa e necessidade de evolução tecnológica

Cenários a evitar:
- CRUDs simples sem lógica de negócio significativa
- Protótipos rápidos onde velocidade supera manutenibilidade
- Equipes pequenas sem experiência em arquitetura orientada a domínio

A arquitetura hexagonal não é uma bala de prata, mas quando aplicada em contextos adequados, oferece um dos mais robustos modelos para manter o núcleo do negócio protegido das constantes mudanças tecnológicas que caracterizam o desenvolvimento de software moderno.

Referências