MÓDULO 14 · CONCEITO 01 DE 12

Framework de System Design — como engenheiros sênior abordam um problema novo

O processo de design de sistemas como disciplina de engenharia: clarificar requisitos funcionais e não-funcionais, estimar escala, definir contratos de API, desenhar arquitetura de alto nível, aprofundar componentes críticos, e documentar trade-offs. O framework não é um roteiro para entrevistas — é a forma como engenheiros experientes pensam antes de escrever código.

Tempo de leitura ~28 min Pré-requisito Módulos 00–13 · trilha completa Próximo 02 · Capacity Estimation →

Em 2012, o time de engenharia do Instagram foi adquirido pelo Facebook com 13 engenheiros operando uma plataforma com 30 milhões de usuários. O que permitiu que um time tão pequeno construísse algo com essa escala não foi talento excepcional individual — foi disciplina de design. Antes de construir qualquer componente, o time fazia perguntas que a maioria dos engenheiros só faz depois: "qual é o padrão de acesso dominante?", "o que o sistema precisa tolerar quando falha?", "onde vão estar os nossos usuários geograficamente?". Essas perguntas, feitas no momento certo, evitam meses de retrabalho.

System design é a prática de tomar decisões de arquitetura antes de escrever código — e documentar essas decisões de forma que outros possam entendê-las, questionar e evoluir. Não é um exercício exclusivo de entrevistas: é o que acontece toda vez que um time precisa construir algo que vai crescer além de um único servidor, servir mais de uma classe de usuários, ou sobreviver à saída dos engenheiros que o construíram originalmente. O framework apresentado aqui é a estrutura que separa "escrever código que funciona" de "design um sistema que escala, falha gracefully, e pode ser operado por pessoas que não o construíram".

Por que design antes de código

A tentação de começar a codificar é real e tem nome: a falácia da concretude. Código parece progresso. Um diagrama parece especulação. Mas a realidade de sistemas que crescem é que decisões de arquitetura tomadas cedo têm consequências que durarão anos. Trocar o banco de dados de um sistema em produção com 100M registros custa meses de trabalho e risco elevado. Trocar antes de escrever a primeira linha de código custa uma hora de discussão.

As decisões que são difíceis de reverter depois que o sistema está em produção:

Passo 1 — Clarificar requisitos

O maior desperdício em projetos de software não é código mal escrito — é código corretamente escrito para o problema errado. A clarificação de requisitos é o momento de reduzir a ambiguidade antes que ela se transforme em semanas de implementação na direção incorreta.

A separação fundamental é entre requisitos funcionais — o que o sistema faz — e requisitos não-funcionais — como ele se comporta. Engenheiros com menos experiência tendem a focar nos funcionais e assumir os não-funcionais. Os não-funcionais são precisamente o que determina a arquitetura.

Requisitos funcionais — o que clarificar

Para qualquer sistema novo, as perguntas que evitam retrabalho:

Requisitos não-funcionais — as perguntas que definem a arquitetura

Escala: quantos usuários ativos? Quantas operações por segundo no pico? O crescimento é linear, sazonal (Black Friday), ou em spike (lançamento de produto)? A diferença entre 1k e 1M requisições por segundo não é só uma questão de provisionar mais servidores — são arquiteturas fundamentalmente diferentes.

Latência: qual é o SLO de latência? Para qual operação? Latência de escrita e leitura frequentemente têm requisitos diferentes — um sistema de analytics pode tolerar escrita lenta se a leitura for rápida. Latência P99 é mais relevante que P50 porque é o que os usuários mais reclamam: o caso médio está bom, o outlier está terrível.

Disponibilidade: 99.9% significa 8.7 horas de downtime por ano aceitável. 99.99% significa 52 minutos. Essa diferença de um nove a mais implica redundância ativa, zero single points of failure, e complexidade operacional significativamente maior. Disponibilidade mais alta custa mais — tanto em infraestrutura quanto em complexidade de design. Vale a pena perguntar: qual é o custo de 1 hora de downtime para o negócio?

