Trilha 3 · Do conceito à produção · ~22 min

Já expliquei o Public API Provider (aula Comunicação entre módulos: o padrão Public API Provider) para times inteiros, todo mundo concordou, documentamos na wiki com diagrama bonito. Três meses depois, auditei o código: 60+ imports furando fronteira. Não por má-fé — por sexta-feira à noite, deadline, "só dessa vez". Diretriz que depende de disciplina humana não é governança; é esperança. Ninguém acorda querendo violar a arquitetura; o acoplamento entra por mil pequenas decisões locais sob pressão, e o review humano não pega todas (o revisor também está cansado, também tem deadline).

A virada de chave da minha carreira como arquiteto foi parar de escrever regras em documentos e começar a escrever regras em código que roda no CI. Uma fitness function é isso: um teste automatizado cuja unidade sob teste não é uma função de negócio, é uma propriedade arquitetural ("nenhum módulo importa o interno de outro"). Se a propriedade é violada, o build quebra. A arquitetura passa a se defender sozinha — que é a única forma de arquitetura que sobrevive ao tempo e à pressão.

O conceito: governança que se auto-aplica

O termo fitness function vem de Building Evolutionary Architectures (Ford, Parsons, Kua). A ideia: definir, de forma objetiva e executável, o que significa "a arquitetura está saudável", e rodar essa verificação continuamente. Se você não consegue medir uma propriedade arquitetural automaticamente, você não consegue defendê-la — ela vai degradar na velocidade da pressão de prazo.

Tipos de fitness function (usaremos as duas primeiras nesta aula, as demais aparecem em outras):

  • De dependência: "módulo X não pode importar o interno de Y". (foco desta aula)
  • De estrutura: "todo módulo de domínio tem core/, http/, persistence/". (foco desta aula)
  • De métrica: complexidade ciclomática abaixo de N, cobertura acima de M.
  • De comportamento: testes de contrato, testes de camada.

O princípio comum: transforme a diretriz da wiki numa regra que quebra o build. A wiki não impede o import; o ESLint impede.

Fitness function de dependência com ESLint

Volte ao problema da aula Comunicação entre módulos: o padrão Public API Provider: queremos garantir que nenhum módulo importe o interno de outro — só a superfície pública (public/) é atravessável. Isso é expressável com no-restricted-imports usando padrões de caminho.

Configuração (ESLint flat config):

// eslint.config.js — regra: só o public/ de um módulo é importável de fora
export default [
  {
    files: ['src/module/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', {
        patterns: [
          {
            // proíbe importar o INTERNO de qualquer módulo (core, http, persistence)
            group: [
              '**/module/*/core/**',
              '**/module/*/http/**',
              '**/module/*/persistence/**',
            ],
            message:
              'Import cruzando fronteira de módulo. Use a Public API do módulo (module/<x>/public).',
          },
        ],
      }],
    },
  },
];

Com isso, import { SubscriptionEntity } from '../../billing/persistence/entity' quebra o lint — e, se o lint roda no CI (e ele roda), quebra o build. O dev recebe a mensagem na hora, com a orientação certa ("use a Public API"). A regra da aula Comunicação entre módulos: o padrão Public API Provider deixou de ser convenção e virou lei aplicada por máquina.

Refinando: um módulo não importa NADA de outro, exceto o public

Uma versão mais estrita usa a granularidade de "ninguém importa o interno; só public/ pode ser importado de fora do próprio módulo". Ferramentas dedicadas (como eslint-plugin-boundaries) tornam isso declarativo:

// com eslint-plugin-boundaries
settings: {
  'boundaries/elements': [
    { type: 'module-public',   pattern: 'src/module/*/public/**' },
    { type: 'module-internal', pattern: 'src/module/*/(core|http|persistence)/**' },
  ],
},
rules: {
  'boundaries/element-types': ['error', {
    default: 'disallow',
    rules: [
      // interno pode falar com interno DO MESMO módulo (mesma distância curta, aula *As dimensões do acoplamento: força, distância e tempo*)
      { from: 'module-internal', allow: ['module-internal'], /* mesmo módulo via matcher */ },
      // qualquer módulo pode consumir o PUBLIC de outro
      { from: ['module-internal', 'module-public'], allow: ['module-public'] },
    ],
  }],
},

