Alguém do seu time acabou de importar pg em src/domain/invoice.ts. O PR compila. Os testes passam. O code review tem trezentas linhas, e ninguém percebe.

Três meses depois, você quer extrair a lógica de domínio para um package compartilhado. Não consegue. Ela depende de tipos do Postgres, lógica de connection pooling, e um wrapper customizado de driver que só existe no monolito. O diagrama de círculos concêntricos no quadro branco virou uma piada.

Esse é o padrão de degradação padrão da clean architecture sem imposição automatizada. Arquitetos desenham setas apontando para dentro. Desenvolvedores escrevem imports apontando para fora. O compiler não se importa com suas camadas.

O que “domínio não importa infraestrutura” realmente significa

Em arquitetura em camadas, dependências fluem para dentro. A camada de domínio, no centro, define entidades, regras de negócio e casos de uso. Ela não tem conhecimento de HTTP, SQL, queues ou sistemas de arquivos.

A infraestrutura vive no anel externo. Ela implementa interfaces que o domínio define. A interface de repository vive no domínio. A implementação do Postgres vive na infraestrutura. O domínio chama save(invoice). Nunca chama pool.query().

Quando o código de domínio importa infraestrutura diretamente, duas coisas acontecem. Primeiro, o domínio se torna impossível de testar sem o banco de dados rodando. Segundo, você nunca consegue trocar Postgres por DynamoDB sem reescrever a lógica de negócio. Todo o propósito da camada — testabilidade e substituibilidade — evapora.

Por que code review não é suficiente

Todo time tem um engenheiro sênior que pega imports ruins no review. Esse engenheiro também está de férias, doente, ou revisando um PR de cinquenta arquivos às 17h de uma sexta-feira.

Humanos são reconhecedores de padrões com RAM limitada. Regras de dependência automatizadas são validadores determinísticos com paciência infinita. O lugar certo para impor limites arquiteturais é o mesmo onde você impõe erros de sintaxe: o pipeline de build. Se compila mas viola o grafo de dependências, deve falhar.

Como o dependency-cruiser impõe limites de camada

Para bases de código TypeScript e JavaScript, o dependency-cruiser transforma seu diagrama de arquitetura em um conjunto de regras testáveis. Ele analisa seu grafo de imports e faz o build falhar quando uma aresta proibida aparece.

Instale:

npm install --save-dev dependency-cruiser
npx depcruise --init

O script de init gera uma configuração .dependency-cruiser.js. Você adiciona uma regra que proíbe o domínio de tocar na infraestrutura:

// .dependency-cruiser.js
module.exports = {
  forbidden: [
    {
      name: "domain-cannot-import-infrastructure",
      comment:
        "Domain code must not depend on infrastructure. Move the import to an adapter or repository.",
      severity: "error",
      from: {
        path: "^src/domain",
      },
      to: {
        path: "^src/infrastructure",
      },
    },
    {
      name: "domain-cannot-import-node-modules",
      comment:
        "Domain code must not depend on external I/O libraries.",
      severity: "error",
      from: {
        path: "^src/domain",
      },
      to: {
        dependencyTypes: ["npm"],
        // Allow only pure logic libraries like date-fns or lodash
        pathNot: "^(date-fns|lodash|ramda)",
      },
    },
  ],
  options: {
    doNotFollow: {
      path: "node_modules",
    },
    tsPreCompilationDeps: true,
    tsConfig: {
      fileName: "tsconfig.json",
    },
  },
};

Essa regra diz: qualquer arquivo em src/domain que importe algo em src/infrastructure quebra o build. A segunda regra pega o import de pg ou axios que chega via node_modules.

Integre ao seu CI:

# .github/workflows/ci.yml
jobs:
  architecture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx depcruise src

Agora o import de pg em src/domain/invoice.ts falha antes que um humano sequer abra o PR.

Como a falha se parece

Quando um desenvolvedor quebra a regra, ele recebe uma saída assim:

error domain-cannot-import-infrastructure: src/domain/invoice.ts → src/infrastructure/database/pool.ts
  Domain code must not depend on infrastructure. Move the import to an adapter or repository.

error domain-cannot-import-node-modules: src/domain/invoice.ts → pg
  Domain code must not depend on external I/O libraries.

✖ 2 dependency violations (2 errors, 0 warnings). Showing only the first 2.

A mensagem de erro diz o que fazer. Mova a chamada ao banco de dados para um repository na camada de infraestrutura. Passe o repository para o caso de uso como uma interface. O código de domínio permanece puro.

Alternativas para outros ecossistemas

Nem todo mundo usa TypeScript. O princípio é o mesmo em toda parte. Só as ferramentas mudam.

Java: ArchUnit é o padrão ouro. Você escreve um teste, não um arquivo de configuração:

@ArchTest
static final ArchRule domain_should_not_access_infrastructure =
    noClasses()
        .that()
        .resideInAPackage("..domain..")
        .should()
        .dependOnClassesThat()
        .resideInAPackage("..infrastructure..");

Isso roda como um teste JUnit, então integra com Maven e Gradle sem nenhuma nova infraestrutura de CI.