Consistência: o sistema pode servir dados desatualizados? Por quanto tempo? Isso varia por operação: o saldo de uma conta bancária não pode estar desatualizado; o número de curtidas em um post pode estar 5 segundos atrás sem problema. Consistência mais forte significa coordenação mais cara e latência maior. Consistência eventual abre espaço para designs mais escaláveis mas exige que a aplicação lide com a possibilidade de ver dados diferentes dependendo de qual réplica atender a requisição.

Durabilidade: perda de dados é aceitável? Em que cenário? Uma sessão de jogo perdida em crash de servidor é tolerável. Uma transação financeira perdida não é. Durabilidade alta implica write-ahead logs, replicação síncrona, e estratégias de backup que adicionam latência e complexidade.

Geolocalização e compliance: o sistema é global? Há requisitos de data residency — dados de usuários europeus não podem sair da Europa por GDPR? Isso transforma um problema de arquitetura em um problema de replicação multi-região com isolamento geográfico, o que é significativamente mais complexo.

Passo 2 — Estimar escala

Estimation é o passo que traduz requisitos em números que informam decisões de arquitetura. O objetivo não é precisão — é ordem de magnitude. A diferença entre "precisamos de cache" e "não precisamos de cache" é derivada de estimation, não de instinto.

As estimativas fundamentais, derivadas dos requisitos:

# Exemplo: sistema de URL shortener
# DAU: 10M usuários ativos por dia
# Razão leitura:escrita = 100:1 (redirects vs criações)
# Retenção: URLs ficam por 5 anos

# --- QPS ---
# Writes: 10M DAU × 1 URL/dia ÷ 86400s ≈ 116 writes/s  (pico: ~350/s)
# Reads:  116 × 100                    ≈ 11.600 reads/s (pico: ~35k/s)

# --- Storage ---
# Tamanho por registro: short_code(7) + original_url(~100) + metadata(~50) ≈ 200 bytes
# Writes/dia: 116 × 86400 ≈ 10M URLs/dia
# Storage/ano: 10M × 365 × 200 bytes ≈ 730 GB/ano
# Em 5 anos: ~3.6 TB (sem replicação)
# Com replicação 3×: ~11 TB

# --- Banda de rede (reads) ---
# Resposta de redirect: ~500 bytes (headers + body mínimo)
# 11.600 reads/s × 500 bytes ≈ 5.8 MB/s em steady state
# Pico: ~18 MB/s — bem dentro de uma única instância de CDN

O que os números acima respondem diretamente:

Estimation é desenvolvida em profundidade no Conceito 02. O ponto aqui é que o resultado de estimation é um conjunto de restrições que eliminam opções arquiteturais: "não precisamos de sharding agora", "precisamos de cache desde o dia 1", "CDN é sobre latência, não capacidade".

Passo 3 — Definir contratos de API

Definir a API antes de desenhar a arquitetura interna não é formalismo — é uma técnica de clareza. Se você não consegue definir o contrato da API, você não entendeu o que o sistema precisa fazer. A API é também o ponto de acoplamento entre serviços: uma API bem definida permite que frontend, mobile, e serviços parceiros evoluam independentemente do backend.

Para um sistema novo, o nível de detalhe adequado é o suficiente para implementar sem fazer perguntas — não uma spec OpenAPI completa, mas mais do que "POST /urls retorna a URL curta":

POST /urls
  Authorization: Bearer {token}
  Body: {
    original_url: string (required, max 2048 chars),
    custom_alias: string? (3-30 chars, [a-zA-Z0-9-_]),
    expires_at: ISO8601? (null = nunca expira)
  }
  Response 201: {
    url_id: UUID,
    short_code: string,
    short_url: string,   # "https://shr.tt/{short_code}"
    original_url: string,
    expires_at: ISO8601 | null,
    created_at: ISO8601
  }
  Response 409: { error: "alias_already_taken", alias: string }
  Response 422: { error: "invalid_url", reason: string }

