Trilha 2 · Modularização e complexidade · ~30 min · pleno/sênior

Toda vez que apresento "os 10 princípios" para um time, alguém revira os olhos: "mais uma listinha de arquiteto". Eu entendo. A internet está cheia de listas de princípios que ninguém aplica — SOLID virou tatuagem, "os 12 fatores" viram slide. O problema das listas não é o conteúdo; é que elas são ensinadas como mandamentos abstratos, sem o antídoto e sem a fatura.

Então vou fazer diferente. Cada princípio abaixo vem com três coisas: (1) o que ele diz, em uma frase; (2) o exemplo aplicado no FakeFlix — como ele se parece no código quando está certo; e (3) o anti-exemplo — o cheiro concreto de quando ele foi violado, porque você reconhece um princípio muito mais rápido pela ausência dele do que pela presença. E, no fim, o mais importante: os 10 não são independentes. Eles são facetas de uma mesma decisão — traçar boas fronteiras — vistas de dez ângulos. Quando você acerta as fronteiras (aula DDD estratégico + Arquitetura Hexagonal no FakeFlix), quase todos os dez caem no lugar juntos. Quando erra, eles quebram em cascata.

Os 10 princípios

Princípio 1 — Well-Defined Boundaries (fronteiras bem definidas)

Em uma frase: cada módulo tem uma fronteira explícita que define claramente o que é dentro e o que é fora.

Uma fronteira não é só uma pasta — é um contrato sobre o que atravessa. Sem fronteira explícita, tudo é público e nada é protegido (aula Comunicação entre módulos: o padrão Public API Provider). A fronteira bem definida responde: "o que este módulo promete ao mundo, e o que ele guarda para si?".

Aplicado (FakeFlix): o módulo billing/ expõe uma pasta public/ com uma API mínima e mantém core/, http/, persistence/ como interno inacessível de fora (via exports do módulo — aula Comunicação entre módulos: o padrão Public API Provider).

// ✅ a fronteira é EXPLÍCITA: só isto sai do módulo
@Module({
  providers: [SubscriptionService, TypeOrmSubscriptionRepository, /* interno */],
  exports: [BILLING_SUBSCRIPTION_STATUS_API], // ⬅️ a fronteira, em código
})
export class BillingModule {}

Anti-exemplo (o cheiro):

// ❌ sem fronteira: content alcança o miolo de billing
import { SubscriptionEntity } from '../../billing/persistence/entity/subscription.entity';

Se você consegue importar a entity do vizinho, não existe fronteira — existe uma sugestão de pasta. O cheiro é o import que atravessa core/, http/ ou persistence/ de outro módulo.

Princípio 2 — Explicit Communication (comunicação explícita)

Em uma frase: módulos se comunicam apenas por contratos declarados, nunca por atalhos escondidos (estado global, tabela do vizinho, import interno).

Aplicado: Content pergunta a Billing via a Public API — um contrato nomeado, versionável, óbvio no código.

// ✅ a comunicação está DECLARADA e visível
const active = await this.billingApi.isUserSubscriptionActive(userId);

Anti-exemplo:

// ❌ comunicação implícita via estado global — ninguém "vê" a dependência
if (globalState.currentUserPlan === 'premium') { /* content depende de quem setou isso? */ }

O cheiro é a dependência que não aparece na assinatura de nada: um global mutável, um SELECT na tabela alheia, uma coincidência de ordem de execução (acoplamento temporal — aula As dimensões do acoplamento: força, distância e tempo). Se a dependência é real mas invisível, ela é implícita — e implícito é onde os bugs moram.

Princípio 3 — Independence (independência)

Em uma frase: um módulo pode ser entendido, desenvolvido, testado e mudado sem exigir conhecimento do interior dos outros.

Independência é a métrica de carga cognitiva (aula Complexidade essencial vs. acidental e carga cognitiva) aplicada a módulos: para mexer em Billing, você precisa entender Billing — e só.

Aplicado: você abre billing/, entende o ciclo de assinatura, e muda a regra de tolerância sem nunca abrir content/ ou identity/. Os testes de Billing rodam sem subir Content.

Anti-exemplo:

// ❌ para testar canWatch de content, preciso montar meio billing
it('libera play', async () => {
  const billingModule = new BillingModule(realDb, realGateway, realInvoices); // 😱
  const content = new ContentModule(billingModule.internals.subscriptionService);
  // ...
});