C#: NetArchTest fornece o mesmo estilo de API para .NET. Escreva um teste unitário, execute no CI.

Go: Não há equivalente maduro. A maioria dos times impõe isso com um simples shell script:

#!/bin/bash
# scripts/check-domain-imports.sh

if grep -r "infrastructure/" internal/domain/ ; then
  echo "Domain code imports infrastructure. Fix the dependency direction."
  exit 1
fi

É grosseiro, mas funciona. Muitos times de Go também usam analyzers customizados do go vet ou o package go/ast para construir verificações adequadas.

Build systems: Bazel, Gradle e Nx suportam grafos de dependência de modules nativamente. Se você define seu domínio e infraestrutura como modules separados com declarações de dependência explícitas, a ferramenta de build impõe o limite de graça. O problema é que você precisa adotar o build system primeiro.

Trade-offs: falsos positivos, dor de migration e atrito com o time

Regras automatizadas não são gratuitas. Você deve conhecer os custos antes de ativá-las.

Falsos positivos acontecem quando você tem utilitários compartilhados legítimos que ficam entre camadas. Um slugifier de string ou uma classe base de erro customizada pode morar em src/shared. Se tanto domínio quanto infraestrutura o importam, a regra está ok. Se você colocá-lo em src/infrastructure/utils e o domínio o importar, o dependency-cruiser vai reclamar. A correção geralmente é mover o código compartilhado, o que muitas vezes é a decisão certa de qualquer forma.

A dor de migration é real. Se sua base de código já viola a regra em trinta lugares, você não consegue apertar o interruptor sem uma refatoração heroica. A abordagem pragmática é começar com warnings, corrigir as violações existentes ao longo de uma sprint, depois promover para errors. Alternativamente, use exceções pathNot para arquivos legados conhecidos e proíba novas violações desde o primeiro dia.

O atrito com o time é o custo oculto. Desenvolvedores que nunca trabalharam com imposição estrita de camadas vão resistir. Eles vão argumentar que um import pequeno é inofensivo, que a regra é burocrática, que isso os atrasa. Isso é um sinal cultural, não técnico. Os mesmos desenvolvedores também serão aqueles que importam express em um modelo de domínio às 23h para entregar uma feature. A regra existe porque humanos sob pressão tomam atalhos.

Como implementar isso em uma base de código existente

Você não precisa de uma grande reescrita. Precisa de um limite e de uma forma de medir.

  1. Desenhe as camadas. Decida quais diretórios são domínio, aplicação e infraestrutura. Escreva em ARCHITECTURE.md.

  2. Adicione o dependency-cruiser (ou o equivalente do seu ecossistema) com uma regra: domínio não pode importar infraestrutura.

  3. Execute. Conte as violações. Se o número for pequeno, corrija antes de mergear a config. Se for grande, adicione os caminhos legados a exceções pathNot.

  4. Faça falhar no CI. Não é opcional. Não é um warning. Um error que bloqueia o merge.

  5. Toda vez que um desenvolvedor esbarra no erro, ajude-o a mover o import. Não apenas diga que o build está vermelho. Explique onde o código deve ficar e por quê.

Em duas semanas, o time vai parar de tentar importar pg em arquivos de domínio. O diagrama de arquitetura no quadro branco finalmente vai corresponder ao código no repository.

FAQ

A camada de aplicação deveria poder importar infraestrutura?

Geralmente, sim. A camada de aplicação orquestra casos de uso. Ela pode conhecer repositories e serviços externos, mas deve depender de interfaces, não de implementações concretas. Se quiser imposição estrita, adicione uma segunda regra: application não pode importar implementações de infrastructure, apenas suas interfaces.

E quanto a events de domínio?

events de domínio são uma brecha comum. Um event de domínio é dado puro, então pertence à camada de domínio. O event bus de infraestrutura que o publica pertence à infraestrutura. A camada de aplicação inscreve handlers de events de domínio no bus. O próprio domínio nunca sabe que o bus existe.

Posso usar isso com Nx ou Turborepo?

Sim. Nx tem regras de limites de module embutidas que funcionam no nível de projeto. Se seu domínio e infraestrutura são bibliotecas Nx separadas, você pode impor a regra sem nenhuma ferramenta extra. O mesmo se aplica a Bazel e Gradle.

E se eu precisar de um utilitário da infraestrutura?

Você não precisa. Mova o utilitário para um package compartilhado common ou kernel que não tenha dependências de infraestrutura. Se ele realmente precisa de infraestrutura, não é um utilitário. É infraestrutura.

Resumo

Clean architecture sem imposição automatizada é um acordo de cavalheiros. Acordos de cavalheiros não sobrevivem a prazos de produção.

Dependency-cruiser, ArchUnit, ou mesmo um shell script com grep transformam sua arquitetura de uma sugestão em uma garantia. O domínio permanece puro. A infraestrutura permanece plugável. E na próxima vez que alguém tentar importar pg em uma regra de negócio, o build falha antes que o PR chegue a um revisor humano.

Comece com uma regra. Faça-a falhar no CI. Corrija a primeira violação. O resto seguirá.