GET /{short_code}
  Response 302: Location: {original_url}
               X-Cache: HIT | MISS
  Response 404: { error: "not_found" }
  Response 410: { error: "expired", expired_at: ISO8601 }

O detalhe dos status codes não é perfeccionismo: 409 vs 422 informam ao cliente a causa do erro e o que fazer a seguir. 410 Gone vs 404 Not Found é a diferença entre "nunca existiu" e "existiu mas expirou" — o cliente pode comunicar isso diferentemente ao usuário. Esses detalhes emergem de pensar no contrato antes de implementar, não depois.

Passo 4 — Arquitetura de alto nível

A arquitetura de alto nível é o mapa do sistema: quais componentes existem, como se comunicam, e onde fica o estado. O nível de abstração é deliberado — não é pseudocódigo nem diagrama de sequência completo. É o suficiente para que um engenheiro novo no sistema entenda os blocos principais antes de mergulhar em qualquer um deles.

Os componentes que aparecem em quase todo sistema distribuído:

O erro mais comum neste passo: nível de detalhe incorreto. Discutir índices de banco de dados antes de decidir qual banco usar é detalhe de implementação antes de decisão arquitetural. Propor sharding horizontal antes de verificar que o volume realmente exige é over-engineering não motivado. A arquitetura de alto nível deve ser a versão mais simples que atende aos requisitos — e os deep dives adicionam complexidade onde os números justificam.

Passo 5 — Deep dives em componentes críticos

Este é o passo onde design de sistemas se torna engenharia real: identificar onde estão os problemas difíceis e resolver com precisão. A heurística para escolher onde aprofundar: onde os requisitos não-funcionais mais exigentes se manifestam? Se o requisito é latência global P99 < 50ms, o mecanismo de cache e a estratégia de CDN merecem deep dive. Se é consistência em writes distribuídos, o mecanismo de replicação e o protocolo de consensus. Se é throughput de 1M eventos/segundo, o pipeline de ingestão e particionamento.

Um deep dive bem feito responde quatro perguntas sobre o componente:

  1. Qual problema específico ele resolve, com números? Não "precisamos de cache para performance", mas "sem cache, 35k reads/s vão ao banco; com cache com hit rate de 95%, o banco recebe 1.7k reads/s — dentro da capacidade de uma única instância com réplicas de leitura".
  2. Quais alternativas foram consideradas e por que foram descartadas? "Consideramos Memcached mas escolhemos Redis porque precisamos de estruturas de dados complexas para o leaderboard de analytics, e Redis Cluster nos dá HA sem configuração adicional".
  3. Quais casos edge o componente precisa tratar? O que acontece quando o Redis fica indisponível? O sistema degrada gracefully (vai direto ao banco com latência maior) ou para completamente? Cache stampede (milhares de requisições vão ao banco simultaneamente após expiração de um item popular) — como mitigar? (probabilistic early expiration, mutex lock no primeiro acesso)
  4. Como o componente falha e qual o impacto? Failure modes não são exceções — são parte do design. Um sistema que só funciona quando todos os componentes estão saudáveis não é um sistema de produção.

Passo 6 — Trade-offs e limitações

Todo design de sistema é uma coleção de trade-offs. Não há design correto — há design que prioriza os atributos certos para o problema específico. Nomear os trade-offs explicitamente serve dois propósitos: demonstra que as escolhas foram conscientes (não acidentais), e cria um mapa para quando os requisitos mudarem — "se o requisito de consistência mudar, esse componente precisa ser revisitado".

Os trade-offs que aparecem em quase todo sistema:

Design sessions com times reais

No trabalho, design não é um processo solitário — é uma sessão colaborativa com o time, product, e às vezes stakeholders de outras áreas. As dinâmicas são diferentes de um processo individual:

