Arquitetura

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 });
  }
}

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.