Arquitetura Hexagonal (Ports & Adapters): Isolando o Domínio da Infraestrutura

A Arquitetura Hexagonal — também conhecida como Ports & Adapters — foi proposta por Alistair Cockburn em 2005 como uma alternativa às arquiteturas em camadas tradicionais. O objetivo central é isolar o núcleo da aplicação (regras de negócio) de agentes externos como bancos de dados, APIs REST, filas de mensagens e interfaces de usuário.
Diferente da arquitetura em camadas clássica (presentation → business → data), onde cada camada depende da anterior, a Arquitetura Hexagonal coloca o domínio no centro e toda comunicação com o mundo externo passa por portas (interfaces) e adaptadores (implementações).
Os Três Princípios Fundamentais
1. Domínio no Centro
O código de negócio (entidades, serviços, regras) não importa nada de fora — nem frameworks, nem bibliotecas externas, nem o banco de dados. Ele conhece apenas suas próprias interfaces e modelos. Isso significa que seu domínio é puramente Java/TypeScript/Python puro, sem annotations de ORM, sem decorators de rota, sem herança de frameworks.
2. Portas (Ports)
As portas são interfaces que definem os contratos de comunicação com o mundo externo. Existem dois tipos:
- Portas de entrada (inbound): casos de uso que o sistema oferece — por exemplo,
CriarPedidoUseCaseouConsultarSaldoUseCase - Portas de saída (outbound): operações que o sistema precisa do mundo externo — por exemplo,
RepositorioDePedidosouServicoDeNotificacao
3. Adaptadores (Adapters)
Os adaptadores implementam as portas. Um adaptador pode ser um controlador HTTP (que traduz requisições REST em chamadas ao caso de uso), um repositório PostgreSQL (que implementa RepositorioDePedidos), ou um adaptador de fila RabbitMQ. O domínio nunca conhece os adaptadores — apenas as interfaces.
Estrutura de Pastas na Prática
Uma estrutura típica de projeto usando Arquitetura Hexagonal com TypeScript seria:
src/
├── domain/ # Núcleo da aplicação
│ ├── entities/ # Entidades de negócio
│ │ └── Pedido.ts
│ ├── ports/ # Interfaces (portas)
│ │ ├── inbound/
│ │ │ └── CriarPedidoUseCase.ts
│ │ └── outbound/
│ │ └── RepositorioDePedidos.ts
│ └── services/ # Casos de uso (regras de negócio)
│ └── CriarPedidoService.ts
├── adapters/ # Adaptadores externos
│ ├── inbound/
│ │ ├── http/ # Controladores REST
│ │ │ └── PedidoController.ts
│ │ └── queue/ # Consumidores de fila
│ │ └── PedidoConsumer.ts
│ └── outbound/
│ ├── database/ # Implementação real do repositório
│ │ └── PedidoRepositoryPostgres.ts
│ ├── queue/ # Produtores de mensagem
│ │ └── NotificacaoProducer.ts
│ └── external/ # APIs externas
│ └── GatewayPagamento.ts
├── config/ # Configurações
│ └── container.ts # Injeção de dependência (DI)
└── main.ts # Ponto de entrada da aplicação
Inversão de Dependência: O Coração da Arquitetura
A Arquitetura Hexagonal aplica rigorosamente o Princípio da Inversão de Dependência (DIP) do SOLID. Módulos de alto nível (domínio) não dependem de módulos de baixo nível (banco de dados, HTTP). Ambos dependem de abstrações (as portas). O fluxo de dependência aponta sempre para dentro:
Adaptadores Inbound → Portas (Interfaces) → Domínio
(HTTP, CLI, Queue) Casos de Uso (Entidades)
↕
Adaptadores Outbound → Portas (Interfaces)
(Postgres, Redis, API) Repositórios
Perceba que o domínio está no centro e não sabe da existência de nenhum adaptador. As flechas de dependência apontam para dentro — do exterior para o núcleo.
Exemplo Prático: Criar Pedido
Vamos implementar um fluxo de criação de pedido para ilustrar os conceitos.
1. Entidade de Domínio
// domain/entities/Pedido.ts
export class Pedido {
constructor(
public readonly id: string,
public readonly clienteId: string,
public readonly itens: ItemDoPedido[],
public readonly total: number,
public status: StatusPedido,
public readonly criadoEm: Date
) {}
public confirmar(): void {
if (this.status !== StatusPedido.PENDENTE) {
throw new Error("Apenas pedidos pendentes podem ser confirmados");
}
this.status = StatusPedido.CONFIRMADO;
}
}2. Porta de Entrada (Use Case)
// domain/ports/inbound/CriarPedidoUseCase.ts
export interface CriarPedidoUseCase {
execute(dados: DadosCriacaoPedido): Promise<Pedido>;
}3. Porta de Saída (Repositório)
// domain/ports/outbound/RepositorioDePedidos.ts
export interface RepositorioDePedidos {
salvar(pedido: Pedido): Promise<void>;
buscarPorId(id: string): Promise<Pedido | null>;
}4. Caso de Uso (Regra de Negócio)
// domain/services/CriarPedidoService.ts
export class CriarPedidoService implements CriarPedidoUseCase {
constructor(
private readonly repositorio: RepositorioDePedidos,
private readonly notificador: ServicoDeNotificacao
) {}
async execute(dados: DadosCriacaoPedido): Promise<Pedido> {
if (dados.itens.length === 0) {
throw new Error("Pedido deve ter ao menos um item");
}
const pedido = new Pedido(
crypto.randomUUID(),
dados.clienteId,
dados.itens,
this.calcularTotal(dados.itens),
StatusPedido.PENDENTE,
new Date()
);
await this.repositorio.salvar(pedido);
await this.notificador.enviar(pedido);
return pedido;
}
private calcularTotal(itens: ItemDoPedido[]): number {
return itens.reduce((acc, item) => acc + item.preco * item.quantidade, 0);
}
}5. Adaptador de Entrada (REST)
// adapters/inbound/http/PedidoController.ts
import { Router, Request, Response } from "express";
export class PedidoController {
constructor(private readonly criarPedido: CriarPedidoUseCase) {}
register(router: Router): void {
router.post("/api/pedidos", async (req: Request, res: Response) => {
try {
const pedido = await this.criarPedido.execute(req.body);
res.status(201).json(pedido);
} catch (error) {
res.status(400).json({ erro: (error as Error).message });
}
});
}
}6. Adaptador de Saída (PostgreSQL)
// adapters/outbound/database/PedidoRepositoryPostgres.ts
export class PedidoRepositoryPostgres implements RepositorioDePedidos {
constructor(private readonly db: PrismaClient) {}
async salvar(pedido: Pedido): Promise<void> {
await this.db.pedido.create({
data: {
id: pedido.id,
clienteId: pedido.clienteId,
total: pedido.total,
status: pedido.status,
criadoEm: pedido.criadoEm
}
});
}
async buscarPorId(id: string): Promise<Pedido | null> {
const dados = await this.db.pedido.findUnique({ where: { id } });
return dados ? this.paraDominio(dados) : null;
}
private paraDominio(dados: any): Pedido {
return new Pedido(
dados.id, dados.clienteId,
dados.itens, dados.total,
dados.status as StatusPedido, dados.criadoEm
);
}
}7. Fábrica / DI Container
// config/container.ts
const prisma = new PrismaClient();
const repositorio = new PedidoRepositoryPostgres(prisma);
const notificador = new NotificacaoProducer(/* RabbitMQ */);
const criarPedidoService = new CriarPedidoService(repositorio, notificador);
const pedidoController = new PedidoController(criarPedidoService);
// pedidoController.register(router);
Benefícios da Arquitetura Hexagonal
- Testabilidade: Você pode testar o domínio sem infraestrutura — basta implementar mocks das portas. Nenhum banco de dados ou servidor HTTP necessário.
- Troca de tecnologia indolor: Migrar de PostgreSQL para MongoDB? Crie um novo adaptador. Trocar Express por Fastify? Novo adaptador inbound. O domínio permanece intacto.
- Domínio rico e expressivo: As regras de negócio vivem em objetos de domínio puros, sem acoplamento com frameworks. O código fica mais legível e mantível.
- Paralelismo de desenvolvimento: A equipe de front-end e a de back-end podem trabalhar contra as interfaces definidas, sem esperar a implementação concreta.
- Adiamento de decisões: Você pode começar com um banco em memória (adaptador simples) e decidir o banco real depois, quando tiver mais dados.
Armadilhas Comuns e Como Evitá-las
- Over-engineering: Para aplicações muito simples (CRUD sem regras complexas), a arquitetura hexagonal pode adicionar complexidade desnecessária. Avalie se o custo-benefício vale a pena.
- Portas espelhando o banco de dados: A porta de saída não deve ser um espelho do banco (ex:
findAll,save). Ela deve representar operações de domínio (ex:buscarPedidosPendentes). - Anemia de domínio: Colocar toda a lógica nos serviços e deixar as entidades como meros structs sem comportamento. O domínio deve ser rico — entidades com métodos de negócio validam regras sozinhas.
- Acoplamento com framework no adaptador: Adaptadores inbound devem traduzir requisições externas para chamadas de domínio, não vazar detalhes do framework (req/res do Express) para o caso de uso.
Quando Usar (e Quando Não Usar)
A Arquitetura Hexagonal brilha em:
- Microsserviços com regras de negócio complexas
- Projetos de longa duração que precisam evoluir a arquitetura
- Sistemas que integram múltiplos bancos de dados ou APIs externas
- Equipes que praticam TDD e precisam de testabilidade máxima
Pode ser excessiva em:
- CRUDs simples sem regras de negócio significativas
- Protótipos e MVPs que priorizam velocidade sobre manutenibilidade
- Scripts únicos ou ferramentas de linha de comando simples
Conclusão
A Arquitetura Hexagonal não é uma bala de prata, mas é uma ferramenta poderosa no arsenal de qualquer desenvolvedor que busca criar sistemas testáveis, adaptáveis e sustentáveis. Ao colocar o domínio no centro e tratar tudo o mais como detalhes substituíveis, você ganha flexibilidade para trocar tecnologias, adicionar novos canais de entrada (CLI, fila, GraphQL) e evoluir o sistema sem reescrever o núcleo de negócio.
O investimento inicial em interfaces e indireção se paga rapidamente quando o projeto cresce — ou quando chega aquela demanda de última hora para adicionar uma nova integração.
Combine a Arquitetura Hexagonal com DDD, Clean Architecture e TDD para maximizar os benefícios. Cada uma aborda uma dimensão diferente: Hexagonal foca em isolamento de infraestrutura, DDD em modelagem de domínio, Clean Architecture em regras de organização, e TDD em qualidade e especificação executável.