O cheiro é: "não consigo testar/rodar/entender A sem instanciar B por dentro". Se o teste de um módulo precisa montar o miolo de outro, a independência morreu.

Princípio 4 — Composability (componibilidade)

Em uma frase: módulos são peças que se combinam de formas diferentes para formar aplicações diferentes, sem precisar mudar por dentro.

Este é o princípio que paga lá na frente (aulas Quando (e quando NÃO) migrar para microserviços e Monorepos modulares com Nx: separando limite lógico de limite de deploy): se os módulos são componíveis, você monta um deploy "só streaming" com identity + content, e um deploy "processador" com só content/video-processor, reusando as mesmas peças.

Aplicado:

// ✅ o mesmo módulo Billing entra em composições diferentes sem mudar
@Module({ imports: [IdentityModule, BillingModule, ContentModule] })
export class MonolithModule {}   // app completo

@Module({ imports: [IdentityModule, ContentModule] })
export class StreamingOnlyModule {}  // app só de streaming — Billing nem entra

Anti-exemplo: um módulo que só funciona se outro específico estiver presente e configurado de um jeito — ele não compõe, ele exige um arranjo fixo. O cheiro é o if (process.env.APP === 'monolith') dentro do módulo: ele está ciente da composição, então não é componível.

Princípio 5 — Individual Scale (escala individual)

Em uma frase: cada módulo pode ser escalado (e ter recursos alocados) de forma independente dos outros, conforme sua própria demanda.

Aplicado: o subdomínio video-processor (pesado em CPU) roda como workers escaláveis via fila (aula Subdomínios e comunicação assíncrona (BullMQ): mensagens vs. eventos); você sobe 12 réplicas dele sem tocar em catalog ou admin.

# ✅ escala só o que precisa
kubectl scale deployment video-processor-worker --replicas=12

Anti-exemplo: o processamento de vídeo roda dentro do processo web, então para dar conta dos picos de upload você precisa escalar a aplicação inteira (incluindo o catálogo, que nem estava sob pressão) — desperdiçando recurso e ainda arriscando derrubar o resto. O cheiro é: "não dá para dar mais máquina só para a parte que está sofrendo".

Princípio 6 — State Isolation (isolamento de estado)

Em uma frase: cada módulo é o dono exclusivo do seu estado; ninguém lê ou escreve o dado do outro diretamente.

Este é o princípio que a aula Persistência e State Isolation aprofunda. Sem ele, "fronteira de código" é teatro — porque quem quiser fura tudo por um SELECT cross-tabela no banco.

Aplicado: Billing é o único que toca as tabelas de billing (schema próprio, migrações próprias). Content, para saber o status, pergunta (Public API), não faz join.

Anti-exemplo:

-- ❌ content furando o estado de billing direto no banco
SELECT v.*, s.status
FROM content.videos v
JOIN billing.subscriptions s ON s.user_id = v.owner_id;  -- join cruzando fronteira

O cheiro é o join (ou o repositório) que atravessa domínios. Ele acopla Content ao schema de Billing pela pior via possível — a do banco, que nenhuma fitness function de import pega (aula Governança automatizada: fitness functions com ESLint + Husky).

Princípio 7 — High Cohesion (alta coesão / responsabilidade única)

Em uma frase: tudo dentro de um módulo pertence junto; o módulo tem uma razão de existir e um ator que pede suas mudanças (aula Coesão: o outro lado da moeda).

Aplicado: billing/ contém tudo (e só) o que muda quando as regras de cobrança mudam. Nada de "utilitário de vídeo" morando lá.

Anti-exemplo: o SharedModule que vira sótão — validações de três domínios, helpers de data, cliente HTTP e gerador de PDF juntos (o UtilsService da aula Coesão: o outro lado da moeda). O cheiro é o módulo cujo nome é genérico (common, shared, core no sentido de "diversos") e cujo conteúdo não tem um ator só.

Princípio 8 — Loose Coupling (acoplamento fraco)

Em uma frase: as dependências entre módulos são as mais fracas possíveis — idealmente data coupling (troca do dado mínimo) a distância grande (aula As dimensões do acoplamento: força, distância e tempo).