Design reviews: o design é proposto por uma pessoa (ou um pequeno grupo) e revisado pelo time mais amplo. A função da revisão é encontrar os problemas que o designer não viu — não validar o design. Um design review onde ninguém levanta problemas ou alternativas ou é um design perfeito (raro) ou é um time que não está engajado (comum). Engenheiros sênior facilitam revisões onde objeções são bem-vindas e tratadas com seriedade.

ADRs (Architecture Decision Records): a ferramenta padrão para documentar decisões de design que sobrevivem à sessão. Um ADR captura: o contexto do problema, a decisão tomada, as alternativas consideradas, e as consequências — positivas e negativas. ADRs ficam no repositório junto com o código, para que qualquer engenheiro que chegue meses depois entenda por que o sistema tem a forma que tem. Sem ADRs, o conhecimento de design fica nas cabeças das pessoas que participaram — e vai embora quando elas saem.

# Formato mínimo de um ADR
# docs/decisions/0042-usar-redis-para-cache-de-urls.md

## Status
Aceito (2024-03-15)

## Contexto
O serviço de redirect processa ~35k reads/s no pico.
PostgreSQL com as réplicas atuais satura em ~8k reads/s.
Precisamos de uma camada de cache na frente do banco.

## Decisão
Usaremos Redis 7 com cluster mode (3 shards, 1 réplica cada)
para cachear mapeamentos short_code → original_url com TTL de 1h.
Cache aside: a aplicação verifica Redis, em miss vai ao banco e popula.

## Alternativas consideradas
- Memcached: descartado porque precisamos de TTL por item e
  estruturas de dados para analytics futuro (Redis Sorted Sets).
- Cache em memória local (Caffeine): descartado porque múltiplas
  instâncias do serviço teriam caches inconsistentes após writes.
- Aumentar réplicas de leitura do PostgreSQL: cobre o volume, mas
  não reduz latência de queries complexas de analytics.

## Consequências
+ Leitura de redirect passa de ~5ms (banco) para ~0.5ms (Redis).
+ Banco recebe ~1.7k reads/s no steady state (95% hit rate estimado).
- Sistema agora tem dependência adicional: Redis indisponível
  degrada para leitura direta no banco (circuit breaker necessário).
- Cache stampede possível em expiração de URLs muito populares;
  mitigar com probabilistic early expiration (ver implementação).

Iteração no design: o primeiro design raramente é o final. O processo saudável é: propor um design, receber feedback, identificar os problemas que o feedback expõe, iterar. Engenheiros experientes tratam seu design como hipótese, não como solução — e ficam genuinamente curiosos quando alguém encontra um problema, não defensivos.

Sinais de maturidade em design

O que diferencia design de um engenheiro sênior não é conhecimento de tecnologias — é a qualidade das perguntas que ele faz e dos problemas que identifica.

Design pensando em falhas: todo componente vai falhar. A questão é o que acontece quando falha. Um design que só funciona quando tudo está saudável não é um design de produção. "O que acontece quando o Redis está indisponível?" não é pergunta paranoica — é a pergunta que previne o incidente às 3h da manhã.

Design pensando em operação: o sistema vai ser operado por pessoas que não o construíram, às vezes às 3h da manhã sob pressão. Logs que permitem diagnóstico rápido, métricas que expõem o estado interno, e runbooks que descrevem os cenários de falha mais comuns não são extras — são parte do design.

Design pensando em evolução: o sistema vai precisar mudar. Features serão adicionadas, padrões de uso vão divergir do previsto, requisitos de negócio vão mudar. Um design que não pode evoluir sem reescrita completa tem vida útil curta. As decisões que mais importam para evolução são as fronteiras — entre serviços, entre componentes, entre dados de diferentes domínios.

