A maioria das bases de código começa com o framework no centro: controllers chamam services, services chamam o ORM, e as regras de negócio vazam para os três. Testar uma regra de preço passa a exigir um servidor web e um banco de dados, e trocar o provedor de pagamentos significa mexer no código que decide quanto um pedido custa.
A arquitetura hexagonal, também chamada de portas e adaptadores, inverte isso. Este artigo explica a ideia e o vocabulário (portas, adaptadores primários e secundários, a regra de dependência), mostra um diagrama para um serviço de pedidos e discute quando o padrão compensa os arquivos a mais.
A ideia em uma frase
Coloque a lógica de negócio num núcleo que não sabe nada de HTTP, bancos de dados ou fornecedores, e conecte todo o resto a ele por meio de interfaces que pertencem ao núcleo.
Alistair Cockburn descreveu o padrão em 2005 no seu artigo original sobre arquitetura hexagonal. A intenção declarada é permitir que uma aplicação seja acionada igualmente por usuários, programas, testes automatizados ou scripts em lote, e que seja desenvolvida e testada isolada dos bancos de dados e dispositivos que vai usar. O formato de hexágono não significa nada além de deixar espaço para desenhar várias portas nos lados.
O vocabulário
| Termo | O que é | Exemplo no serviço de pedidos |
|---|---|---|
| Núcleo | Os casos de uso e o modelo de domínio | PlaceOrder, Order, Money, regras de preço |
| Porta de entrada (primária) | Uma interface que o núcleo oferece para fora | PlaceOrder.execute(command) |
| Porta de saída (secundária) | Uma interface de que o núcleo precisa lá de fora | OrderRepository, PaymentGateway |
| Adaptador primário | Transforma uma entrada externa em chamadas de porta | Controller REST, CLI, consumidor de mensagens, teste |
| Adaptador secundário | Implementa uma porta de saída com tecnologia real | Repositório Postgres, cliente da API de pagamentos |
A regra de dependência amarra tudo: as dependências do código-fonte apontam para dentro. Os adaptadores dependem das portas; as portas e o núcleo não dependem de nada fora do hexágono. Em tempo de execução o núcleo chama o gateway de pagamento, mas no código é o adaptador de pagamento que depende da interface do núcleo, e não o contrário.
Um exemplo completo
Fazer um pedido exige salvar o pedido e cobrar o cliente. No núcleo:
// ports, owned by the core
interface OrderRepository { save(order: Order): Promise<void> }
interface PaymentGateway { authorize(amount: Money, token: string): Promise<AuthResult> }
// use case
class PlaceOrder {
constructor(private orders: OrderRepository, private payments: PaymentGateway) {}
async execute(cmd: PlaceOrderCommand): Promise<OrderId> {
const order = Order.create(cmd.items, cmd.customerId) // domain rules
const auth = await this.payments.authorize(order.total(), cmd.paymentToken)
if (!auth.ok) throw new PaymentDeclined(auth.reason)
await this.orders.save(order.markPaid(auth.id))
return order.id
}
}
Fora do núcleo, PostgresOrderRepository implementa OrderRepository com SQL, e PaymentApiGateway implementa PaymentGateway com chamadas HTTP, retries e mapeamento de erros. Um controller REST interpreta a requisição, monta um PlaceOrderCommand e chama o caso de uso. Um teste faz o mesmo com adaptadores em memória, sem servidor web nem banco de dados.
Mapeando para pastas
A estrutura é mais fácil de manter quando as pastas a dizem em voz alta:
src/
core/
domain/ Order, Money, pricing rules
usecases/ PlaceOrder, CancelOrder
ports/ OrderRepository, PaymentGateway
adapters/
http/ REST controllers (driving)
postgres/ repository implementations (driven)
payments/ payment API client (driven)
main.ts wiring: builds adapters and passes them to use cases
Adicione uma regra de lint ou um teste de arquitetura que quebre o build sempre que algo dentro de core/ importar de adapters/ ou de um pacote do framework. A regra é fácil de quebrar sem querer e barata de verificar automaticamente.
Erros comuns
- Portas com a cara da tecnologia.
OrderRepository.executeQuery(sql)é uma API de banco de dados com outro nome. Dê às portas nomes do que o núcleo precisa:save,findOpenOrdersFor(customer). - Objetos de domínio que são entidades do ORM. As anotações do framework de persistência arrastam o banco de dados para dentro do núcleo. Faça o mapeamento entre objetos de domínio e modelos de persistência no adaptador.
- Adaptadores com regras de negócio. Um adaptador de pagamento que decide que reembolsos acima de 500 dólares precisam de aprovação tirou uma regra do núcleo. Limite os adaptadores a tradução e transporte.
- Uma porta por classe. As portas existem para as fronteiras, não para cada colaborador dentro do núcleo.
O diagrama de arquitetura
Conteúdo do diagrama
- PlaceOrder use case
- Domain model
- PlaceOrder port
- OrderRepository port
- PaymentGateway port
- REST controller
- Test driver
- Postgres adapter
- Payment API adapter
- Postgres
- Payment provider
- calls
- implements
- SQL
- HTTPS
Como ler:
- O hexágono é o núcleo da aplicação: o caso de uso PlaceOrder e o modelo de domínio, sem código de framework ou de fornecedor.
- Esquerda, lado primário: um controller REST e um driver de testes chamam a porta de entrada PlaceOrder. Do ponto de vista do núcleo, eles são intercambiáveis.
- Direita, lado secundário: o caso de uso chama duas portas de saída. As arestas tracejadas com ponta de seta vazada mostram o adaptador Postgres e o adaptador da API de pagamentos implementando-as, que é a direção da dependência do código.
- Extrema direita: o banco Postgres real e o provedor de pagamentos, que só os adaptadores conhecem.
- O bloco Markdown enuncia a regra de dependência, a única coisa que os revisores precisam checar.
Os testes ficam mais baratos
A maior parte do benefício aparece nos testes:
- Os testes do núcleo usam adaptadores em memória e rodam em milissegundos. Regras de preço, transições de estado e tratamento de erros são testados sem I/O.
- Os testes de adaptador verificam um adaptador contra a tecnologia real: o repositório Postgres contra um Postgres de verdade, o adaptador de pagamento contra o sandbox do provedor.
- Alguns testes de ponta a ponta comprovam a ligação entre as partes.
Essa divisão é a pirâmide de testes na prática, e é por isso que os times adotam o padrão até em serviços pequenos.
Adicionando uma nova entrada
A porta de entrada se paga na primeira vez que aparece um segundo canal. Digamos que o negócio queira receber pedidos pela fila de mensagens de um parceiro, além da web. Você escreve um consumidor que lê uma mensagem, monta o mesmo PlaceOrderCommand e chama a mesma porta. Validação, preços e lógica de pagamento não mudam, e os testes deles também não. Sem a porta, o resultado comum é uma segunda cópia da lógica de pedidos dentro do consumidor, que se afasta da primeira em poucas versões.
Quando não vale a pena
- CRUD sem regras. Se o serviço basicamente move dados entre o HTTP e uma tabela, as portas são só cerimônia.
- Código de vida curta. Protótipos e scripts não pagam as interfaces extras.
- Um único adaptador para sempre. Se você nunca vai trocar o banco de dados e já testa contra um banco real, uma porta de saída adiciona indireção com pouco retorno. Mesmo assim, mantenha o núcleo livre de código de framework.
A arquitetura hexagonal é parente próxima da arquitetura cebola e da clean architecture. Elas diferem no vocabulário das camadas, mas compartilham a regra de dependência.
O que colocar no diagrama
- A fronteira do núcleo, desenhada como uma forma que envolva claramente só o domínio e os casos de uso.
- As portas na fronteira, com os nomes que o núcleo dá a elas.
- Os adaptadores do lado de fora, rotulados como primários ou secundários, com a tecnologia que encapsulam.
- A direção da dependência dos adaptadores secundários, diferente da direção da chamada.
Para um sistema inteiro em vez de um único serviço, o exemplo de diagrama com o modelo C4 mostra como dar zoom de contêineres para componentes, e o diagrama de arquitetura de microsserviços mostra onde serviços como este se encaixam. O diagrama de integração com gateway de pagamento cobre o que acontece por trás do adaptador de pagamento.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Adicione um Hexágono do pack Básico para o núcleo da aplicação, com um Texto como rótulo.
- Dentro dele, adicione nós Componente do pack Modelo C4 para o caso de uso e o modelo de domínio, e nós Componente na fronteira para as três portas.
- Adicione do lado de fora nós Contêiner para os adaptadores primários e secundários, além de Postgres do pack Dados e um Sistema externo para o provedor de pagamentos.
- Desenhe as arestas de implementação tracejadas com ponta de seta vazada e adicione um bloco Markdown do pack Básico com a regra de dependência.
O exemplo usa 13 nós de diagrama e fica dentro do plano Free.
Perguntas frequentes
Qual a diferença entre uma porta e um adaptador? Uma porta é uma interface que pertence ao núcleo. Um adaptador é o código que conecta essa interface a uma tecnologia específica, como um framework REST ou um banco de dados.
O que são adaptadores primários e secundários? Os adaptadores primários iniciam as interações com o núcleo, como controllers, consumidores e testes. Os adaptadores secundários são chamados pelo núcleo, como repositórios e clientes de API.
Arquitetura hexagonal é a mesma coisa que clean architecture? Elas compartilham a regra de dependência: as dependências apontam para o domínio. A clean architecture e a arquitetura cebola adicionam camadas com nome dentro do núcleo; a arquitetura hexagonal só distingue o lado de dentro do lado de fora.
Todo serviço precisa de arquitetura hexagonal? Não. Ela compensa onde há regras de negócio reais e mais de uma forma de acioná-las ou armazená-las. Em serviços CRUD finos, são basicamente arquivos a mais.
Onde ficam as transações? Normalmente no caso de uso ou numa porta de unit of work, para que o núcleo decida o que precisa ser confirmado junto e o adaptador decida como.