O que Arquitetura Hexagonal faz?
A arquitetura hexagonal (Ports and Adapters) mantém a lógica de negócios independente de frameworks, transporte e detalhes de persistência. O aplicativo central depende de portas abstratas, e os adaptadores implementam essas portas nas bordas.
Quando Usar
- Construir novos recursos onde a capacidade de manutenção e testabilidade a longo prazo são importantes.
- Refatorar código em camadas ou com muitos frameworks 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ócios.
Use este 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ócios e entidades/objetos de valor. Sem imports de framework.
- Casos de uso (camada de aplicação): Orquestram o comportamento do domínio e as 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 necessita (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: Única localização de fiação onde adaptadores concretos são vinculados a casos de uso.
As interfaces de porta de saída geralmente vivem na camada de aplicação (ou apenas no domínio quando a abstração é verdadeiramente de nível de domínio), enquanto os 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 de domínio (sem dependências de framework ou infraestrutura)
- Domínio -> nada externo
Como Funciona
Etapa 1: Modele um limite de caso de uso
Defina um único caso de uso com um DTO de entrada e saída claro. Mantenha detalhes de transporte (Express req, GraphQL context, wrappers de payload de job) fora deste limite.
Etapa 2: Defina as portas de saída primeiro
Identifique cada efeito colateral como uma porta:
- persistência (
UserRepositoryPort) - chamadas externas (
BillingGatewayPort) - transversais (
LoggerPort,ClockPort)
As portas devem modelar capacidades, não tecnologias.
Etapa 3: Implemente o caso de uso com orquestração pura
A classe/função do caso de uso recebe as portas via construtor/argumentos. Valida invariantes de nível de aplicação, coordena regras de domínio e retorna estruturas de dados simples.
Etapa 4: Construa adaptadores na borda
- Adaptador de entrada: converte a entrada do protocolo para a entrada do caso de uso.
- Adaptador de saída: mapeia os contratos da aplicação para APIs/ORM/query builders concretos.
- O mapeamento permanece nos adaptadores, não dentro dos casos de uso.
Etapa 5: Conecte tudo em uma raiz de composição
Instancie adaptadores e, em seguida, injete-os nos casos de uso. Mantenha essa fiação centralizada para evitar comportamento oculto de localizador de serviços.
Etapa 6: Teste por limite
- Teste unitário de casos de uso com portas falsas (fakes).
- Teste de integração de adaptadores com dependências de infraestrutura reais.
- Teste 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["Caso de Uso (Camada de Aplicação)"]
UseCase -->|"usa"| OutboundPort["Porta de Saída (Interface)"]
OutboundAdapter["Adaptador de Saída"] -->|"implementa"| OutboundPort
OutboundAdapter --> ExternalSystem["BD/API/Fila"]
UseCase --> DomainModel["Modelo de Domínio"]
Layout de Módulo Sugerido
Use organização por funcionalidade com limites explícitos:
src/
features/
orders/
domain/
Order.ts
OrderPolicy.ts
application/
ports/
inbound/
CreateOrder.ts
outbound/
OrderRepositoryPort.ts
PaymentGatewayPort.ts
use-cases/
CreateOrderUseCase.ts
adapters/
inbound/
http/
createOrderRoute.ts
outbound/
postgres/
PostgresOrderRepository.ts
stripe/
StripePaymentGateway.ts
composition/
ordersContainer.ts
Exemplo em TypeScript
Definições de Porta
export interface OrderRepositoryPort {
save(order: Order): Promise<void>;
findById(orderId: string): Promise<Order | null>;
}
export interface PaymentGatewayPort {
authorize(input: { orderId: string; amountCents: number }): Promise<{ authorizationId: string }>;
}
Caso de Uso
type CreateOrderInput = {
orderId: string;
amountCents: number;
};
type CreateOrderOutput = {
orderId: string;
authorizationId: string;
};
export class CreateOrderUseCase {
constructor(
private readonly orderRepository: OrderRepositoryPort,
private readonly paymentGateway: PaymentGatewayPort
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
const order = Order.create({ id: input.orderId, amountCents: input.amountCents });
const auth = await this.paymentGateway.authorize({
orderId: order.id,
amountCents: order.amountCents,
});
// markAuthorized retorna uma nova instância de Order; não sofre mutação no local.
const authorizedOrder = order.markAuthorized(auth.authorizationId);
await this.orderRepository.save(authorizedOrder);
return {
orderId: order.id,
authorizationId: auth.authorizationId,
};
}
}
Adaptador de Saída
export class PostgresOrderRepository implements OrderRepositoryPort {
constructor(private readonly db: SqlClient) {}
async save(order: Order): Promise<void> {
await this.db.query(
"insert into orders (id, amount_cents, status, authorization_id) values ($1, $2, $3, $4)",
[order.id, order.amountCents, order.status, order.authorizationId]
);
}
async findById(orderId: string): Promise<Order | null> {
const row = await this.db.oneOrNone("select * from orders where id = $1", [orderId]);
return row ? Order.rehydrate(row) : null;
}
}
Raiz de Composição
export const buildCreateOrderUseCase = (deps: { db: SqlClient; stripe: StripeClient }) => {
const orderRepository = new PostgresOrderRepository(deps.db);
const paymentGateway = new StripePaymentGateway(deps.stripe);
return new CreateOrderUseCase(orderRepository, paymentGateway);
};
Mapeamento Multi-Linguagem
Use as mesmas regras de limite em diferentes ecossistemas; apenas a sintaxe e o estilo de fiação mudam.
- TypeScript/JavaScript
- Portas:
application/ports/*como interfaces/types. - Casos de uso: classes/funções com injeção por construtor/argumento.
- Adaptadores:
adapters/inbound/*,adapters/outbound/*. - Composição: módulo factory/container explícito (sem globais ocultas).
- 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 fiação manual; mantenha a fiação fora das classes de domínio/caso de uso.
- Pacotes:
- Kotlin
- Módulos/pacotes espelham a divisão do Java (
domain,application.port,application.usecase,adapter). - Portas: interfaces Kotlin.
- Casos de uso: classes com injeção por 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 do Java (
- Go
- Pacotes:
internal/<feature>/domain,application,ports,adapters/inbound,adapters/outbound. - Portas: interfaces pequenas de propriedade do pacote da aplicação consumidora.
- Casos de uso: structs com campos de interface mais construtores
New...explícitos. - Composição: fie em
cmd/<app>/main.go(ou pacote de fiação dedicado), mantenha construtores explícitos.
- Pacotes:
Anti-Padrõ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.
- Permitir que adaptadores chamem uns aos outros diretamente em vez de fluir através das portas do caso de uso.
- Espalhar a fiação de dependências por muitos arquivos com singletons globais ocultos.
Playbook de Migração
- Escolha uma fatia vertical (um endpoint/job) com dor de mudança frequente.
- 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 ao novo caso de uso.
- Adicione testes em torno do novo limite (unitário + integração de adaptador).
- Repita fatia por fatia; evite reescritas completas.
Refatoração de Sistemas Existentes
- Abordagem Strangler: mantenha os endpoints atuais, direcione um caso de uso por vez através de novas portas/adaptadores.
- Sem reescritas big-bang: migre por fatia de funcionalidade e preserve o comportamento com testes de caracterização.
- Facade primeiro: envolva serviços legados por trás de portas de saída antes de substituir os internos.
- Congelamento da composição: centralize a fiação desde o início 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 explosão primeiro.
- Caminho de rollback: mantenha um toggle reversível ou chave de rota por fatia migrada até que o comportamento em produção seja verificado.
Orientação de Teste (Mesmos Limites Hexagonais)
- Testes de domínio: teste entidades/objetos de valor como regras de negócios puras (sem mocks, sem configuração de framework).
- Testes unitários de caso de uso: teste a orquestração com fakes/stubs para portas de saída; afirme resultados de negócios e interações de porta.
- 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 (HTTP/CLI/payload de fila para entrada do caso de uso e 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 esquema/consulta, retentativas e timeouts.
- Testes ponta a ponta: cubra jornadas críticas do usuário através de adaptador de entrada -> caso de uso -> adaptador de saída.
- Segurança de 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
- As 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.
- A 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 sofrer mutação em 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.