Dimensionamento honesto: propor a solução mais simples que atende aos requisitos atuais, com uma análise de quando o sistema precisaria evoluir e em que direção. Over-engineering tem custos reais: mais complexidade para operar, mais tempo para implementar, mais superfície para bugs. Under-engineering tem custos diferentes: dívida técnica que acumula juros quando o sistema cresce além do que foi projetado. A calibração entre os dois é uma das habilidades mais valiosas de um engenheiro sênior.

design é uma habilidade, não um ritual

O framework dos seis passos não é um checklist a ser executado mecanicamente — é a estrutura que engenheiros experientes internalizaram ao errar suficientemente. Pular clarificação de requisitos porque "parece óbvio" é um erro que qualquer engenheiro experiente já cometeu uma vez e nunca mais cometeu. Não documentar trade-offs é um erro que o mesmo engenheiro cometeu quando voltou ao sistema seis meses depois sem lembrar por que a decisão foi tomada. O framework existe porque cada passo compensa uma forma específica de engenheiros cometerem erros custosos. Entender por que cada passo importa é mais valioso do que saber executar os passos.

Decisões de engenharia

Quando o design é suficiente para começar a implementar

Design tem retornos decrescentes: depois de certo ponto, mais design não reduz mais risco — só posterga a implementação. O critério de suficiência: você consegue tomar as decisões técnicas principais sem mais informações? Sabe qual banco usar, qual estratégia de particionamento, e quais são os componentes críticos para os requisitos não-funcionais?

Regra prática: o design está pronto para implementar quando você consegue estimar o esforço de cada componente com confiança de 2x (não 10x). Se a incerteza ainda é alta, há perguntas não respondidas que design adicional ou um spike técnico pode resolver.

Monolito vs microsserviços no design inicial

A pressão para começar com microsserviços é real mas frequentemente equivocada. Microsserviços adicionam complexidade operacional (service discovery, distributed tracing, gestão de múltiplos deploys) que faz sentido quando a escala de time ou de sistema justifica. Para um sistema novo, com um time pequeno, ou com domínio ainda pouco entendido, um monolito bem estruturado (com módulos claros que podem ser extraídos depois) é mais rápido de construir, mais simples de operar, e mais fácil de refatorar quando os requisitos mudam.

Regra prática: comece monolítico quando o domínio não está completamente entendido ou o time tem menos de 10 engenheiros. Extraia serviços quando um módulo tem requisitos de escala, deployment, ou confiabilidade diferentes do restante — não antes.

Design técnico detalhado vs princípios arquiteturais

Um design técnico detalhado (schema de banco, endpoints de API, estruturas de dados) é necessário antes de implementar. Um documento de princípios arquiteturais ("usaremos event sourcing", "todas as APIs devem ser idempotentes") é necessário para alinhar o time em decisões recorrentes. Confundir os dois leva ou a documentação de alto nível que não guia implementação, ou a especificações detalhadas que ficam desatualizadas imediatamente após o primeiro PR.

Regra prática: princípios arquiteturais no README ou ARCHITECTURE.md do repositório; decisões específicas em ADRs por tópico; detalhes de implementação no código e nos comentários inline — cada tipo de documentação no lugar certo.

Quanto documentar vs quanto deixar no código

Código bem escrito documenta o quê e o como. O que o código não consegue documentar é o porquê de uma decisão, especialmente quando a alternativa óbvia foi descartada por uma razão não-óbvia. "Usamos polling aqui em vez de webhooks porque o parceiro X tem um rate limit de 10 chamadas/minuto na API de webhook" é conhecimento que vai embora com quem saiu, se não estiver documentado.

Regra prática: documente toda decisão cujo racional não é óbvio a partir do código. Se um engenheiro novo leria o código e diria "por que foi feito assim quando Y seria mais óbvio?", a resposta para essa pergunta deve estar documentada — seja num ADR, num comentário inline, ou no PR que fez a mudança.

Perguntas de entrevista

Como você aborda um sistema que precisa ser redesenhado — não construído do zero?

Redesign é diferente de design inicial em aspectos críticos: há um sistema existente em produção com usuários reais, há código legado com comportamentos implícitos que ninguém documentou, e há uma migração que precisa acontecer sem downtime.

