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
- On the Criteria To Be Used in Decomposing Systems into Modules — David Parnas — De 1972, e é a origem de quase tudo nesta lista: decompor por decisão de projeto escondida, não por etapa de processamento. Sete páginas, leitura obrigatória.
- A Philosophy of Software Design — John Ousterhout — Reformula Parnas para hoje: módulo bom é aquele cuja interface é muito menor que a implementação.
Os princípios em escala
- Fundamentals of Software Architecture — Mark Richards e Neal Ford — O capítulo de acoplamento de componentes e as "características arquiteturais" cobrem escala individual, substituibilidade e testabilidade com vocabulário compatível com o desta aula.
- Building Evolutionary Architectures — Neal Ford, Rebecca Parsons e Patrick Kua — Trata evolutibilidade como característica projetada e mensurável, não como consequência feliz.
- Modular Monolith: A Primer — Kamil Grzybek — Série detalhada sobre monolito modular na prática, com o mesmo espírito: fronteira lógica forte, deploy único.
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.