O detalhe importante não é a sintaxe exata (varia por versão/plugin) — é o modelo mental: você declara os "tipos" de código e quais podem depender de quais. Isso codifica a regra da distância da aula As dimensões do acoplamento: força, distância e tempo diretamente no linter.

Fitness function de estrutura

Queremos também garantir a forma de cada módulo — que ela materialize DDD + Hexagonal (aula DDD estratégico + Arquitetura Hexagonal no FakeFlix). Isto é, todo módulo de domínio deve ter core/, http/, persistence/. Ferramentas como eslint-plugin-project-structure validam isso:

// regras de estrutura de projeto
{
  "rules": {
    "project-structure/folder-structure": "error",
    "project-structure/file-structure": "error"
  },
  "settings": {
    "projectStructure": {
      "structure": {
        "src/module/{domain}/": {
          "children": ["core/", "http/", "persistence/", "public/"]
        }
      }
    }
  }
}

Agora, criar um módulo payments/ sem a pasta public/, ou jogar um arquivo solto na raiz do módulo, quebra o build. A estrutura deixou de depender de o dev "lembrar do padrão" — ela é verificada. Isso protege o novo integrante do time tanto quanto o veterano cansado.

Fechando o cerco: Husky (pre-commit) + CI

Duas linhas de defesa, propositalmente redundantes:

Pre-commit (Husky) — feedback imediato, antes mesmo do push:

# .husky/pre-commit
npx lint-staged
// package.json — só linta o que mudou (rápido)
{
  "lint-staged": {
    "src/**/*.ts": ["eslint --max-warnings=0"]
  }
}

O dev tenta commitar um import proibido → o commit é barrado localmente, com a mensagem explicativa. A violação nunca chega ao repositório. Feedback em segundos, no menor loop possível.

CI — a rede de segurança que ninguém pula:

# .github/workflows/ci.yml (trecho)
- name: Fitness functions (arquitetura)
  run: npx eslint src --max-warnings=0

Por que ambos? Porque hooks locais podem ser pulados (git commit --no-verify). O CI é a autoridade final: nada entra na branch principal sem passar. O Husky otimiza o feedback; o CI garante a lei. Defesa em profundidade.

A mudança cultural: com isso, a revisão humana para de gastar energia em "você furou a fronteira aqui" (a máquina já pegou) e passa a discutir o que importa — se o design está certo, se a fronteira está no lugar certo. A automação não substitui o arquiteto; ela libera o arquiteto para pensar em vez de patrulhar.

Antes → Depois

Antes Depois
Fronteira documentada na wiki Fronteira verificada no CI
Import furado passa no review cansado Import furado quebra o build na hora
"Padrão de pastas" depende de memória Estrutura validada por regra
Arquiteto patrulha PRs Máquina patrulha; arquiteto projeta
Arquitetura degrada sob pressão de prazo Arquitetura resiste à pressão

Tradeoffs e limites (o que fitness functions NÃO fazem)

