03 - Hexagonal (Ports & Adapters)
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.