Como um console de videogame
O console (domínio) não sabe se você usa controle com fio, sem fio ou volante — ele só expõe a entrada padrão (porta). Qualquer acessório que implemente aquele plugue (adaptador) funciona. Trocar de controle nunca exige abrir o console.
O problema
Regras de negócio grudadas no framework web, no ORM ou em SDKs fazem o domínio depender de detalhes de infraestrutura. Trocar banco, broker ou camada HTTP obriga a reescrever lógica que não tem nada a ver com essas tecnologias.
O acoplamento também sabota os testes: para exercitar uma regra simples é preciso subir banco, servidor e filas — o teste de unidade vira integração lenta e frágil.
O sintoma clássico é o 'domínio anêmico invertido': as entidades são structs vazias e as regras moram em controllers e services amarrados ao framework. Qualquer upgrade de versão do framework vira um projeto de risco, porque as regras passam por cima dele.
A raiz do problema é a direção das dependências. Em um sistema em camadas tradicional, o negócio depende da infraestrutura (chama o ORM, conhece o SQL). A hexagonal inverte isso: a infraestrutura passa a depender do negócio, implementando interfaces que o próprio domínio define. É o Dependency Inversion Principle aplicado à arquitetura inteira.
Onion Architecture e Clean Architecture são variações do mesmo princípio — mudam os nomes dos anéis, mas a regra de ouro é idêntica: dependências apontam sempre para dentro, e o centro não conhece ninguém.
- HTTP / Fila / CLI
- acionaPorta de entrada
- implementada porDomínio
- declaraPorta de saída
- implementada porSQL / APIs / Broker
Navegue pelas camadas
Camada 1/5Adaptadores de entrada (driving)
use ← →Camada 1
Adaptadores de entrada (driving)
quem aciona o domínio
Controllers HTTP, handlers de CLI, consumers de fila. Traduzem o mundo externo (request, mensagem, argumento) para uma chamada de caso de uso. Não contêm regra de negócio — só tradução e validação de formato.
No código
app.post("/pedidos", (req) => criarPedido.executar(req.body));Se violar: regra de negócio no controller → duplicação entre canais (REST faz uma coisa, fila outra) e testes que só rodam subindo HTTP.
Estrutura
Código
// Porta de saída (driven): o domínio declara o que precisa
interface RepositorioPedido {
salvar(pedido: Pedido): Promise<void>;
}
// Porta de entrada (driving): caso de uso
interface CriarPedido {
executar(cmd: { itens: string[] }): Promise<string>;
}
// Domínio: depende só das interfaces
class ServicoPedido implements CriarPedido {
constructor(private repo: RepositorioPedido) {}
async executar(cmd: { itens: string[] }) {
const pedido = Pedido.novo(cmd.itens);
await this.repo.salvar(pedido);
return pedido.id;
}
}
// Adaptador driven: implementa a porta com infra concreta
class RepositorioSQL implements RepositorioPedido {
async salvar(pedido: Pedido) { /* INSERT ... */ }
}from typing import Protocol
# Porta de saída (driven)
class RepositorioPedido(Protocol):
def salvar(self, pedido: "Pedido") -> None: ...
# Domínio depende só da abstração
class ServicoPedido:
def __init__(self, repo: RepositorioPedido):
self._repo = repo
def criar(self, itens: list[str]) -> str:
pedido = Pedido.novo(itens)
self._repo.salvar(pedido)
return pedido.id
# Adaptador driven concreto
class RepositorioSQL:
def salvar(self, pedido: "Pedido") -> None:
... # INSERT ...
# Em teste, injeta-se um dublê sem tocar no domínio
class RepositorioFake:
def __init__(self):
self.itens = []
def salvar(self, pedido):
self.itens.append(pedido)Casos de uso
Migrar de banco sem tocar no negócio
Cenário
Uma fintech precisa trocar Postgres por DynamoDB no serviço de pedidos por causa de escala.
Como o padrão entra
Só nasce um novo adaptador driven implementando RepositorioPedido. O domínio, os casos de uso e os controllers não mudam uma linha — o deploy pode até rodar com os dois adaptadores em paralelo para comparação.
Trade-off: Exige disciplina no design das portas: se a interface vazou detalhes de SQL, a troca deixa de ser plugável.
Mesma lógica, três canais
Cenário
Um pedido pode ser criado por REST (app), por fila (integração B2B) e por CLI (backoffice).
Como o padrão entra
Três adaptadores driving finos chamam a mesma porta de entrada CriarPedido. Validação de negócio existe uma vez só, no domínio — os canais apenas traduzem formato.
Trade-off: Cada canal ganha uma camada de tradução própria — mais arquivos, menos duplicação.
CI rápido com testes de unidade
Cenário
O pipeline levava 25 min porque todo teste subia banco e broker via containers.
Como o padrão entra
Com as regras atrás de portas, os testes de domínio usam dublês em memória e rodam em milissegundos; integração real fica para uma suíte menor de testes de contrato dos adaptadores.
Trade-off: Os testes de contrato continuam necessários — dublê que diverge do adaptador real cria falsa confiança.
Armadilhas
Hexagonal de fachada
Criar pastas ports/ e adapters/ mas deixar as regras nos controllers. A arquitetura vira cerimônia: você paga a indireção sem ganhar o isolamento.
Porta espelhando o banco
Desenhar portas como CRUD genérico (findById, updateAll) em vez de necessidades do domínio ('reservar estoque'). O vocabulário da infra invade o núcleo e a troca de tecnologia volta a doer.
Uma porta para cada coisa
Abstrair até lib de log e util de data. Portas nascem de necessidades do domínio que variam de implementação — abstração especulativa é só indireção sem valor.
Quando usar × quando evitar
Use quando
3- O domínio é rico e valioso, e você quer protegê-lo de mudanças de framework/infra.
- Precisa testar regras de negócio sem subir banco, HTTP ou filas.
- Há múltiplos pontos de entrada (REST, CLI, fila) ou de saída (SQL, NoSQL, APIs) para a mesma lógica.
Evite quando
2- CRUDs simples sem regras relevantes — as camadas viram cerimônia sem retorno.
- Protótipos e MVPs de vida curta, onde a indireção atrasa a entrega.
Testar o que ficou
3 armadilhas de Arquitetura Hexagonal viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
banco real × in-memory nos testesTrocar o repositório Postgres por um em memória no teste, sem tocar no domínio, é a prova de que a porta está no lugar certo.controllers do NestJSO controller é um adaptador de entrada: traduz HTTP em uma chamada de caso de uso e nada do protocolo vaza para dentro do domínio.repositório com Prisma trocávelA interface de repositório é a porta; a implementação com Prisma é um adaptador que dá para substituir por outro ORM sem mexer na regra.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Domínio no centro; adapters nas bordas.
dominio.executar(cmd, { repo: adapterPostgres });O que ele cobra
2 saltos a mais entre a chamada e o efeito.
- Uma porta e um adaptador por integração, mais os mapeamentos entre o mundo externo e o domínio
- A indireção extra em cada borda é peso morto num sistema pequeno
Não vale se: o sistema é pequeno, tem uma integração só e não vai trocar de tecnologia — a cerimônia de portas custa mais do que protege.
Costuma ser confundido com
Onde isto aparece
Continue por aqui
- AdapterConverte a interface de uma classe em outra que o cliente espera, permitindo que classes incompatíveis trabalhem juntas.
- CQRS (Command Query Responsibility Segregation)Separa o modelo de escrita (commands) do modelo de leitura (queries), podendo usar stores e serviços distintos otimizados para cada lado.