Software · Arquitetura de Software
Arquitetura Hexagonal: Desacoplamento através de Portas e Adaptadores
Entenda o padrão Ports and Adapters, sua divisão simétrica entre núcleo e mundo externo e como ele simplifica testes automatizados e evolução tecnológica.
Proposta originalmente por Alistair Cockburn com a denominação de Ports and Adapters, a Arquitetura Hexagonal surgiu para combater a assimetria com que o software tradicionalmente tratava seus componentes externos. Frequentemente, a lógica da aplicação tornava-se refém de controladores de tela, mecanismos de roteamento HTTP ou detalhes do banco de dados, tornando inviável executar o sistema por meio de scripts de console, automações em lote ou testes automatizados sem levantar todo o ecossistema de infraestrutura.
O hexágono representa uma metáfora visual de fronteira: em seu interior reside a lógica pura da aplicação, cercada por múltiplas faces (portas) através das quais agentes externos se conectam mediante adaptadores especializados.
A Simetria entre o Lado de Dentro e o Lado de Fora
A premissa central da Arquitetura Hexagonal divide o sistema em duas metades lógicas: o Núcleo (Lado de Dentro) e a Infraestrutura (Lado de Fora). O núcleo desconhece completamente a tecnologia que o cerca, enquanto a infraestrutura existe para atender às necessidades contratuais estabelecidas por ele.
graph TD
subgraph Driving["Lado Condutor (Driving / Inbound)"]
User["Usuário Web / REST"]
Test["Testes Automatizados"]
CLI["Comandos de Console / CLI"]
end
subgraph AdaptersIn["Adaptadores Inbound"]
RestAdapter["Controller REST / Web"]
CliAdapter["Handler CLI"]
end
User --> RestAdapter
Test --> RestAdapter
CLI --> CliAdapter
subgraph Core["NÚCLEO DO DOMÍNIO (Lado de Dentro)"]
direction TB
PortIn["<b>Porta Inbound</b><br/><i>(Interface de Caso de Uso)</i>"]
Domain["<b>Entidades & Regras de Negócio</b>"]
PortOut["<b>Porta Outbound</b><br/><i>(Contrato de Saída)</i>"]
PortIn --> Domain
Domain --> PortOut
end
RestAdapter --> PortIn
CliAdapter --> PortIn
subgraph AdaptersOut["Adaptadores Outbound (Driven)"]
PgAdapter["Adaptador Repositório"]
KafkaAdapter["Adaptador Mensageria"]
end
subgraph Driven["Lado Conduzido (Driven / Outbound)"]
DB[("PostgreSQL")]
Kafka[["Cluster Kafka"]]
end
PortOut --> PgAdapter
PortOut --> KafkaAdapter
PgAdapter --> DB
KafkaAdapter --> Kafka
classDef side fill:#0f172a,stroke:#64748b,stroke-width:1px,color:#f8fafc;
classDef adapter fill:#132338,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
classDef core fill:#0f2724,stroke:#34d399,stroke-width:2px,color:#f8fafc;
classDef port fill:#1e293b,stroke:#fbbf24,stroke-width:1px,color:#f8fafc;
class User,Test,CLI,DB,Kafka side;
class RestAdapter,CliAdapter,PgAdapter,KafkaAdapter adapter;
class Domain core;
class PortIn,PortOut port;
Essa separação é operacionalizada por dois tipos de portas:
1. Portas Condutoras ou Primárias (Driving / Inbound Ports)
Representam as interfaces que expõem o que a aplicação é capaz de fazer. Elas definem os pontos de entrada para quem deseja utilizar as regras de negócio, funcionando como uma API pública de casos de uso. O adaptador condutor (por exemplo, um controlador REST ou um comando CLI) invoca essa porta para iniciar uma ação no núcleo.
2. Portas Conduzidas ou Secundárias (Driven / Outbound Ports)
Representam os contratos que o núcleo precisa que o mundo externo satisfaça para concluir seu trabalho, como persistir registros, emitir mensagens para corretores assíncronos ou consultar cotações em serviços terceiros. O núcleo define a assinatura do método que necessita, e o adaptador conduzido (um repositório que conversa com PostgreSQL ou um cliente HTTP externo) implementa a porta.
Adaptadores como Tradutores de Protocolos
Os adaptadores realizam a conversão entre o vocabulário do mundo externo e a semântica interna do domínio. Se uma requisição HTTP chega codificada em JSON com nomes de campos específicos da web, o adaptador de entrada valida o payload, instancia os tipos de dados exigidos pela porta primária e despacha a chamada.
Da mesma forma, quando o domínio decide persistir um agregado, ele entrega o objeto à porta secundária; o adaptador secundário é quem serializa esses dados para o formato relacional, gerencia transações com a biblioteca do banco e traduz exceções de conexão de rede em respostas padronizadas.
// Porta Secundária (Contrato de Saída definido pelo Núcleo)
export interface NotificationPort {
notifyCustomer(customerId: string, message: string): Promise<void>;
}
// Adaptador Conduzido Concreto (Implementação de Infraestrutura)
export class TwilioSmsAdapter implements NotificationPort {
constructor(private readonly apiKey: string) {}
async notifyCustomer(customerId: string, message: string): Promise<void> {
// Comunicação com a API externa da Twilio sem contaminar o domínio
await fetch('https://api.twilio.com/messages', {
method: 'POST',
headers: { Authorization: `Bearer ${this.apiKey}` },
body: JSON.stringify({ to: customerId, text: message })
});
}
}
Caso a empresa decida substituir a Twilio por mensageria interna via RabbitMQ, nenhuma linha de código do núcleo do domínio é modificada: basta implementar um novo adaptador que satisfaça a mesma NotificationPort.
Diferenças entre Arquitetura Hexagonal e Clean Architecture
Embora compartilhem a intenção de proteger a lógica de negócio por meio de inversão de dependências, os dois padrões apresentam ênfases complementares:
| Aspecto | Arquitetura Hexagonal | Clean Architecture |
|---|---|---|
| Origem e Foco | Foco na simetria de portas de entrada e saída (Alistair Cockburn, 2005). | Foco na hierarquia de círculos de abstração e políticas de dependência (Robert C. Martin, 2012). |
| Estrutura Interna | Não prescreve como organizar o núcleo, concentrando-se na fronteira com adaptadores. | Define divisões explícitas dentro do núcleo: Entidades corporativas versus Casos de Uso. |
| Interação com Dispositivos | Trata banco de dados e usuários com o mesmo modelo de portas simétricas. | Posiciona o banco de dados e a interface na camada periférica de Frameworks e Drivers. |
Benefícios Práticos na Engenharia de Software
A adoção de portas e adaptadores transforma o ciclo de testes da equipe. Em vez de orquestrar bancos de dados transitórios ou instâncias de simulação externa para cada execução de teste, os desenvolvedores criam adaptadores em memória para as portas secundárias. Os testes unitários validam todas as regras de negócio em milissegundos, com isolamento total e determinismo absoluto.
Além disso, a evolução tecnológica da empresa ganha flexibilidade real: sistemas podem nascer com armazenamento simples em arquivos JSON ou SQLite e migrar posteriormente para bancos distribuídos sem que uma única regra de negócio sofra alterações.