ATLAS / ARQUITETURA DE SOFTWARE / FABRÍCIO CASTRO
Dependências são escolhas.
Não destinos inevitáveis.
Clean Architecture e Hexagonal ajudam a separar regras estáveis das tecnologias que mudam. Não são sinônimos de uma estrutura fixa de diretórios. Este estudo acompanha o mesmo fluxo fictício de compras e demonstra a diferença entre portas, adaptadores, caso de uso, agregado e persistência.
01 / O PROBLEMA NÃO É A PASTA
Quando o domínio conhece o ORM, o teste paga a conta.
Uma solicitação de compras deve respeitar organização, solicitante, valores e transições. Se essa regra depende de instanciar NestJS, Prisma, PostgreSQL e um serviço remoto, uma alteração simples vira operação de infraestrutura. A separação útil permite exercitar as regras sem servidor e trocar os mecanismos externos sob contratos explícitos.
O caso de uso sabe demais.
O handler recebe DTO HTTP, interpreta sessão, executa SQL, aplica regras e dispara webhook. Uma falha do ERP impede testar as invariantes e a lógica passa a existir em múltiplos controllers.
Cuidado: extrair interfaces sem motivo pode apenas esconder o acoplamento sob nomes diferentes.
Regras executam sem transporte.
O domínio modela o que é permitido; a aplicação coordena um caso de uso; adaptadores convertem HTTP, fila, banco e ERP em chamadas adequadas às portas. A composição escolhe implementações na borda, não dentro do agregado.
Teste arquitetural: domínio e aplicação não importam cliente de banco, framework web nem SDK de cloud.
02 / CLEAN E PORTS & ADAPTERS
Duas descrições que se aproximam. Ênfases diferentes.
Clean Architecture destaca a regra de dependência: os mecanismos externos conhecem políticas internas, mas o núcleo não deve conhecer detalhes externos. Ports & Adapters, proposta por Alistair Cockburn, destaca conversas com o sistema por portas, desacopladas dos dispositivos, interfaces e persistências que as implementam. Ambas favorecem testes do núcleo sem UI ou banco real.
A política não importa detalhes técnicos.
Uma função de domínio que depende de PrismaClient ou Request viola o objetivo de isolamento. Tipos e contratos mais estáveis ficam internos; infraestrutura depende do caso de uso ou de interfaces de porta.
Uma porta tem propósito de negócio.
Uma porta de entrada expõe o caso de uso de envio; a porta de saída descreve operações de persistência necessárias. O adaptador HTTP conduz a aplicação; o adaptador PostgreSQL seria conduzido pela aplicação. Nem toda interface é automaticamente uma porta.
03 / DRIVER E DRIVEN
O que entra é diferente do que a aplicação precisa acessar.
O capítulo implementa SubmitPurchasePort como contrato de entrada, SubmitPurchaseUseCase como orquestrador e PurchaseRequestRepository como contrato de saída. O fake em memória adapta o mesmo contrato para testes, sem exigir framework.
- 01Driving adapter — HTTP, CLI ou processamento de fila
Autentica e valida dados de transporte, converte para comando de aplicação e traduz resultado/erro em resposta. O exemplo usa chamadas diretas ao caso de uso; não há endpoint NestJS criado nesta fatia.
- 02Primary port — intenção do usuário
SubmitPurchasePort.execute(command)recebe ID da solicitação e identidade já verificada no servidor. Não deve aceitar autoridade de aprovação ou tenant arbitrário informado pelo navegador. - 03Use case — coordenação da transição
Carrega o agregado com escopo, chama regra pura de domínio, tenta gravar a nova versão e comunica conflito quando a versão observada não corresponde. Não inspeciona headers HTTP nem instancia Prisma.
- 04Driven port — persistência e evento
PurchaseRequestRepositorydeclara leitura por tenant e escrita condicional agregando eventos. A implementação de produção deve garantir atomicidade dentro de transação local; a interface não faz essa garantia surgir sozinha.
04 / EXEMPLO ORIGINAL / CÓDIGO REAL NO REPOSITÓRIO
A dependência aponta para o contrato do caso de uso.
Este é o desenho do módulo procurement-hexagonal.ts. O repositório concreto é injetado pela composição do sistema; o caso de uso chama o domínio submitPurchaseRequest e usa uma porta que precisa preservar versão e eventos no mesmo limite de consistência.
// Contrato da aplicação: sem import de Prisma ou HTTP.
interface PurchaseRequestRepository {
findForTenant(tenantId: string, id: string): Promise<PurchaseRequest | null>;
saveWithEventsIfVersion(
expectedVersion: number,
aggregate: PurchaseRequest,
events: readonly PurchaseSubmitted[],
): Promise<"saved" | "conflict">;
}
// A aplicação depende da porta, não do ORM concreto:
const submit = new SubmitPurchaseUseCase(repositoryAdapter);
await submit.execute({
purchaseId: "req-001",
actor: authenticatedServerActor,
});Trecho ilustrativo alinhado ao módulo TypeScript versionado. Não existe PrismaClient nem transação PostgreSQL no exemplo executável; ambos seriam responsabilidades de adaptadores posteriores.
// Acoplamento que este exemplo EVITA no núcleo:
class BadSubmitService {
constructor(private readonly prisma: PrismaClient) {}
async execute(id: string) {
// Query, regra de alçada, HTTP e outbox misturados.
// Trocar tecnologia ou testar invariantes exige a infra toda.
}
}
// Este trecho é um contraexemplo ilustrativo, não compilável.Contraexemplo intencionalmente não compilável; serve para comparar direções de dependência, não para demonstrar uma implementação.
05 / TRANSAÇÃO E CONSISTÊNCIA
Uma porta bem escrita não cria uma transação.
A assinatura saveWithEventsIfVersion descreve uma exigência arquitetural: salvar o agregado com versão esperada e persistir eventos associados. No banco real, um adaptador precisaria usar transação e verificar resultado de escrita condicional. Um fake que implementa Map pode evidenciar o contrato, mas nunca equivale à execução concorrente do PostgreSQL.
Versão é condição, não um contador decorativo.
Duas requisições podem ler a versão 0. A gravação real exige condição equivalente a WHERE tenant_id = ? AND id = ? AND version = 0 e sucesso de exatamente uma transição permitida. O efeito da segunda tentativa precisa ser erro de conflito ou reavaliação explícita.
Próxima prova: duas conexões SQL reais, conflito demonstrável e auditoria por tenant.
Persistência e publicação são momentos diferentes.
Um evento precisa ser gravado com a alteração do agregado no mesmo banco. Só depois outra operação o publica, observando duplicidade, retry e idempotência. Enviar HTTP ao ERP dentro da função pura não corrige dual write.
Escopo é aplicado em leitura e escrita.
O caso de uso consulta dados apenas do tenant verificado do ator. O adaptador real também deve incluir esse escopo no update e nas constraints pertinentes; confiar apenas numa comparação na memória é insuficiente.
Erros de negócio não são erros de infraestrutura.
Um pedido inexistente, uma transição ilegal e uma versão concorrente precisam de categorias distintas. O transporte HTTP pode convertê-las em respostas adequadas sem o domínio depender de códigos HTTP.
06 / TESTAR SEM ENGANAR O RESULTADO
Um fake prova contrato de orquestração. Não prova isolamento de banco.
Os testes usam FakePurchaseRequestRepository para alimentar o caso de uso com um agregado fictício, observar o evento produzido, garantir que a segunda submissão falhe e simular escrita com versão desatualizada. A função de domínio continua testável separadamente, sem mock de framework ou controller.
- 01Sucesso observável
Submeter como solicitante correto resulta em status
submitted, versão 1 e um eventoPurchaseSubmittedcoerente. - 02Negativas de segurança
Um ator de outro tenant recebe resposta genérica de não encontrado, e outro usuário do mesmo tenant não pode substituir o solicitante.
- 03Conflito injetado
Se o adaptador retorna
conflict, o caso de uso emiteWRITE_CONFLICTe não assume que a operação foi persistida. - 04Durabilidade não comprovada
O Fake usa
Mape eventos em array. Exige-se laboratório separado com PostgreSQL descartável, falha entre gravação e publicação, restauração e reprocessamento para estabelecer garantias de produção.
07 / CRITÉRIOS PARA NÃO COMPLICAR
Inversão de dependências não é colecionar interfaces.
Um módulo pequeno e estável que lê dados sem regras complexas pode ser mais claro com uma função de acesso direto. Introduza portas quando representam uma conversa significativa, protegem uma fronteira de domínio ou permitem substituir infraestrutura sem reescrever casos de uso.
Interface por tabela, fábrica por classe, adaptador por função.
Essa organização cria arquivos sem reduzir risco. Prefira contratos de intenção: submitPurchase, findForTenant e saveWithEventsIfVersion comunicam o que a aplicação necessita, não o CRUD inteiro do ORM.
Um monólito modular pode ser hexagonal.
Separar o domínio e a aplicação de infraestrutura não obriga microsserviços, filas ou containers. Só distribua quando independência de deploy, escala ou isolamento de falhas superar seus custos reais.
ALICERCE DO CAPÍTULO
Antes de criar portas, modele a regra.
O capítulo DDD aprofunda o significado das solicitações, seus invariantes, os bounded contexts e o evento que este caso de uso coordena.
08 / FONTES PRIMÁRIAS E LIMITES
Arquitetura é uma hipótese que precisa sobreviver à prática.
Este estudo usa o relato original de Alistair Cockburn sobre Ports & Adapters (2005), orientações de uso de arquiteturas hexagonais e o princípio de dependência das arquiteturas centradas em políticas. O código é original e fictício; não representa um sistema de cliente ou uma transação de produção.
Alistair Cockburn — Ports & Adapters.
Artigo original com intenção de permitir testar o núcleo sem UI nem banco, usando adaptadores para diversas conversas com o sistema. A figura hexagonal não determina a quantidade de portas.
AWS — Hexagonal Architecture Pattern.
Uma interpretação de arquitetura que explica independência de persistência e UI, e a separação de lógica de negócio dos adaptadores concretos.
Patterns complementam, não substituem as fronteiras.
Adapter do catálogo GoF é um mecanismo de compatibilidade de interface; Ports & Adapters é uma organização arquitetural mais ampla. O termo parecido não transforma os conceitos em sinônimos.
Status: estudo em branch, com código TypeScript e testes a executar. Sem banco real, endpoint HTTP, integração ERP, build publicado ou certificação de qualidade.