en
· 4 min de leitura

Arquitetura hexagonal sem a cerimônia

Ports and adapters é uma boa ideia enterrada debaixo de uma pilha de pastas. Fique com a ideia, jogue fora a pilha.

architecture

Arquitetura hexagonal tem uma ideia que vale a pena guardar: o núcleo da sua aplicação não deveria saber em qual banco, framework, fila ou biblioteca HTTP está rodando. Todo o resto que costuma vir junto, a estrutura de seis camadas de pastas, interfaces para tudo, classes de mapeamento entre DTOs idênticos, é cerimônia que os times adotam porque um diagrama mandou.

Já auditei bases de código em que uma única operação de 'criar pedido' passava por um controller, um DTO de request, uma interface de caso de uso, uma implementação de caso de uso, um serviço de domínio, uma interface de repositório, uma implementação de repositório, um mapper de entidade e um model do ORM. Nove arquivos. A regra de negócio tinha quatro linhas. A cerimônia tinha devorado a arquitetura.

A única regra que importa

Dependências apontam para dentro. A lógica de domínio não importa nada do mundo externo. O mundo externo importa o domínio. Esse é o princípio inteiro. Se suas regras de precificação podem ser testadas em unidade sem banco, sem framework e sem rede, você está fazendo arquitetura hexagonal, independentemente do nome das suas pastas.

Todo o resto é técnica para fazer valer essa regra, e técnicas devem ser escolhidas pelo custo. Um projeto TypeScript pode garantir isso com uma regra de lint que proíbe imports de 'infra' dentro de 'domain'. É uma linha de configuração. Não exige uma interface para cada repositório.

O que eu realmente faço num projeto Next.js

Três pastas por feature, não seis. Uma pasta de domínio com funções e tipos simples: sem classes obrigatórias, sem decorators, sem imports de framework. É onde as regras vivem, e é a única pasta que recebe testes de unidade exaustivos. Uma pasta de aplicação com os casos de uso, que são funções que recebem o domínio mais um conjunto pequeno de capacidades injetadas: um jeito de carregar um pedido, um jeito de salvar, um jeito de publicar um evento. E uma pasta de infra onde essas capacidades são implementadas contra Postgres, uma fila, um provedor de e-mail.

As capacidades injetadas são as portas. São apenas tipos de função em TypeScript, definidos ao lado do caso de uso que precisa delas. Não uma classe abstrata num pacote separado. Não um Repository<T> genérico do qual toda entidade herda. Um caso de uso que precisa carregar e salvar pedidos declara exatamente essas duas funções e nada mais. O adaptador é um arquivo em infra que exporta um objeto compatível com esse tipo.

Server Actions e route handlers são os adaptadores de entrada. Eles fazem o parse da requisição, chamam um caso de uso com a infra real conectada e moldam a resposta. Não contêm regras. Se um route handler tem um if sobre lógica de negócio, ele está no lugar errado.

A cerimônia que eu recuso

Interfaces com uma única implementação, criadas 'caso a gente troque de banco'. Em quinze anos vi o banco ser trocado talvez três vezes, e em cada uma delas a interface não sobreviveu à troca mesmo assim, porque o banco novo tinha semântica de transação diferente. Escreva a interface quando a segunda implementação chegar, que geralmente é o fake em memória para testes. Essa vale a pena.

Mappers de DTO para entidade para model quando as três formas são idênticas. Mapeamento se paga quando as formas realmente diferem, quando o formato de transporte tem um campo que o domínio não pode ver, ou quando o domínio tem um valor calculado que o banco não armazena. Mapear objetos idênticos por três camadas é ruído que esconde os lugares onde isso importa.

Um pacote de 'domínio' separado, publicado num registry para ser 'reutilizado'. Nunca é. Só adiciona um passo de build e um número de versão a cada mudança.

Entidades anêmicas com uma camada de serviço que faz todo o trabalho. Se o domínio é só dados e as regras vivem em serviços, a arquitetura é procedural com passos extras. Ou coloque as regras nos objetos de domínio, ou admita que está escrevendo código procedural, o que é aceitável, e pare de fingir.

Como saber se está funcionando

A pasta de domínio tem a maior cobertura de testes e os testes mais rápidos. Um adaptador novo, digamos trocar de um provedor de e-mail para outro, toca um arquivo em infra e zero arquivos em outro lugar. Uma regra de negócio nova toca o domínio e talvez um caso de uso, e nunca um route handler. E um engenheiro novo consegue encontrar onde 'elegibilidade para estorno' é decidida em menos de um minuto olhando os nomes dos arquivos.

Se essas quatro coisas forem verdade, mantenha a estrutura que te levou até ali. Se não forem, nenhuma quantidade de pastas adicionais vai resolver. Arquitetura hexagonal é uma direção de dependências. Não é um layout de pastas.