Aplicado: Content passa userId, recebe boolean. Não conhece o formato de Subscription.

Anti-exemplo: a Public API que retorna a SubscriptionEntity inteira (stamp coupling), ou o método que recebe flags de controle (billing.run(id, { dryRun, skipCache }) — control coupling). O cheiro é o contrato que expõe estrutura interna ou pede que o chamador conheça o "como" do outro.

Princípio 9 — Testability in Isolation (testabilidade isolada)

Em uma frase: cada módulo é testável sozinho, com suas dependências externas substituídas por fakes nas portas (aula DDD estratégico + Arquitetura Hexagonal no FakeFlix).

Testabilidade não é um bônus — é o sintoma observável de que os outros princípios estão de pé. Se você consegue testar um módulo isolado em milissegundos, é porque ele tem fronteira (1), comunica-se por contrato (2), é independente (3) e desacoplado (8). Se não consegue, algum deles quebrou.

Aplicado:

// ✅ testa a regra de billing sem banco, sem HTTP, sem content
const service = new SubscriptionService(fakeRepo);
expect(await service.isUserSubscriptionActive('u1')).toBe(false);

Anti-exemplo: o teste que precisa de docker-compose up com Postgres + Redis + o serviço de billing só para verificar uma regra de content. O cheiro é o "teste unitário" que leva 30 segundos e sobe meia infraestrutura.

Princípio 10 — Evolvability / Replaceability (evolutibilidade / substituibilidade)

Em uma frase: um módulo pode ser reescrito, substituído ou extraído (para um serviço) sem forçar mudança nos outros — porque eles dependem do contrato, não da implementação.

Este é o princípio que fecha o curso: é ele que torna a arquitetura evolutiva (aula Quando (e quando NÃO) migrar para microserviços). Ele é a consequência de todos os outros nove bem feitos.

Aplicado: trocar a implementação da Public API de Billing de local para remota (extração) não muda uma linha de Content (aulas Comunicação entre módulos: o padrão Public API Provider e Quando (e quando NÃO) migrar para microserviços).

// ✅ mesma interface, implementação substituível — o resto do sistema nem percebe
{ provide: BILLING_SUBSCRIPTION_STATUS_API, useClass: HttpBillingProvider }

Anti-exemplo: o "monolito modular" em que extrair qualquer módulo exige desemaranhar joins de banco, imports internos e estado compartilhado — ou seja, uma reescrita. O cheiro é: "a gente não consegue separar isso sem parar tudo por três meses".

O insight que amarra os 10: é tudo a mesma decisão

Releia os anti-exemplos e note o padrão: quase todos derivam de uma fronteira mal traçada ou mal defendida. O import interno (viola 1), o estado global (viola 2), o join cross-tabela (viola 6), o teste que sobe tudo (viola 9), a extração impossível (viola 10) — são todos a mesma doença vista de ângulos diferentes: fronteiras que não existem ou não são respeitadas.

Isso tem uma consequência prática enorme: você não precisa gerenciar 10 checklists. Você precisa acertar as fronteiras (DDD estratégico + Hexagonal, aula DDD estratégico + Arquitetura Hexagonal no FakeFlix), defendê-las (Public API + State Isolation, aulas Comunicação entre módulos: o padrão Public API Provider e Persistência e State Isolation), automatizar a defesa (fitness functions, aula Governança automatizada: fitness functions com ESLint + Husky) — e os 10 princípios emergem juntos. Da mesma forma, quando você fura uma fronteira, costuma violar 4 ou 5 princípios de uma vez. Eles não são dez leis; são dez sintomas de uma saúde só.

Como usar de verdade: os 10 não são para "seguir" um a um. São um vocabulário de diagnóstico. Quando algo no design incomoda mas você não sabe nomear, percorra os 10 — um deles vai dar nome ao cheiro, e o nome aponta a fronteira que precisa de conserto.

Tabela de bolso: princípio × onde ele vive no curso

