Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

03 - Hexagonal (Ports & Adapters)

video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

A arquitetura hexagonal, descrita por Alistair Cockburn, também conhecida como portas e adaptadores, isola a lógica de aplicação em um núcleo central que não depende de nenhuma tecnologia concreta. Toda interação com o mundo externo, seja receber uma requisição ou gravar um dado, acontece por meio de portas, que são interfaces definidas pelo próprio núcleo, e adaptadores, que implementam essas portas com tecnologias específicas. O nome hexagonal expressa a ideia central: não existe cima e baixo, como nas camadas, existem lados equivalentes ao redor do núcleo, cada um conectado por uma porta. Em produção, o estilo vale pelo que ele protege: o núcleo compila sem framework, roda em testes sem infraestrutura e sobrevive a trocas de banco, protocolo e provedor.

Conceito: o Domínio no Centro

O núcleo contém duas coisas apenas: o modelo de domínio, com entidades, regras e invariâncias, e os casos de uso, que orquestram essas regras para cumprir intenções do negócio. Nada de HTTP, SQL, filas ou SDK de nuvem entra nessa região; se um arquivo do núcleo importa biblioteca de infraestrutura, a fronteira já foi rompida. As dependências apontam sempre para dentro: o mundo depende do núcleo, e o núcleo não depende de nada externo além de linguagem e bibliotecas puras.

Essa inversão é o que separa hexagonal de uma simples organização de pastas. Em uma arquitetura em camadas tradicional, a camada de negócio importa a camada de persistência concreta; aqui, a persistência concreta importa a interface que o núcleo definiu, e o runtime conecta uma à outra por injeção de dependência. O resultado prático é que o núcleo é testável isolado, substituível de infraestrutura e legível sem pular entre contextos tecnológicos.

A divisão de território fica explícita ao listar o que entra em cada região:

  • Dentro do núcleo, entidades e agregados: modelos com identidade, estado e comportamento que preservam as regras do negócio.
  • Dentro do núcleo, casos de uso: orquestrações com nomes de intenção, como AprovarReserva, incluindo validação e efeitos declarados.
  • Dentro do núcleo, portas: todas as interfaces, primárias e secundárias, escritas no vocabulário do domínio.
  • Fora do núcleo, transporte: HTTP, gRPC, consumidores de fila e disparadores agendados.
  • Fora do núcleo, persistência e integrações: bancos, caches, filas, gateways de pagamento e APIs de terceiros.
  • Fora do núcleo, bootstrap: leitura de configuração e composição que instancia adaptadores e injeta nas portas.

Portas Primárias e Secundárias

As portas se dividem em dois grupos com papéis opostos. Portas primárias, ou driving, expõem intenções do núcleo: são as interfaces de caso de uso, como RegistrarPedido ou AprovarReserva, que o mundo chama para fazer o sistema trabalhar. Portas secundárias, ou driven, declaram necessidades do núcleo: interfaces de repositório, gateway de pagamento, publicador de eventos, que o núcleo chama para obter ou enviar informação. A distinção importa porque a direção da adaptação muda: adaptadores primários chamam o núcleo; adaptadores secundários são chamados por ele e implementam o que ele precisa.

Em ambos os casos, o núcleo é dono da interface, e é isso que mantém a fronteira. Quando a porta secundária de repositório descreve operações em vocabulário do domínio, buscarPedidosVencidos, a tecnologia de armazenamento se torna detalhe; quando a porta espelha o ORM, a fronteira é nominal e o acoplamento continua real.

Um teste rápido de sanidade para qualquer porta: reescreva a assinatura em voz alta como frase do negócio. Se a frase faz sentido, a porta está certa; se a frase fala de tabela, payload ou endpoint, ela é reflexo de infraestrutura e precisa ser redesenhada antes que a contaminação chegue às regras.

Adaptadores e Inversão de Dependência

