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.js nã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

Sobre reescrita e escolha de tecnologia

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.