Honestidade — automação não é bala de prata:

  • Pegam a letra, não o espírito. O ESLint garante que Content não importa o interno de Billing. Não garante que o design está bom: Content pode chamar a Public API de forma burra, ou a fronteira pode estar no lugar errado. A máquina defende a regra; ela não decide se a regra é a certa. Isso continua sendo trabalho humano.
  • Custo de manutenção das regras. Regras muito rígidas geram atrito e falsos positivos que o time aprende a silenciar (// eslint-disable), e aí a governança morre por mil exceções. Regras devem ser poucas, valiosas e com mensagens que ensinam. Comece pela regra que dói mais (imports cruzando fronteira) e adicione só com evidência — é a aula Por que não existe arquitetura perfeita aplicada à própria governança.
  • Falso senso de segurança. "Passou no lint = arquitetura ok" é perigoso. Fitness function é piso, não teto. Ela impede regressões óbvias; não substitui pensar.
  • Escape hatches precisam de disciplina. Todo linter tem como desabilitar. Uma exceção legítima deve ser rara, comentada com o motivo e idealmente revisada. Exceções silenciosas viram a nova wiki ignorada.

O equilíbrio: automatize as regras objetivas e de alto valor (dependência, estrutura), e reserve o julgamento humano para o design. Fitness functions compram tempo e consistência; elas não compram bom gosto arquitetural.

Cola mental

  • Fitness function = teste executável de uma propriedade arquitetural; se falha, quebra o build.
  • Regra de ouro da governança: transforme a diretriz da wiki numa regra que quebra o build.
  • Dependência entre módulos: no-restricted-imports / eslint-plugin-boundaries proíbem importar o interno; só public/ atravessa (defende a aula Comunicação entre módulos: o padrão Public API Provider).
  • Estrutura: eslint-plugin-project-structure garante core/http/persistence/public (defende a aula DDD estratégico + Arquitetura Hexagonal no FakeFlix).
  • Husky = feedback rápido (pode ser pulado); CI = lei final (não pode). Use os dois.
  • Limite: automação pega a letra, não o espírito. Design continua sendo trabalho humano. Poucas regras, valiosas, com mensagens que ensinam.

Referências para Aprofundamento

O conceito

As ferramentas

  • ESLint — no-restricted-imports — A regra nativa que sustenta a maior parte das restrições de fronteira desta aula.
  • eslint-plugin-boundaries — Permite declarar os tipos de módulo e quais podem importar quais, que é a fitness function de dependência em forma declarativa.
  • dependency-cruiser — Valida e desenha o grafo de dependências; útil quando a regra é sobre o formato do grafo (ciclos, camadas) e não sobre um import específico.
  • Husky e lint-staged — O gancho de pre-commit e a execução só sobre o que mudou, para a checagem não virar espera.
  • ArchUnit — O equivalente no mundo Java/JVM, citado como referência de quão longe dá para levar o teste de arquitetura.

Exercícios

Exercício 1 — Escreva a regra

Você quer proibir que qualquer arquivo em src/module/content/** importe qualquer coisa de src/module/billing/ que não esteja em billing/public/. Escreva o bloco de no-restricted-imports (padrão + mensagem).

Ver resposta

✓ Resposta: javascript 'no-restricted-imports': ['error', { patterns: [ { group: ['**/module/billing/!(public)/**', '**/module/billing/!(public)'], message: 'content só pode importar de billing/public. Use a Public API de Billing.', }, ], }],

O padrão billing/!(public)/** casa qualquer subpasta de billing que não seja public (a sintaxe exata de negação pode variar por versão do ESLint/minimatch; a intenção é: bloquear tudo de billing exceto public/). A mensagem ensina o caminho certo, em vez de só negar.

Exercício 2 — Classifique a fitness function

Diga o tipo (dependência / estrutura / métrica / comportamento) de cada uma: (a) "complexidade ciclomática máxima 15 por função"; (b) "todo módulo tem pasta public/"; (c) "content não importa billing/persistence"; (d) "existe teste de contrato para cada método da Public API".

Ver resposta

✓ Resposta: (a) Métrica — limita um número mensurável (complexidade ciclomática). (b) Estrutura — valida a forma de pastas. (c) Dependência — regra de import entre módulos. (d) Comportamento — exige que exista um teste de contrato validando o comportamento da API.

Exercício 3 — Husky vs. CI

Um dev diz: "se temos o hook de pre-commit do Husky barrando imports proibidos, o check no CI é redundante, vamos remover pra acelerar". Você concorda? Justifique com base em como hooks locais podem ser contornados.

Ver resposta

✓ Resposta: Não concordo. Hooks de pre-commit rodam na máquina do dev e podem ser pulados trivialmente com git commit --no-verify, além de dependerem de o Husky estar instalado corretamente naquele clone (nem sempre está — clone novo, ambiente de CI de outra pessoa, etc.). Ou seja, o Husky é uma conveniência de feedback rápido, não uma garantia. O check no CI é a autoridade que não pode ser contornada: nenhum PR entra na branch protegida sem passar. Remover o CI transferiria a garantia da arquitetura para a boa vontade de cada dev em não usar --no-verify — que é exatamente o tipo de "governança por esperança" que a aula inteira combate. Mantenha os dois: Husky para velocidade, CI para lei.

Exercício 4 — O limite da automação

Sua fitness function de dependência está verde: nenhum módulo importa o interno de outro. Mesmo assim, um sênior olha o código e diz "a arquitetura está degradando". Cite dois problemas arquiteturais reais que passariam por essa fitness function sem serem detectados.

Ver resposta

✓ Resposta: Dois exemplos (bastam dois): - Fronteira no lugar errado / má modelagem. As regras garantem que ninguém fura as fronteiras existentes, mas não que as fronteiras estejam certas. Se billing e content deveriam ter um terceiro contexto entre eles (ou se um conceito foi posto no módulo errado), a fitness function fica verde enquanto o design está errado. Isso é DDD estratégico (aula DDD estratégico + Arquitetura Hexagonal no FakeFlix), julgamento humano. - Acoplamento via contrato inchado / provider anêmico. Content pode respeitar a regra (só importa billing/public) e ainda assim a Public API pode estar vazando o modelo interno (ex.: retornando uma estrutura que espelha a entity — stamp coupling da aula As dimensões do acoplamento: força, distância e tempo). O import é "legal", mas o acoplamento semântico é forte. A máquina vê o caminho do import, não a qualidade do contrato. - (Também válido: acoplamento temporal — Content e Billing acoplados por ordem/eventos sem import nenhum; ou baixa coesão dentro de um módulo que respeita todas as pastas.) Moral: fitness function é piso, não teto.

Exercício 5 — Desafio: a regra que dá falso verde

Um time escreveu esta fitness function para impedir que qualquer módulo importe o interior de outro:

// .eslintrc.js
rules: {
  'no-restricted-imports': ['error', {
    patterns: ['**/billing/core/**', '**/billing/persistence/**'],
  }],
}

O CI está verde há três meses. Mesmo assim, na semana passada uma mudança de coluna em Billing quebrou Content em produção. Aponte três falhas dessa regra e diga o que fazer.

Ver resposta

✓ Resposta: (1) A regra cobre um módulo só. Ela protege o interior de billing/, e nada impede billing/ de importar content/core/**, nem identity/ de importar qualquer um dos dois. Fronteira defendida em uma direção só não é fronteira: basta a dependência nascer do outro lado para o cerco furar, e foi assim que o acoplamento entrou sem ninguém ver. A regra tem que ser simétrica e por módulo, declarando para cada um quem pode importar o quê — que é o que o eslint-plugin-boundaries faz melhor do que uma lista de padrões escrita à mão. (2) A regra também vale para o próprio módulo. no-restricted-imports com esse padrão bloqueia billing/http/ de importar billing/core/ — que é exatamente o que ele deve fazer. Na prática, ou o time desligou a regra com eslint-disable nos arquivos internos de Billing (e cada disable desses vira um buraco permanente), ou usou caminho relativo para escapar dela. Isso leva à terceira falha. (3) O padrão só casa import por caminho absoluto/alias. import { X } from '../../billing/core/service' — relativo — passa direto, porque a string não bate com **/billing/core/** em muitas configurações. A regra dá verde sobre um import que ela deveria barrar, e é o pior tipo de fitness function: a que produz confiança sem produzir garantia. O que fazer: declarar os módulos e as relações permitidas com eslint-plugin-boundaries (ou dependency-cruiser), cobrindo os três bounded contexts nos dois sentidos e normalizando caminho relativo e alias; permitir explicitamente o acesso de http/ e persistence/ ao core/ do próprio módulo; e acrescentar um teste que verifique a fitness function — um arquivo de fixture com um import proibido que o lint precisa recusar. Sem esse último passo, ninguém descobre que a regra parou de pegar; a aula insiste nisso porque uma regra silenciosamente quebrada é pior do que regra nenhuma: ela remove a vigilância humana sem colocar nada no lugar.