Adaptadores são classes finas de tradução. Um adaptador primário recebe HTTP, JSON de fila ou disparo agendado e chama a porta primária correspondente com objetos do núcleo; um adaptador secundário recebe chamadas da porta secundária e conversa com PostgreSQL, Redis ou API externa. A regra de tradução é simétrica: nada de objeto de transporte atravessa a fronteira em nenhuma direção, e cada lado mantém vocabulário próprio. O código a seguir mostra a estrutura típica:

// Porta secundária: o núcleo declara o que precisa, sem saber quem entrega
public interface RepositorioPedidos {
    Optional<Pedido> buscarPorId(UUID id);
    void salvar(Pedido pedido);
}

// Porta primária: caso de uso exposto ao exterior, em vocabulário do domínio
public interface RegistrarPedido {
    Recibo registrar(ComandoNovoPedido comando);
}

// Implementação do caso de uso no núcleo: pura, sem framework
class RegistrarPedidoImpl implements RegistrarPedido {
    // recebe a porta secundária por injeção, nunca a implementação concreta
    // aplica regras: limite de crédito, itens mínimos, cálculo de frete
}

// Adaptador PostgreSQL: implementa a porta secundária com a tecnologia real
class RepositorioPedidosPostgres implements RepositorioPedidos {
    // traduz entre a entidade do domínio e o esquema relacional
}

// Adaptador de teste: dublê em memória usado nos testes do núcleo
class RepositorioPedidosMemoria implements RepositorioPedidos {
    // guarda objetos em estrutura de dados simples, sem banco
}

// Adaptador REST: adaptador primário que recebe HTTP e chama a porta primária
class ControllerPedidos {
    // mapeia JSON para ComandoNovoPedido e devolve Recibo como resposta HTTP
}

A inversão de dependência é o mecanismo que sustenta a estrutura: como o núcleo declara as interfaces, a dependência de compilação aponta para dentro, mesmo que o fluxo de execução entre e saia. A montagem acontece na borda, em um módulo de composição que instancia adaptadores e injeta nas portas, geralmente no bootstrap da aplicação.

A organização física dos arquivos segue a mesma direção da dependência:

aplicacao/
  nucleo/
    dominio/        # entidades, agregados, eventos e regras de invariância
    casodeuso/      # portas primárias e suas implementações
    porta/          # interfaces secundárias declaradas pelo núcleo
  adaptadores/
    entrada/
      rest/         # adaptador primário HTTP
      fila/         # adaptador primário de consumidor de mensagens
    saida/
      postgres/     # adaptador secundário de persistência
      pagamentos/   # adaptador secundário de gateway externo
  bootstrap/        # composição: instancia adaptadores e injeta nas portas

Nessa disposição, a verificação é visual e automatizável: nenhum arquivo em nucleo importa nada de adaptadores, e o pipeline pode defender essa regra a cada build. O módulo bootstrap é o único que enxerga os dois lados, e existe justamente para concentrar a montagem.

Aplicação Prática: Substituição de Infraestrutura

O benefício aparece quando a produção muda de direção. Trocar PostgreSQL por DynamoDB em um módulo de sessões vira escrever um novo adaptador secundário, com testes de contrato, sem tocar em regra nenhuma. Expor o mesmo caso de uso por REST, gRPC e consumidor de fila é criar três adaptadores primários sobre a mesma porta. Executar o núcleo inteiro em testes de unidade, com repositórios em memória, elimina a classe de testes lentos que depende de ambiente. Em migrações legadas, o estilo permite colocar adaptadores novos sobre um núcleo antigo, modernizando o transporte sem reescrever regras.

Cenários recorrentes em que a fronteira paga o custo:

  • Multi canal de verdade: aplicativo, portal e integração de parceiro consumindo os mesmos casos de uso com regras idênticas.
  • Troca de provedor por custo ou compliance: mover armazenamento ou fila entre provedores adaptando só a camada de infraestrutura.
  • Processamento adicional: um job em lote entrando por adaptador próprio, sem duplicar regras que já existem no caso de uso.
  • Experimentação de tecnologia: testar um novo banco em paralelo, mantendo os dois adaptadores vivos atrás da mesma porta.

