Arquitetura Hexagonal
Isola o núcleo de domínio de toda infraestrutura externa por meio de portas (interfaces definidas pelo domínio) e adaptadores (implementações nas bordas) — garantindo que a infraestrutura dependa do domínio, nunca o contrário.
Intenção
Permitir que o núcleo de domínio seja desenvolvido, testado e executado de forma completamente independente de qualquer tecnologia externa — banco de dados, framework HTTP, fila de mensagens, UI. O domínio define as interfaces (portas) que ele precisa; o mundo externo fornece as implementações (adaptadores).
A Arquitetura Hexagonal foi descrita por Alistair Cockburn em 2005 com o nome formal Ports and Adapters. O hexágono não tem um significado geométrico especial — é apenas uma forma de representar que o domínio tem múltiplos pontos de conexão com o exterior (HTTP, banco, UI, testes, filas) sem que nenhum deles seja privilegiado ou embutido no núcleo.
O princípio central é a Inversão de Dependência: módulos de alto nível (domínio) não dependem de módulos de baixo nível (infraestrutura). Ambos dependem de abstrações — as portas.
Problema
Em arquiteturas tradicionais, o domínio frequentemente importa diretamente a infraestrutura: entidades que estendem classes ORM, serviços de domínio que chamam bibliotecas de banco de dados, regras de negócio que dependem de tipos de um framework HTTP. O resultado:
- Testabilidade comprometida: para testar uma regra de negócio, é preciso subir banco de dados, servidor HTTP, mocks de terceiros.
- Acoplamento ao framework: migrar de um ORM para outro, ou de REST para GraphQL, exige tocar no código de domínio.
- Dificuldade de raciocinar sobre o negócio: as regras de domínio ficam escondidas entre código de infraestrutura, dificultando a leitura e a evolução.
A Arquitetura Hexagonal resolve isso de forma direta: o domínio define o que precisa (interfaces/portas) sem saber quem vai implementar. Os adaptadores implementam essas interfaces e ficam completamente fora do domínio.
Estrutura
┌─────────────────────────────────────────────────┐
│ ADAPTADORES PRIMÁRIOS │
│ (dirigem o domínio — driving side) │
│ │
│ Controller HTTP CLI Teste unitário │
└────────────────┬────────────────────────────────┘
│ usa porta de entrada (interface)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ NÚCLEO DE DOMÍNIO │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Portas de entrada (interfaces de casos de uso/application) │ │
│ │ Ex.: CriarPedidoUseCase, BuscarProdutoUseCase │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Entidades · Value Objects · Serviços de Domínio │
│ Regras de negócio · Eventos de domínio │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Portas de saída (interfaces de infraestrutura) │ │
│ │ Ex.: PedidoRepository, EmailService, PaymentGateway │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────┬──────────────────────────────────────┘
│ implementado por
▼
┌─────────────────────────────────────────────────┐
│ ADAPTADORES SECUNDÁRIOS │
│ (dirigidos pelo domínio — driven side) │
│ │
│ PostgresRepository StripeAdapter SESAdapter│
└─────────────────────────────────────────────────┘
Regra de dependência: as setas apontam PARA o domínio, nunca para fora.
O domínio não importa nada dos adaptadores.
Adaptadores primários vs secundários
Adaptadores primários (ou driving adapters) são aqueles que iniciam a interação com o domínio: eles recebem uma entrada externa (requisição HTTP, comando CLI, mensagem de fila, chamada de teste) e a traduzem em chamadas às portas de entrada do domínio. O Controller HTTP é o exemplo clássico.
Adaptadores secundários (ou driven adapters) são aqueles que o domínio aciona: eles implementam as portas de saída definidas pelo domínio e fazem a ponte com sistemas externos — banco de dados, serviços de e-mail, gateways de pagamento, sistemas de fila. O domínio não sabe que tipo de banco está sendo usado; ele apenas chama a interface do repositório.
Portas e adaptadores — exemplo de código
O trecho abaixo mostra uma porta de saída (interface de repositório definida pelo domínio) e um adaptador secundário (implementação concreta com acesso ao banco). A porta fica dentro do domínio; o adaptador fica na infraestrutura.
// ── PORTA de saída — definida dentro do domínio ──────────────
// O domínio declara o que precisa; não sabe quem implementa.
interface PedidoRepository {
salvar(pedido: Pedido): Promise<void>;
buscarPorId(id: string): Promise<Pedido | null>;
}
// ── ADAPTADOR secundário — fora do domínio (infraestrutura) ──
// Implementa a porta usando o mecanismo de persistência real.
class PostgresPedidoRepository implements PedidoRepository {
constructor(private readonly db: DatabaseConnection) {}
async salvar(pedido: Pedido): Promise<void> {
await this.db.query(
'INSERT INTO pedidos (id, total, status) VALUES ($1, $2, $3)',
[pedido.id, pedido.total, pedido.status]
);
}
async buscarPorId(id: string): Promise<Pedido | null> {
const row = await this.db.queryOne(
'SELECT * FROM pedidos WHERE id = $1', [id]
);
return row ? Pedido.reconstituir(row) : null;
}
}
// ── ADAPTADOR primário — Controller HTTP ──────────────────────
// Traduz a requisição HTTP em chamada ao caso de uso do domínio.
class PedidoController {
constructor(private readonly criarPedido: CriarPedidoUseCase) {}
async post(req: Request): Promise<Response> {
const resultado = await this.criarPedido.executar({
clienteId: req.body.clienteId,
itens: req.body.itens,
});
return Response.json({ pedidoId: resultado.id }, { status: 201 });
}
}
<?php
// ── PORTA de saída — definida dentro do domínio ──────────────
// O domínio declara o que precisa; não sabe quem implementa.
interface PedidoRepository
{
public function salvar(Pedido $pedido): void;
public function buscarPorId(string $id): ?Pedido;
}
// ── ADAPTADOR secundário — fora do domínio (infraestrutura) ──
// Implementa a porta usando o mecanismo de persistência real.
class DoctrinePedidoRepository implements PedidoRepository
{
public function __construct(
private readonly EntityManagerInterface $em
) {}
public function salvar(Pedido $pedido): void
{
$this->em->persist($pedido);
$this->em->flush();
}
public function buscarPorId(string $id): ?Pedido
{
return $this->em->find(Pedido::class, $id);
}
}
// ── ADAPTADOR primário — Controller HTTP ──────────────────────
// Traduz a requisição HTTP em chamada ao caso de uso do domínio.
class PedidoController
{
public function __construct(
private readonly CriarPedidoUseCase $criarPedido
) {}
public function post(Request $request): JsonResponse
{
$resultado = ($this->criarPedido)(
new CriarPedidoCommand(
clienteId: $request->input('clienteId'),
itens: $request->input('itens'),
)
);
return new JsonResponse(['pedidoId' => $resultado->id], 201);
}
}
O ponto central: PedidoRepository é uma interface que vive
dentro do pacote de domínio. PostgresPedidoRepository (ou
DoctrinePedidoRepository) vive na infraestrutura e importa o
domínio — nunca o contrário. Trocar o banco de dados é trocar o adaptador;
o domínio não muda.
Quando usar
- Domínios com lógica de negócio complexa: quando as regras de negócio precisam ser desenvolvidas, testadas e evoluídas de forma independente da tecnologia. A Arquitetura Hexagonal permite testar todo o domínio com mocks simples das portas — sem banco real, sem servidor HTTP.
- Múltiplos adaptadores primários: quando o mesmo domínio precisa ser acionado por HTTP, CLI, consumidores de fila e testes — cada um com seu próprio adaptador, sem alterar o domínio.
- Alta probabilidade de troca de infraestrutura: projetos que preveem migração de banco de dados, de gateway de pagamento ou de serviço de e-mail se beneficiam muito da separação por portas e adaptadores.
- Projetos com ciclo de vida longo e equipe crescente: a explicitação das fronteiras reduz o acoplamento acidental e facilita o onboarding — os contratos (portas) são a documentação viva das integrações.
Quando evitar
- CRUDs simples sem domínio real: criar portas e adaptadores para uma aplicação que apenas persiste e lê dados sem regras de negócio é overengineering. O custo da estrutura não se paga.
- Projetos pequenos de vida curta: a separação entre portas e adaptadores tem um custo de organização inicial. Para projetos de curta duração ou equipes de uma pessoa, pode ser mais produtivo começar simples e refatorar conforme a complexidade cresce.
- Quando a equipe não entende o padrão: aplicar Hexagonal sem que a equipe compreenda a diferença entre porta e adaptador, ou entre dependência do domínio para a infraestrutura vs. o contrário, tende a resultar em uma Arquitetura em Camadas mal nomeada com classes "Port" e "Adapter" que não invertem nada.
Prós e contras
Prós
- Domínio completamente testável com mocks simples das portas — sem banco, sem HTTP, sem dependências externas.
- Substituição de adaptadores sem impacto no domínio — trocar o banco é trocar apenas o adaptador secundário.
- Múltiplos pontos de entrada (HTTP, CLI, fila, testes) sem modificar o núcleo.
- Contratos explícitos entre domínio e infraestrutura — portas são a documentação das integrações.
- Facilita a evolução do domínio de forma independente do mundo externo.
Contras
- Overengineering para aplicações simples — a estrutura de portas e adaptadores tem custo de configuração e manutenção.
- Proliferação de interfaces: em sistemas grandes, pode haver dezenas de portas; sem disciplina, o número cresce de forma não gerenciada.
- Curva de aprendizado: a distinção entre adaptador primário e secundário, e a inversão de dependência, são conceitos não-óbvios para desenvolvedores sem exposição prévia.
- Injeção de dependência se torna obrigatória — frameworks DI (NestJS, Spring, Laravel) ajudam, mas adicionam complexidade de configuração.
Armadilhas comuns
1. Colocar a interface (porta) fora do domínio
A porta deve ser definida pelo domínio, não pela infraestrutura. Se a
interface PedidoRepository está no pacote de infraestrutura e
o domínio importa a infraestrutura para usá-la, a inversão foi feita ao
contrário — o domínio voltou a depender da infraestrutura. A porta vive
no domínio; o adaptador vive fora e aponta para dentro.
2. Overengineering — portas para tudo
Não é necessário criar uma porta para cada interação. Um logger de diagnóstico, um gerador de UUID, um relógio de sistema — às vezes é aceitável injetá-los diretamente sem criar uma interface elaborada. A porta tem valor quando a implementação concreta precisa ser trocável (banco, gateway, e-mail) ou quando o isolamento para testes é crítico. Para utilidades estáveis e sem impacto no comportamento de negócio, a abstração pode ser desnecessária.
Regra prática: crie uma porta quando você precisar de mais de uma implementação (produção vs. teste, ou provider A vs. provider B), ou quando a dependência concreta impossibilitaria o teste unitário do domínio. Se a resposta for "nunca precisarei trocar isso", não crie a porta.
3. Confundir Hexagonal com "só mais camadas"
A diferença entre Arquitetura em Camadas e Hexagonal não é cosmética. Na Arquitetura em Camadas clássica, a Infraestrutura fica "abaixo" do Domínio e o Domínio pode referenciar tipos de infraestrutura indiretamente. Na Hexagonal, a direção é invertida explicitamente: a infraestrutura implementa interfaces do domínio e portanto aponta para o domínio. Se os adaptadores no seu projeto não implementam interfaces definidas dentro do pacote de domínio, você tem Arquitetura em Camadas com nomes diferentes.
4. Não testar o domínio de forma isolada
O principal benefício da Arquitetura Hexagonal é a testabilidade do domínio sem infraestrutura real. Se os testes do domínio ainda precisam de banco de dados, de servidor HTTP ou de mocks de framework, as fronteiras entre domínio e infraestrutura foram violadas em algum ponto. Testes de domínio devem usar apenas mocks ou stubs das portas — implementações in-memory simples que satisfazem a interface.
Arquiteturas e padrões relacionados
No MVC, o Controller que recebe requisições HTTP atua exatamente como um adaptador primário (driving adapter): traduz a entrada externa em chamadas ao domínio sem que o domínio precise saber da camada HTTP. A Arquitetura Hexagonal formaliza e generaliza esse papel — qualquer ponto de entrada (CLI, fila, teste) é um adaptador primário, não apenas o Controller.
A Arquitetura em Camadas compartilha o objetivo de isolar o domínio, mas com uma diferença fundamental de direção: nas camadas, a infraestrutura fica "abaixo" e pode ser referenciada pelas camadas superiores via interfaces. Na Hexagonal, a infraestrutura fica "fora" e implementa interfaces que o domínio define — a dependência é explicitamente invertida. Na prática, as duas abordagens convergem quando a Arquitetura em Camadas usa repositórios com inversão de dependência.
O padrão Adapter (GoF) é o mecanismo de implementação de cada adaptador hexagonal: um adaptador secundário é um Adapter que implementa a porta (interface-alvo do domínio) e envolve a tecnologia externa (adaptee). A diferença é de escala e contexto: o Adapter do GoF é um padrão de classe; o adaptador hexagonal é um conceito arquitetural que frequentemente usa o padrão Adapter internamente.
O Strategy (GoF) é estruturalmente equivalente à relação porta/adaptador: uma interface e múltiplas implementações intercambiáveis. A Arquitetura Hexagonal aplica esse mesmo mecanismo em escala arquitetural — cada porta é uma Strategy de infraestrutura, e o domínio injeta a implementação correta por injeção de dependência.