# Princípio Aula que aprofunda Cheiro da violação
1 Well-Defined Boundaries 02, 08 import atravessa core/http/persistence alheio
2 Explicit Communication 08 estado global, dependência invisível
3 Independence 04 não testo/entendo A sem montar B
4 Composability 14, 16 if (APP === 'monolith') dentro do módulo
5 Individual Scale 13 escalo tudo para socorrer uma parte
6 State Isolation 09 join/repositório cruzando domínio
7 High Cohesion 06 SharedModule/UtilsService sótão
8 Loose Coupling 05 contrato expõe entity ou pede flags
9 Testability in Isolation 02 "unitário" que sobe Docker
10 Evolvability 14 extrair = reescrever

Tradeoffs e armadilhas

  • Princípio não é dogma (aula Por que não existe arquitetura perfeita). Num MVP de uma semana, violar State Isolation (ler a tabela do vizinho) pode ser a decisão certa pela velocidade. Os 10 são o destino de um sistema que vingou, não a obrigação do dia zero. Aplique-os na proporção da maturidade e da dor.
  • Perseguir os 10 ao extremo recria a aula Coesão: o outro lado da moeda. Independência e fronteiras levadas fina demais geram fragmentação: micro-módulos que dependem uns dos outros para tudo. Os princípios têm um ponto de equilíbrio (coesão vs. acoplamento) — não são "quanto mais, melhor".
  • Individual Scale e State Isolation custam. Escala individual pede infra (filas, orquestração); isolamento de estado às vezes força duplicação de dado (Content mantém sua projeção de entitlement). Esses custos só valem com evidência de escala/autonomia (aulas Subdomínios e comunicação assíncrona (BullMQ): mensagens vs. eventos e Quando (e quando NÃO) migrar para microserviços).
  • Testability como termômetro, não como meta cega. Buscar testabilidade isolada é ótimo; mas cobrir 100% com testes que mockam tudo pode mascarar acoplamento real. O objetivo é o design saudável que torna o teste fácil — não o número de cobertura.

Cola mental

  • 10 princípios: Fronteiras definidas, Comunicação explícita, Independência, Componibilidade, Escala individual, Isolamento de estado, Alta coesão, Acoplamento fraco, Testabilidade isolada, Evolutibilidade.
  • Você reconhece um princípio mais rápido pela ausência (o anti-exemplo) do que pela presença.
  • Os 10 são facetas de uma decisão só: traçar e defender boas fronteiras. Acertou a fronteira → quase todos caem juntos. Furou uma → viola 4-5 de uma vez.
  • Use-os como vocabulário de diagnóstico, não como mandamentos. O nome do cheiro aponta a fronteira a consertar.
  • Aplique na proporção da maturidade (aula Por que não existe arquitetura perfeita); não persiga ao extremo (aula Coesão: o outro lado da moeda).

Referências para Aprofundamento

Modularidade como disciplina

Os princípios em escala

Exercícios

Exercício 1 — Nomeie o princípio violado

Para cada situação, diga qual(is) princípio(s) foi(ram) violado(s): (a) content faz SELECT ... JOIN billing.subscriptions; (b) o teste de identity precisa subir Redis e o módulo de billing; (c) a Public API de billing retorna SubscriptionEntity; (d) para escalar o processamento de vídeo nos picos, o time escala a aplicação web inteira; (e) extrair content para um serviço exige mudar 40 arquivos em billing e identity.

Ver resposta

✓ Resposta: (a) State Isolation (6) e Well-Defined Boundaries (1) — o join cruza a fronteira e toca o estado alheio; também Explicit Communication (2) (a dependência via banco é implícita) e Loose Coupling (8) (acopla ao schema). (b) Testability in Isolation (9) e Independence (3) — o teste de identity não roda sozinho. (c) Loose Coupling (8) (stamp coupling) e Well-Defined Boundaries (1) (vaza o interno). (d) Individual Scale (5) — não dá para escalar só a parte que sofre. (e) Evolvability (10) e Independence (3) — extração vira reescrita.

Exercício 2 — Um furo, vários princípios

Um dev adiciona import { SubscriptionService } from '../../billing/core/service/subscription.service' dentro de content. Liste todos os princípios que esse único import compromete e explique o encadeamento.

Ver resposta