O custo honesto é a cerimônia de tradução: mais interfaces, mais mapeamento, mais arquivos. Para um CRUD puro de baixa regra, esse preço raramente se paga; para domínios com regras densas e infraestrutura que muda, o investimento retorna em meses.

Relação com DDD e Arquiteturas Derivadas

Hexagonal é o recipiente natural para o DDD tático: agregados e entidades moram no núcleo, e a porta secundária de repositório é a interface prevista por Eric Evans para persistir agregados, preservando invariâncias dentro de transação única. A linguagem ubíqua aparece nas assinaturas das portas, e o contexto delimitado fica fisicamente isolado de detalhes. Arquiteturas como cebola e clean são descendentes diretas, que acrescentam nomenclatura e anéis, mas preservam o mesmo princípio: dependência apontando para o centro. Na prática, times que aplicam DDD sem hexagonal costumam ver o modelo contaminado por ORM e HTTP, e o valor da linguagem ubíqua se perde nas bordas.

A combinação também esclarece escopo de transação: o caso de uso é a fronteira transacional, e o repositório persiste o agregado inteiro em uma passagem. Requisições que precisam tocar dois agregados pedem repensar o desenho do modelo, e não criar transações que atravessam portas várias vezes, o que esconde consistência parcial em produção.

Testes com Dublês

A estrutura rende uma pirâmide de testes natural. O núcleo é testado em unidade com adaptadores falsos em memória atrás das portas secundárias, rápido o suficiente para milhares de casos por segundo. Cada adaptador real recebe testes de integração próprios, verificando sua parte do contrato contra a tecnologia verdadeira. Testes de contrato garantem que o falso em memória e o adaptador real obedecem à mesma semântica da porta, inclusive em casos de borda como duplicidade e ordem de resultados; sem isso, o falso mente, e os testes de núcleo passam enquanto produção falha.

  • Falso em memória: implementação simples da porta que guarda estado em estrutura de dados, priorizada em vez de mock, porque comporta cenários de múltiplas chamadas.
  • Teste de contrato compartilhado: suíte executada contra o falso e contra o adaptador real, garantindo comportamento equivalente.
  • Mock restrito a interações: uso de mock limitado a verificar que o núcleo chamou a porta em situações de erro, e não como repositório de estado.
  • Integração por adaptador: cada tecnologia real, banco, fila, API externa, testada isoladamente, mantendo a falha localizada.

Erros Comuns e Boas Práticas

Elemento Papel correto Erro recorrente
Porta primária Expõe caso de uso em vocabulário de domínio Assinatura espelhando DTO de HTTP
Porta secundária Declara necessidade do núcleo Interface copiada do ORM ou do cliente de SDK
Adaptador primário Traduz transporte para comando Regra de negócio decidida no controller
Adaptador secundário Implementa tecnologia da porta Lógica de domínio embutida na consulta
Núcleo Regras puras, sem dependência externa Import de framework "só para o decorator"

Boas práticas que preservam o estilo ao longo do tempo:

  • Fale o idioma do domínio nas portas: assinaturas com termos de negócio expõem fronteira rompida apenas lendo o código.
  • Mantenha um módulo de composição único: a montagem de adaptadores fica em um lugar só, e a dependência concreta não se espalha.
  • Proíba import de infraestrutura no núcleo por teste automatizado: análise de dependência no pipeline defende a fronteira sem vigilância humana.
  • Escreva contratos para as portas secundárias: o falso em memória só é confiável se verificada a igualdade de semântica com o adaptador real.
  • Evite portas de uma chamada só: interfaces muito finas multiplicam cerimônia; agrupe operações coesas por intenção do domínio.
  • Traduza erros na fronteira: exceções de infraestrutura viram erros do domínio antes de entrar no núcleo, mantendo o vocabulário coerente.
  • Comece pela porta, não pelo adaptador: desenhar a interface pelo lado do domínio impede que o desenho do banco dite as regras.
  • Meça a pureza do núcleo no tempo de build: se o núcleo não compila sem dependências externas declaradas, a fronteira já está furada.