O passo que muda mais é o de clarificação de requisitos: além dos requisitos do sistema futuro, você precisa entender o comportamento atual — incluindo os bugs que os usuários dependem. "Esse endpoint retorna 200 com corpo vazio quando o recurso não existe — isso é intencional?" é uma pergunta que pode não ter uma resposta documentada. Se clientes dependem desse comportamento, mudar para 404 é uma breaking change.

A estratégia preferida para redesign é strangler fig: construir o novo sistema em paralelo, mover tráfego incrementalmente, e desligar o sistema antigo quando 100% do tráfego já migrou. Isso permite validar o novo sistema com tráfego real antes de desligar o safety net. A alternativa — big bang migration — tem taxa de falha muito mais alta porque os problemas só aparecem em produção, quando o sistema antigo já foi desligado.

Quais são os sinais de que um design inicial foi ruim, depois que o sistema está em produção?

Os sinais aparecem em padrões recorrentes:

  • Queries lentas não-otimizáveis: se o padrão de acesso ao banco exige queries que cruzam múltiplas tabelas sem índices possíveis porque o schema foi modelado para o caso de uso errado, o problema não é falta de índice — é o modelo de dados. Adicionar índices trata o sintoma; refatorar o schema trata a causa.
  • Acoplamento oculto: quando uma mudança em serviço A exige mudança coordenada em serviços B, C e D, as fronteiras de serviço foram mal desenhadas. Microserviços com acoplamento forte são distributed monolith — a pior combinação de complexidade de distribuição com acoplamento de monolito.
  • Toil operacional crescente: se cada deploy exige passos manuais, cada novo tenant exige configuração manual, ou cada incidente exige intervenção de quem construiu o sistema, esses são sinais de que operabilidade não foi considerada no design. Não é falta de automação — é falta de design para automação.
  • Impossibilidade de testar: código que não pode ser testado sem subir a stack completa foi escrito sem considerar testabilidade. Isso é um sinal de design — não de qualidade de código individualmente.

O padrão comum em todos esses sinais: o design original fez suposições sobre como o sistema seria usado que se revelaram incorretas. O problema não é que o design foi ruim — é que as suposições não foram documentadas, então quando mudaram, ninguém sabia o que estava sendo quebrado.

Como você valida que um design vai funcionar antes de implementar?

Há três técnicas complementares para validar um design antes de investir na implementação completa:

Proof of concept (spike): implementar a parte mais arriscada do design — não o que é mais fácil, mas o que você tem menos certeza que funciona. Se o design depende de um mecanismo de sincronização distribuída que você nunca implementou, o spike deve ser exatamente esse mecanismo, com dados realistas. Um spike de 2 dias que revela que o approach é inviável economiza semanas de implementação.

Load testing do modelo de capacidade: antes de implementar o sistema completo, validar as suposições de capacity estimation. Se o design assume 95% de cache hit rate, meça com tráfego simulado — talvez o padrão de acesso real resulte em 70%, o que muda completamente o dimensionamento do banco.

Design review com quem vai operar: o engenheiro de operações ou SRE que vai acordar às 3h por causa desse sistema tem uma perspectiva diferente do designer. "Como você vai debugar isso quando estiver lento?" é uma pergunta que revela lacunas de observabilidade que o designer não considerou.

Como o framework de system design se aplica a sistemas que cresceram organicamente sem design formal?

A maioria dos sistemas em produção cresceu organicamente — feature a feature, sem design formal. Aplicar o framework retroativamente não é sobre documentar o que existe, mas sobre entender o que deveria existir e identificar o gap.

O processo prático: primeiro, reverse-engineer os requisitos implícitos. O sistema que existe hoje atende a um conjunto de requisitos — funcionais (o que ele faz) e não-funcionais (como ele performa, mesmo que sem SLO formal). Documentar esses requisitos implícitos é o primeiro passo.