✓ Resposta: Um único import de billing/core em content compromete, em cascata: Well-Defined Boundaries (1) — atravessou a fronteira interna de billing; Explicit Communication (2) — passou a depender do interno em vez do contrato público; Loose Coupling (8) — criou acoplamento forte a distância grande (content agora conhece a implementação de billing); Independence (3) — content deixou de ser entendível/testável sem billing por dentro; Testability in Isolation (9) — testar content agora exige o SubscriptionService real; e, por consequência, Evolvability (10) — reescrever ou extrair billing agora quebra content. Encadeamento: a violação de fronteira (1) puxa a comunicação implícita (2), que cria acoplamento forte (8), que destrói independência (3) e testabilidade (9), que inviabiliza a evolução (10). É a prova de que os 10 são sintomas de uma doença só.

Exercício 3 — Aplique o checklist

Você revisa um novo módulo recommendations/. Ele: (i) tem pasta public/ com um contrato mínimo; (ii) mantém sua própria tabela de scores; (iii) mas, para gerar recomendações, importa content/core/catalog.service diretamente e chama métodos internos dele. Quais princípios estão OK e qual está quebrado? Como corrigir?

Ver resposta

✓ Resposta: OK: Well-Defined Boundaries (1) e Explicit Communication (2) do lado de recommendations (tem public/), e State Isolation (6) parcial (tabela própria de scores). Quebrado: ao importar content/core/catalog.service e chamar métodos internos, recommendations viola Well-Defined Boundaries (1) e Explicit Communication (2) em relação a content — está furando a fronteira de content, não a sua. Consequentemente compromete Loose Coupling (8), Independence (3) e Testability (9). Correção: content deve expor uma Public API (ex.: ContentCatalogApi.getCatalogSnapshot() ou publicar eventos de catálogo — aulas Comunicação entre módulos: o padrão Public API Provider e Subdomínios e comunicação assíncrona (BullMQ): mensagens vs. eventos), e recommendations consome esse contrato em vez do catalog.service interno. Assim ambos os módulos ficam com fronteiras respeitadas nos dois sentidos.

Exercício 4 — Tradeoff consciente

Sua startup tem 4 devs e 2 meses de vida. Você decide, deliberadamente, que Content vai ler a tabela de Billing direto (violando State Isolation) para entregar mais rápido. Isso é errado? Justifique com a aula Por que não existe arquitetura perfeita e diga qual evidência futura te faria reverter a decisão.

Ver resposta

✓ Resposta: Não é errado — pode ser a decisão certa. Pela aula Por que não existe arquitetura perfeita, princípios são o destino de um sistema que vingou, não obrigação do dia zero; com 4 devs e 2 meses, o objetivo é validar o produto, e a velocidade de ler a tabela direto (Dose 0) pode superar o custo, que ainda é baixo (o sistema cabe na cabeça, billing quase não muda). O importante é que a decisão seja consciente e nomeada ("estamos violando State Isolation de propósito, pela velocidade"), não acidental. Evidência futura para reverter: quando billing começar a mudar de schema e essas mudanças quebrarem content (dor recorrente), ou quando o time crescer a ponto de haver conflito de quem mexe na tabela, ou quando você precisar escalar/extrair — qualquer uma dessas é o sinal medível (aula Por que não existe arquitetura perfeita) para subir para a Dose 1 (Public API) e restaurar o isolamento.

Exercício 5 — Testabilidade como termômetro

Explique por que "consigo testar este módulo isolado em milissegundos" é evidência de que os princípios 1, 2, 3 e 8 estão de pé. E por que o inverso ("meu teste unitário sobe Docker") aponta violação — de qual princípio, principalmente?

Ver resposta

✓ Resposta: "Testo isolado em ms" implica: existe uma fronteira clara do módulo (1) — senão eu precisaria arrastar código alheio; a comunicação com o exterior é por contrato/porta (2, e aula DDD estratégico + Arquitetura Hexagonal no FakeFlix) — senão eu não conseguiria substituir por fake; o módulo é independente (3) — roda sem os outros; e é fracamente acoplado (8) — depende de interfaces mínimas, não de implementações. Ou seja, a testabilidade isolada é o sintoma observável de que esses quatro estão de pé. O inverso — "meu unitário sobe Docker" — significa que o módulo não pode ser exercitado sem sua infraestrutura/vizinhos reais, apontando violação principalmente de Testability in Isolation (9), que por sua vez denuncia falha em Independence (3) e/ou Loose Coupling (8): se eu preciso do banco e do billing reais, é porque a regra está soldada à infra ou ao vizinho, não isolada atrás de portas.