O que Arquitetura Hexagonal faz?
A arquitetura hexagonal (Ports e Adapters) mantém a lógica de negócio independente de frameworks, transporte e detalhes de persistência. O aplicativo principal depende de portas abstratas, e os adaptadores implementam essas portas nas bordas.
Quando Usar
- Construir novos recursos onde manutenibilidade e testabilidade a longo prazo são importantes.
- Refatorar código em camadas ou com forte dependência de framework onde a lógica de domínio está misturada com preocupações de E/S.
- Suportar múltiplas interfaces para o mesmo caso de uso (HTTP, CLI, workers de fila, cron jobs).
- Substituir infraestrutura (banco de dados, APIs externas, barramento de mensagens) sem reescrever regras de negócio.
Use esta skill quando a solicitação envolver limites, design centrado em domínio, refatoração de serviços fortemente acoplados ou desacoplamento da lógica de aplicação de bibliotecas específicas.
Conceitos Principais
- Modelo de domínio: Regras de negócio e entidades/objetos de valor. Sem importações de framework.
- Casos de uso (camada de aplicação): Orquestrar comportamento de domínio e etapas do fluxo de trabalho.
- Portas de entrada: Contratos que descrevem o que a aplicação pode fazer (interfaces de comandos/consultas/casos de uso).
- Portas de saída: Contratos para dependências que a aplicação precisa (repositórios, gateways, publicadores de eventos, relógio, UUID, etc.).
- Adaptadores: Implementações de infraestrutura e entrega das portas (controladores HTTP, repositórios de banco de dados, consumidores de fila, wrappers de SDK).
- Raiz de composição: Único local de montagem onde adaptadores concretos são vinculados a casos de uso.
Interfaces de portas de saída geralmente residem na camada de aplicação (ou apenas no domínio quando a abstração é verdadeiramente de nível de domínio), enquanto adaptadores de infraestrutura as implementam.
A direção da dependência é sempre para dentro:
- Adaptadores -> aplicação/domínio
- Aplicação -> interfaces de porta (contratos de entrada/saída)
- Domínio -> abstrações apenas do domínio (sem dependências de framework ou infraestrutura)
- Domínio -> nada externo
Como Funciona
Passo 1: Modele um limite de caso de uso
Defina um único caso de uso com um DTO de entrada e saída claros. Mantenha detalhes de transporte (Express req, GraphQL context, wrappers de payload de job) fora deste limite.
Passo 2: Defina primeiro as portas de saída
Identifique cada efeito colateral como uma porta:
- persistência (
UserRepositoryPort) - chamadas externas (
BillingGatewayPort) - transversais (
LoggerPort,ClockPort)
As portas devem modelar capacidades, não tecnologias.
Passo 3: Implemente o caso de uso com orquestração pura
A classe/função de caso de uso recebe portas via construtor/argumentos. Ela valida invariantes de nível de aplicação, coordena regras de domínio e retorna estruturas de dados simples.
Passo 4: Construa adaptadores na borda
- Adaptador de entrada converte entrada do protocolo em entrada do caso de uso.
- Adaptador de saída mapeia contratos da aplicação para APIs/ORM/query builders concretos.
- O mapeamento permanece nos adaptadores, não dentro dos casos de uso.
Passo 5: Conecte tudo em uma raiz de composição
Instancie adaptadores e injete-os nos casos de uso. Mantenha essa montagem centralizada para evitar comportamento oculto de localizador de serviços.
Passo 6: Teste por limite
- Teste unitário de casos de uso com portas simuladas (fakes).
- Teste de integração de adaptadores com dependências de infraestrutura reais.
- Teste ponta a ponta (E2E) de fluxos voltados ao usuário através de adaptadores de entrada.
Diagrama de Arquitetura
flowchart LR
Client["Cliente (HTTP/CLI/Worker)"] --> InboundAdapter["Adaptador de Entrada"]
InboundAdapter -->|"chama"| UseCase["CasoDeUso (Camada de Aplicação)"]
UseCase -->|"usa"| OutboundPort["PortaDeSaída (Interface)"]
OutboundAdapter["Adaptador de Saída"] -->|"implementa"| OutboundPort
OutboundAdapter --> ExternalSystem["BD/API/Fila"]
UseCase --> DomainModel["ModeloDeDomínio"]
Layout de Módulo Sugerido
Use organização por funcionalidade com limites explícitos:
src/
features/
pedidos/
domain/
Pedido.ts
PoliticaDePedido.ts
application/
ports/
inbound/
CriarPedido.ts
outbound/
PortaRepositorioPedido.ts
PortaGatewayPagamento.ts
use-cases/
CasoDeUsoCriarPedido.ts
adapters/
inbound/
http/
rotaCriarPedido.ts
outbound/
postgres/
RepositorioPedidoPostgres.ts
stripe/
GatewayPagamentoStripe.ts
composition/
containerPedidos.ts
Exemplo em TypeScript
Definições de Porta
export interface PortaRepositorioPedido {
salvar(pedido: Pedido): Promise<void>;
encontrarPorId(id: string): Promise<Pedido | null>;
}
export interface PortaGatewayPagamento {
autorizar(input: { idPedido: string; valorCentavos: number }): Promise<{ idAutorizacao: string }>;
}
Caso de Uso
type EntradaCriarPedido = {
idPedido: string;
valorCentavos: number;
};
type SaidaCriarPedido = {
idPedido: string;
idAutorizacao: string;
};
export class CasoDeUsoCriarPedido {
constructor(
private readonly repositorioPedido: PortaRepositorioPedido,
private readonly gatewayPagamento: PortaGatewayPagamento
) {}
async executar(input: EntradaCriarPedido): Promise<SaidaCriarPedido> {
const pedido = Pedido.criar({ id: input.idPedido, valorCentavos: input.valorCentavos });
const auth = await this.gatewayPagamento.autorizar({
idPedido: pedido.id,
valorCentavos: pedido.valorCentavos,
});
// marcarAutorizado retorna uma nova instância de Pedido; não modifica no local.
const pedidoAutorizado = pedido.marcarAutorizado(auth.idAutorizacao);
await this.repositorioPedido.salvar(pedidoAutorizado);
return {
idPedido: pedido.id,
idAutorizacao: auth.idAutorizacao,
};
}
}
Adaptador de Saída
export class RepositorioPedidoPostgres implements PortaRepositorioPedido {
constructor(private readonly db: SqlClient) {}
async salvar(pedido: Pedido): Promise<void> {
await this.db.query(
"insert into pedidos (id, valor_centavos, status, id_autorizacao) values ($1, $2, $3, $4)",
[pedido.id, pedido.valorCentavos, pedido.status, pedido.idAutorizacao]
);
}
async encontrarPorId(idPedido: string): Promise<Pedido | null> {
const linha = await this.db.oneOrNone("select * from pedidos where id = $1", [idPedido]);
return linha ? Pedido.reidratar(linha) : null;
}
}
Raiz de Composição
export const construirCasoDeUsoCriarPedido = (deps: { db: SqlClient; stripe: StripeClient }) => {
const repositorioPedido = new RepositorioPedidoPostgres(deps.db);
const gatewayPagamento = new GatewayPagamentoStripe(deps.stripe);
return new CasoDeUsoCriarPedido(repositorioPedido, gatewayPagamento);
};
Mapeamento Multi-Linguagem
Use as mesmas regras de limite em todos os ecossistemas; apenas a sintaxe e o estilo de montagem mudam.
- TypeScript/JavaScript
- Portas:
application/ports/*como interfaces/tipos. - Casos de uso: classes/funções com injeção via construtor/argumento.
- Adaptadores:
adapters/inbound/*,adapters/outbound/*. - Composição: módulo de fábrica/container explícito (sem globais ocultos).
- Portas:
- Java
- Pacotes:
domain,application.port.in,application.port.out,application.usecase,adapter.in,adapter.out. - Portas: interfaces em
application.port.*. - Casos de uso: classes simples (Spring
@Serviceé opcional, não obrigatório). - Composição: configuração Spring ou classe de montagem manual; mantenha a montagem fora das classes de domínio/caso de uso.
- Pacotes:
- Kotlin
- Módulos/pacotes espelham a divisão Java (
domain,application.port,application.usecase,adapter). - Portas: interfaces Kotlin.
- Casos de uso: classes com injeção via construtor (Koin/Dagger/Spring/manual).
- Composição: definições de módulo ou funções de composição dedicadas; evite padrões de localizador de serviços.
- Módulos/pacotes espelham a divisão Java (
- Go
- Pacotes:
internal/<funcionalidade>/domain,application,ports,adapters/inbound,adapters/outbound. - Portas: interfaces pequenas de propriedade do pacote de aplicação consumidor.
- Casos de uso: structs com campos de interface e construtores explícitos
New.... - Composição: montagem em
cmd/<app>/main.go(ou pacote de montagem dedicado), mantenha construtores explícitos.
- Pacotes:
Antipadrões a Evitar
- Entidades de domínio importando modelos ORM, tipos de framework web ou clientes SDK.
- Casos de uso lendo diretamente de
req,resou metadados de fila. - Retornar linhas de banco de dados diretamente dos casos de uso sem mapeamento de domínio/aplicação.
- Deixar adaptadores chamarem uns aos outros diretamente em vez de fluir através de portas de caso de uso.
- Espalhar a montagem de dependências por muitos arquivos com singletons globais ocultos.
Roteiro de Migração
- Escolha uma fatia vertical (único endpoint/job) com dor frequente de alteração.
- Extraia um limite de caso de uso com tipos de entrada/saída explícitos.
- Introduza portas de saída em torno de chamadas de infraestrutura existentes.
- Mova a lógica de orquestração de controladores/serviços para o caso de uso.
- Mantenha os adaptadores antigos, mas faça-os delegar para o novo caso de uso.
- Adicione testes em torno do novo limite (testes de unidade + integração de adaptador).
- Repita fatia por fatia; evite reescritas completas.
Refatoração de Sistemas Existentes
- Abordagem estranguladora: mantenha os endpoints atuais, roteie um caso de uso por vez através de novas portas/adaptadores.
- Sem reescritas de big-bang: migre por fatia de funcionalidade e preserve o comportamento com testes de caracterização.
- Fachada primeiro: envolva serviços legados por trás de portas de saída antes de substituir os internos.
- Congelamento de composição: centralize a montagem cedo para que novas dependências não vazem para as camadas de domínio/caso de uso.
- Regra de seleção de fatia: priorize fluxos de alta rotatividade e baixo raio de impacto primeiro.
- Caminho de reversão: mantenha um toggle reversível ou chave de rota por fatia migrada até que o comportamento em produção seja verificado.
Orientações de Teste (Mesmos Limites Hexagonais)
- Testes de domínio: teste entidades/objetos de valor como regras de negócio puras (sem mocks, sem configuração de framework).
- Testes de unidade de caso de uso: teste a orquestração com fakes/stubs para portas de saída; afirme resultados de negócio e interações com portas.
- Testes de contrato de adaptador de saída: defina suítes de contrato compartilhadas no nível da porta e execute-as contra cada implementação de adaptador.
- Testes de adaptador de entrada: verifique o mapeamento de protocolo (payload HTTP/CLI/fila para entrada do caso de uso e mapeamento de saída/erro de volta para o protocolo).
- Testes de integração de adaptador: execute contra infraestrutura real (BD/API/fila) para serialização, comportamento de schema/consulta, retentativas e timeouts.
- Testes ponta a ponta: cubra jornadas críticas do usuário através do adaptador de entrada -> caso de uso -> adaptador de saída.
- Segurança para refatoração: adicione testes de caracterização antes da extração; mantenha-os até que o comportamento do novo limite esteja estável e equivalente.
Lista de Verificação de Melhores Práticas
- Camadas de domínio e caso de uso importam apenas tipos internos e portas.
- Toda dependência externa é representada por uma porta de saída.
- Validação ocorre nos limites (adaptador de entrada + invariantes do caso de uso).
- Use transformações imutáveis (retorne novos valores/entidades em vez de mutar estado compartilhado).
- Erros são traduzidos entre limites (erros de infraestrutura -> erros de aplicação/domínio).
- A raiz de composição é explícita e fácil de auditar.
- Casos de uso são testáveis com fakes simples em memória para portas.
- A refatoração começa a partir de uma fatia vertical com testes de preservação de comportamento.
- Especificidades de linguagem/framework permanecem nos adaptadores, nunca nas regras de domínio.