Segundo, mapear onde o sistema existente diverge de um design intencional. As divergências mais comuns: acoplamento que não deveria existir (módulos que deveriam ser independentes mas compartilham estado), ausência de cache onde os números justificariam, falta de degradação graciosa em falhas de componentes downstream.

Terceiro, priorizar as divergências por impacto. Não é possível redesenhar tudo de uma vez — e na maioria dos casos não é necessário. As divergências que causam incidentes recorrentes, que bloqueiam features importantes, ou que criam toil operacional significativo são as candidatas a redesign iterativo.

Qual é a relação entre system design e os módulos anteriores da formação?

System design é a síntese dos módulos anteriores aplicada a problemas concretos. Cada decisão de design apoia em conhecimento específico:

  • Disponibilidade (M08): os SLOs que você define em requisitos não-funcionais, os circuit breakers que você propõe entre serviços, os health checks que você inclui no design.
  • Observabilidade (M10): as métricas que cada componente deve expor, os logs estruturados que permitem diagnóstico, os traces distribuídos que conectam requisições através de múltiplos serviços.
  • Segurança (M11): autenticação no API Gateway, autorização no nível de serviço, criptografia de dados em trânsito e em repouso, rotação de secrets.
  • Cloud e operação (M12): onde cada componente roda (managed services vs self-hosted), estratégia de deploy (canary para serviços críticos), FinOps (custo de cada escolha de arquitetura).
  • Mensageria (M09): quando usar Kafka vs RabbitMQ vs SQS, como modelar eventos, garantias de entrega.

O engenheiro sênior não aplica esses conhecimentos em silos — os aplica simultaneamente ao pensar sobre um sistema. Uma escolha de banco de dados afeta disponibilidade, custo, observabilidade, e segurança ao mesmo tempo. A capacidade de raciocinar sobre essas interações é o que define senioridade técnica.

Exercícios práticos

Exercício 1 — Mapeie os requisitos não-documentados do seu sistema atual

Escolha um serviço que você mantém no trabalho. Sem olhar documentação existente, escreva os requisitos funcionais e não-funcionais que você acredita que o sistema atende — escala, latência, disponibilidade, consistência. Depois, valide esses requisitos: olhe os SLOs configurados no sistema de alertas, consulte os dashboards de latência histórica, verifique a configuração de replicação do banco. Compare o que você assumia com o que está configurado. Identifique as divergências.

Critério: você identificou pelo menos um requisito não-funcional que está implícito (assumido pelo time mas não documentado formalmente) e pelo menos uma divergência entre o que o sistema deveria atender e o que está configurado para atender.

Exercício 2 — Escreva um ADR para uma decisão técnica relevante

Identifique uma decisão de design importante no sistema que você mantém — pode ser a escolha de banco de dados, a estratégia de cache, o modelo de autenticação, ou a fronteira entre dois serviços. Escreva um ADR completo: status, contexto, decisão, alternativas consideradas com razões explícitas de descarte, e consequências (positivas e negativas). Se a decisão original foi ruim em retrospecto, documente isso também — ADRs depreciados que apontam para o substituto são igualmente valiosos.

Critério: um engenheiro novo no time consegue ler o ADR e entender por que a decisão foi tomada, sem fazer perguntas. O ADR está no repositório do projeto.

Exercício 3 — Design de alto nível de um pastebin em 30 minutos

Sem referência externa, faça o design de alto nível de um Pastebin (criar snippets de texto com URLs compartilháveis, visualizações por paste, expiração opcional). Siga os passos: clarificar requisitos (você pode assumir os requisitos, mas deve documentá-los explicitamente), estimativa de escala, definição de API (3 endpoints principais), diagrama de componentes com fluxo de dados, e identificação dos dois componentes que merecem deep dive. Ao final, escreva os três trade-offs mais importantes do design.

