Trilha 1 · Fundamentos e mentalidade · ~18 min · pleno/sênior
Início de 2019. Recebo um sistema para "só adicionar uma feature". Era um e-commerce com o que o time chamava de "Clean Architecture de verdade": cada caso de uso numa classe, entidades de domínio, DTOs de entrada, DTOs de saída, mappers em toda fronteira, interfaces para tudo — inclusive para coisas que nunca teriam uma segunda implementação.
Para adicionar um campo couponCode no checkout, precisei tocar em onze arquivos. Nenhum continha regra de negócio. Eram mappers, DTOs e interfaces repassando o mesmo dado de camada em camada. O sistema tinha três telas.
No mesmo mês, peguei o oposto: um monolito de um app.js com 14 mil linhas, tudo procedural, SQL concatenado com string, regra de negócio dentro da rota HTTP. Adicionar o mesmo couponCode também era um pesadelo — porque eu não sabia quais das 14 mil linhas eu ia quebrar.
Dois sistemas, dois extremos, a mesma dor. Um pagou o preço do excesso de estrutura; o outro, o da ausência dela. É aqui que a maioria dos cursos erra: te vende um dos lados como "a resposta certa". Este não vai. A resposta certa é uma pergunta: quanto custa, e o que ganho em troca?
"Melhor arquitetura" não existe. "Adequada" existe.
Toda decisão de arquitetura é uma troca. Você nunca ganha de graça; você redistribui custo.
| Você ganha… | …pagando com |
|---|---|
| Módulos independentes | Mais indireção, mais código de fronteira |
| Escala granular (por serviço) | Complexidade operacional (rede, deploy, observabilidade) |
| Consistência forte | Menos disponibilidade e mais contenção (locks) |
| Flexibilidade (abstrações) | Menos legibilidade imediata, mais camadas |
| Simplicidade de monolito | Acoplamento fácil de acontecer sem querer |
Nenhuma linha é "sempre boa". A escala granular que salva a Netflix afunda uma startup de 4 devs em complexidade operacional. A consistência forte obrigatória num débito bancário é desperdício de contenção num contador de likes.
Regra de bolso: quando alguém disser "a arquitetura X é melhor", complete: "…melhor para qual restrição?" Sem restrição no ar, é dogma, não engenharia.
O que muda a resposta certa é o contexto: tamanho/maturidade do time (4 vs. 120 devs muda tudo — microserviço existe para times autônomos deployarem sem se pisar; com um time só, você paga o custo sem colher o benefício); estágio do negócio (descoberta pede código descartável; produto maduro paga estrutura); e restrições reais (SLA, volume, latência, regulação), não imaginadas.
Arquitetura é a resposta a uma pergunta sobre restrições. Sem a pergunta, qualquer resposta é chute.
Os dois vilões são a mesma pessoa
Vilão A — Arquitetura que quebra. Mudança num módulo quebra outro sem aviso; não escala quando o negócio cresce; ninguém tem coragem de mexer no core. Isso é falta de estrutura: fronteiras inexistentes ou não defendidas, acoplamento crescendo solto.
Vilão B — Over-engineering por medo. Abstração sobre abstração "preparando para o futuro"; interfaces com uma única implementação; onze arquivos para um campo. Isso é excesso de estrutura: complexidade acidental para um problema que ainda não existe.
O nome do medo é sempre "e se um dia precisar?". A resposta madura: quando precisar, a evidência aparece, e aí você adiciona. Estrutura por especulação quase sempre é a errada, porque você projetou para um futuro imaginado.
Os dois vilões são o mesmo pecado: decidir sem contexto. O A decide "não preciso de fronteira" sem medir acoplamento. O B decide "vou precisar de flexibilidade" sem ter a dor.
O mesmo caso, três doses de estrutura
FakeFlix. Requisito: verificar se a assinatura está ativa antes do play.
Dose 0 — Direto (protótipo, MVP de 1 semana):
@Get('videos/:id/play')
async play(@Param('id') id: string, @Req() req) {
const sub = await this.db.query(
'SELECT status FROM subscriptions WHERE user_id = $1', [req.user.id],
);
if (sub?.status !== 'active') throw new ForbiddenException();
return this.videos.stream(id);
}
Content lê a tabela de Billing direto. Acoplamento péssimo a longo prazo, mas se o objetivo é validar o produto nesta semana, é adequado: rápido, legível, zero cerimônia. O custo só vira dor quando Billing evoluir — e talvez o produto nem chegue lá.
Dose 1 — Fronteira explícita (produto que vingou, monolito modular):
@Injectable()
export class BillingPublicApiProvider implements BillingSubscriptionStatusApi {
constructor(private readonly subscriptionService: SubscriptionService) {}
async isUserSubscriptionActive(userId: string): Promise<boolean> {
return this.subscriptionService.isUserSubscriptionActive(userId);
}
}
@Get('videos/:id/play')
async play(@Param('id') id: string, @Req() req) {
const active = await this.billingApi.isUserSubscriptionActive(req.user.id);
if (!active) throw new ForbiddenException();
return this.videos.stream(id);
}
Content não sabe mais que existe uma tabela subscriptions nem o que status='active' significa. Ele pergunta por um contrato. Billing pode trocar coluna, adicionar trial, mudar de banco — nada quebra em Content. Pagamos uma indireção, ganhamos uma fronteira defensável (padrão Public API Provider, aula Comunicação entre módulos: o padrão Public API Provider).
Dose 2 — Assíncrono/eventual (escala alta, times separados):
@OnEvent('billing.subscription.changed')
handleSubscriptionChanged(e: SubscriptionChangedEvent) {
this.entitlementCache.set(e.userId, e.active);
}
@Get('videos/:id/play')
async play(@Param('id') id: string, @Req() req) {
if (!this.entitlementCache.get(req.user.id)) throw new ForbiddenException();
return this.videos.stream(id);
}
Content reage a eventos e guarda o que precisa. Ganha independência em runtime (Billing pode cair que o play segue com o último status) e escala. Paga com consistência eventual (janela de status desatualizado) e complexidade (eventos, cache, invalidação). Só vale quando escala/autonomia justificam (aulas Subdomínios e comunicação assíncrona (BullMQ): mensagens vs. eventos e Event-Driven na prática: Transactional Outbox, idempotência e sagas).
O ponto: nenhuma é "a certa". Dose 2 num MVP é over-engineering. Dose 0 num sistema com 40 devs é arquitetura que quebra. A mesma linha muda de "boa" para "ruim" só de trocar o contexto.
Evolução incremental: o método que evita os dois vilões
As três doses são um caminho evolutivo, não opções excludentes. Comece na Dose 0. Quando o acoplamento doer (você mexe em Billing e quebra Content), você tem a evidência para a Dose 1. Quando a escala/autonomia doer, tem a evidência para a Dose 2.
O oposto disso é o big bang rewrite — parar tudo e "reescrever direito". Reescrita total quase sempre falha: joga fora conhecimento embutido (todos os bugs sutis já resolvidos) e aposta o negócio numa entrega de meses sem valor incremental.
Cole no monitor: Simples não é o oposto de bom. Simples é o ponto de partida de bom.
Como saber que é hora de subir de dose? Não é sentimento, é evidência: acoplamento medível aumentando (commits que tocam A frequentemente tocam B — dá pra extrair do git); dor recorrente (o mesmo bug de fronteira pela terceira vez); restrição real batendo (latência saindo do SLA). Se você não consegue nomear a evidência, provavelmente ainda não é hora.
Tradeoffs: quando este raciocínio NÃO se aplica
- Custo de reversão altíssimo: escolha de banco, contrato público com terceiros, esquema de dados regulatórios. Difícil voltar atrás → analise mais antes.
- Restrição conhecida e dura no dia zero: "processar 200 mil eventos/s desde o lançamento" já é evidência; começar num
app.jsnão é simples, é ingênuo. - Segurança e compliance: não se evolui incrementalmente rumo a "autenticação". É fundação.
Critério que une as exceções: quando errar e reverter é proibitivo, o investimento antecipado deixa de ser over-engineering e vira prudência. Continua sendo tradeoff — só que a conta pende para o outro lado.
Cola mental
- Não existe "melhor arquitetura", existe adequada ao contexto.
- Toda decisão é troca: ganha X pagando Y. Nomeie o Y.
- Arquitetura que quebra (falta) e over-engineering (excesso) são o mesmo erro: decidir sem contexto.
- Comece simples, suba de dose com evidência.
- Exceção: quando errar é caro demais de reverter, invista cedo.
Referências para Aprofundamento
Leitura de fundo
- Fundamentals of Software Architecture — Mark Richards e Neal Ford — A primeira lei dos autores é literalmente "tudo em arquitetura é um tradeoff". É o livro mais próximo do raciocínio desta aula.
- Design Stamina Hypothesis — Martin Fowler — O gráfico que explica por que "comece simples" não é desleixo: existe um ponto a partir do qual a falta de estrutura passa a custar mais do que a estrutura custaria.
- Sacrificial Architecture — Martin Fowler — Defende escrever código que você já sabe que vai jogar fora. É a Dose 0 com nome próprio.
Sobre reescrita e escolha de tecnologia
- Things You Should Never Do, Part I — Joel Spolsky — O texto clássico sobre por que o big bang rewrite quase sempre falha: você joga fora o conhecimento embutido nos bugs já resolvidos.
- Choose Boring Technology — Dan McKinley — A ideia de "fichas de inovação": você só tem algumas para gastar, e gastá-las onde não há restrição real é o Vilão B em forma de stack.
Exercícios
Exercício 1 — Nomeie o tradeoff
Um colega propõe: "vamos pôr uma fila entre todos os módulos, aí tudo fica desacoplado". Descreva o que se ganha e o que se paga. É adequado para um MVP de 3 devs validando a ideia?
Ver resposta
✓ Resposta: Ganha: desacoplamento temporal (não precisam estar no ar juntos), absorção de picos, resiliência (retry). Paga: consistência eventual, complexidade operacional enorme (broker para manter/monitorar, garantir entrega, idempotência, ordenação) e debug muito mais difícil de fluxos que antes eram uma chamada de função. Para MVP de 3 devs: não é adequado — over-engineering. Você paga toda a mensageria para desacoplar módulos que ainda cabem na cabeça de uma pessoa. Fila entra com evidência real de acoplamento temporal (aula As dimensões do acoplamento: força, distância e tempo) ou necessidade de absorver carga.
Exercício 2 — Classifique o vilão
Vilão A, Vilão B ou adequado ao contexto? Justifique:
(a) startup de 2 devs, semana 1, index.ts de 400 linhas servindo o MVP;
(b) sistema com 30 devs onde pedidos lê direto a tabela de estoque;
(c) CRUD interno de fornecedores com repositório, use case, DTO de entrada, DTO de saída, entidade e 4 mappers.
Ver resposta
✓ Resposta: (a) Adequado — semana 1, 2 devs, validando: estrutura mínima é certa. (b) Vilão A — com 30 devs, ler a tabela do vizinho é fronteira furada; qualquer mudança de schema quebra pedidos em silêncio. Falta uma Public API (aula Comunicação entre módulos: o padrão Public API Provider). (c) Vilão B — CRUD interno estável não justifica 8 artefatos por operação; camadas só repassam dados, sem regra nem segunda implementação à vista.
Exercício 3 — Escolha a dose
No FakeFlix, Content precisa do plano (basic/premium) para decidir a resolução. Produto tem 6 meses, ~50 mil usuários, time único de 8 devs, Billing muda de regra a cada 2 meses. Dose 0, 1 ou 2? Justifique pelo contexto.
Ver resposta
✓ Resposta: Dose 1 (Public API Provider). O produto vingou (6 meses, 50 mil usuários) e Billing muda a cada 2 meses → Dose 0 é arriscada (cada mudança poderia quebrar Content). Mas é time único de 8 devs, sem necessidade comprovada de autonomia de deploy nem sinal de gargalo de escala → Dose 2 (eventos + cache + consistência eventual) seria pagar complexidade sem benefício. A Dose 1 dá o que o contexto pede: fronteira contratual que protege Content das mudanças frequentes, custo baixo. Se Content passar a travar por causa de Billing, aí há evidência para a Dose 2.
Exercício 4 — Ache a evidência
Você suspeita que billing e identity estão acoplados demais. Cite duas evidências objetivas (não sentimentos) para confirmar antes de refatorar.
Ver resposta
✓ Resposta: (bastam duas) Coacoplamento no git: % de commits que alteram billing/ e também identity/; alto (>40%) = mudam juntos. Imports cruzados: quantos arquivos de billing/ importam de identity/core/ pulando a Public API; idealmente zero. (Também válido: bugs cujo root cause atravessou os dois módulos; impossibilidade de buildar/testar um sem o outro.) O hábito é o ponto: antes de refatorar, meça.
Exercício 5 — Desafio: a restrição que muda a resposta
O mesmo requisito, dois contextos. Em cada um, escolha a dose (0, 1 ou 2) e diga qual restrição decidiu:
(a) Uma fintech precisa registrar toda consulta a dados de cliente para auditoria do regulador, com retenção de 5 anos e prova de integridade. O time tem 6 devs e o produto está em produção há 3 meses.
(b) Uma startup de 3 devs quer registrar as consultas "para um dia analisar o comportamento dos usuários". Não há requisito externo nem alguém esperando o dado.
Ver resposta
✓ Resposta: (a) Dose 2, ou perto disso — e a restrição que decide é regulatória, não técnica. Auditoria com retenção de 5 anos e prova de integridade é exatamente a exceção listada nos tradeoffs: custo de reversão altíssimo e restrição dura e conhecida no dia zero. Não se evolui incrementalmente rumo a "conseguir provar ao regulador o que aconteceu em 2023" — o dado que você não gravou não volta. Repare que o tamanho do time (6 devs) e a idade do produto (3 meses), que normalmente empurrariam para a Dose 0 ou 1, não mandam aqui: a restrição externa domina. (b) Dose 0, se tanto. Não há requisito, não há prazo, não há ninguém esperando o dado — é o "e se um dia precisar?" em estado puro, o Vilão B. Registrar num log simples é suficiente; quando alguém realmente quiser analisar comportamento, a evidência aparece (a pergunta de negócio existe, alguém a fez) e aí se investe. O ponto do exercício é que o mesmo requisito, palavra por palavra, muda de dose conforme a restrição — e que a restrição relevante frequentemente não é técnica. Quem responde "depende do volume de dados" está olhando o eixo errado.