Trilha 2 · Modularização e complexidade (o coração do curso) · ~20 min
Um time me chamou para "ajudar a resolver a lentidão do sistema". Passei duas semanas achando que o problema era performance. Não era. O problema era que ninguém entendia o sistema inteiro. Cada dev conhecia seu pedaço; mudanças exigiam reuniões de arqueologia. Uma feature de 3 dias levava 3 semanas — não porque o código fosse lento, mas porque entendê-lo era lento.
Medimos uma coisa simples: para adicionar um campo numa tela, quantos arquivos um dev precisava ler e entender antes de escrever a primeira linha? A resposta era 23. Vinte e três arquivos na cabeça só para começar. Isso tem nome: carga cognitiva. E a maior parte dela era desnecessária — não vinha do problema do negócio, vinha das decisões de código. Essa distinção — o que é inerente ao problema vs. o que nós adicionamos — é o conceito mais importante deste curso.
As duas complexidades
Fred Brooks, em 1986, cortou o problema em dois:
- Complexidade essencial: a que vem do problema. Cobrar assinaturas recorrentes com dias de tolerância, prorata, impostos, reembolsos e inadimplência é genuinamente complexo — porque o negócio de cobrança é complexo. Você não pode "codar isso mais simples" sem simplificar o negócio. Ela é irredutível.
- Complexidade acidental: a que vem das nossas soluções. Um framework mal escolhido, cinco camadas de abstração para um CRUD, um estado global compartilhado, configuração espalhada em dez lugares, nomes ruins. Nada disso é exigido pelo problema. Nós adicionamos.
A frase que resume: você não escolhe a complexidade essencial (ela vem com o negócio); você escolhe quanta complexidade acidental vai empilhar por cima. O trabalho de arquitetura é minimizar a acidental para que a energia do time seja gasta na essencial (que é onde está o valor).
Repare como isso conecta com a aula Por que não existe arquitetura perfeita: over-engineering é complexidade acidental adicionada por medo. Arquitetura que quebra é complexidade acidental que emergiu de fronteiras ausentes. Os dois vilões são fábricas de complexidade acidental.
Carga cognitiva: a métrica que importa
Complexidade é abstrata; carga cognitiva é o efeito dela na cabeça de quem programa. Carga cognitiva é a quantidade de coisas que você precisa segurar na mente ao mesmo tempo para entender ou mudar um trecho de código. A pesquisa clássica (Miller) diz que a memória de trabalho segura ~4 a 7 "pedaços" de cada vez. Quando o código exige que você segure 23 arquivos, você estoura o buffer — e aí vêm os bugs, a lentidão e o medo de mexer.
Três coisas inflam carga cognitiva sem precisar:
- Acoplamento (aula As dimensões do acoplamento: força, distância e tempo): para entender A, você precisa entender B, C e D, porque A depende deles de formas não óbvias.
- Baixa coesão (aula Coesão: o outro lado da moeda): coisas relacionadas estão espalhadas; você caça pedaços em cinco lugares para montar o quadro.
- Indireção sem propósito: camadas que não fazem nada além de repassar (os onze arquivos da aula Por que não existe arquitetura perfeita). Cada salto é um item a mais no buffer.
O objetivo da modularização é fechar a carga cognitiva dentro de fronteiras: para mexer em Billing, você deveria precisar entender Billing — e só Billing. Se para mexer em Billing você precisa entender Content e Identity, a fronteira falhou.
Teste prático: pegue uma tarefa média e conte quantos arquivos um dev precisa ler e compreender antes de escrever a primeira linha útil. Esse número é seu termômetro de carga cognitiva. Acompanhe-o ao longo do tempo — se ele sobe, sua arquitetura está degradando.
Fontes comuns de complexidade acidental (com código)
Fonte 1 — Estado compartilhado mutável
// ❌ acidental: um objeto global que qualquer um muta
export const appState = { currentUser: null, cart: [], featureFlags: {} };
// em algum lugar de billing:
appState.cart.push(item);
// em algum lugar de content, 40 arquivos depois:
if (appState.currentUser?.plan === 'premium') { /* ... */ }
Para entender qualquer trecho, você precisa saber quem mais mexe em appState — potencialmente o sistema inteiro. Carga cognitiva ilimitada. A correção é encapsular estado dentro do módulo dono e expor só contratos (aula Comunicação entre módulos: o padrão Public API Provider).
Fonte 2 — Condicionais que codificam regras implícitas
// ❌ acidental: a regra de negócio virou um labirinto de flags
function canWatch(user, video) {
if (user.plan === 'premium' || (user.plan === 'basic' && !video.isPremium)) {
if (user.country !== 'BR' || video.licensedRegions.includes(user.country)) {
if (!user.isSuspended && (user.trialEndsAt > Date.now() || user.isPaying)) {
return true;
}
}
}
return false;
}
Cada if aninhado é um item no buffer. Ninguém consegue afirmar com segurança o que essa função faz sem simular casos na cabeça. A complexidade essencial aqui (regras de licenciamento e plano existem de verdade) é real, mas a forma acidental (tudo espremido numa função aninhada, sem nomes) multiplica a carga. Corrige-se dando nomes às regras:
// ✅ a essência aparece; a carga cognitiva cai
function canWatch(user, video) {
return hasPlanAccess(user, video)
&& isLicensedInUserRegion(user, video)
&& hasActiveEntitlement(user);
}
Mesma regra de negócio, mesma complexidade essencial — mas agora você lê a intenção em três linhas e só desce ao detalhe do pedaço que precisa mudar.
Fonte 3 — Abstração prematura
// ❌ acidental: uma fábrica abstrata para... criar um objeto
abstract class NotificationStrategyFactory {
abstract create(): NotificationStrategy;
}
class EmailNotificationStrategyFactoryImpl extends NotificationStrategyFactory { /* ... */ }
// ...e só existe UM tipo de notificação no sistema.
Isso é o Vilão B (aula Por que não existe arquitetura perfeita). A abstração não isola variação real — não há variação. Ela só adiciona camadas que o leitor precisa atravessar. Regra: abstraia sobre variação que existe, não sobre variação que você imagina.
O alvo certo: acidental, não essencial
Um erro comum de quem aprende esse conceito é tentar "simplificar" a complexidade essencial — e aí você quebra o negócio (esquece a prorata, ignora o imposto, some com o caso de borda que dá processo judicial). Complexidade essencial não se elimina; se organiza. Você a coloca em um lugar coeso, com bons nomes, testada, atrás de uma fronteira clara — para que ela seja gerenciável, não invisível.
O trabalho é cirúrgico: separar as duas. Diante de um trecho complexo, pergunte de cada pedaço: "isso é exigido pelo problema do negócio, ou é uma consequência da minha solução?" O primeiro você preserva e organiza; o segundo você elimina.
Tradeoffs e armadilhas
- "Reduzir carga cognitiva" pode virar fragmentação. Quebrar tudo em micro-arquivos de 5 linhas parece reduzir complexidade local, mas aumenta a global: agora você salta por 30 arquivos. Isso é acoplamento temporal/de distância (aula As dimensões do acoplamento: força, distância e tempo). O ótimo não é "menor pedaço possível", é "pedaço que cabe na cabeça sozinho".
- Simplicidade tem custo de flexibilidade. Às vezes uma abstração legítima adiciona carga local para reduzir carga global (ex.: uma porta que isola o banco). O critério de novo é evidência: a variação existe?
- Nem toda indireção é acidental. A indireção da porta hexagonal (aula DDD estratégico + Arquitetura Hexagonal no FakeFlix) paga a carga que cobra, porque isola variação real. A indireção dos onze mappers da aula Por que não existe arquitetura perfeita não paga nada. Saber diferenciar é a habilidade.
Cola mental
- Essencial vem do problema (irredutível, organize-a). Acidental vem da sua solução (elimine-a).
- Over-engineering e arquitetura-que-quebra são fábricas de complexidade acidental.
- Carga cognitiva = quantas coisas você segura na mente para mudar um trecho. É a métrica real de manutenibilidade.
- Termômetro prático: nº de arquivos que um dev lê antes da primeira linha útil.
- Modularização = fechar a carga cognitiva dentro de fronteiras. Mexer em Billing deve exigir entender só Billing.
- Fragmentar demais também aumenta carga (global). O alvo é "cabe na cabeça", não "o menor possível".
Referências para Aprofundamento
Os dois ensaios fundadores
- No Silver Bullet: Essence and Accidents of Software Engineering — Fred Brooks — De 1986, e é a origem da distinção que a aula inteira usa. Doze páginas que continuam valendo mais do que a maioria do que se escreveu depois.
- Out of the Tar Pit — Ben Moseley e Peter Marks — Retoma Brooks e argumenta que estado mutável compartilhado é a maior fonte isolada de complexidade acidental. Denso, mas a primeira metade é acessível.
Carga cognitiva como métrica
- Team Topologies — Matthew Skelton e Manuel Pais — Trata carga cognitiva do time como restrição de projeto, não como queixa. É de onde vem a ideia de dimensionar o software pelo que a equipe consegue sustentar.
- A Philosophy of Software Design — John Ousterhout — Nomeia os sintomas com precisão: "change amplification", "cognitive load" e "unknown unknowns". Os capítulos 2 e 4 são os mais próximos desta aula.
- Cognitive Load — Team Topologies — Resumo curto e gratuito dos três tipos de carga, útil para levar para uma discussão de time.
Exercícios
Exercício 1 — Classifique
Para cada item, diga se é complexidade essencial ou acidental: (a) regras de imposto por estado numa cobrança brasileira; (b) três ORMs diferentes no mesmo projeto por decisões históricas; (c) o fato de uma assinatura ter estados (trial, ativa, inadimplente, cancelada); (d) configuração de banco lida de env vars, de um arquivo JSON e de um serviço remoto, misturados.
Ver resposta
✓ Resposta: (a) Essencial — o negócio de cobrança no Brasil tem imposto por estado; é irredutível. (b) Acidental — três ORMs não é exigido pelo problema; é dívida de decisões históricas. (c) Essencial — o ciclo de vida de uma assinatura é do negócio; não dá para "simplificar" tirando estados sem quebrar regras reais. (d) Acidental — config poderia vir de uma fonte única; espalhá-la em três lugares é solução, não problema.
Exercício 2 — Estime a carga
Duas implementações da mesma feature. Na A, mexer numa regra de preço exige entender 3 arquivos coesos dentro de billing/. Na B, a mesma regra está espalhada em um controller, um "helper" global, um "utils", um trigger de banco e um front-end. Qual tem menor carga cognitiva e por quê? O que isso diz sobre coesão?
Ver resposta
✓ Resposta: A tem menor carga cognitiva. Na A, tudo que importa para a regra está junto e dentro de uma fronteira (billing/), então o dev segura 3 itens coesos. Na B, os 5 lugares estão espalhados por camadas e tecnologias diferentes, forçando o dev a caçar e correlacionar pedaços distantes — carga alta e frágil (mudar um lugar e esquecer o trigger = bug). Isso mostra que baixa coesão infla carga cognitiva: quando o que muda junto não está junto, entender exige reconstruir o quebra-cabeça toda vez (gancho para a aula Coesão: o outro lado da moeda).
Exercício 3 — Separe o joio
No canWatch aninhado da Fonte 2, aponte o que é essencial (não pode sumir) e o que é acidental (a forma que inflou a carga). Reescreva a assinatura de forma que a essência apareça.
Ver resposta
✓ Resposta: Essencial: existem de fato três regras de negócio — acesso por plano, licenciamento por região e entitlement ativo (trial/pagante/não suspenso). Isso não pode sumir; são regras reais de um streaming. Acidental: a forma — três níveis de if aninhados, condições booleanas cruas sem nome, tudo numa função. Isso multiplica a carga sem adicionar valor. Reescrita que expõe a essência:
function canWatch(user, video) {
return hasPlanAccess(user, video)
&& isLicensedInUserRegion(user, video)
&& hasActiveEntitlement(user);
}
As três regras essenciais viram três nomes; o detalhe fica em funções pequenas e testáveis.
Exercício 4 — Ache a abstração prematura
Você encontra IUserIdProvider, uma interface com um único método getUserId(): string e uma única implementação que retorna req.user.id. Ela nunca terá segunda implementação. Isso é complexidade essencial ou acidental? O que você faria?
Ver resposta
✓ Resposta: Acidental. É abstração prematura (Vilão B): uma porta que não isola variação alguma, porque não há nem haverá segunda implementação. Ela só adiciona um salto de indireção que o leitor precisa atravessar. O que fazer: remover a interface e usar req.user.id diretamente (ou um helper simples e concreto). Guarde o critério: porta/abstração se justifica por isolar variação real (bancos, gateways, protocolos) — não por reflexo de "toda dependência precisa de interface".
Exercício 5 — Desafio: separe a conta
Um time reclama que o módulo de cálculo de frete "é complexo demais" e pede autorização para reescrevê-lo. Levantando o que existe, você encontra:
- sete transportadoras, cada uma com sua tabela de preço, prazo e regra de área de cobertura;
- quatro tipos de embalagem que mudam a cubagem;
- uma classe
FreteHelpercom 40 métodos estáticos, sem estado, usada por todo o módulo; - regras fiscais diferentes por estado de destino;
- três formatos de configuração para a mesma coisa (YAML, variável de ambiente e uma tabela no banco), porque cada dev que passou preferiu um;
- um retorno em
anyque obriga cada chamador a adivinhar o formato.
Classifique cada item em essencial ou acidental, e diga o que a reescrita proposta vai — e não vai — resolver.
Ver resposta
✓ Resposta: Essenciais (1, 2, 4). Sete transportadoras com regras próprias, cubagem por embalagem e regra fiscal por estado vêm do problema, não do código: nenhuma reescrita faz o Brasil ter uma regra fiscal só. Esses itens podem ser organizados melhor — uma estratégia por transportadora, tabelas de dados em vez de ifs —, mas a quantidade de regra permanece. Acidentais (3, 5, 6). O FreteHelper com 40 métodos estáticos é coesão lógica: um balaio que obriga a ler tudo para achar qualquer coisa. Os três formatos de configuração são complexidade pura de decisão não tomada — três caminhos para o mesmo dado, três lugares onde procurar quando o valor está errado. O retorno any empurra o custo para todo chamador, que precisa inferir o formato lendo a implementação: é change amplification garantido. O que a reescrita resolve: só os itens 3, 5 e 6 — e nenhum deles exige reescrita. Consolidar configuração num só lugar, tipar o retorno e quebrar o helper por responsabilidade são refatorações incrementais, feitas com o sistema no ar. O que ela não resolve: os itens 1, 2 e 4 reaparecem inteiros do outro lado, agora sem os bugs sutis que os anos já corrigiram — é o argumento contra o big bang rewrite. O diagnóstico correto para o time: "o módulo não é complexo demais, está com complexidade acidental identificável; vamos atacar essas três e medir de novo". Reescrever pagaria o custo de reconstruir a parte essencial para eliminar a acidental — que era a parte barata de eliminar.