Critério: o design tem componentes suficientes para ser implementado por um time sem fazer perguntas arquiteturais adicionais. Os trade-offs são articulados no formato "priorizamos X sobre Y porque Z" — não como listas de vantagens e desvantagens.

Exercício 4 — Reverse-engineer o design de um sistema open-source

Escolha um sistema open-source que você usa ou admira (Prometheus, MinIO, Gitea, Temporal, etc.). Leia a documentação de arquitetura, o README, e os arquivos de decisão (se existirem). Reconstrua: (1) quais eram os requisitos não-funcionais que guiaram o design, (2) quais foram as três decisões arquiteturais mais importantes, (3) quais alternativas os autores provavelmente consideraram e descartaram. Depois, leia os issues e PRs históricos para validar suas hipóteses — frequentemente a discussão de decisões aparece nos PRs originais.

Critério: você identificou pelo menos duas decisões arquiteturais do sistema e encontrou evidência (documentação, PR, issue) que confirma ou refuta a sua hipótese sobre o porquê da decisão.

Exercício 5 — Conduza uma design review com o seu time

Proponha um novo feature ou melhoria arquitetural para um sistema do seu trabalho. Prepare um documento de design de uma página: contexto do problema, proposta de solução com diagrama, alternativas consideradas, trade-offs, e perguntas abertas onde você quer input do time. Agende uma sessão de 30 minutos para review. Durante a sessão, facilite ativamente: "alguém vê problemas com essa abordagem?", "há alguma alternativa que não considerei?". Ao final, documente os pontos levantados e como eles mudaram o design.

Critério: a review gerou pelo menos um ajuste real no design — não só validação do que já estava proposto. O documento atualizado com o feedback está disponível no repositório ou wiki do time.

Referências

  1. book Martin Kleppmann — Designing Data-Intensive Applications O'Reilly · 2017 · o livro que mais profundamente explora os trade-offs de sistemas distribuídos — leitura obrigatória antes deste módulo
  2. book Alex Xu — System Design Interview (vol. 1 e 2) bytebytego.com · 2020–2022 · referência para os sistemas clássicos com framework estruturado e estimativas detalhadas
  3. article Michael Nygard — Architecture Decision Records cognitect.com/blog · a proposta original de ADRs como ferramenta de documentação de decisões técnicas que sobrevivem ao tempo
  4. docs MADR — Markdown Architectural Decision Records adr.github.io · template e tooling para ADRs em repositórios Git, com exemplos de projetos reais e tooling de automação
  5. article Martin Fowler — Strangler Fig Application martinfowler.com · o padrão canônico para substituição incremental de sistemas legados sem big bang migration
  6. article High Scalability — Real Architecture Posts highscalability.com · análises de arquiteturas reais de Twitter, Facebook, Netflix, Uber — o que funciona em produção em escala
  7. article Gergely Orosz — How Big Tech Runs Tech Reviews pragmaticengineer.com · como Google, Meta, Stripe e Airbnb conduzem design reviews e tomam decisões técnicas em times grandes
  8. book Sam Newman — Building Microservices (2ª ed.) O'Reilly · 2021 · quando microsserviços fazem sentido, fronteiras de serviço, e os trade-offs de distribuição que o design precisa considerar
  9. article Jeff Dean — Numbers Every Engineer Should Know norvig.com · latências de I/O de referência (RAM, SSD, rede) que informam capacity estimation e decisões de cache vs banco
  10. article Netflix Tech Blog — Lessons from the Netflix API Redesign netflixtechblog.com · caso real de redesign de API com strangler fig, migração gradual de tráfego e documentação de trade-offs
  11. book Gregor Hohpe — The Software Architect Elevator O'Reilly · 2020 · sobre a habilidade de comunicar design técnico para diferentes audiências — de código a estratégia
  12. article Charity Majors — The Engineer/Manager Pendulum charity.wtf · perspectiva sobre como engenheiros sênior evoluem sua capacidade de design à medida que ganham mais contexto de negócio e operações