Operação ao redor do código
Volume II

Projetar
sistemas
de verdade.

Design de sistemas para quem constrói, não para quem decora. Os trade-offs reais, os componentes que importam, e a disciplina de não resolver problemas que você não tem.

Um livro técnico 5 partes · 24 capítulos
Parte I
O raciocínio

Antes de qualquer diagrama, a forma de pensar. O que é de fato projetar um sistema, como extrair o problema real por trás do pedido, como usar estimativas para decidir em vez de impressionar, e o vocabulário dos trade-offs que você vai negociar pelo resto do livro. Quatro capítulos sobre pensar antes de desenhar.

Design Requisitos Estimativas Trade-offs
Parte I · Capítulo 1 · O raciocínio

O que é
projetar um
sistema.

Pergunte a dez engenheiros o que é "system design" e você ouvirá dez respostas: caixas e setas num quadro branco, escolher entre Kafka e RabbitMQ, decorar como o Twitter funciona para uma entrevista. Nenhuma está errada, e todas erram o essencial. Projetar um sistema não é escolher tecnologias nem desenhar diagramas — é tomar e justificar decisões sob restrições, gerenciando trade-offs que não têm resposta certa. Este livro é sobre fazer isso bem, para construir sistemas reais, não para impressionar num quadro branco.

Este primeiro capítulo estabelece a postura que sustenta todos os outros. Vamos desmontar o que design não é, chegar à sua verdadeira essência (a gestão de trade-offs), entender por que restrições vêm antes de soluções, defender a virtude subestimada de escolher a tecnologia entediante, distinguir os níveis em que uma decisão de arquitetura acontece, e separar as decisões reversíveis das que você não desfaz — porque elas merecem rigor diferente. Se você vem do volume anterior, vai reconhecer o fio: a maturidade operacional e a maturidade de design são a mesma coisa vista de ângulos diferentes — fazer o necessário com excelência e recusar o desnecessário com disciplina.

1.1 A história — de "programação no grande" ao design distribuído

Contexto histórico

A ideia de que projetar a estrutura de um sistema é uma atividade distinta de escrever o código nasceu cedo. Em 1968, a conferência de engenharia de software da OTAN nomeou a "crise do software": sistemas grandes demais para uma cabeça só, que falhavam não por bugs de linha, mas por estrutura ruim. No mesmo ano, Melvin Conway formulou a observação que viraria lei: a arquitetura de um sistema espelha a estrutura de comunicação da organização que o constrói. Design, desde o início, foi reconhecido como um problema tanto técnico quanto humano.

Nos anos 1970-80, DeRemer e Kron distinguiram "programming in the small" (escrever um módulo) de "programming in the large" (compor módulos num sistema). Parnas ensinou ocultação de informação e o desenho por módulos que escondem decisões que podem mudar. O foco era um sistema rodando numa máquina: a arte estava em organizar a complexidade interna.

A virada foi a rede. Quando os sistemas deixaram de caber numa máquina e passaram a conversar por rede não confiável, o design ganhou uma dimensão nova e brutal: a falha parcial. Numa máquina só, ou tudo funciona ou tudo para. Num sistema distribuído, metade funciona enquanto a outra metade falha, e nenhum componente sabe ao certo o estado dos outros. Os anos 2000 codificaram as leis dessa realidade (CAP, em 2000; os artigos de Dynamo e Bigtable, 2006-2007), e o "system design" moderno nasceu: projetar sistemas que se mantêm úteis apesar de partes deles estarem sempre, em algum grau, quebradas.

Em 2026, há uma ironia. A indústria tem ferramentas extraordinárias para construir sistemas distribuídos — e uma tendência igualmente extraordinária de usá-las onde não são necessárias. O design maduro hoje não é saber montar a arquitetura mais sofisticada; é saber qual problema você realmente tem e escolher a solução mais simples que o resolve. A história do design começou domando a complexidade interna de uma máquina; hoje, boa parte dela é resistir à complexidade externa que ninguém pediu.

1.2 O que design de sistemas não é

É mais fácil chegar ao que design é eliminando o que ele não é — porque os equívocos comuns são justamente o que trava bons engenheiros.

O que parece ser

Desenhar caixas e setas. Escolher entre tecnologias da moda. Memorizar a arquitetura de empresas famosas. Aplicar o padrão "correto" para cada situação. Conhecer o maior número de ferramentas. Construir para a maior escala imaginável.

O que de fato é

Entender o problema real e suas restrições. Enumerar as opções e seus trade-offs. Escolher conscientemente, sabendo o que se ganha e o que se perde. Justificar a escolha. Preferir o simples até que o complexo se prove necessário. Projetar para o problema que existe.

O diagrama é o resíduo, não o trabalho
O quadro branco cheio de caixas é o resultado visível do design, e por isso o confundimos com o design em si. Mas o diagrama é o resíduo de um raciocínio: cada caixa existe porque uma decisão foi tomada, e cada seta porque um trade-off foi aceito. Dois engenheiros podem desenhar o mesmo diagrama e ter feito design de qualidades opostas — um por imitação, copiando o que viu; outro por dedução, derivando cada peça das restrições do problema. O valor não está nas caixas; está nas razões por trás delas. Um sistema cujas decisões você não consegue justificar não foi projetado — foi montado.

1.3 A essência: design é gestão de trade-offs

Se há uma única frase para levar deste livro, é esta: em design de sistemas, não existem soluções, só trade-offs. Toda decisão de arquitetura compra uma propriedade pagando com outra. Você não escolhe a opção "certa"; escolhe qual conjunto de problemas prefere ter.

Quando você ganha……costuma pagar com
Consistência forteDisponibilidade ou latência (Cap 5)
Mais throughput por cacheFrescor / risco de dado velho (Cap 10)
Escala horizontalComplexidade de coordenação (Cap 13)
Desacoplamento por filasLatência e complexidade de entrega (Cap 11)
Flexibilidade de schema (NoSQL)Garantias transacionais (Cap 12)
Baixa latência por replicaçãoConsistência entre réplicas (Cap 6)
A pergunta que revela o design maduro
Quando alguém propõe uma solução, a pergunta que separa o engenheiro maduro do iniciante não é "isso funciona?" — quase tudo funciona em algum cenário. É "o que isso custa, e esse custo vale para o nosso caso?". Adicionar um cache funciona; o custo é invalidação e dado velho. Migrar para microsserviços funciona; o custo é latência de rede e complexidade operacional. O iniciante vê a propriedade que ganha; o maduro vê também a fatura que vem junto, e pergunta se o saldo é positivo para este problema. Não existe almoço grátis em sistemas; existe almoço cujo preço você entende ou almoço cujo preço você descobre tarde.

1.4 Restrições antes de soluções

O erro mais comum de quem projeta é começar pela solução. Você ouve "sistema de notificações" e já está pensando em filas e workers — antes de saber quantas notificações por segundo, se elas podem atrasar, se podem se perder, se a ordem importa. A solução certa é indeterminada até as restrições estarem na mesa. Restrições não são detalhes a preencher depois; são o que define o espaço de soluções válidas.

As restrições que moldam quase toda decisão:

  • Escala: quantas requisições/dados/usuários, agora e na trajetória provável. Um sistema para mil usuários e um para cem milhões são problemas diferentes — e construir o segundo para resolver o primeiro é o erro do volume anterior (escala prematura), agora no nível de arquitetura.
  • Latência: quão rápido a resposta precisa ser. "Instantâneo" (uma busca) e "pode levar segundos" (um relatório) levam a arquiteturas opostas.
  • Consistência: o dado precisa estar sempre correto e atual, ou pode estar levemente atrasado? Esta é a restrição que mais define a arquitetura de dados (Parte II).
  • Disponibilidade: quanto custa ficar fora? Um sistema que pode cair 1h/mês e um que não pode cair nunca exigem investimentos de redundância radicalmente diferentes.
  • Durabilidade: perder este dado é tolerável ou catastrófico? Um log de debug e um registro de pagamento têm exigências opostas.
  • Custo e prazo: as restrições que a teoria ignora e a realidade impõe. A arquitetura "correta" que você não tem tempo nem dinheiro de construir não é a correta.
"Depende" é a resposta certa — e a próxima pergunta é "de quê?"
Quando alguém pergunta "qual o melhor banco?" ou "devo usar microsserviços?", a resposta honesta é sempre "depende". Isso soa como evasão, mas é o contrário: é o reconhecimento de que a pergunta está incompleta. "Depende" não encerra a conversa — abre a pergunta que importa: depende de quê?. Depende da escala, da consistência exigida, do padrão de leitura vs escrita, do time, do prazo. O engenheiro maduro não tem medo do "depende"; ele o usa como ponte para extrair as restrições que tornam a pergunta respondível. Quem responde "use X" sem perguntar nada não é decidido — é negligente.

1.5 A favor do tédio — escolha a tecnologia entediante

Há uma força cultural poderosa puxando todo engenheiro para a tecnologia nova, distribuída, impressionante. Resistir a ela é uma das habilidades de design mais valiosas e menos celebradas. O princípio, às vezes chamado de "choose boring technology": prefira a ferramenta madura, conhecida e entediante à novidade brilhante, a menos que o problema exija a novidade.

Por que o entediante quase sempre vence
A tecnologia entediante — o Postgres, o monólito, a fila simples — tem uma vantagem que não aparece na comparação de features: ela é conhecida. Seus modos de falha estão documentados, suas pegadinhas foram descobertas por milhões antes de você, e quando algo dá errado às 3h, há respostas no primeiro resultado de busca. A tecnologia nova e brilhante traz capacidades que você talvez não precise e modos de falha que ninguém ainda mapeou — você vira o explorador que descobre os bugs. Cada peça nova e exótica na sua arquitetura é um "orçamento de inovação" gasto. Você tem pouco desse orçamento; gaste-o no que diferencia o seu produto, não na infraestrutura, que deve ser o mais entediante e confiável possível. O Postgres resolve mais problemas de "preciso de um banco especial" do que quase qualquer banco especial.

Isto não é conservadorismo nem medo de aprender. É alocação de risco. A novidade que resolve um problema real que o entediante não resolve vale o custo; a novidade adotada por ser novidade é dívida disfarçada de sofisticação. A pergunta antes de adotar algo novo: "qual problema concreto isto resolve que minha ferramenta atual não resolve, e esse problema é meu?".

1.6 Os níveis de uma decisão de design

"Design de sistemas" acontece em várias altitudes ao mesmo tempo, e confundi-las gera discussões onde as pessoas falam de coisas diferentes achando que falam da mesma. Vale separar:

NívelA perguntaExemplo de decisão
ArquiteturaQuais são as grandes peças e como se relacionam?Monólito ou serviços? Síncrono ou orientado a eventos?
ComponenteComo cada peça é estruturada por dentro?Qual banco? Qual estratégia de cache?
DadosComo a informação é modelada, guardada, acessada?Normalizar ou desnormalizar? Como particionar?
IntegraçãoComo as peças conversam?REST, gRPC, fila? Qual contrato? Qual garantia de entrega?
OperaçãoComo isto roda, é observado e recuperado?Todo o volume anterior deste livro
A decisão de dados é a mais difícil de mudar
Entre os níveis, há uma hierarquia de custo de mudança que deveria guiar onde você gasta mais rigor. Trocar um framework web é caro mas factível. Reescrever a comunicação entre serviços, mais difícil. Mas mudar o modelo de dados de um sistema em produção — como a informação é particionada, qual o esquema, qual a chave de sharding — é frequentemente a coisa mais cara e arriscada que existe, porque os dados já estão lá, em volume, sendo lidos e escritos o tempo todo. Por isso as decisões de dados (Parte III) merecem o pensamento mais cuidadoso e antecipado: o código você refatora num fim de semana; os dados de um sistema vivo você migra com suor, downtime e risco. Erre no framework, não no particionamento.

1.7 Decisões reversíveis e irreversíveis

Nem toda decisão merece o mesmo peso, e tratar todas como se fossem definitivas é uma forma de paralisia. A distinção que organiza isso vem de uma metáfora útil: algumas decisões são portas de mão dupla (você entra, e se não gostar, volta) e outras são portas de mão única (uma vez do outro lado, não há retorno fácil).

Porta de mão dupla (reversível)

Trocar uma biblioteca, ajustar um cache, mudar um endpoint, escolher um formato de log. Se errar, você reverte com custo baixo. Decida rápido, experimente, aprenda com o resultado. Gastar semanas deliberando sobre uma decisão reversível é desperdício.

Porta de mão única (irreversível)

A escolha do modelo de dados, da chave de particionamento, da garantia de consistência fundamental, do contrato público de uma API com muitos consumidores. Reverter é caríssimo ou impossível. Decida devagar, com rigor, prototipando e questionando.

Combine o rigor da decisão à sua reversibilidade
O erro vem em dois sabores opostos, e ambos custam caro. Tratar uma decisão reversível como irreversível gera paralisia por análise: a equipe debate por semanas a escolha de um framework que poderia ter trocado em dois dias se desse errado. Tratar uma decisão irreversível como reversível gera dívida catastrófica: alguém escolhe a chave de sharding numa tarde, "a gente muda depois", e dois anos depois o "depois" custa uma migração de meses. A habilidade é classificar cada decisão antes de decidir: esta porta é de mão única? Se sim, devagar e com gente experiente. Se não, rápido e com disposição de reverter. O rigor é um recurso escasso; gaste-o nas portas que não têm volta.

1.8 Estudo de caso — duas equipes, o mesmo problema

"Precisamos de um sistema de busca"
O pedido

Duas equipes recebem o mesmo pedido do produto: "os usuários precisam buscar produtos no nosso catálogo por nome e descrição." Ambas são competentes. Elas chegam a arquiteturas radicalmente diferentes — e a diferença não está no conhecimento técnico, mas em como pensaram.

Equipe A · começa pela solução

"Busca? Isso é Elasticsearch." Em uma semana, montam um cluster Elasticsearch, um pipeline de sincronização do banco para o índice, lidam com a consistência entre os dois, com o cluster que precisa de manutenção, com o modo de falha novo (e se o índice e o banco divergirem?). Funciona. Também é, agora, mais um sistema distribuído para operar, monitorar e debugar às 3h.

Equipe B · começa pelas restrições
as perguntas antes da solução
Quantos produtos?        → 50 mil. (não 50 milhões)
Quantas buscas/segundo?  → dezenas no pico.
Busca precisa ser fuzzy / relevância avançada? → não, por ora.
Já temos um banco?       → sim, Postgres.
Qual o custo de operar mais um sistema? → alto, time pequeno.

Com 50 mil produtos e busca simples, o full-text search nativo do Postgres (que a Equipe B já opera) resolve. Zero sistemas novos, zero pipeline de sincronização, zero consistência entre dois stores, zero cluster para manter. Uma tarde de trabalho.

O ponto — e quando A estaria certa

A Equipe B não foi mais inteligente; foi mais disciplinada. Começou pelas restrições e descobriu que o problema real (50 mil produtos, busca simples) não exigia a solução cara. Mas note: se as respostas fossem outras — 50 milhões de produtos, busca fuzzy com relevância, autocompletar, milhares de buscas por segundo — então o Elasticsearch da Equipe A seria a escolha certa, e o Postgres seria a ingenuidade. A lição não é "Postgres sempre" nem "Elasticsearch nunca". É que a mesma pergunta tem respostas opostas dependendo das restrições, e quem pula direto para a solução nunca descobre qual caso tem em mãos.

Resultado: a Equipe B entregou em uma tarde o que a Equipe A entregou em uma semana, e — mais importante — ficou com um sistema a menos para operar. Se o catálogo crescer e a busca ficar sofisticada, a Equipe B migra para o Elasticsearch quando o gargalo for real, com dados sobre o uso, e a migração será uma porta de mão dupla bem informada. A Equipe A pagou adiantado por uma escala e uma sofisticação que talvez nunca cheguem. Ambas sabiam usar Elasticsearch; só uma perguntou se devia. Esse é o capítulo inteiro: design não é conhecer a solução sofisticada, é saber se você tem o problema que a justifica.

1.9 Erros comuns

Erro 1 · Começar pela solução

Ouvir o pedido e já estar escolhendo tecnologia, antes de conhecer escala, latência, consistência e custo. A solução certa é indeterminada até as restrições estarem na mesa. Pergunte antes de propor; as restrições definem o espaço de soluções válidas.

Erro 2 · Copiar a arquitetura dos gigantes

Adotar a arquitetura do Netflix/Uber/Google porque é o que os melhores fazem — ignorando que eles resolvem problemas de escala que você não tem, com times de centenas que você não tem. A arquitetura deles é a resposta ao problema deles, não ao seu. Imitar a solução sem ter o problema é importar a complexidade sem o motivo.

Erro 3 · Ver a propriedade, ignorar o custo

Adotar uma técnica pela propriedade que ela ganha (escala, desacoplamento) sem contabilizar o que ela cobra (complexidade, latência, consistência). Não existe solução, só trade-off. A pergunta nunca é "isso funciona?", é "o que isso custa, e vale para o meu caso?".

Erro 4 · Gastar o orçamento de inovação na infraestrutura

Encher a arquitetura de tecnologias novas e exóticas onde o entediante e confiável resolveria, transformando cada peça num modo de falha não mapeado. Gaste a inovação no que diferencia seu produto; faça a infraestrutura o mais entediante possível. Novidade sem problema que a exija é dívida disfarçada.

Erro 5 · Tratar toda decisão como irreversível

Paralisar a equipe deliberando por semanas uma escolha de porta de mão dupla, que poderia ser testada e revertida em dias. Decisões reversíveis pedem velocidade e experimentação. Gastar rigor onde ele não é necessário rouba o rigor de onde ele é.

Erro 6 · Tratar a decisão irreversível como reversível

Escolher a chave de sharding ou o modelo de dados numa tarde com "a gente muda depois". Decisões de porta de mão única, especialmente em dados, custam migrações de meses para desfazer. Identifique o que não tem volta e dê a essas decisões o rigor que elas exigem, antes de atravessar a porta.

Erro 7 · Confundir o diagrama com o design

Achar que desenhou um bom sistema porque o diagrama ficou bonito, sem conseguir justificar por que cada caixa existe e cada trade-off foi aceito. O diagrama é o resíduo do raciocínio. Um sistema cujas decisões você não sabe defender foi montado, não projetado.

Verifique seu entendimento
Um colega propõe migrar a aplicação de um monólito para microsserviços, argumentando que "é assim que sistemas escaláveis são construídos, é o que as grandes empresas fazem". Qual é a resposta mais alinhada com um bom raciocínio de design?

1.10 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · As restrições antes da solução

Pegue um pedido vago e realista — "precisamos de um sistema de comentários para nosso site". Sem propor nenhuma solução ainda, liste as restrições que você precisaria conhecer antes de projetar qualquer coisa. Para cada restrição, dê um exemplo de duas respostas que levariam a arquiteturas diferentes.

Restrições a extrair, e como cada resposta muda o design:

  • Escala: centenas de comentários/dia (um blog) ou milhões (uma rede social)? O primeiro cabe numa tabela simples; o segundo exige particionamento e cache pesados.
  • Latência / frescor: o comentário precisa aparecer instantaneamente para todos, ou pode levar segundos? "Instantâneo e consistente" e "aparece em alguns segundos" levam a arquiteturas diferentes (síncrono vs eventual).
  • Estrutura: comentários planos ou aninhados (threads infinitas)? Aninhamento profundo muda radicalmente o modelo de dados e as queries.
  • Moderação: precisa de aprovação antes de publicar, antifraude, antispam? Isso adiciona uma etapa assíncrona e estado.
  • Leitura vs escrita: muito mais gente lê do que comenta (quase sempre)? Isso favorece cache agressivo de leitura.
  • Durabilidade: perder um comentário é tolerável ou inaceitável?

O insight: "sistema de comentários" não tem uma resposta — tem uma família de respostas, e as restrições selecionam qual. Um engenheiro que pula direto para "uso tal banco e tal fila" está respondendo uma pergunta que ainda não foi feita. Extrair as restrições é o começo do design.

Médio
Exercício 2 · Nomear o trade-off

Para cada decisão de design abaixo, nomeie explicitamente a propriedade que se ganha e o preço que se paga: (a) adicionar um cache na frente do banco; (b) replicar o banco em múltiplas regiões; (c) processar pedidos por uma fila assíncrona em vez de síncrona; (d) desnormalizar dados (duplicar informação para evitar joins).

(a) Cache na frente do banco. Ganha: latência baixa e menos carga no banco (lê da memória). Paga: invalidação (o dado em cache pode estar velho), um modo de falha novo, e a complexidade de manter cache e fonte coerentes. O trade-off central: frescor vs performance.

(b) Replicar o banco em múltiplas regiões. Ganha: latência baixa para usuários distantes (leem da réplica próxima) e disponibilidade (uma região cai, outra serve). Paga: consistência — as réplicas divergem por um intervalo (replicação assíncrona), e reconciliar escritas concorrentes entre regiões é difícil. Trade-off: proximidade/disponibilidade vs consistência (é o CAP, Cap 5, em forma concreta).

(c) Fila assíncrona em vez de síncrona. Ganha: desacoplamento (o produtor não espera o consumidor), absorção de picos (a fila amortece), e resiliência (o consumidor pode estar fora um tempo). Paga: latência fim-a-fim maior, complexidade de entrega (duplicatas, ordem, o que fazer com o que falha), e a perda da resposta imediata — o resultado vem depois, não na hora. Trade-off: acoplamento/imediatismo vs resiliência/escala.

(d) Desnormalizar (duplicar dados). Ganha: leitura rápida (sem joins caros, o dado já está montado). Paga: consistência (a mesma informação em vários lugares pode divergir), mais escrita (atualizar todas as cópias), e mais armazenamento. Trade-off: velocidade de leitura vs integridade/custo de escrita.

O insight: em todos, a estrutura é a mesma — uma propriedade ganha, outra paga. Saber nomear o que se paga, e não só o que se ganha, é a diferença entre escolher um trade-off e tropeçar nele. Quem só vê o ganho descobre o custo em produção.

Médio
Exercício 3 · Reversível ou irreversível?

Classifique cada decisão como porta de mão dupla (reversível, decida rápido) ou mão única (irreversível, decida devagar), justificando o custo de reverter: (a) escolher React ou Vue para o frontend; (b) escolher a chave de particionamento de uma tabela que terá bilhões de linhas; (c) escolher o formato de um evento publicado num tópico consumido por 15 serviços de outras equipes; (d) escolher o algoritmo de balanceamento de carga (round-robin vs least-connections).

(a) React vs Vue → mão dupla (reversível-ish). Caro de trocar, mas factível e localizado ao frontend; não corrompe dados nem quebra contratos externos. Não vale paralisar semanas: escolha por familiaridade do time e siga. Se doer muito, migra.

(b) Chave de particionamento de tabela com bilhões de linhas → mão única (irreversível). O caso clássico. Uma vez que os dados estão particionados por aquela chave e em volume, mudar exige migrar bilhões de linhas, frequentemente com downtime e risco. Decida devagar, com gente experiente, prototipando o padrão de acesso. Errar aqui é uma das dívidas mais caras que existem.

(c) Formato de evento consumido por 15 serviços → mão única (irreversível na prática). O contrato é público e tem muitos consumidores que você não controla. Mudá-lo quebra todos eles, ou exige versionamento e migração coordenada entre equipes — caríssimo. Decida o esquema com cuidado, e projete para evolução (campos opcionais, versionamento) desde o início.

(d) Algoritmo de balanceamento → mão dupla (muito reversível). É uma configuração; trocar round-robin por least-connections é mudar um parâmetro e observar. Decida rápido, meça, ajuste. Zero motivo para deliberação longa.

O insight: o custo de reverter é o que define o rigor da decisão, e ele correlaciona fortemente com dados e contratos externos — as duas coisas que "já estão lá fora" e não voltam fácil. Código e configuração são quase sempre mão dupla; dados em volume e contratos com muitos consumidores são quase sempre mão única. Classifique antes de decidir, e aloque seu rigor de acordo.

Difícil
Exercício 4 · O design review

Você está revisando a proposta de arquitetura de um colega para um novo serviço. A proposta tem uma fila Kafka, um cache Redis, três microsserviços, um banco NoSQL e um cluster de busca — para um produto que ainda não foi lançado e cuja base de usuários esperada no primeiro ano é de alguns milhares. Estruture as perguntas que você faria no review, o que você suspeitaria, e como conduziria a conversa sem ser apenas "isso é overengineering".

O sinal de alerta: uma arquitetura distribuída sofisticada (Kafka, Redis, NoSQL, busca, microsserviços) para um produto não lançado com escala esperada modesta tem todos os sintomas de escala prematura e de design que começou pela solução. Mas a abordagem certa no review não é decretar "overengineering" — é fazer as perguntas que levam o próprio autor a justificar (ou rever) cada peça.

As perguntas, peça por peça (cada uma testa se há problema real):

  • Sobre a escala: "Que números estão por trás dessas escolhas? Qual o RPS esperado no primeiro ano, qual o volume de dados?" — se a resposta é "alguns milhares de usuários", quase nada disso se justifica por escala.
  • Kafka: "Qual o problema concreto que a fila resolve aqui? Há um processamento assíncrono real, ou um pico a absorver?" — se não há, é uma peça distribuída sem motivo.
  • Microsserviços (3): "Por que três serviços e não um? Há equipes separadas que precisam deployar independentemente? Há partes com requisitos de escala muito diferentes?" — sem isso, é complexidade de coordenação sem benefício; o monólito modular seria mais simples e reversível.
  • NoSQL: "Qual característica dos dados ou do acesso exige NoSQL em vez do Postgres? Há um padrão de acesso que o relacional não atende?" — frequentemente a resposta revela que o relacional resolveria.
  • Busca e Redis: "Qual o gargalo medido que justifica cada um? Ou são preventivos?"

Como conduzir (sem antagonizar): enquadrar como curiosidade genuína, não julgamento — "me ajuda a entender o problema que cada peça resolve". Isso transforma o review de uma briga ("seu design é exagerado") numa investigação conjunta ("vamos ver quais dessas peças o problema realmente pede"). Reconhecer o custo invisível: cada peça é mais um sistema para operar, monitorar, debugar e que pode falhar — com um time provavelmente pequeno, esse custo operacional é real e recai sobre todos. E oferecer o caminho construtivo: "que tal começarmos com o mínimo que resolve o problema de hoje — provavelmente um monólito com Postgres — e adicionarmos cada peça quando um gargalo medido a justificar? Assim entregamos mais rápido, operamos menos, e cada peça que entrar terá uma razão concreta."

O que um bom revisor demonstra: que não está contra sofisticação, está a favor de sofisticação justificada; que a pergunta sempre é "qual problema isto resolve, e nós o temos?"; e que o caminho é começar simples e crescer sob gatilho medido, mantendo reversibilidade. Frase de fechamento: "não tenho nada contra nenhuma dessas tecnologias — cada uma é certa para o problema que ela resolve. Minha única pergunta é, peça por peça: nós temos esse problema agora? Porque cada peça que adicionamos sem ter o problema é complexidade que vamos operar de graça, e atenção que sai do produto que ainda precisa ser lançado."

Fim do capítulo 1
Próximo capítulo: requisitos, restrições e o problema real. Como extrair, de um pedido vago e de stakeholders que não sabem o que querem, as restrições concretas que tornam o design possível — separando o requisito do desejo, o essencial do acidental, e o problema declarado do problema verdadeiro.
Parte I · Capítulo 2 · O raciocínio

Requisitos,
restrições e o
problema real.

O capítulo anterior estabeleceu que as restrições vêm antes da solução. Mas há um problema: ninguém te entrega as restrições prontas. O que você recebe é um pedido vago ("precisamos de um sistema de pedidos"), de pessoas que sabem o que querem sentir mas não o que precisam tecnicamente, e que muitas vezes pedem a solução que imaginaram em vez de descreverem o problema que têm. Extrair, desse material confuso, as restrições concretas que tornam o design possível — essa é a habilidade que precede todo design, e a que mais separa o sênior do júnior.

Este capítulo é sobre transformar névoa em especificação. Vamos ver como encontrar o problema real por trás do pedido declarado, distinguir requisitos funcionais (o que o sistema faz) de não-funcionais (como ele faz — onde mora quase toda a dificuldade de arquitetura), traduzir desejos vagos ("rápido", "confiável") em números que orientam decisões, fazer a pergunta que organiza qualquer sistema de dados (a razão entre leitura e escrita), descobrir os requisitos implícitos que ninguém menciona mas todos esperam, definir escopo com a coragem de dizer o que fica de fora, e projetar para a mudança que virá. No fim, você terá o método de chegar de "precisamos de algo" a um conjunto de restrições sobre o qual se pode, enfim, projetar.

2.1 A história — da especificação perfeita ao diálogo contínuo

Contexto histórico

Por décadas, a engenharia de software acreditou numa fantasia: a de que era possível, e desejável, especificar tudo antes de construir. O modelo cascata (waterfall), dominante dos anos 1970 aos 1990, pressupunha uma fase de "levantamento de requisitos" que produzia um documento completo e congelado, do qual o design e o código decorreriam. A premissa: se você pensar o suficiente, captura todos os requisitos de antemão.

A realidade demoliu a fantasia. Estudos clássicos da época — o relatório CHAOS do Standish Group entre eles — mostraram que requisitos incompletos e mutáveis eram a causa número um do fracasso de projetos. As pessoas não sabem o que querem até verem algo; o que querem muda quando o mundo muda; e o documento perfeito, quando finalmente pronto, já descrevia um problema que não existia mais.

A resposta foi o movimento ágil (anos 2000) e a ideia de que requisitos não são capturados de uma vez, mas descobertos ao longo do tempo, num diálogo contínuo entre quem constrói e quem usa. Surgiram técnicas para extrair melhor — histórias de usuário focadas no porquê, os "cinco porquês" para chegar à raiz, o design orientado ao domínio (DDD) de Eric Evans, que insistia que entender profundamente o negócio precede modelar o sistema.

Em 2026, o pêndulo está num lugar maduro: nem a fantasia da especificação completa, nem o caos do "vamos descobrindo". O bom engenheiro sabe que requisitos evoluem, mas também que algumas restrições precisam ser cravadas cedo porque definem decisões irreversíveis (Cap 1). A arte não é prever tudo — é descobrir, no momento do design, quais restrições já estão claras o suficiente para decidir, quais ainda são névoa que exige conversa, e quais são desejos disfarçados de requisitos que vão mudar na primeira semana. Extrair requisitos virou menos "preencher um formulário" e mais "investigar até entender o problema de verdade".

2.2 O problema por trás do pedido

O primeiro erro, e o mais caro, é aceitar o pedido pelo valor de face. Quando alguém pede um sistema, quase sempre está pedindo a solução que imaginou para um problema que não descreveu. Sua tarefa é escavar de volta: do pedido (a solução imaginada) até o problema real (a necessidade que a originou). Só sobre o problema real se pode projetar bem.

"Eu quero uma broca" — na verdade quer um furo
A velha máxima do marketing vale dobrado em design de sistemas: ninguém quer uma broca de um quarto de polegada; querem um furo de um quarto de polegada. Quando o stakeholder pede "um dashboard em tempo real", ele talvez não queira tempo real — queira confiança de que os números estão certos, ou ser avisado quando algo dá errado, e "tempo real" foi a forma que ele imaginou de conseguir isso. Se você construir o dashboard em tempo real (caro, complexo) sem investigar, pode entregar exatamente o pedido e errar a necessidade — enquanto um alerta simples (barato) resolveria o problema real. A pergunta-chave para escavar: "para que você precisa disso? O que você vai fazer com essa informação?". A resposta revela o furo por trás da broca.

A técnica dos cinco porquês formaliza essa escavação. Cada "por quê?" remove uma camada de solução assumida e aproxima do problema real:

escavando o problema real
Pedido: "Precisamos de um sistema de relatórios que roda toda noite."

Por quê?  → "Para o time de vendas ver os números da véspera de manhã."
Por quê?  → "Porque eles decidem o foco do dia com base nisso."
Por quê de manhã? → "Porque a reunião é às 9h."
E se atrasar? → "Aí a reunião usa dado velho, atrapalha."
O dado precisa ser EXATO ou aproximado serve? → "Aproximado serve."

Problema real: ter números confiáveis-o-suficiente disponíveis
ANTES das 9h. Não é "rodar toda noite" — é "estar pronto às 9h,
com dado aproximado bastando". Isso abre soluções que o pedido
literal escondia (pré-agregação, cache, aproximação).

2.3 Requisitos funcionais vs não-funcionais

Com o problema real em mãos, os requisitos se separam em duas naturezas, e entender a diferença é fundamental porque a dificuldade de arquitetura quase nunca está nos requisitos funcionais.

Funcionais — o que o sistema faz

As capacidades, as funcionalidades. "Usuário pode postar um comentário." "Sistema envia um email de confirmação." "Admin pode banir um usuário." São o quê. Geralmente claros, listáveis, e — surpreendentemente — raramente o que torna o design difícil.

Não-funcionais — como o sistema se comporta

As qualidades: latência, throughput, disponibilidade, consistência, durabilidade, escala, segurança. "Quão rápido?" "Quantos por segundo?" "Pode cair?" "Pode perder dado?" São o como. Quase invisíveis no pedido, e onde mora praticamente toda a dificuldade de arquitetura.

"Postar um comentário" é trivial; "um milhão por segundo, ordenados, sem perder nenhum" é um sistema
Considere o requisito funcional "o usuário pode postar um comentário". Sozinho, é um INSERT numa tabela — trivial. Agora adicione os não-funcionais: um milhão de comentários por segundo (escala), aparecendo para todos em menos de 100ms (latência + consistência), sem nunca perder um (durabilidade), mesmo se um data center cair (disponibilidade). O mesmo requisito funcional virou um dos sistemas mais difíceis de projetar que existem. Esta é a lição central: a função é quase sempre fácil; as qualidades que a cercam é que definem a arquitetura. Quando você ouvir um pedido funcional, sua atenção deve ir imediatamente para os números não-funcionais que ninguém mencionou — porque é ali que o design vive ou morre.

2.4 Traduzir desejo em número

Os requisitos não-funcionais chegam em palavras vagas e perigosas: "rápido", "confiável", "escalável", "em tempo real". Essas palavras não são especificações — são desejos. Cada pessoa as interpreta diferente, e nenhuma decisão de arquitetura pode ser tomada sobre elas. O trabalho é traduzi-las em números, porque só números orientam decisões e permitem saber se você os atingiu.

O desejo vagoA pergunta que o aterraO número que orienta
"Rápido"Em quanto tempo? Para qual percentil?p99 abaixo de 200ms
"Escalável"Quantas requisições por segundo, e até quanto cresce?10k RPS hoje, 100k em 2 anos
"Confiável" / "sempre no ar"Quanto downtime é tolerável?99,9% (43min/mês)
"Não pode perder dados"Zero perda mesmo em desastre? Ou RPO de minutos?RPO de 5 min, RTO de 1h
"Em tempo real"Latência de quê? Segundos? Milissegundos?atualização visível em < 2s
"Muitos usuários"Total? Simultâneos? Ativos por dia?1M registrados, 50k simultâneos no pico
"Tempo real" é a palavra mais perigosa do design
Nenhum termo causa mais sobre-engenharia que "tempo real", porque quase ninguém quer dizer o que ele significa tecnicamente. Tempo real de verdade (garantias de latência em milissegundos, como num sistema de controle industrial) é raro, caro e raramente necessário. O que as pessoas costumam querer ao dizer "tempo real" é "razoavelmente atualizado" — segundos, às vezes minutos, são perfeitamente aceitáveis. A diferença de custo entre "aparece em 2 segundos" e "garantido em 50 milissegundos" é a diferença entre uma arquitetura simples e uma extremamente complexa. Sempre, sempre pergunte: "quando você diz tempo real, qual atraso é aceitável? Um segundo? Cinco? Trinta?". A resposta quase nunca exige o que a palavra sugere — e descobrir isso economiza meses.

E há uma sutileza nos números de latência que separa o ingênuo do experiente: a média mente, use percentis. "Latência média de 50ms" pode esconder que 1% das requisições levam 5 segundos. Esse 1% é o usuário irritado, o pico do incidente, a cauda que derruba a experiência. Por isso se especifica em p95, p99, p999 (o percentil 95, 99, 99,9) — "99% das requisições abaixo de 200ms" diz algo real; a média não. Em escala, a cauda de latência não é exceção rara; é a experiência de uma fração significativa dos usuários, e cresce conforme o sistema fica ocupado.

2.5 A pergunta que organiza qualquer sistema de dados

Entre todos os números, há um que merece destaque porque molda a arquitetura de dados inteira: qual a razão entre leitura e escrita? Sistemas dominados por leitura e sistemas dominados por escrita têm soluções opostas, e descobrir cedo de qual lado você está direciona quase todas as decisões seguintes.

Read-heavy (muito mais leitura)

Um feed, um blog, um catálogo: milhares leem para cada um que escreve. As soluções: cache agressivo (a mesma leitura serve muitos), réplicas de leitura (distribuir a carga de SELECT), desnormalização (montar o dado de leitura adiantado). Otimize o caminho de leitura; a escrita pode ser mais cara.

Write-heavy (muita escrita)

Telemetria, logs, eventos, IoT: torrentes de escrita, leitura esparsa. As soluções são outras: particionamento de escrita, bancos otimizados para ingestão (LSM-trees, time-series), buffering e batching. Cache e réplicas de leitura ajudam pouco; o gargalo é absorver a escrita.

Cache e réplica não salvam um sistema write-heavy
O reflexo de "está lento, põe um cache" é tão automático que vira erro. Cache acelera leitura repetida; se o seu gargalo é absorver um milhão de escritas por segundo, o cache não ajuda em nada — você não lê o suficiente para ele importar, e a escrita continua afogando o banco. Da mesma forma, réplicas de leitura distribuem SELECTs, não INSERTs; num sistema write-heavy, todas as escritas ainda vão para o primário, que continua sendo o gargalo. Saber se você é read- ou write-heavy antes de escolher as ferramentas evita aplicar a solução de um problema no outro. É uma das primeiras perguntas que um designer experiente faz, porque ela elimina metade das soluções erradas de cara.

2.6 Os requisitos implícitos

Há uma classe de requisitos que ninguém menciona porque são tão óbvios para quem pede que parecem dispensar menção — e tão fáceis de esquecer para quem constrói que viram os bugs e incidentes mais constrangedores. São os requisitos implícitos: o sistema deve fazer X, "obviamente" sem perder dados, sem expor informação de um usuário a outro, sem cair quando um componente falha. Ninguém escreve isso no pedido; todo mundo espera.

  • Segurança e privacidade: "obviamente" um usuário não vê os dados de outro, senhas não vazam, e há controle de acesso. Quase nunca está no pedido funcional; sempre é esperado, e sua ausência é incidente (e, com dados pessoais, questão legal — o volume anterior, LGPD).
  • Tratamento de falha: o que acontece quando uma dependência cai? Quando a rede falha no meio de uma operação? "Obviamente" o sistema não deve corromper dados nem ficar num estado inconsistente — mas isso exige design explícito (Parte IV).
  • Idempotência e duplicação: o que acontece se o usuário clicar "pagar" duas vezes, ou se uma mensagem chegar repetida? "Obviamente" não deve cobrar duas vezes — mas garantir isso é trabalho (Cap 16).
  • Observabilidade: quando der errado, alguém precisa conseguir descobrir o quê e por quê. "Obviamente" — mas só se foi projetado para isso.
  • Evolução e compatibilidade: o sistema vai mudar; versões novas conviverão com antigas durante deploys. "Obviamente" não pode quebrar quem ainda usa a versão velha.
O requisito não dito é o que vira o incidente
Há um padrão cruel: os requisitos que viram os piores incidentes são justamente os que ninguém escreveu, porque eram "óbvios demais". O sistema fazia perfeitamente o que o pedido pedia — e expunha dados de um usuário a outro, ou cobrava em duplicidade, ou corrompia tudo quando o banco piscava, porque essas garantias nunca foram tratadas como requisitos a projetar. A disciplina do designer experiente é ter uma checklist mental dos implícitos e passá-la por cima de todo pedido: e a segurança? e a falha parcial? e a duplicação? e a observabilidade? e a evolução? Tornar explícito o implícito antes de projetar é mais barato que descobri-lo no pós-mortem. O pedido te dá os requisitos óbvios; sua experiência precisa fornecer os invisíveis.

2.7 Escopo — a coragem de dizer o que fica de fora

Definir requisitos é tanto sobre o que entra quanto sobre o que fica de fora. Um escopo que tenta abraçar tudo produz um sistema que não faz nada bem, atrasa sem fim, e é complexo demais para operar. A maturidade inclui a coragem de declarar explicitamente as não-metas: "este sistema não vai fazer X (por ora)", "vamos assumir Y para simplificar", "Z fica para depois".

As não-metas valem tanto quanto as metas
Tão importante quanto listar o que o sistema fará é listar o que ele explicitamente não fará — as não-metas. Elas não são covardia; são foco. "Este sistema atende o Brasil, não o mundo" elimina a complexidade de multi-região. "Assumimos até 10 mil usuários nesta versão" justifica o monólito e adia o sharding. "Não suportamos edição de comentários, só criar e apagar" remove um caso difícil. Cada não-meta é uma decisão de design que simplifica, e torná-la explícita evita que ela seja revogada por engano ("ah, mas e se for global?") e impede o escopo de inchar silenciosamente. Um documento de design sem uma seção de não-metas é um documento que ainda não decidiu o que está construindo. Dizer "não" a um requisito é, muitas vezes, a decisão de arquitetura mais valiosa que você toma.

2.8 Projetar para a mudança certa

Os requisitos vão mudar — isto é certeza, não risco. Mas a resposta não é tentar prever todas as mudanças e projetar para elas (isso é o overengineering do Cap 1, agora disfarçado de "flexibilidade"). A resposta é distinguir quais mudanças são prováveis e baratas de acomodar agora, das que são improváveis ou caras demais para antecipar.

Flexibilidade que vale a pena

O que é barato deixar aberto e provável mudar: campos opcionais e versionáveis num evento (Cap 1, contrato irreversível), uma camada de abstração fina sobre o que pode ser trocado, evitar premissas cravadas sobre números que vão crescer. Custo baixo agora, economia grande depois.

Flexibilidade que é overengineering

Construir suporte a multi-região, multi-tenant, plugins genéricos e configurabilidade infinita "porque um dia pode precisar" — pagando alta complexidade hoje por uma flexibilidade que talvez nunca se use. É a escala prematura vestida de "arquitetura extensível".

YAGNI, mas não nas portas de mão única
"You Aren't Gonna Need It" — não construa para a necessidade hipotética — é um princípio excelente, com uma exceção crucial que vem do Cap 1. Para decisões reversíveis (porta de mão dupla), YAGNI manda: não antecipe, adicione quando precisar, porque adicionar depois é barato. Mas para decisões irreversíveis (porta de mão única — modelo de dados, chave de particionamento, contrato público), YAGNI é perigoso: ali, "adicionar depois" é caríssimo, e vale pensar na evolução provável agora, deixando o caminho aberto. A regra combinada: aplique YAGNI agressivamente nas decisões reversíveis, e pense na evolução cuidadosamente nas irreversíveis. Não construa o multi-região que talvez nunca venha; mas projete o esquema do evento para poder evoluir, porque mudá-lo depois quebra quinze consumidores.

2.9 Estudo de caso — o "sistema de notificações" que era outra coisa

Quando a investigação muda o sistema inteiro
O pedido

O produto chega com um pedido claro: "precisamos de um sistema de notificações que avise os usuários por email e push quando algo relevante acontecer na conta deles." Parece direto — uma fila, workers que mandam email e push, pronto. O designer júnior já está desenhando. O sênior começa a investigar.

Passo 1 · Escavar o problema real (os porquês)
a investigação
"Que tipo de evento gera notificação?" → "Pagamento recebido,
   fatura vencendo, e mudanças de segurança."
"Qual a urgência de cada um?" → "Segurança é IMEDIATO. Fatura
   pode esperar horas. Pagamento, alguns minutos."
"O que acontece se uma notificação se perder?" → "Segurança
   NÃO pode perder. As outras, tolerável reenviar."
"Quantas por segundo?" → "Dezenas normalmente, milhares se
   rodarmos a cobrança de faturas em lote no dia 1º."
"O usuário escolhe os canais?" → "Sim, e pode desativar."
Passo 2 · O pedido se decompõe em requisitos diferentes

A investigação revelou que "sistema de notificações" não é um sistema com requisitos uniformes — são três classes com requisitos opostos. Segurança: baixíssima latência, durabilidade máxima (não pode perder), volume baixo. Fatura: latência tolerante (horas), mas com picos enormes (lote do dia 1º). Pagamento: latência média, volume médio. Tratar os três igual produziria um sistema ou caro demais (se otimizado para o pior caso de cada dimensão) ou frágil (se otimizado para o caso médio).

Passo 3 · Os requisitos implícitos que ninguém disse
os implícitos, tornados explícitos
Não dito, mas esperado:
- Não notificar duas vezes pelo mesmo evento (idempotência)
- Respeitar a preferência de canal do usuário (e o opt-out — lei)
- Não vazar: a notificação do usuário A não vai para B
- O que fazer quando o provedor de email está fora? (falha)
- Conseguir saber se uma notificação foi entregue (observabilidade)
- Absorver o pico do dia 1º sem derrubar o resto (backpressure)
Passo 4 · O design que emerge — e as não-metas

Agora o design tem fundamento: uma fila com prioridades (segurança fura a fila), garantia de entrega diferenciada por classe (durabilidade forte para segurança, best-effort com retry para o resto), idempotência por ID de evento, e backpressure para o lote de faturas não afogar as notificações urgentes. E as não-metas explícitas: "não suportamos SMS nesta versão", "não garantimos ordem entre notificações de tipos diferentes", "assumimos no máximo X mil/segundo no pico". Cada não-meta corta uma complexidade que o pedido literal teria deixado ambígua.

Resultado: o sênior entregou um sistema diferente do que o pedido literal pediria — não por capricho, mas porque a investigação revelou que "notificações" eram três problemas com requisitos opostos, cercados de requisitos implícitos críticos (idempotência, opt-out legal, falha de provedor) que o pedido não mencionava. O júnior teria construído uma fila uniforme que funcionaria na demo e falharia no dia 1º (lote afogando o urgente) ou notificaria em duplicidade no primeiro retry. A diferença não foi conhecimento de filas — ambos sabiam. Foi a disciplina de investigar o problema antes de projetar a solução: escavar os porquês, decompor o pedido uniforme em requisitos distintos, tornar explícitos os implícitos, e ter a coragem das não-metas. O design começou bem porque os requisitos foram extraídos bem. Próximo capítulo: estimativas — como pôr números nesses requisitos com aritmética de guardanapo, para que as decisões de arquitetura se apoiem em ordens de grandeza, não em achismo.

2.10 Erros comuns

Erro 1 · Aceitar o pedido pelo valor de face

Construir exatamente a solução que o stakeholder imaginou, sem escavar o problema real por trás dela. Você pode entregar o pedido com perfeição e errar a necessidade. Pergunte "para quê?" até chegar ao furo por trás da broca — o problema real, não a solução imaginada.

Erro 2 · Focar no funcional, ignorar o não-funcional

Tratar "o que o sistema faz" como o trabalho de design, quando a dificuldade quase toda está em "como ele se comporta" — latência, escala, consistência, durabilidade. A função é trivial; as qualidades que a cercam definem a arquitetura. Vá atrás dos números não-funcionais que ninguém mencionou.

Erro 3 · Projetar sobre palavras vagas

Tomar decisões de arquitetura sobre "rápido", "escalável", "tempo real" sem traduzi-las em números. Cada pessoa interpreta diferente, e nenhuma decisão se sustenta sobre um desejo. Aterre cada qualidade num número (p99, RPS, % de uptime) antes de projetar — e desconfie sempre de "tempo real".

Erro 4 · Otimizar a média, ignorar a cauda

Especificar e medir latência pela média, escondendo que 1% das requisições leva segundos — o usuário irritado, o pico do incidente. Em escala, a cauda é a experiência de muitos. Use percentis (p95, p99, p999); a média mente sobre a experiência real.

Erro 5 · Não perguntar a razão leitura/escrita

Escolher ferramentas sem saber se o sistema é read-heavy ou write-heavy, e aplicar a solução de um no outro — cache e réplicas num sistema afogado por escrita, que não ajudam em nada. Essa razão molda a arquitetura de dados inteira; pergunte-a cedo, antes de escolher qualquer ferramenta.

Erro 6 · Esquecer os requisitos implícitos

Construir o que o pedido diz e esquecer o que ele pressupõe — segurança, idempotência, tratamento de falha, observabilidade, evolução. São os "óbvios" que ninguém escreve e que viram os piores incidentes. Passe uma checklist mental dos implícitos sobre todo pedido.

Erro 7 · Escopo sem não-metas

Tentar atender tudo, sem declarar explicitamente o que fica de fora, e ver o escopo inchar até o sistema não fazer nada bem e nunca ficar pronto. As não-metas são foco, não covardia. Dizer "não" a um requisito é frequentemente a decisão de arquitetura mais valiosa.

Verifique seu entendimento
Um stakeholder pede: "quero um dashboard em tempo real mostrando as vendas, para eu acompanhar como estamos indo." Antes de projetar a infraestrutura de streaming em tempo real que o pedido sugere, qual é a abordagem mais alinhada com um bom levantamento de requisitos?

2.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Traduzir desejos em números

Para cada requisito vago, escreva as perguntas que você faria para aterrá-lo e proponha um número concreto plausível: (a) "o site tem que ser rápido"; (b) "não podemos perder pedidos"; (c) "precisa aguentar a Black Friday"; (d) "os dados precisam estar sempre atualizados".

(a) "Rápido" → Perguntas: rápido em quê (carregar a página? responder uma busca?), e para qual percentil? Número: "p95 da resposta da API abaixo de 200ms; página interativa em menos de 1s". Note o percentil — "rápido em média" esconderia a cauda lenta.

(b) "Não perder pedidos" → Perguntas: zero perda mesmo em desastre total, ou um RPO de alguns minutos é aceitável? O pedido precisa sobreviver à falha de um data center? Número: "RPO de 0 para pedidos confirmados (durabilidade síncrona antes de confirmar ao cliente); tolerância a reprocessar pedidos não confirmados". Isso é uma exigência de durabilidade forte, que muda a arquitetura.

(c) "Aguentar a Black Friday" → Perguntas: qual o pico esperado em RPS, quantas vezes o tráfego normal? Por quanto tempo dura o pico? Número: "10× o tráfego normal, chegando a 50k RPS, sustentado por ~6 horas". Sem o múltiplo e a duração, "aguentar" não dimensiona nada.

(d) "Sempre atualizados" → Perguntas: atualizados em quanto tempo? Quem lê precisa ver a escrita imediatamente (consistência forte) ou um atraso de segundos é aceitável (eventual)? Número: "leituras refletem escritas em até 2 segundos (consistência eventual aceitável)" — ou, se for crítico, "consistência forte, a leitura sempre vê a última escrita". Essa distinção é uma das mais caras do design (Parte II).

O insight: cada desejo vago esconde uma ou mais dimensões mensuráveis, e o número escolhido muda radicalmente a arquitetura. O trabalho de requisitos é fazer as perguntas que convertem o desejo no número — porque é sobre o número que se projeta.

Médio
Exercício 2 · Caçar os requisitos implícitos

O pedido é: "um sistema onde usuários podem fazer upload de documentos e compartilhá-los por um link." Liste os requisitos implícitos — os que ninguém mencionou mas todos esperam — e explique que incidente cada um evitaria se fosse tratado (ou causaria se esquecido).

Requisitos implícitos e o incidente que cada um previne:

  • Controle de acesso ao link: o link é público para quem o tiver, ou só para pessoas autorizadas? Esquecido → um documento privado fica acessível a quem adivinhar/vazar a URL (o vazamento de manchete).
  • Links não adivinháveis: se o ID do documento é sequencial, alguém itera os IDs e baixa tudo. Esquecido → enumeração e vazamento em massa. (Use IDs aleatórios.)
  • Validação do upload: tipo e tamanho do arquivo, varredura de malware. Esquecido → alguém sobe um executável malicioso ou um arquivo gigante que enche o storage.
  • Isolamento entre usuários: o usuário A não acessa os documentos de B sem autorização. Esquecido → vazamento entre contas.
  • Expiração / revogação do link: dá para revogar um link compartilhado por engano? Esquecido → não há como "desfazer" um compartilhamento.
  • Durabilidade e backup: o documento não pode sumir. Esquecido → perda de dados do usuário.
  • Limites e abuso: rate limiting no upload, cota por usuário. Esquecido → abuso enche o storage e a conta.
  • Privacidade / LGPD: documentos podem conter dados pessoais — retenção, exclusão a pedido. Esquecido → questão legal.

O insight: o pedido funcional ("upload e compartilhar por link") tem duas linhas; os requisitos implícitos são onde mora o sistema de verdade — e cada um esquecido é um incidente esperando acontecer. A maioria desses nunca aparece no pedido porque são "óbvios" para quem pede. A experiência do designer é justamente fornecer os invisíveis.

Médio
Exercício 3 · Read-heavy ou write-heavy?

Para cada sistema, determine se é dominado por leitura ou por escrita, e indique como isso muda a arquitetura (que técnicas ajudam, quais não): (a) o feed de uma rede social; (b) um sistema de coleta de métricas de servidores (telemetria); (c) um encurtador de URLs; (d) um sistema de logs de auditoria.

(a) Feed de rede social → fortemente read-heavy. Cada post é escrito uma vez e lido por centenas ou milhares de seguidores, muitas vezes. Técnicas que ajudam: cache agressivo, réplicas de leitura, desnormalização (montar o feed adiantado — "fan-out on write"). O caminho de leitura é o que se otimiza; a escrita pode ser mais cara. (É o caso do Cap 21.)

(b) Telemetria de servidores → fortemente write-heavy. Milhares de servidores escrevendo métricas continuamente; leitura só quando alguém abre um dashboard. Técnicas: banco time-series otimizado para ingestão, particionamento por tempo, batching, downsampling de dados antigos. Cache e réplicas de leitura ajudam pouco — o gargalo é absorver a torrente de escrita.

(c) Encurtador de URLs → fortemente read-heavy. A URL é criada uma vez e redirecionada (lida) muitas vezes. Técnicas: cache é quase perfeito aqui (a mesma URL curta resolve sempre para o mesmo destino, dado que muda pouco — cache hit altíssimo). Réplicas de leitura. A escrita (criar URL) é rara e barata.

(d) Logs de auditoria → write-heavy. Escreve-se muito (todo evento auditável), lê-se raramente (investigação, compliance). Técnicas: otimizar ingestão, append-only, particionamento por tempo, storage barato para histórico. Importante: aqui durabilidade é crítica (é auditoria), então a escrita precisa ser durável, não só rápida.

O insight: determinar o lado da balança é a primeira pergunta porque ela elimina metade das soluções erradas. Pôr cache num write-heavy não ajuda; otimizar ingestão num read-heavy é energia no lugar errado. E note (d): além de read/write, há a dimensão de durabilidade — as perguntas se compõem.

Difícil
Exercício 4 · Conduzir o levantamento de requisitos — entrevista

Você foi designado para projetar "um sistema de chat para nosso aplicativo" e tem 30 minutos com o gerente de produto, que tem ideias vagas. Estruture como você conduziria essa conversa: que perguntas faria e em que ordem, como separaria o problema real do pedido, como descobriria os requisitos não-funcionais e implícitos, e como terminaria com um escopo (incluindo não-metas) sobre o qual se possa projetar.

Enquadramento: "sistema de chat" é um dos pedidos mais traiçoeiros, porque esconde uma enorme variação — de um chat de suporte 1-para-1 a um Slack com milhões de canais. Os 30 minutos devem ir do problema real às restrições que selecionam qual chat é esse. A condução tem quatro movimentos.

1. Escavar o problema real (primeiros minutos):

  • "Para que serve esse chat? Qual problema do usuário ele resolve?" — suporte ao cliente? conversa entre usuários? colaboração em equipe? Cada um é um sistema diferente.
  • "O que acontece hoje sem ele? Como as pessoas se comunicam agora?" — revela a necessidade verdadeira e o que "bom o suficiente" significa.

2. Definir a forma funcional (o quê):

  • 1-para-1, grupos, ou ambos? Quantas pessoas por conversa?
  • Só texto, ou anexos, imagens, áudio? Histórico persistente ou efêmero? Presença ("online agora"), confirmação de leitura, digitação?
  • Cada "sim" aqui adiciona complexidade — e cada um é candidato a virar não-meta.

3. Extrair os não-funcionais (o como — onde mora a dificuldade):

  • Latência: "quão rápido a mensagem precisa chegar? Instantâneo de verdade (sub-segundo) ou alguns segundos servem?" — define se precisa de conexões persistentes (WebSocket) ou polling basta.
  • Escala: quantos usuários, quantos simultâneos, quantas mensagens/segundo no pico? — separa um chat de suporte (simples) de um Slack (sistema sério).
  • Durabilidade e ordem: mensagens podem se perder? A ordem dentro de uma conversa precisa ser garantida? (Quase sempre sim para ordem — é um requisito que parece óbvio mas exige design.)
  • Entrega offline: se o destinatário está offline, a mensagem espera e é entregue depois? (Quase sempre sim — muda a arquitetura.)
  • Read/write: mais leitura (histórico) ou escrita (mensagens novas)? Geralmente equilibrado, com picos de escrita.

4. Os implícitos e o escopo com não-metas:

  • Implícitos a levantar: privacidade (A não lê a conversa de B), criptografia (em trânsito? ponta a ponta?), moderação/abuso, conformidade legal (retenção de mensagens, LGPD), o que acontece quando uma entrega falha.
  • Fechar com não-metas explícitas: "nesta versão: sem chamadas de voz/vídeo, sem criptografia ponta a ponta, sem grupos acima de N pessoas, sem busca no histórico — assumimos até X usuários simultâneos." Cada não-meta corta uma complexidade e foca o escopo.

Como terminar: sair da conversa com um resumo de uma página: o problema real (em uma frase), os requisitos funcionais essenciais, os números não-funcionais (latência, escala, durabilidade), os implícitos a tratar, e as não-metas. Frase de fechamento: "não saio desta reunião com um design — saio com as restrições sobre as quais o design vai ser possível. 'Sistema de chat' pode ser dez sistemas diferentes; minhas perguntas existem para descobrir qual deles vocês realmente precisam, porque projetar o errado é caro e projetar antes de saber é adivinhação."

Fim do capítulo 2
Próximo capítulo: estimativas que orientam decisões. Com os requisitos traduzidos em números, falta saber se eles são viáveis e o que implicam — quantos servidores, quanto armazenamento, quanta banda. A aritmética de guardanapo (back-of-the-envelope) que transforma "um milhão de usuários" em decisões concretas de arquitetura, usada como ferramenta de raciocínio, não de entrevista.
Parte I · Capítulo 3 · O raciocínio

Estimativas
que orientam
decisões.

"Um milhão de usuários" não é uma decisão de arquitetura — é uma frase. Para que ela vire design, você precisa traduzi-la: quantas requisições por segundo isso gera? cabe num servidor ou exige cem? os dados cabem em memória ou precisam de disco distribuído? A aritmética de guardanapo — back-of-the-envelope — é a ferramenta que transforma números de requisitos em decisões de arquitetura. E o ponto que este capítulo defende: ela serve para decidir e descartar, não para impressionar numa entrevista.

Este capítulo é sobre estimar com propósito. Vamos ver por que uma estimativa grosseira e rápida vale mais que uma precisa e lenta, quais poucos números você precisa ter na cabeça para estimar qualquer coisa, o método para fazer as contas sem se enrolar, como estimar tráfego (RPS, picos), armazenamento e banda, e — o que de fato importa — como ler o resultado para tomar uma decisão: cabe num servidor ou não? em memória ou em disco? precisa de cache ou não? Terminamos com os limites honestos da estimativa: quando ela basta e quando ela engana. O objetivo não é virar uma calculadora humana; é desenvolver a intuição de ordem de grandeza que descarta arquiteturas inviáveis em cinco minutos, antes de gastar cinco semanas construindo a errada.

3.1 A história — Fermi, e a arte da aproximação útil

Contexto histórico

A técnica é mais velha que a computação. Enrico Fermi, o físico, era lendário por sua capacidade de estimar quantidades aparentemente impossíveis com aritmética mental e suposições razoáveis. A história mais famosa: durante o teste da primeira bomba atômica, em 1945, Fermi soltou pedacinhos de papel no ar quando a onda de choque chegou, mediu o quanto eles se deslocaram, e estimou a energia da explosão de cabeça — chegando notavelmente perto do valor que os instrumentos precisos calculariam depois. A lição: uma estimativa rápida e grosseira, baseada em raciocínio sólido, é frequentemente boa o suficiente para a decisão que importa.

Esses "problemas de Fermi" — "quantos afinadores de piano há em Chicago?" — viraram exercício clássico de raciocínio justamente porque ensinam a decompor o desconhecido em partes estimáveis, multiplicar ordens de grandeza, e chegar a uma resposta na faixa certa sem nenhum dado preciso. A precisão não é o objetivo; a ordem de grandeza é.

Na engenharia de software, a prática ganhou nome e fama através de uma tabela que circulou amplamente nos anos 2000, atribuída a Jeff Dean, do Google: "Latency Numbers Every Programmer Should Know" — os tempos relativos de ler da cache L1, da memória, do disco, da rede. A ideia era a mesma de Fermi: internalizar algumas ordens de grandeza permite estimar o comportamento de um sistema antes de construí-lo, e descartar designs inviáveis no papel.

Em 2026, a aritmética de guardanapo sofre de uma distorção: virou ritual de entrevista. Candidatos decoram fórmulas de "calcule o QPS do Twitter" como teatro, sem entender que a técnica existe para tomar decisões reais. Este capítulo recupera o propósito original. Estimar não é para impressionar um entrevistador com aritmética; é para você, projetando um sistema de verdade, responder em cinco minutos perguntas que definem a arquitetura inteira: cabe numa máquina? precisa de cache? os dados cabem em RAM? A estimativa é uma ferramenta de descarte — ela elimina rapidamente os caminhos que não fecham, deixando você gastar o tempo caro só nos que fecham.

3.2 Por que estimar — a estimativa como ferramenta de descarte

O valor da estimativa não está em prever o futuro com precisão; está em descartar o inviável rápido. Antes de desenhar uma arquitetura elaborada, cinco minutos de aritmética respondem perguntas que mudam tudo — e cuja resposta errada custaria semanas.

A estimativa responde "cabe?" antes de você construir
As perguntas que uma estimativa de guardanapo responde são as que mais determinam a arquitetura: Cabe num único servidor, ou preciso distribuir? Os dados cabem em memória (e aí tudo fica simples e rápido), ou precisam de disco? Esse volume de escrita afoga um banco relacional, ou ele aguenta? A banda necessária é trivial ou vai dominar o custo? Cada uma dessas, respondida no papel, elimina ou habilita famílias inteiras de soluções. Descobrir que "tudo cabe em 16GB de RAM" transforma um problema que parecia exigir um cluster distribuído num que roda numa máquina — e vice-versa. A estimativa é o filtro que você passa antes de investir tempo de design, e ela frequentemente revela que o problema é muito menor (ou muito maior) do que a intuição sugeria.

E há um efeito secundário valioso: a estimativa é um detector de premissas erradas. Quando você faz as contas e o resultado é absurdo (precisaria de dez mil servidores para um app de bairro), isso quase sempre significa que uma premissa está errada — você superestimou o tráfego, ou esqueceu que a leitura é cacheável. O absurdo no resultado te força a revisar as suposições, e é aí que entendimentos errados sobre o sistema vêm à tona, no papel, e não em produção.

3.3 Os números que você precisa ter na cabeça

Estimar bem não exige memorizar tabelas extensas. Exige internalizar um punhado de ordens de grandeza — não os valores exatos (que mudam com o hardware), mas as relações entre eles. O essencial:

A hierarquia de latência (as relações importam, não os números exatos)

OperaçãoOrdem de grandezaO que ensina
Ler da memória (RAM)~100 nanossegundosA referência: rápido
Ler de SSD~100 microssegundos (~1000× mais lento)Disco é ordens de magnitude mais lento que RAM
Ida e volta na mesma região (rede)~0,5 milissegundoRede local custa, mas é tolerável
Ida e volta entre continentes~100-150 milissegundosA luz tem velocidade: distância = latência
Ler 1 MB sequencial de SSD~1 milissegundoThroughput de disco é decente, latência é o problema
A única lição que você precisa: RAM ≪ disco ≪ rede entre regiões
Não decore os números — internalize as razões. Memória é cerca de mil vezes mais rápida que SSD. Uma chamada de rede entre continentes é centenas de vezes mais lenta que uma local. Essas relações, e não os valores exatos, é que orientam design: por isso cache em memória vale tanto (evita o disco), por isso reduzir chamadas de rede importa mais que otimizar CPU, por isso servir usuários globais de um único continente adiciona 100ms inelimináveis (a luz não anda mais rápido). Quando você projeta, esses números não são trivialidades — são as forças físicas que tornam algumas arquiteturas rápidas e outras lentas, independentemente de quão bom é seu código. A velocidade da luz é uma restrição de design, não uma curiosidade.

Os fatos de conversão que aceleram a conta

atalhos para a aritmética mental
TEMPO
  1 dia ≈ 86.400 segundos ≈ 100.000 (arredonde para 10^5!)
  1 mês ≈ 2,5 milhões de segundos

POTÊNCIAS DE 10 (aproxime sempre)
  mil = 10^3 (KB)
  milhão = 10^6 (MB)
  bilhão = 10^9 (GB)
  trilhão = 10^12 (TB)

O TRUQUE DO "POR DIA → POR SEGUNDO"
  1 milhão de eventos/dia ÷ 100.000 seg ≈ 10/segundo (média)
  → e o PICO costuma ser 2 a 10× a média.

DISPONIBILIDADE → downtime (do volume anterior)
  99,9% ≈ 43 min/mês    99,99% ≈ 4 min/mês
Arredonde sem dó — a precisão é inimiga da velocidade aqui
O instinto do engenheiro é ser preciso, e aqui esse instinto atrapalha. Numa estimativa de guardanapo, 86.400 segundos por dia é 100.000. Um milhão é mil mil. Você arredonda agressivamente para potências de 10 porque o objetivo é a ordem de grandeza, e arredondar de 86.400 para 100.000 introduz um erro de 15% num cálculo cujo objetivo é distinguir "cabe num servidor" (10 RPS) de "precisa de cluster" (100.000 RPS) — uma diferença de quatro ordens de grandeza. Buscar precisão decimal numa estimativa cuja resposta útil é "ordem de milhares" é desperdiçar tempo e arriscar travar na aritmética. Arredonde para o número redondo mais próximo, faça a conta de cabeça em segundos, e confie que a ordem de grandeza está certa. Se a decisão depender de 15%, você não precisava de uma estimativa — precisava de um benchmark.

3.4 O método do guardanapo

Estimar qualquer coisa segue o mesmo procedimento, e tê-lo explícito evita se perder no meio das contas:

  • 1. Comece pelo número de topo conhecido. Usuários totais, ou usuários ativos por dia (DAU), ou eventos por dia. Esse é seu ponto de partida — geralmente vem dos requisitos (Cap 2).
  • 2. Decomponha em ações por usuário. Quantas vezes o usuário médio faz a operação por dia? (Lê o feed 5 vezes, posta 1 vez, etc.) Multiplique.
  • 3. Converta para por segundo. Divida o total diário por ~100.000 (segundos no dia). Obtém a média de RPS.
  • 4. Aplique o fator de pico. O tráfego não é uniforme; concentra-se em horários. Multiplique a média por 2 a 10 (depende do app) para estimar o pico — que é o que dimensiona a infraestrutura.
  • 5. Estime os recursos. A partir do RPS de pico e do tamanho de cada operação, calcule armazenamento (por dia, por ano), banda, memória.
  • 6. Leia o resultado como decisão. O número diz "cabe num servidor / precisa distribuir", "cabe em RAM / precisa de disco", "trivial / domina o custo". A decisão é o produto, não o número.
Dimensione pelo pico, não pela média
O passo que os iniciantes pulam, e que mais importa: o tráfego médio não dimensiona nada, porque o sistema precisa sobreviver ao pico, não à média. Um app pode ter 10 RPS de média e 200 RPS às 20h, quando todo mundo usa ao mesmo tempo. Se você dimensionar para 10, cai todo dia às 20h. O fator de pico — quantas vezes o pico excede a média — varia por tipo de sistema: um app de notícias tem picos suaves; um sistema de venda de ingressos para um show tem picos brutais (todo mundo no mesmo minuto). Sempre estime a média e depois multiplique pelo fator de pico apropriado; é o pico que você precisa aguentar.

3.5 Estimar tráfego

Vamos aplicar o método a um exemplo concreto. Suponha um app social com 10 milhões de usuários ativos por dia (DAU). Cada um, em média, lê o feed 20 vezes por dia e posta 2 vezes. Qual o tráfego?

estimativa de tráfego, passo a passo
LEITURAS (feed)
  10M usuários × 20 leituras/dia = 200M leituras/dia
  ÷ 100.000 seg/dia ≈ 2.000 leituras/segundo (média)
  × fator de pico (~5) ≈ 10.000 leituras/segundo (pico)

ESCRITAS (posts)
  10M usuários × 2 posts/dia = 20M posts/dia
  ÷ 100.000 ≈ 200 posts/segundo (média)
  × 5 ≈ 1.000 posts/segundo (pico)

LEITURA OBSERVAÇÃO: razão leitura/escrita ≈ 10:1
  → fortemente READ-HEAVY (Cap 2). Cache e réplicas vão ajudar.
  → 10.000 leituras/s no pico é muito para um banco sozinho,
    MAS se forem cacheáveis, o cache absorve a maioria.

Note o que a estimativa já entregou, sem nenhum design ainda: o sistema é read-heavy (10:1), o pico de leitura (10k/s) é alto demais para um banco sozinho mas tratável com cache, e o pico de escrita (1k/s) é confortável para um banco relacional bem dimensionado. Três decisões de arquitetura — cache obrigatório, banco aguenta a escrita, otimizar leitura — caíram de cinco minutos de aritmética.

3.6 Estimar armazenamento e banda

Continuando o exemplo: cada post tem em média 300 bytes de texto mais metadados, totalizando ~1 KB. Quanto armazenamento por ano? E a banda?

armazenamento e banda
ARMAZENAMENTO (posts)
  20M posts/dia × 1 KB = 20 GB/dia
  × 365 ≈ 7 TB/ano (texto puro)
  → CABE confortavelmente. Texto é barato. Um banco aguenta
    anos disso. A decisão: texto não é o problema de storage.

E SE TIVER IMAGENS? (a estimativa muda tudo)
  Se 10% dos posts têm imagem de ~500 KB:
  2M imagens/dia × 500 KB = 1 TB/dia = 365 TB/ano
  → 50× mais que o texto. Imagens DOMINAM o storage.
  → Decisão: object storage (Cap 18 do vol. anterior),
    NÃO o banco. O texto vai no banco, a mídia no S3.

BANDA (servir as leituras)
  10.000 leituras/s × 1 KB (resposta) = 10 MB/s
  → trivial para texto. MAS imagens:
  se cada leitura puxa uma imagem de 500 KB:
  10.000/s × 500 KB = 5 GB/s → ENORME.
  → Decisão: CDN obrigatória para mídia (Cap 13 do vol.
    anterior). Servir 5 GB/s do servidor é inviável e caro.
A estimativa revela onde está o problema de verdade
Repare na lição do exemplo: texto não é problema, mídia é tudo. A intuição diria "armazenar posts é o desafio"; a estimativa revela que o texto cabe num banco por anos, e que são as imagens — 50× mais volumosas — que dominam armazenamento e banda, exigindo object storage e CDN. Sem a estimativa, você poderia gastar energia otimizando o armazenamento de texto (irrelevante) e ser pego de surpresa pela conta de banda de imagem (dominante). Estimar não só dimensiona — ele aponta onde o problema real está, que muitas vezes não é onde a intuição apontava. É o mesmo princípio do FinOps do volume anterior (foque no grande, não no centavo), aplicado antes de construir: a estimativa mostra qual é o "grande".

3.7 Da estimativa à decisão — o passo que importa

Uma estimativa que não vira decisão é aritmética desperdiçada. O passo final, e o único que justifica os anteriores, é traduzir cada número numa escolha de arquitetura. Os mapeamentos mais comuns:

Se a estimativa diz……a decisão tende a ser
"O dataset todo cabe em RAM" (ex.: < 64 GB)Mantenha em memória — simples e rápido, talvez nem precise de banco no caminho quente
"Pico de leitura alto, mas dados cacheáveis"Cache na frente; o banco vê só os misses
"Pico de escrita alto demais para um banco"Particionar a escrita, ou banco otimizado para ingestão, ou buffer/batch
"Mídia domina banda"CDN obrigatória; nunca sirva mídia do servidor de app
"Cabe num servidor com folga"NÃO distribua. Um servidor (mais réplica para HA) basta — resista à escala prematura
"Não cabe em nenhum servidor único"Aí sim, sharding/distribuição é necessária (Cap 13)
A estimativa frequentemente diz "é menor do que você pensava"
O resultado mais comum — e mais subestimado — de uma estimativa honesta é descobrir que o problema é menor do que a ambição sugeria, e que um único servidor robusto resolve com folga. "Um milhão de usuários" soa como um problema de escala que exige arquitetura distribuída; a estimativa frequentemente revela que são alguns milhares de RPS no pico, com dados que cabem em memória, perfeitamente atendíveis por uma máquina e uma réplica. Essa descoberta é libertadora: ela autoriza a simplicidade que o Cap 1 defendia, agora com números para sustentá-la. Quando alguém propõe um cluster e a estimativa mostra que cabe num servidor, você tem o argumento concreto contra a complexidade desnecessária. A estimativa é a melhor defesa contra a escala prematura, porque substitui o medo ("e se não escalar?") por um número ("cabe, com 10× de folga").

3.8 Os limites honestos da estimativa

A estimativa é poderosa justamente porque é grosseira e rápida — mas isso também define onde ela para de servir. Saber seus limites evita confiar nela onde ela engana:

  • Ela dá ordem de grandeza, não precisão. "Cerca de 10 mil RPS" é útil para decidir a arquitetura; não confie nela para dimensionar exatamente quantos servidores comprar — para isso, meça (benchmark, load test).
  • Ela depende das premissas. Garbage in, garbage out: se você chutou o DAU ou o fator de pico errado por uma ordem de grandeza, a conclusão está errada. Por isso o resultado absurdo é valioso — ele denuncia a premissa ruim.
  • Ela ignora o comportamento real sob carga. A aritmética assume linearidade, mas sistemas reais têm não-linearidades: contenção de locks, degradação de cache, filas que explodem perto da saturação (Cap 7, lei de Little e a teoria de filas). Um sistema que "deveria" aguentar 10k RPS pela conta pode colapsar a 7k por contenção que a estimativa não vê.
  • Ela não substitui a medição quando a precisão importa. A estimativa decide a forma da arquitetura; a medição valida e ajusta os números. São complementares: estime para escolher o caminho, meça para percorrê-lo.
Estime para decidir a forma; meça para confirmar os números
A divisão de trabalho correta entre estimativa e medição: a estimativa responde perguntas qualitativas de arquitetura — cabe num servidor? precisa de cache? mídia domina a banda? — que você precisa responder antes de construir, e para as quais ordem de grandeza basta. A medição (benchmark, profiling, load test) responde perguntas quantitativas precisas — exatamente quantos servidores, qual o ponto de saturação, onde está o gargalo real — que só fazem sentido depois de existir algo para medir. Confiar na estimativa para a precisão é o erro; descartar a estimativa porque "só medição vale" é o erro oposto, e leva a construir a arquitetura errada antes de ter o que medir. Estime cedo para escolher certo; meça depois para ajustar fino.

3.9 Estudo de caso — a estimativa que matou um projeto (no bom sentido)

Cinco minutos que economizaram cinco meses
O cenário

Uma startup quer construir um produto de analytics que processa eventos de cliques dos sites dos clientes em "tempo real". O time está animado, já falando em Kafka, processamento de stream, um cluster de banco de séries temporais distribuído. Antes de aprovar a arquitetura, o líder técnico faz a conta de guardanapo — e ela muda a conversa inteira.

Passo 1 · A estimativa do cenário otimista (sucesso)
se o produto for um SUCESSO
Meta ambiciosa: 1.000 sites clientes, cada um com 100k
  visitantes/dia, cada visitante gerando 10 eventos.

  1.000 × 100.000 × 10 = 1 bilhão de eventos/dia
  ÷ 100.000 seg ≈ 10.000 eventos/segundo (média)
  × pico 5 ≈ 50.000 eventos/segundo (pico)

ARMAZENAMENTO: 1B eventos/dia × 200 bytes = 200 GB/dia
  → 73 TB/ano. Aí SIM precisa de storage sério e particionado.

Conclusão do cenário de sucesso: a arquitetura distribuída
  que o time quer SE justifica — NESTE volume.
Passo 2 · A estimativa do cenário real (o lançamento)
o que vai acontecer no PRIMEIRO ANO (realista)
Realista: 20 sites clientes pagantes no primeiro ano,
  cada um com 5.000 visitantes/dia, 10 eventos cada.

  20 × 5.000 × 10 = 1 milhão de eventos/dia
  ÷ 100.000 ≈ 10 eventos/segundo (média)
  × pico 5 ≈ 50 eventos/segundo (pico)

ARMAZENAMENTO: 1M eventos/dia × 200 bytes = 200 MB/dia
  → 73 GB/ano. CABE NUM LAPTOP. Num único Postgres, anos.

Conclusão do cenário real: 50 eventos/segundo e 73 GB/ano
  são RISÍVEIS para a arquitetura distribuída proposta.
  Um único Postgres bem indexado engole isso sem suar.
Passo 3 · A decisão que a estimativa forçou

A estimativa revelou um abismo entre o cenário de sucesso (que justificaria Kafka e cluster) e o cenário real do primeiro ano (que cabe num Postgres). Construir a arquitetura do cenário de sucesso agora seria gastar cinco meses montando um cluster distribuído para processar 50 eventos por segundo — o ônibus de 40 lugares para a família de quatro do volume anterior. A decisão: começar com o Postgres simples, que atende o primeiro ano com folga absurda, e migrar para a arquitetura distribuída se e quando o crescimento real se aproximar do volume que a justifica.

Passo 4 · O caminho de evolução, com gatilho numérico
o gatilho de migração, definido pela estimativa
Postgres único aguenta confortável até ~5.000 eventos/s e
  alguns TB. Isso dá MUITA margem sobre os 50/s iniciais.

Gatilho de migração (medido, não temido):
  - quando o pico passar de ~2.000 eventos/s, OU
  - quando o storage passar de ~1 TB
  → AÍ avaliar particionamento / banco time-series / Kafka.

Até lá: Postgres. E o tempo dos 5 meses economizados vai
  para o PRODUTO — que é o que traz os clientes que um dia
  justificariam a arquitetura grande.

Resultado: cinco minutos de aritmética economizaram cinco meses de engenharia e mataram — no bom sentido — um projeto de infraestrutura que não precisava existir ainda. A estimativa fez três coisas: dimensionou o cenário real (risível para a arquitetura proposta), validou que a arquitetura grande seria certa num cenário de sucesso (não era ignorância, era timing), e definiu um gatilho numérico concreto para migrar quando a hora chegasse. O time construiu o produto sobre Postgres, entregou meses antes, e ganhou um critério objetivo — não emocional — para decidir quando escalar. Esta é a estimativa cumprindo seu propósito real: não impressionar ninguém, mas substituir o medo e a ambição por números que decidem. Próximo capítulo: o vocabulário dos trade-offs — os termos precisos (latência vs throughput, consistência vs disponibilidade, acoplamento) que você vai negociar pelo resto do livro, agora que sabe estimar as grandezas que eles governam.

3.10 Erros comuns

Erro 1 · Estimar pela média, dimensionar pela média

Calcular o RPS médio e dimensionar a infraestrutura para ele — e cair todo dia no horário de pico. O sistema precisa aguentar o pico, não a média. Sempre multiplique a média pelo fator de pico (2 a 10×, conforme o app) e dimensione para esse número.

Erro 2 · Buscar precisão decimal numa estimativa

Travar na aritmética tentando ser exato (86.400 em vez de 100.000), quando o objetivo é ordem de grandeza. A precisão é inimiga da velocidade aqui. Arredonde agressivamente para potências de 10; se a decisão depende de 15%, você precisava de um benchmark, não de uma estimativa.

Erro 3 · Estimar e não decidir

Fazer as contas, obter os números, e não traduzi-los em decisão de arquitetura. Uma estimativa que não responde "cabe num servidor? precisa de cache? mídia domina a banda?" é aritmética desperdiçada. O número é o meio; a decisão é o fim.

Erro 4 · Ignorar o resultado absurdo

Obter uma estimativa absurda (dez mil servidores para um app de bairro) e seguir em frente sem questionar. O absurdo é um detector de premissa errada — quase sempre significa que você chutou um número por uma ordem de grandeza. Revise as suposições; o erro está nelas.

Erro 5 · Confiar na estimativa onde precisa medir

Usar a estimativa grosseira para dimensionar exatamente quantos servidores comprar, ou garantir que aguenta X — ignorando não-linearidades (contenção, filas perto da saturação) que a aritmética não vê. A estimativa decide a forma; a medição confirma os números. Não confunda os papéis.

Erro 6 · Estimar só o cenário de sucesso

Dimensionar para o sonho (milhões de usuários) e construir a arquitetura grande para um lançamento que terá centenas. Estime o cenário real do primeiro ano também — ele costuma caber num servidor — e defina o gatilho numérico para migrar quando o sucesso chegar de verdade.

Erro 7 · Esquecer a dimensão dominante

Estimar o óbvio (armazenamento de texto) e ser pego pela dimensão que domina (banda de imagem, 50× maior). A estimativa serve para achar onde o problema real está, que nem sempre é onde a intuição aponta. Estime todas as dimensões — storage, banda, memória, RPS — e ache a que domina.

Verifique seu entendimento
Você está projetando um sistema e estima que ele terá "1 milhão de usuários". Um colega quer começar a montar um cluster distribuído. Qual é o uso mais correto da estimativa de guardanapo neste momento?

3.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · A conversão fundamental

Sem calculadora, estime: (a) um app com 50 milhões de eventos por dia gera quantos eventos por segundo, em média? (b) e no pico, assumindo fator 4? (c) se cada evento ocupa 500 bytes, quanto armazenamento por dia? (d) e por ano? Mostre os arredondamentos que você usou.

(a) Eventos por segundo (média): 50.000.000 ÷ 100.000 (segundos/dia arredondado) = 500 eventos/segundo. O truque: dividir por 10^5 é só remover 5 zeros — 50M (5×10^7) ÷ 10^5 = 5×10^2 = 500.

(b) Pico (fator 4): 500 × 4 = 2.000 eventos/segundo no pico. Esse é o número que dimensiona a infraestrutura.

(c) Armazenamento por dia: 50M eventos × 500 bytes = 25.000.000.000 bytes = 25 GB/dia. (50×10^6 × 5×10^2 = 25×10^9 = 25 GB.)

(d) Por ano: 25 GB/dia × 365 ≈ 25 × 365 ≈ 9 TB/ano (arredondando 365 para ~360 ou simplesmente 25×365≈9.125 GB ≈ 9 TB).

A leitura como decisão: 2.000 eventos/s de pico e 9 TB/ano. O RPS é tratável para um banco bem dimensionado; 9 TB/ano cabe, mas em alguns anos pede particionamento por tempo. Nada disso exige arquitetura distribuída de cara. A estimativa diz: comece simples, planeje o particionamento por data desde o esquema (porque mexer em dados depois é porta de mão única, Cap 1).

Médio
Exercício 2 · Estimar um encurtador de URLs

Projete a estimativa de um encurtador de URLs que recebe 100 milhões de novas URLs por mês e tem uma razão de leitura/escrita de 100:1 (cada URL é acessada ~100 vezes ao longo da vida). Estime: RPS de escrita e leitura (médio e pico), armazenamento por ano, e — o mais importante — leia os números para decidir a arquitetura.

estimativa do encurtador
ESCRITA (criar URL)
  100M/mês ÷ 2,5M seg/mês ≈ 40 escritas/s (média)
  × pico 5 ≈ 200 escritas/s (pico)
  → BAIXÍSSIMO. Qualquer banco engole.

LEITURA (redirecionar) — razão 100:1
  40/s × 100 ≈ 4.000 leituras/s (média)
  × pico 5 ≈ 20.000 leituras/s (pico)
  → ALTO, mas...

ARMAZENAMENTO
  100M URLs/mês × 12 = 1,2 bilhão/ano
  × ~500 bytes (URL longa + curta + metadados) ≈ 600 GB/ano
  → cabe tranquilo. Em poucos anos, alguns TB.

A leitura como decisão — e aqui está o ouro:

  • Escrita trivial (200/s pico): um único banco resolve, sem complexidade.
  • Leitura alta (20.000/s pico), MAS — e este é o ponto — uma URL curta sempre resolve para o mesmo destino (o mapeamento é imutável). Isso significa cache hit altíssimo: o cache em memória absorve quase todas as 20.000 leituras/s, e o banco vê quase nada. O encurtador é o caso quase perfeito de cache (Cap 10).
  • Dataset cabe em memória parcialmente: as URLs populares (poucas, muito acessadas) cabem em cache facilmente; a cauda longa rara vai ao banco. Distribuição de Pareto a seu favor.

Arquitetura que a estimativa indica: um banco simples (até relacional) para o mapeamento, com cache agressivo na frente. As 20k leituras/s assustam até você perceber que são quase todas o mesmo punhado de URLs populares, servidas da memória. Sem a estimativa + a observação sobre cacheabilidade, alguém montaria um cluster para um problema que um banco e um Redis resolvem. A estimativa não é só os números — é lê-los com entendimento do padrão de acesso.

Médio
Exercício 3 · A estimativa que revela a dimensão dominante

Um app de compartilhamento de vídeos tem 1 milhão de uploads de vídeo por dia, vídeo médio de 50 MB, e cada vídeo é assistido em média 1.000 vezes. Estime armazenamento por ano, banda de upload e banda de download (servir as visualizações). Identifique qual dimensão domina e qual decisão de arquitetura ela força.

estimativa do app de vídeo
ARMAZENAMENTO
  1M vídeos/dia × 50 MB = 50 TB/dia (!)
  × 365 ≈ 18 PETABYTES/ano (sem contar transcodificação,
    que multiplica por várias resoluções)
  → ENORME. Object storage obrigatório, com tiering de
    custo (vídeos antigos/raros para storage frio).

BANDA DE UPLOAD
  1M/dia × 50 MB ÷ 100.000 seg ≈ 500 MB/s (média)
  × pico ≈ alguns GB/s. Significativo, mas...

BANDA DE DOWNLOAD (servir visualizações) — 1.000× mais!
  1M vídeos × 1.000 views × 50 MB = 50 PB/dia servidos
  ÷ 100.000 ≈ 500 GB/s (média!) × pico = TB/s
  → ASTRONÔMICO.

A dimensão dominante: banda de download. O download (servir as visualizações) é ~1.000× a banda de upload, e domina absolutamente tudo — 500 GB/s de média é uma quantidade de banda que define o negócio inteiro. Servir isso de servidores de origem é financeira e tecnicamente inviável.

A decisão que isso força: CDN não é otimização, é a fundação. Praticamente todo o tráfego de vídeo tem que ser servido da borda (CDN), com o servidor de origem vendo só uma fração mínima (cache miss). A arquitetura inteira gira em torno de minimizar a banda de origem: CDN agressiva, possivelmente múltiplas CDNs, edge caching, e o custo de banda/CDN será provavelmente a maior linha do orçamento (FinOps, vol. anterior). Também: transcodificação para múltiplas resoluções (servir 4K para quem está no celular é desperdício de banda), que adiciona armazenamento mas economiza a banda dominante.

O insight: a intuição diria "armazenar petabytes de vídeo é o desafio". A estimativa revela que o armazenamento é grande mas a banda de download é o problema que define a arquitetura — e que sem CDN o negócio é impossível. Estimar todas as dimensões e achar a dominante muda completamente onde você foca o design.

Difícil
Exercício 4 · A estimativa como argumento — entrevista

Numa reunião de arquitetura, o time propõe construir uma plataforma com Kafka, um data lake, processamento de stream com Flink, e um cluster Cassandra — para um produto interno que será usado por cerca de 5.000 funcionários da empresa. Você suspeita de escala prematura. Estruture como usaria estimativas de guardanapo para conduzir a conversa, qual conta faria, e como transformaria o número num argumento construtivo (não num "isso é exagero").

Enquadramento: a estimativa é a ferramenta perfeita aqui porque transforma uma discussão de opinião ("acho exagero" vs "acho que precisa") numa de fatos (o número diz o que cabe). O objetivo não é vencer a discussão, é deixar os números decidirem — e a abordagem é fazer a conta junto com o time, não jogá-la contra eles.

A conta de guardanapo:

a estimativa do produto interno
5.000 funcionários, uso interno (horário comercial).
Assuma uso intenso: cada um faz 1.000 ações/dia.

  5.000 × 1.000 = 5 milhões de ações/dia
  ÷ 100.000 seg ≈ 50 ações/segundo (média)
  Mas uso interno concentra em ~8h, não 24h:
  ÷ 30.000 seg (8h) ≈ 170/s (média no horário)
  × pico 3 ≈ 500 ações/segundo (pico)

DADOS: mesmo com 5M ações/dia × 1 KB = 5 GB/dia
  → ~2 TB/ano. Cabe num Postgres por muitos anos.

VEREDITO: 500 ações/s no pico e 2 TB/ano.
  Um Postgres bem indexado engole isso rindo.

Como conduzir com o número (construtivo, não acusatório):

  • Fazer a conta juntos, em voz alta: "vamos só estimar o volume para dimensionar certo — 5.000 pessoas, quantas ações por dia cada uma?". Isso convida o time a co-construir o número, não o impõe.
  • Deixar o número falar: 500 ações/s e 2 TB/ano é uma carga que um único Postgres atende com ordens de grandeza de folga. Kafka, Flink e Cassandra são tecnologias para milhões de eventos por segundo e petabytes — três a quatro ordens de grandeza acima deste caso.
  • Validar quando estariam certos: "essa stack seria exatamente certa se fôssemos processar, digamos, centenas de milhares de eventos por segundo — mas a conta diz que estamos na casa das centenas, não das centenas de milhares. É uma diferença de mil vezes." Isso mostra respeito pela competência do time (a stack não é burra, é dimensionada para outro problema).
  • Reconhecer o custo da complexidade: cada uma dessas peças é um sistema distribuído para operar, monitorar e debugar — caro em tempo de equipe, justamente para um produto interno onde esse tempo seria melhor gasto nas funcionalidades.
  • Oferecer o caminho com gatilho: "que tal começarmos com Postgres, que a conta mostra atender com folga enorme, e definirmos um gatilho — se o volume passar de, digamos, alguns milhares de eventos/s, aí reavaliamos essa stack? Assim entregamos rápido e mantemos a porta aberta para escalar com base em número, não em previsão."

O que um bom engenheiro demonstra: que usa a estimativa como ferramenta de decisão objetiva, não como arma; que reconhece quando a stack grande seria certa (validando o conhecimento do time); e que transforma "acho exagero" num fato numérico que decide sozinho. Frase de fechamento: "não é que Kafka e Cassandra sejam errados — são certos para um volume mil vezes maior que o nosso. A conta de guardanapo diz que estamos a três ordens de grandeza desse ponto. Proponho começarmos com o que a estimativa mostra bastar, com um gatilho numérico para reavaliar — assim a decisão de escalar vira um fato medido, não um medo antecipado."

Fim do capítulo 3 · e da abertura da Parte I
Próximo capítulo: o vocabulário dos trade-offs. Você já sabe extrair requisitos (Cap 2) e estimar suas grandezas (Cap 3); falta o vocabulário preciso para nomear as tensões que vai negociar pelo resto do livro — latência vs throughput, consistência vs disponibilidade, acoplamento, e os pares de forças que todo design equilibra. Com ele, fechamos o raciocínio e entramos nas leis que governam os sistemas distribuídos (Parte II).
Parte I · Capítulo 4 · O raciocínio

O vocabulário
dos trade-offs.

O Capítulo 1 disse que design é gestão de trade-offs. Mas você não pode negociar o que não consegue nomear. Quando um engenheiro confunde latência com throughput, ou trata "assíncrono" como sinônimo de "rápido", ou não enxerga o acoplamento que acabou de criar, ele toma decisões erradas não por falta de inteligência, mas por falta de vocabulário — as distinções que tornam o trade-off visível. Este capítulo, que fecha a Parte I, instala esse vocabulário: os pares de forças que todo sistema equilibra, com a precisão que transforma intuição vaga em raciocínio.

Este é o capítulo de definições — mas definições com consequência, cada uma amarrada à decisão que ela governa. Vamos separar latência de throughput (confundi-los leva a otimizar a coisa errada), entender por que a cauda da latência, não a média, é a experiência real, distinguir acoplamento de coesão (a tensão que organiza toda decomposição de sistema), ver o trade-off universal entre espaço e tempo, a diferença entre síncrono e assíncrono (que não é "lento vs rápido"), o que significa estado e por que onde ele mora define tudo, e a escolha entre push e pull. Terminamos com um mapa das tensões que servirá de bússola para o livro inteiro. Quando você sair daqui, terá os nomes para enxergar os trade-offs que antes só sentia.

4.1 A história — quando os nomes ganharam precisão

Contexto histórico

O vocabulário do design de sistemas foi emprestado, peça por peça, de disciplinas mais velhas. Latência e throughput vêm da engenharia e das telecomunicações, onde a distinção entre "quão rápido um sinal chega" e "quanta informação passa por segundo" era vida ou morte para projetar redes. Acoplamento e coesão foram cunhados por Larry Constantine no design estruturado dos anos 1960-70, e refinados por Stevens, Myers e o próprio Constantine como as métricas centrais de um bom design modular.

A teoria de filas — que governa latência sob carga — é ainda mais antiga: Agner Erlang a desenvolveu para dimensionar redes telefônicas no início do século XX, e a lei de Little (Cap 7) a formalizou em 1954. Quando os sistemas de computação encontraram os mesmos problemas (requisições esperando em fila por um recurso ocupado), eles importaram a matemática pronta.

A virada para sistemas distribuídos refinou e às vezes redefiniu esses termos. "Consistência", que num banco local tinha um significado (o C de ACID), ganhou um significado diferente e mais sutil em sistemas distribuídos (o C de CAP — Cap 5). "Disponibilidade" deixou de ser "o servidor está ligado" e virou uma propriedade matemática de quórum. A precisão importava cada vez mais, porque em sistemas distribuídos os mal-entendidos de vocabulário levavam a falhas que ninguém antecipava.

Em 2026, o vocabulário é vasto e, às vezes, abusado — termos como "escalável", "resiliente" e "tempo real" são usados com tanta imprecisão que perderam significado em muitas conversas. A reação madura é voltar às distinções precisas: o engenheiro que sabe exatamente o que diferencia latência de throughput, ou acoplamento de coesão, pensa com mais clareza e decide melhor que aquele que usa os termos como sinônimos vagos de "bom" e "ruim". Este capítulo é uma defesa da precisão: porque você não pode equilibrar forças que não consegue distinguir.

4.2 Latência vs throughput — a confusão mais cara

Estas duas palavras são confundidas constantemente, e a confusão leva a otimizar a métrica errada. São independentes: você pode ter uma sem a outra, e melhorar uma às vezes piora a outra.

Latência — o tempo de uma operação

Quanto tempo leva uma requisição, do início ao fim. Medida em tempo (ms). "Esta busca respondeu em 50ms." É a experiência de um usuário individual esperando. Otimizar latência: tornar cada operação mais rápida.

Throughput — o volume por tempo

Quantas operações o sistema completa por unidade de tempo. Medida em taxa (req/s). "O sistema processa 10.000 requisições por segundo." É a capacidade agregada. Otimizar throughput: completar mais operações no total, mesmo que cada uma demore.

A analogia da rodovia (e por que melhorar uma piora a outra)
Pense numa rodovia. Latência é quanto tempo um carro leva para ir de A a B. Throughput é quantos carros chegam a B por hora. São independentes: uma rodovia pode ter latência baixa (carros rápidos) e throughput baixo (poucas faixas), ou latência alta e throughput alto (uma autoestrada congestionada e larga move muitos carros, cada um lentamente). E aqui está o trade-off: batching — agrupar muitas operações e processá-las juntas — aumenta o throughput (menos overhead por operação) mas piora a latência (a primeira operação do lote espera as outras chegarem). Um sistema de pagamentos otimizado para latência processa cada transação na hora; um otimizado para throughput acumula e processa em lotes. Você não pode maximizar os dois ao mesmo tempo sem custo — e saber qual o seu sistema precisa é uma decisão de design, não um detalhe.

A consequência prática: pergunte sempre qual das duas o sistema otimiza. Um sistema interativo (uma busca, um checkout) é dominado por latência — o usuário espera, e cada milissegundo conta. Um sistema de processamento em lote (gerar relatórios, processar uploads, treinar um modelo) é dominado por throughput — ninguém espera uma operação individual; importa quanto trabalho é feito no total. Otimizar throughput num sistema de latência (juntando requisições em lote para "ser eficiente") destrói a experiência; otimizar latência num sistema de throughput (processar um por um para "ser responsivo") desperdiça capacidade.

4.3 A cauda é a experiência — percentis, de novo

Já tocamos nisto no Cap 2, mas merece o status de conceito de vocabulário porque governa como você fala de latência. A latência não é um número — é uma distribuição. E a parte da distribuição que importa não é o meio (a média, a mediana), é a cauda: os 1%, 0,1% das requisições mais lentas.

Em escala, todo usuário encontra a cauda
Por que a cauda importa tanto mais do que parece? Porque uma única ação do usuário frequentemente dispara muitas requisições internas, e a ação só termina quando a mais lenta delas termina. Se carregar uma página faz 100 chamadas a serviços internos, e cada uma tem 1% de chance de ser lenta (p99), a probabilidade de a página inteira pegar pelo menos uma chamada lenta é enorme — cerca de 63%. A cauda de cada serviço individual vira a experiência típica da página composta. Por isso gigantes obcecam com p99 e p999, não com a média: em escala, com fan-out de requisições, a cauda rara de um componente é a experiência comum do sistema. Otimizar a média enquanto a cauda é ruim é otimizar um número que nenhum usuário sente. O vocabulário correto de latência é sempre em percentis, e a atenção vai para a cauda.

4.4 Acoplamento e coesão — a tensão de toda decomposição

Quando você divide um sistema em partes — módulos, serviços, componentes — duas forças governam se a divisão é boa: acoplamento (o quanto as partes dependem umas das outras) e coesão (o quanto cada parte é internamente unida em torno de um propósito). A meta clássica e atemporal: baixo acoplamento, alta coesão.

PropriedadeO que medeVocê quer
AcoplamentoQuanto uma parte depende de/conhece outraBaixo — partes que mudam independentemente
CoesãoQuanto o conteúdo de uma parte pertence juntoAlta — cada parte tem um propósito claro
Acoplamento é o que determina o que muda junto
A definição operacional de acoplamento que importa: duas partes estão acopladas se mudar uma obriga a mudar a outra. Alto acoplamento significa que uma alteração se propaga em cascata — você toca num serviço e quebra três outros, e nada pode ser mudado isoladamente. Baixo acoplamento significa que cada parte evolui sozinha, atrás de um contrato estável. Coesão é o complemento: uma parte coesa faz uma coisa e tudo nela serve a esse propósito; uma parte de baixa coesão é uma gaveta de bugigangas sem relação, que muda por mil razões diferentes. A boa decomposição agrupa o que muda junto (coesão) e separa o que muda por razões diferentes (desacoplamento). É a mesma lei de Conway do Cap 1 vista por dentro: você quer que os limites do sistema sigam os limites do problema, não os acidentes de quem o construiu.

Este par é o que torna a decisão "monólito vs microsserviços" (Cap 1) inteligível. Microsserviços forçam baixo acoplamento (a rede entre serviços impõe um contrato explícito) ao custo de complexidade distribuída. Um monólito permite alta coesão e baixo acoplamento internos (módulos bem desenhados) sem o custo da rede — mas também permite, se mal feito, o "monólito espaguete" de alto acoplamento. A questão nunca é "monólito ou serviços?"; é "como consigo baixo acoplamento e alta coesão com o menor custo?", e a resposta depende do problema (organização, escala, evolução).

4.5 Espaço vs tempo — o trade-off mais universal

O trade-off mais fundamental da computação, presente desde os primeiros algoritmos: você quase sempre pode gastar mais espaço (memória, armazenamento) para economizar tempo (computação), e vice-versa. Quase toda otimização de performance é, no fundo, uma troca entre os dois.

  • Cache (Cap 10) é espaço por tempo: você guarda um resultado calculado (gasta memória) para não recalculá-lo (economiza tempo). O custo do cache é exatamente o espaço — e a invalidação.
  • Índices em banco são espaço por tempo: a estrutura do índice ocupa disco (espaço) para tornar a busca rápida (tempo). Cada índice acelera leitura e desacelera escrita (manter o índice atualizado) — espaço e tempo de escrita por tempo de leitura.
  • Desnormalização (Cap 12) é espaço por tempo: você duplica dados (gasta espaço, gasta escrita) para evitar joins caros na leitura (economiza tempo de leitura).
  • Compressão é o inverso: gasta tempo (comprimir/descomprimir) para economizar espaço (e banda). Útil quando espaço/banda é o gargalo, não a CPU.
  • Materialização (pré-computar resultados, Cap 18) é espaço por tempo: guarda a resposta pronta para servir sem computar na hora.
Reconheça o trade-off espaço-tempo por baixo de cada otimização
Uma vez que você enxerga o eixo espaço-tempo, ele aparece em quase toda decisão de performance, e nomeá-lo clarifica a escolha. Toda vez que algo está lento e a solução proposta é "guardar/pré-calcular/duplicar/indexar", você está gastando espaço para comprar tempo — e a pergunta certa vira "tenho espaço sobrando, e o custo de manter essa cópia/índice/cache atualizado vale o tempo que economizo?". Toda vez que algo está ocupando muito espaço e a solução é "comprimir/calcular sob demanda", você está gastando tempo para comprar espaço. Não há otimização grátis: você está sempre movendo o custo de um eixo para o outro, e o design consiste em mover o custo para o eixo onde você tem folga. Quem tem memória sobrando e CPU escassa cacheia; quem tem o inverso, recalcula.

4.6 Síncrono vs assíncrono — não é "lento vs rápido"

Uma confusão comum: tratar "assíncrono" como sinônimo de "rápido" e "síncrono" de "lento". Errado. A distinção é sobre quem espera por quem, não sobre velocidade.

Síncrono — o chamador espera

Você faz a requisição e espera a resposta antes de continuar. Acoplamento temporal: o chamador e o chamado precisam estar ambos disponíveis no mesmo instante. Simples de raciocinar (a resposta vem na hora), mas frágil (se o chamado está lento ou fora, o chamador trava).

Assíncrono — o chamador não espera

Você dispara a requisição e segue em frente; o resultado chega depois (callback, fila, evento). Desacoplamento temporal: o chamado pode processar quando puder. Resiliente e escalável, mas complexo (a resposta vem depois — como? e se falhar? em que ordem?).

Assíncrono não é mais rápido — é desacoplado no tempo
O ponto que dissolve a confusão: processar algo assincronamente não torna o trabalho mais rápido — a mesma operação leva o mesmo tempo de CPU para terminar. O que o assíncrono muda é que o chamador não fica bloqueado esperando: ele recebe um "ok, vou cuidar disso" imediatamente e segue, enquanto o trabalho acontece em segundo plano. A resposta percebida é mais rápida (o usuário não espera), mas o trabalho total não diminuiu — só foi desacoplado no tempo do chamador. Isso traz benefícios reais (absorver picos numa fila, resiliência se o consumidor cai, paralelismo) e custos reais (a complexidade de "a resposta vem depois": como entregar, como lidar com falha e duplicação e ordem — todo o Cap 16). Escolher assíncrono é escolher desacoplamento temporal e seus benefícios, aceitando sua complexidade — não é escolher "rápido". Confundir os dois leva a tornar tudo assíncrono "para ser rápido" e herdar complexidade sem motivo.

4.7 Estado — a propriedade que define tudo

Estado é informação que persiste entre operações e que o sistema precisa lembrar. É, de longe, a propriedade que mais complica o design de sistemas — porque tudo que é difícil em sistemas distribuídos (consistência, replicação, particionamento, recuperação) é difícil por causa do estado. Componentes sem estado (stateless) são triviais de escalar e recuperar; componentes com estado (stateful) são o problema central.

Onde o estado mora determina a forma do sistema inteiro
Uma das decisões mais consequentes do design é onde o estado vive, e mantê-lo fora dos componentes que processam é o que torna esses componentes descartáveis e escaláveis (o "gado, não bicho de estimação" do volume anterior). Um servidor de aplicação stateless — que guarda o estado num banco ou cache externo, não em si mesmo — pode ser morto, recriado e multiplicado livremente, porque não há nada nele para perder; qualquer instância serve qualquer requisição. Um servidor stateful — que guarda a sessão na própria memória, o arquivo no próprio disco — é um ponto único: matá-lo perde o estado, e múltiplas cópias divergem. Por isso o padrão moderno empurra o estado para a borda do sistema (bancos, caches, object storage), deixando o miolo de processamento sem estado. O estado não desaparece — ele se concentra em poucos lugares projetados para guardá-lo com cuidado (a Parte III), e o resto fica livre. Saber separar o que tem estado do que não tem é metade da arte de projetar para escala.

4.8 Push vs pull — quem inicia

Quando uma parte do sistema precisa de informação de outra, há duas direções possíveis, e escolher entre elas é um trade-off recorrente: push (a fonte envia ativamente quando há novidade) ou pull (o interessado pergunta periodicamente se há novidade).

Push (fonte envia)Pull (interessado pergunta)
LatênciaBaixa — chega assim que aconteceAlta — só na próxima checagem
EficiênciaAlta se eventos são raros (só envia quando há)Desperdício se pergunta e não há nada
CargaPico quando muitos eventos ao mesmo tempoConstante e previsível (intervalo fixo)
ComplexidadeMaior (manter conexões, saber quem quer o quê)Menor (o cliente só pergunta)
AcoplamentoFonte precisa conhecer os interessadosInteressado conhece a fonte; fonte não sabe de ninguém
A escolha depende da frequência e da urgência
Push e pull não têm vencedor universal; a escolha depende de duas perguntas. Com que frequência há novidade? Se eventos são raros, pull desperdiça (pergunta mil vezes para uma resposta útil) e push é eficiente (só fala quando há algo). Se eventos são constantes, pull em lote pode ser mais eful que um dilúvio de pushes. Quão urgente é a entrega? Se precisa ser imediato (uma mensagem de chat, um alerta), push vence pela latência baixa. Se um atraso é tolerável (sincronizar um cache a cada minuto), pull é mais simples e previsível. Muitos sistemas reais combinam os dois — um "long polling" ou um push de notificação que dispara um pull dos dados — justamente para equilibrar a latência do push com a simplicidade do pull. É o mesmo eixo que reaparece no fan-out de feeds (Cap 21): empurrar o post para os seguidores (push/fan-out on write) ou deixá-los buscar (pull/fan-out on read).

4.9 O mapa das tensões

Reunindo o vocabulário, eis a bússola dos trade-offs que você vai negociar pelo resto do livro. Cada par é uma tensão real — ganhar de um lado custa do outro — e o design é escolher onde, em cada eixo, o seu problema deve ficar:

De um lado……do outroOnde aparece
Latência (op individual rápida)Throughput (volume total)Batching, filas (Cap 11)
Consistência (sempre correto)Disponibilidade / latênciaCAP, PACELC (Cap 5, 6)
Espaço (memória/disco)Tempo (computação)Cache, índices, desnorm. (Cap 10, 12)
Acoplamento baixoSimplicidade (sem a rede)Monólito vs serviços (Cap 1, 15)
Síncrono (simples)Assíncrono (resiliente)Comunicação (Cap 15)
Push (baixa latência)Pull (simples, previsível)Feeds, notificações (Cap 21, 23)
Stateless (escala fácil)Stateful (lembra, mas complica)Onde o estado mora (Parte III)
Não há lado "certo" — há o lado certo para o seu problema
A tentação ao ver uma tabela assim é procurar a coluna "boa" e sempre escolhê-la. Não existe. Cada eixo é uma tensão genuína onde os dois lados são legítimos, e o design maduro é posicionar cada decisão no ponto do eixo que o problema específico pede. Um sistema de pagamentos quer consistência forte (paga o preço em latência); um feed social quer disponibilidade e baixa latência (aceita consistência eventual). Um processamento em lote quer throughput (sacrifica latência individual); uma busca quer latência (sacrifica eficiência de lote). O mesmo eixo, respostas opostas, ambas certas — porque os problemas são diferentes. Esta tabela não é uma lista de escolhas certas; é uma lista de perguntas que todo design responde, e a maturidade está em responder cada uma conscientemente, sabendo o que se ganha e o que se paga. Quando alguém diz "este sistema é eventualmente consistente", você não deve pensar "que ruim" nem "que bom" — deve pensar "ok, então eles trocaram consistência por disponibilidade/latência; isso faz sentido para o problema deles?".

4.10 Estudo de caso — a mesma feature, dois sistemas, eixos opostos

"Mostrar a contagem de curtidas"
O cenário

Dois produtos precisam da mesma feature aparentemente trivial: mostrar quantas curtidas um item tem. Um é uma rede social com posts virais; o outro é uma plataforma de votação para premiações, onde o resultado é oficial. A feature é idêntica na superfície — "exibir um número que conta curtidas" — mas as respostas em cada eixo de trade-off são opostas, e por isso os sistemas são diferentes. O caso mostra o vocabulário inteiro em ação.

Eixo da consistência: o que cada um escolhe
a mesma feature, posições opostas no eixo
REDE SOCIAL (post viral)
  Consistência: EVENTUAL. Mostrar "1.2M curtidas" quando são
    1.200.050 não importa nada. Ninguém liga para o número exato.
  → escolhe disponibilidade + latência sobre exatidão.

VOTAÇÃO OFICIAL (premiação)
  Consistência: FORTE. O número É o resultado. Contar errado
    ou duas vezes invalida a premiação — é fraude.
  → escolhe exatidão sobre tudo, paga em latência/disponib.
Eixo latência/throughput e push/pull
os outros eixos divergindo
REDE SOCIAL (post viral — milhões curtindo no mesmo minuto)
  Write-heavy no pico → THROUGHPUT importa: agregar curtidas
    em lote, contador aproximado, escrita amortecida.
  Espaço×tempo: contador desnormalizado (cache do total)
    para não fazer COUNT(*) a cada leitura. Espaço por tempo.
  Push/pull: pull — a contagem atualiza quando você olha,
    não precisa empurrar cada curtida para cada espectador.

VOTAÇÃO OFICIAL
  LATÊNCIA por voto menos crítica; CORREÇÃO é tudo:
    cada voto é uma transação durável, idempotente (Cap 16),
    contada exatamente uma vez. Throughput menor é aceitável.
  Espaço×tempo: talvez recontar do log de votos (fonte da
    verdade) em vez de confiar num contador cacheado.
O acoplamento e o estado também divergem

Na rede social, a contagem é um dado desacoplado e tolerante: pode viver num cache separado, atualizado de forma assíncrona, e se o serviço de contagem cair, o post ainda aparece (sem o número, ou com um número velho — degradação graciosa). Na votação, a contagem é fortemente acoplada à integridade do voto: o estado (cada voto) precisa ser durável e consistente, e não há "degradação graciosa" aceitável — um número errado é pior que nenhum número. O estado, na rede social, é descartável e reconstruível; na votação, é o ativo mais crítico do sistema.

Resultado: a mesma feature de uma linha ("mostrar contagem de curtidas") produz dois sistemas radicalmente diferentes porque ela ocupa posições opostas em quase todos os eixos de trade-off: consistência (eventual vs forte), prioridade (throughput/latência vs correção), push/pull, acoplamento, e a natureza do estado. Nenhuma das duas está "mais certa" — cada uma posicionou cada eixo onde o seu problema pedia. O engenheiro que tratasse as duas como "a mesma feature de contagem" construiria uma errada: ou uma votação com contador aproximado e eventual (fraude), ou uma rede social com transação durável e consistência forte por curtida (não escalaria no post viral). O vocabulário dos trade-offs é o que torna visível que duas features idênticas na superfície são problemas opostos por baixo. Isto fecha a Parte I — o raciocínio. Você sabe extrair requisitos, estimar grandezas e nomear as tensões. A Parte II entra nas leis que governam esses trade-offs nos sistemas distribuídos, começando pelo mais famoso de todos: o teorema CAP.

4.11 Erros comuns

Erro 1 · Confundir latência com throughput

Otimizar volume agregado quando o usuário sofre com lentidão individual, ou vice-versa. São independentes, e melhorar uma pode piorar a outra (batching aumenta throughput, piora latência). Pergunte sempre qual das duas o sistema otimiza — interativo é latência, lote é throughput.

Erro 2 · Otimizar a média da latência

Medir e melhorar a latência média, ignorando a cauda (p99, p999) que é a experiência real em escala. Com fan-out de requisições, a cauda rara de cada componente vira a experiência típica do todo. Fale de latência em percentis, e olhe a cauda.

Erro 3 · Tratar "assíncrono" como "rápido"

Tornar tudo assíncrono "para acelerar", quando o assíncrono não torna o trabalho mais rápido — só desacopla o chamador no tempo, ao custo de complexidade (entrega, ordem, falha, duplicação). Escolha assíncrono pelo desacoplamento que ele dá, não por uma velocidade que ele não dá.

Erro 4 · Criar acoplamento sem perceber

Dividir o sistema em partes que parecem separadas mas mudam sempre juntas — alto acoplamento disfarçado de modularidade. Se mudar A obriga a mudar B, eles estão acoplados, não importa quantos serviços você criou. Agrupe o que muda junto, separe o que muda por razões diferentes.

Erro 5 · Ignorar o eixo espaço-tempo

Adicionar cache, índices, desnormalização sem reconhecer que está gastando espaço (e escrita) para comprar tempo de leitura — e sem perguntar se tem o espaço e se manter a cópia atualizada vale. Toda otimização move custo entre espaço e tempo; mova-o para o eixo onde você tem folga.

Erro 6 · Espalhar estado por todo lado

Deixar estado (sessão, arquivos, cache local) dentro dos componentes de processamento, tornando-os impossíveis de escalar e recuperar. Concentre o estado em poucos lugares projetados para guardá-lo, e mantenha o miolo de processamento stateless — descartável e multiplicável.

Erro 7 · Procurar o lado "certo" do trade-off

Achar que há uma resposta universalmente correta para cada eixo (consistência forte é sempre melhor, microsserviços é sempre o futuro). Cada eixo é uma tensão genuína; o lado certo depende do problema. A mesma feature pode pedir lados opostos em sistemas diferentes. Decida cada eixo pelo seu caso.

Verifique seu entendimento
Um engenheiro propõe tornar o processamento de pedidos do checkout assíncrono (via fila) "para deixar o checkout mais rápido". O tempo de processar cada pedido é o mesmo; o que muda é que o usuário recebe a confirmação imediatamente e o processamento acontece em segundo plano. Qual é a leitura mais precisa dessa decisão?

4.12 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Latência ou throughput?

Para cada sistema, diga se ele é dominado por latência ou por throughput, e o que isso implica para o design: (a) a busca de um e-commerce; (b) um job noturno que gera relatórios de todos os clientes; (c) um jogo online multiplayer; (d) um pipeline que processa uploads de vídeo para gerar miniaturas.

(a) Busca de e-commerce → latência. O usuário espera o resultado e cada milissegundo afeta a conversão. Design: otimizar o caminho de cada query (índices, cache), nunca agrupar buscas em lote. Importa que esta busca seja rápida.

(b) Job noturno de relatórios → throughput. Ninguém espera um relatório individual; importa processar todos no total dentro da janela noturna. Design: batching agressivo, paralelismo, eficiência por operação. A latência de um relatório individual é irrelevante.

(c) Jogo multiplayer → latência (extrema). Cada ação precisa de resposta em dezenas de milissegundos, ou o jogo fica injogável. Design: tudo otimizado para latência mínima e previsível — inclusive a cauda (um lag ocasional arruína a experiência). Throughput importa menos que a latência consistente.

(d) Pipeline de miniaturas → throughput. O usuário tolera esperar alguns segundos pela miniatura (é assíncrono); importa processar o volume de uploads. Design: fila, workers paralelos, batching. Otimiza-se quantos vídeos por minuto, não a latência de um.

O insight: sistemas interativos (alguém esperando agora) são de latência; sistemas de processamento (trabalho a completar, ninguém esperando individualmente) são de throughput. Trocar a otimização — batching numa busca, ou processar relatórios um a um "para ser responsivo" — degrada o que importa.

Médio
Exercício 2 · Nomear o eixo espaço-tempo

Para cada técnica, identifique se ela gasta espaço para ganhar tempo ou o inverso, e em que situação ela não compensaria: (a) adicionar um índice numa coluna muito consultada; (b) comprimir logs antigos antes de arquivá-los; (c) pré-computar e guardar o resultado de um relatório caro; (d) calcular um valor derivado sob demanda em vez de armazená-lo.

(a) Índice numa coluna consultada → espaço (e tempo de escrita) por tempo de leitura. O índice ocupa disco e desacelera cada escrita (manter o índice), para acelerar muito a leitura. Não compensa se a coluna é raramente consultada mas muito escrita — você paga o custo de escrita sem colher o benefício de leitura. Índice em tabela write-heavy pouco lida é prejuízo.

(b) Comprimir logs antigos → tempo (comprimir/descomprimir) por espaço. Gasta CPU para economizar armazenamento e banda. Não compensa se os logs são acessados com frequência (você paga descompressão toda hora) ou se a CPU é o gargalo. Compensa para arquivo morto raramente lido, onde espaço é caro e CPU sobra.

(c) Pré-computar relatório → espaço por tempo. Guarda o resultado pronto (espaço) para servir sem recalcular (tempo). Não compensa se o relatório é pedido raramente (você pré-computa em vão) ou se os dados mudam tão rápido que o pré-computado fica obsoleto antes de ser lido (gasta espaço e ainda entrega dado velho — o problema de invalidação).

(d) Calcular sob demanda em vez de armazenar → tempo por espaço. O inverso: não guarda o valor derivado (economiza espaço) e recalcula quando precisa (gasta tempo). Não compensa se o valor é lido com muita frequência e o cálculo é caro — aí você recalcula a mesma coisa mil vezes, e armazenar (ou cachear) seria melhor. Compensa quando a leitura é rara ou o cálculo é barato.

O insight: nenhuma técnica é boa ou ruim em si — cada uma move custo entre espaço e tempo, e compensa quando você tem folga no eixo que gasta e escassez no que economiza. A pergunta é sempre "qual eixo está sobrando e qual está apertado?".

Médio
Exercício 3 · Diagnosticar o acoplamento

Uma equipe dividiu o sistema em "microsserviços", mas reclama que quase toda mudança exige alterar três ou quatro serviços ao mesmo tempo, deploys precisam ser coordenados, e nada pode ser lançado isoladamente. Diagnostique o que provavelmente aconteceu em termos de acoplamento e coesão, e o que indica que a decomposição foi mal feita.

O diagnóstico: a equipe criou microsserviços com alto acoplamento e provavelmente baixa coesão — o pior dos dois mundos. Eles pagaram todo o custo da distribuição (rede, deploys separados, complexidade operacional) sem colher o benefício que justifica esse custo (a capacidade de mudar e lançar cada parte independentemente).

Os sintomas e o que revelam:

  • "Toda mudança exige alterar 3-4 serviços": sinal claro de alto acoplamento. Se uma única mudança de negócio se espalha por vários serviços, eles não estão de fato separados — estão amarrados, só que agora por chamadas de rede em vez de chamadas de função (o que é pior: mais lento e mais frágil).
  • "Deploys precisam ser coordenados": acoplamento temporal e de versão. Serviços verdadeiramente desacoplados deployam sozinhos, atrás de contratos estáveis. Coordenar deploys significa que eles dependem das versões uns dos outros.
  • "Nada pode ser lançado isoladamente": a negação do principal benefício dos microsserviços. Se você não pode lançar um serviço sem os outros, você tem um monólito distribuído — todo o custo da distribuição, nenhum dos ganhos.

A causa raiz provável: a decomposição seguiu os limites errados — por camada técnica (um serviço "de banco", um "de lógica", um "de API") em vez de por capacidade de negócio coesa. Quando você divide por camada, qualquer feature atravessa todas as camadas e toca todos os serviços (alto acoplamento). A boa decomposição segue os limites do domínio: cada serviço encapsula uma capacidade de negócio inteira (alta coesão), de modo que uma mudança de negócio típica fica contida em um serviço (baixo acoplamento). A lição: microsserviços não dão desacoplamento de graça — eles só compensam se a fronteira entre eles seguir a fronteira natural do problema. Cortar nos lugares errados produz um monólito distribuído, que é estritamente pior que um monólito. Às vezes a resposta certa é voltar para um monólito bem modularizado.

Difícil
Exercício 4 · Posicionar todos os eixos — entrevista

Você vai projetar o sistema de "stories" de um app social (conteúdo efêmero que some em 24h, visto por seguidores). Para cada eixo de trade-off do capítulo (latência/throughput, consistência, espaço/tempo, síncrono/assíncrono, push/pull, stateless/stateful), diga onde você posicionaria a decisão e por quê. O objetivo é demonstrar que você raciocina pelo vocabulário dos trade-offs, não por reflexo.

Enquadramento: stories têm um perfil bem definido — conteúdo efêmero (some em 24h), fortemente read-heavy (poucos postam, muitos veem), tolerante a inconsistência leve, com mídia pesada. Cada eixo se posiciona a partir desse perfil.

Latência vs throughput: latência na leitura (o usuário abre os stories e espera ver instantaneamente — experiência interativa), com atenção à cauda (um story que demora a carregar quebra o fluxo de "passar" rápido). Na escrita (postar), throughput importa menos — postar pode levar um segundo a mais. Posição: otimizar latência de leitura, especialmente p99.

Consistência: eventual, tranquilamente. Se um seguidor vê o story novo 2 segundos depois de postado, ninguém se importa; se um story expirado leva alguns segundos para sumir, idem. Não há exatidão crítica aqui (diferente de pagamentos ou votação). Posição: consistência eventual, trocada por disponibilidade e latência.

Espaço vs tempo: gastar espaço por tempo na leitura — pré-montar a lista de stories disponíveis para cada usuário (materialização), cachear agressivamente, servir mídia de CDN. Como é read-heavy e efêmero, vale duplicar/cachear para leitura rápida. Detalhe do efêmero: o espaço é temporário (some em 24h), então o custo de armazenamento é limitado — favorece ainda mais gastar espaço.

Síncrono vs assíncrono: a leitura é síncrona (o usuário pede e vê). O processamento da postagem é assíncrono: ao postar, transcodificar a mídia para várias resoluções, gerar miniaturas, e fazer o fan-out para os seguidores acontece em segundo plano — o usuário recebe "postado!" na hora, o trabalho pesado roda depois. Posição: leitura síncrona, pós-processamento assíncrono.

Push vs pull: provavelmente pull para a leitura dos stories (o usuário abre o app e busca quem tem story novo) — empurrar cada story para cada seguidor seria desperdício, já que nem todos abrem. Mas push leve para a notificação ("fulano postou um story") se o produto quiser. Híbrido, como o capítulo descreve: pull dos dados, push opcional do aviso. (Conecta com o fan-out do Cap 21.)

Stateless vs stateful: os servidores que servem stories são stateless — o estado (os stories, quem viu o quê, a expiração) vive em stores externos (banco/cache para metadados, object storage + CDN para a mídia). Isso permite escalar o serviço de leitura horizontalmente para o pico (read-heavy). O estado efêmero é, na verdade, uma vantagem: como tudo expira em 24h, o volume de estado é limitado e auto-limpante (TTL).

O que um bom candidato demonstra: que cada decisão saiu do perfil do problema (efêmero, read-heavy, tolerante a inconsistência, mídia pesada), não de reflexo; que reconhece quando o mesmo sistema combina os dois lados de um eixo (leitura síncrona + processamento assíncrono; pull de dados + push de aviso); e que usa o vocabulário com precisão. Frase de fechamento: "stories são efêmeros, muito mais lidos que escritos, e tolerantes a um atraso de segundos — esse perfil empurra quase todos os eixos para o lado da disponibilidade, da leitura rápida e da simplicidade eventual, com o trabalho pesado escondido em processamento assíncrono. Eu não escolhi 'o lado bom' de cada eixo; escolhi o lado que o perfil efêmero e read-heavy pede."

Fim do capítulo 4 · Fim da Parte I
Você concluiu o raciocínio. Sabe distinguir o problema real do pedido (Cap 2), estimar suas grandezas (Cap 3), e nomear as tensões que vai equilibrar (Cap 4). Tem a postura (design é gestão de trade-offs) e o vocabulário (os eixos onde os trade-offs vivem). A Parte II entra nas leis que governam esses trade-offs no território mais traiçoeiro do design — os sistemas distribuídos, onde a falha é parcial e a verdade é negociada. Começamos pelo teorema mais famoso e mais mal-entendido da área: o CAP. Peça "continua" para a Parte II.
Parte II
As leis

O que governa todo sistema distribuído, queira você ou não. O teorema CAP e sua extensão PACELC, o espectro da consistência, as leis matemáticas da escala (Amdahl, Little, Universal Scalability), e as falácias que todo iniciante comete sobre a rede. Quatro capítulos sobre as forças que você não negocia — só obedece, ou paga o preço.

CAP / PACELC Consistência As leis da escala As falácias
Parte II · Capítulo 5 · As leis

CAP e PACELC
na prática.

Nenhum teorema da computação distribuída é citado com mais frequência e entendido com menos precisão que o CAP. "Escolha dois de três: consistência, disponibilidade, tolerância a partição" — a frase que todo mundo decora está, segundo o próprio autor, errada o suficiente para confundir mais do que esclarece. Este capítulo desfaz os mitos e recupera o que o CAP de fato ensina, mais a extensão (PACELC) que captura o trade-off que ele ignora — porque entendido corretamente, CAP é uma das ferramentas de design mais úteis que existem; entendido como slogan, é fonte de decisões erradas.

Este capítulo abre a Parte II — as leis — com a mais famosa e mais mal-compreendida delas. Vamos ver o que o CAP realmente enuncia (e o que não enuncia), por que o "2 de 3" é enganoso segundo o próprio Brewer, por que sistemas "CA" simplesmente não existem num mundo distribuído, por que a escolha entre consistência e disponibilidade só acontece durante uma partição (que é rara), o que o PACELC acrescenta para o tempo normal (a tensão latência vs consistência), e a sutileza que mais importa na prática: a escolha não é global do sistema, é por operação. Terminamos transformando tudo isso numa ferramenta concreta de design. Quando você sair daqui, o CAP deixa de ser um slogan para entrevista e vira uma lente para decidir como cada parte do seu sistema se comporta quando a rede falha.

5.1 A história — uma conjectura, uma prova, e um retrato mal pintado

Contexto histórico

Em 2000, numa palestra (o keynote do PODC), Eric Brewer apresentou o que chamou de uma conjectura: num sistema distribuído, você não pode ter simultaneamente consistência, disponibilidade e tolerância a partições. Era uma observação informal, nascida da experiência prática de construir sistemas web em escala na Inktomi. Em 2002, Seth Gilbert e Nancy Lynch, do MIT, provaram formalmente uma versão da conjectura, e ela virou o "teorema CAP" — ganhando o peso de uma lei matemática.

A formulação que pegou — "escolha 2 de 3" — era didática e memorável, e exatamente por isso se espalhou e se distorceu. Virou um slogan repetido em entrevistas e slides, frequentemente por gente que nunca leu a prova de Gilbert e Lynch nem a conjectura original de Brewer. O slogan sugeria que você escolhia, como num menu, abrir mão de uma das três propriedades — uma simplificação que escondia as sutilezas reais e gerava conclusões erradas (a mais comum: "vou construir um sistema CA").

Em 2010, Daniel Abadi, de Yale, apontou uma limitação importante do CAP: ele só fala do que acontece durante uma partição, e ignora completamente o trade-off que existe o tempo todo, mesmo sem partição — entre latência e consistência. Propôs o PACELC para capturar isso. Em 2012, o próprio Brewer escreveu um retrospecto célebre ("CAP Twelve Years Later") admitindo que o "2 de 3" sempre foi enganoso, porque simplificava demais as tensões reais, e esclarecendo que a escolha entre C e A só se impõe durante partições.

Em 2026, o CAP ocupa um lugar curioso: é simultaneamente o teorema mais ensinado e o mais mal-ensinado da área. A versão de slogan ainda domina entrevistas; a versão correta — nuançada, prática, complementada pelo PACELC — é o que de fato orienta o design de sistemas distribuídos reais. Este capítulo escolhe a segunda. O objetivo não é você recitar "2 de 3", é você raciocinar sobre o que de fato acontece quando a rede entre as partes do seu sistema falha — porque ela vai falhar, e o que o sistema faz nesse momento é uma das decisões mais importantes do design.

5.2 O que o CAP realmente diz

Primeiro, as três propriedades, com as definições precisas (não as intuitivas, que enganam):

  • Consistência (C): toda leitura recebe a escrita mais recente, ou um erro. Todos os nós veem o mesmo dado ao mesmo tempo. (Atenção: este é o C de CAP — linearizabilidade —, não o C de ACID, que é outra coisa. A coincidência de nome causa confusão real.)
  • Disponibilidade (A): toda requisição a um nó não-falho recebe uma resposta (não-erro) — sem garantia de que seja o dado mais recente. O sistema sempre responde algo.
  • Tolerância a partição (P): o sistema continua operando apesar de mensagens entre nós serem perdidas ou atrasadas — ou seja, apesar da rede se dividir em pedaços que não conseguem se comunicar.

O teorema, na sua forma correta: quando ocorre uma partição (P), você é forçado a escolher entre consistência (C) e disponibilidade (A). Não dá para ter as duas durante a partição. Fora da partição, você pode ter as duas. Note como isso já é diferente do "escolha 2 de 3" — é "quando P acontece, escolha entre C e A".

A intuição da prova, em dois nós
Por que C e A são incompatíveis durante uma partição? Imagine dois nós, A e B, que deveriam ter o mesmo dado, e a rede entre eles cai (partição). Um cliente escreve um valor novo no nó A. Outro cliente lê do nó B. Agora o sistema enfrenta uma escolha impossível: ou o nó B responde com o dado que tem (que é o velho, porque a escrita não chegou — isso é disponível mas inconsistente), ou o nó B se recusa a responder até conseguir confirmar que tem o dado mais recente (o que não consegue, pois a rede caiu — isso é consistente mas indisponível). Não há terceira opção: durante a partição, o nó B ou mente (dá dado velho) ou cala (recusa). Essa é a prova, em uma imagem. A partição força a escolha porque os nós não conseguem se coordenar, e coordenação é o que a consistência exige.

5.3 O mito do "2 de 3"

Agora podemos demolir o slogan. "Escolha 2 de 3" sugere três escolhas simétricas — CA, CP, AP — como se você pudesse abrir mão de qualquer uma das três livremente. Está errado por dois motivos profundos.

P não é uma escolha — é uma condição imposta pela realidade
O primeiro erro do "2 de 3" é tratar a tolerância a partição como uma opção que você pode dispensar, no mesmo nível de C e A. Mas partições acontecem — cabos se rompem, switches falham, pacotes se perdem, latências disparam. Você não escolhe se a rede vai falhar; você só escolhe o que fazer quando ela falhar. Num sistema verdadeiramente distribuído (múltiplos nós comunicando por rede), abrir mão de P significaria assumir que a rede nunca falha — uma fantasia. Por isso a escolha real nunca é "C, A ou P, pegue dois"; é "dado que P vai acontecer (você não tem escolha), quando ela acontecer, você prefere C ou A?". P não está no menu — está na conta, sempre. As únicas escolhas reais são CP (sacrifica disponibilidade na partição) ou AP (sacrifica consistência na partição).

5.4 Por que sistemas "CA" não existem

Decorre diretamente do anterior: a categoria "CA" — consistente e disponível, abrindo mão de tolerância a partição — não existe num sistema distribuído. Não é que seja difícil; é que é uma categoria sem sentido.

"CA" é só "C e A enquanto a rede não falhar" — o que é todo sistema
Dizer que um sistema é "CA" é dizer que ele é consistente e disponível contanto que não haja partição. Mas isso é verdade de qualquer sistema, inclusive os CP e os AP — todos eles são consistentes e disponíveis quando a rede está funcionando perfeitamente. A pergunta do CAP só fica interessante durante a partição, e é exatamente aí que o "CA" não tem resposta: o que ele faz quando a rede falha? Se ele para (vira indisponível), era CP o tempo todo. Se ele responde dado velho (vira inconsistente), era AP. "CA" não é uma terceira opção — é a recusa de responder a pergunta que o teorema faz. Um banco de dados de nó único é, num sentido trivial, "CA" — mas porque não é distribuído, então o CAP nem se aplica a ele. No momento em que você distribui (replica), a partição entra na conta, e você tem que escolher CP ou AP. Não há fuga para o "CA".

5.5 A escolha só acontece durante a partição (que é rara)

A nuance que o slogan apaga, e que muda tudo na prática: o trade-off do CAP só se manifesta durante uma partição. No tempo normal — que é a esmagadora maioria do tempo — você tem consistência e disponibilidade simultaneamente. Partições, em sistemas bem construídos, são eventos raros (segundos a minutos, algumas vezes por ano).

CAP não é uma restrição permanente — é um plano de contingência
A reformulação que torna o CAP útil: ele não diz "seu sistema é permanentemente ou consistente ou disponível". Diz "quando a rede falhar (raramente), seu sistema vai ter que escolher entre parar e responder dado velho — decida agora qual". É um plano de contingência para um evento raro, não uma camisa de força permanente. Isso reposiciona a pergunta de design: não é "quero um sistema CP ou AP?" (que soa como uma escolha de identidade), é "quando a partição rara acontecer, esta operação específica prefere recusar-se a responder (CP) ou responder com dado possivelmente velho (AP)?". Para a maioria das operações, no resto do tempo, você tem as duas. O CAP te força a pensar no comportamento sob falha — que é precisamente o que a maioria dos designs ignora até o incidente. Ele é valioso não apesar de só valer na partição, mas porque te obriga a planejar o momento da partição antes que ele chegue.

5.6 PACELC — o trade-off que o CAP ignora

O CAP tem um ponto cego enorme: ele só fala da partição, e não diz nada sobre o tempo normal — que é quase todo o tempo. E no tempo normal existe um trade-off real e constante que o CAP ignora: entre latência e consistência. O PACELC, de Abadi, completa o quadro.

PACELC: o quadro completo
PAC : se há Partição (P),
      escolha entre Availability e Consistency   ← isto é o CAP

ELC : Else (sem partição, o tempo normal),
      escolha entre Latency e Consistency        ← isto o CAP ignora

A razão do ELC: para garantir consistência forte entre réplicas,
você precisa COORDENAR (esperar a confirmação de outras réplicas
antes de responder). Coordenar custa TEMPO. Logo, mesmo sem
partição, mais consistência = mais latência. Você troca um pelo
outro o tempo TODO, não só na falha.

A intuição do ELC: consistência forte exige que as réplicas concordem antes de responder, e concordar significa trocar mensagens e esperar — latência. Um sistema que responde da réplica mais próxima sem esperar as outras é rápido (baixa latência) mas pode dar dado levemente velho (consistência fraca). Um que espera todas as réplicas confirmarem é consistente mas mais lento. Esse trade-off existe sempre, não só na partição — e é o que o PACELC adiciona.

SistemaClassificação PACELCO que significa
DynamoDB, Cassandra (default)PA / ELNa partição, escolhe disponibilidade; no normal, escolhe latência. Prioriza estar rápido e no ar; aceita inconsistência.
Spanner, sistemas de consensoPC / ECNa partição, escolhe consistência (pode parar); no normal, escolhe consistência (paga latência). Prioriza correção sempre.
PNUTS (Yahoo)PC / ELCurioso: no normal troca consistência por latência, mas na partição vira consistente. Mostra que as duas escolhas são independentes.
PACELC é mais útil no dia a dia do que CAP
Há uma razão pela qual o PACELC orienta mais o design moderno que o CAP puro: o trade-off do CAP (C vs A) só importa durante partições, que são raras. O trade-off do PACELC (L vs C) importa o tempo todo, porque toda leitura e escrita num sistema replicado paga, ou não, o custo de coordenação. Na prática, a pergunta "quanto de latência estou disposto a pagar por quanta consistência, em cada operação, no funcionamento normal?" afeta a experiência do usuário todos os dias, enquanto a pergunta do CAP afeta-a algumas vezes por ano. Por isso, ao avaliar um banco distribuído, a classificação PACELC (PA/EL, PC/EC...) diz mais sobre como ele vai se comportar no seu sistema real do que saber se ele é "CP ou AP". O CAP te faz pensar na falha; o PACELC te faz pensar no dia comum — e o dia comum é onde você vive.

5.7 A sutileza que mais importa: a escolha é por operação

O erro mais consequente do "2 de 3" é sugerir que CP/AP é uma propriedade global do sistema — que você escolhe uma vez, para tudo. Na prática moderna, a escolha é por operação: o mesmo sistema pode ser CP para algumas operações e AP para outras, conforme o que cada uma exige.

Um banco pode ser CP para o saldo e AP para o histórico
Considere um sistema bancário. A operação "atualizar saldo / autorizar saque" exige consistência forte (CP): na dúvida durante uma partição, é melhor recusar a operação (indisponível) do que permitir um saque que estoura o saldo (inconsistente) — dinheiro não admite "dado velho". Mas a operação "ver o histórico de transações" tolera bem disponibilidade (AP): mostrar um histórico que está alguns segundos atrasado durante uma partição rara é perfeitamente aceitável, e muito melhor que recusar-se a mostrar o histórico. O mesmo sistema, duas operações, dois pontos opostos no eixo CAP — cada um correto para a sua operação. Isto destrói a ideia de "meu sistema é CP" como uma identidade: sistemas maduros são CP onde precisam (a verdade que não pode errar) e AP onde podem (a informação que tolera atraso). A granularidade da escolha é a operação, não o sistema. É a mesma lição do Cap 4: não há lado certo do eixo, há o lado certo para cada caso — e aqui "cada caso" desce ao nível de cada operação.

5.8 CAP como ferramenta de design (não de entrevista)

Juntando tudo, o CAP/PACELC vira um procedimento concreto de design, aplicável a cada operação importante do seu sistema:

  • 1. Esta operação lida com estado replicado? Se não (nó único, sem réplica), CAP não se aplica — não há partição entre réplicas. A pergunta só surge quando há dados em mais de um lugar.
  • 2. Durante uma partição, esta operação prefere parar ou responder dado velho? Se o dado errado causa dano (dinheiro, estoque, autorização), prefira parar — CP. Se o dado velho é tolerável (um feed, um histórico, uma contagem), prefira responder — AP.
  • 3. No tempo normal, quanta latência vale a consistência desta operação? (O ELC do PACELC.) Se a operação exige ver sempre o último valor, pague a latência de coordenação. Se um valor levemente velho serve, leia da réplica próxima e seja rápido.
  • 4. Documente a escolha. "Esta operação é CP/EC; aquela é AP/EL." Isso torna explícito o comportamento sob falha — que é o que ninguém pensa até o incidente, e o que o CAP existe para forçar você a pensar antes.
A pergunta que o CAP força (e que salva incidentes)
O verdadeiro valor do CAP não é classificar sistemas — é forçar uma pergunta que a maioria dos designs evita até ser tarde: "o que exatamente este sistema faz quando a rede entre suas partes falhar?". A maioria dos sistemas é projetada assumindo, implicitamente, que a rede sempre funciona — e então, no dia da partição (que vem), eles se comportam de formas não planejadas e frequentemente catastróficas: dão respostas inconsistentes que ninguém esperava, ou travam de formas que ninguém previu. O CAP, levado a sério, te obriga a decidir esse comportamento de propósito, antes: "quando particionar, esta operação para, e aquela degrada para dado velho — conscientemente". Essa decisão antecipada é a diferença entre um sistema que degrada de forma planejada na falha e um que falha de forma surpreendente. O CAP é, no fundo, uma disciplina para pensar na falha parcial antes que ela pense em você.

5.9 Estudo de caso — o carrinho de compras que escolheu AP

A decisão clássica da Amazon (e por que ela é certa)
O cenário

Um e-commerce de grande escala precisa decidir como o carrinho de compras se comporta durante uma partição de rede. O carrinho é replicado entre data centers para disponibilidade e baixa latência. A pergunta do CAP: quando a rede entre as réplicas falha, o carrinho deve recusar operações (CP) ou continuar aceitando, arriscando inconsistência (AP)? É a decisão que o Dynamo, da Amazon, tornou célebre.

Passo 1 · O que custa cada escolha
o trade-off do carrinho, concretamente
SE CP (consistência forte, recusa na partição):
  Durante a partição, o cliente não consegue adicionar itens
  ao carrinho. "Erro, tente mais tarde." → carrinho abandonado,
  venda perdida. Para um e-commerce, indisponibilidade do
  carrinho = dinheiro saindo pela porta, a cada segundo.

SE AP (disponibilidade, aceita na partição):
  O cliente continua adicionando itens normalmente, mesmo
  durante a partição. As réplicas divergem temporariamente.
  Pior caso: um item adicionado some, ou um item removido
  reaparece, quando as réplicas reconciliam.
Passo 2 · A assimetria que decide

A decisão se torna óbvia quando você pesa os custos: o custo de indisponibilidade (carrinho que não funciona = venda perdida, garantida) é muito pior que o custo de inconsistência (um item ressurgir no carrinho = um pequeno aborrecimento, raro, recuperável). Para um carrinho, é melhor estar sempre disponível e ocasionalmente errado do que correto e às vezes indisponível. A escolha é AP. A famosa decisão do Dynamo: "add to cart" nunca deve falhar.

Passo 3 · Como o AP lida com a inconsistência
AP não é "vale tudo" — é reconciliação
O carrinho AP não ignora a inconsistência — ele a RESOLVE
depois, com uma regra de reconciliação que minimiza o dano:

Regra do carrinho: na dúvida, UNIR os carrinhos divergentes
(merge), preferindo NÃO perder itens. Se uma réplica tem
{A, B} e outra {A, C}, reconcilia para {A, B, C}.

Por quê? Porque o erro "item a mais no carrinho" (cliente
remove) é MUITO menos custoso que "item a menos" (cliente
desiste de comprar). A reconciliação é projetada para errar
para o lado da venda. Consistência eventual COM uma regra
de convergência pensada — não caos.
Passo 4 · Mas o pagamento é CP — a mesma loja, outra operação

E aqui está a lição que completa o caso: a mesma loja que escolhe AP para o carrinho escolhe CP para o pagamento e o controle de estoque final. Cobrar o cartão, debitar o estoque real, confirmar o pedido — essas operações não toleram inconsistência (cobrar duas vezes, vender o último item para duas pessoas). Na partição, elas preferem recusar/esperar (indisponível) a errar. A loja é AP onde a disponibilidade vale mais (o carrinho, o catálogo) e CP onde a correção é inegociável (o dinheiro, o estoque no checkout). Um sistema, escolhas opostas por operação.

Resultado: a decisão do carrinho ilustra o CAP usado como ferramenta de verdade. Não foi "a Amazon é uma empresa AP" (identidade global, o erro do slogan) — foi uma escolha por operação, baseada na assimetria de custos: para o carrinho, indisponibilidade custa mais que inconsistência (escolhe AP, com reconciliação que erra para o lado da venda); para o pagamento, o oposto (escolhe CP). Cada operação posicionada no eixo CAP conforme o que ela não pode perder. E o "AP" do carrinho não é caos — é consistência eventual com uma regra de convergência cuidadosamente projetada. Esse é o CAP maduro: por operação, baseado em custo assimétrico, com a inconsistência gerenciada, não ignorada. Próximo capítulo: a consistência em si — porque "consistência eventual" e "consistência forte" são só dois pontos de um espectro rico (causal, sequencial, read-your-writes), e escolher o nível certo é mais sutil que CP vs AP.

5.10 Erros comuns

Erro 1 · Recitar "2 de 3"

Tratar CAP como um menu de três escolhas simétricas. O próprio Brewer chamou essa formulação de enganosa. P não é opcional (a rede vai falhar), então a escolha real é só CP ou AP — o que fazer quando a partição, inevitável, acontecer. Pense em "C ou A durante P", não em "2 de 3".

Erro 2 · Acreditar em sistemas "CA"

Projetar um sistema distribuído "CA", abrindo mão de tolerância a partição. CA não existe num sistema distribuído — é só "C e A enquanto a rede não falha", o que é todo sistema. No momento da partição (que vem), um "CA" ou para (era CP) ou dá dado velho (era AP). Não há terceira opção.

Erro 3 · Achar que o trade-off vale o tempo todo

Concluir que um sistema é "ou consistente ou disponível, permanentemente". A escolha CAP só se impõe durante a partição, que é rara; no tempo normal você tem as duas. CAP é um plano de contingência para a falha, não uma camisa de força permanente. Não sacrifique consistência o tempo todo por uma partição que ocorre minutos por ano.

Erro 4 · Ignorar o trade-off latência-consistência (o ELC)

Pensar só no CAP e esquecer que, mesmo sem partição, consistência forte custa latência (coordenação). Esse trade-off (PACELC, o ELC) afeta toda operação o tempo todo, não só na falha. Ao escolher um banco distribuído, a classe PACELC (PA/EL, PC/EC) diz mais sobre o dia a dia que "CP ou AP".

Erro 5 · Tratar CP/AP como propriedade global

Decidir "meu sistema é CP" uma vez, para tudo. A escolha é por operação: o mesmo sistema pode (e deve) ser CP onde a correção é inegociável (dinheiro, estoque) e AP onde a disponibilidade vale mais (feed, histórico, carrinho). Decida o eixo CAP para cada operação, não para o sistema inteiro.

Erro 6 · Achar que "eventual" significa "sem consistência"

Tratar AP / consistência eventual como "vale tudo, qualquer resposta serve". Consistência eventual é uma garantia específica: sem novas escritas, todas as réplicas convergem — e sistemas AP sérios têm regras de reconciliação projetadas (como o merge do carrinho). É consistência adiada e gerenciada, não ausência de consistência.

Erro 7 · Não decidir o comportamento sob partição

Projetar assumindo implicitamente que a rede sempre funciona, e descobrir o comportamento sob partição no dia do incidente — quando o sistema age de formas não planejadas. O valor do CAP é forçar você a decidir, de propósito e antes, o que cada operação faz quando a rede falha. Decida agora; não deixe a partição decidir por você.

Verifique seu entendimento
Um colega afirma: "vamos construir nosso sistema como CA — consistente e disponível — já que nossas partições de rede são muito raras, podemos abrir mão de tolerância a partição." Qual é a resposta correta?

5.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · CP ou AP por operação

Para cada operação, decida se ela deveria ser CP (recusar na partição, priorizar correção) ou AP (responder na partição, priorizar disponibilidade), justificando pela assimetria de custos: (a) curtir um post; (b) transferir dinheiro entre contas; (c) ler o número de seguidores de um perfil; (d) reservar o último ingresso de um show.

(a) Curtir um post → AP. Se durante uma partição a curtida demora a propagar ou a contagem fica momentaneamente errada, ninguém se importa. O custo de inconsistência (contagem levemente off) é trivial; o custo de indisponibilidade ("não foi possível curtir") é uma experiência ruim desnecessária. Disponibilidade vence.

(b) Transferir dinheiro → CP. Dinheiro não admite "dado velho". Durante uma partição, é melhor recusar a transferência (indisponível, o cliente tenta de novo) do que arriscar uma inconsistência que duplica ou perde dinheiro. O custo de inconsistência é catastrófico (e ilegal); o de indisponibilidade é um aborrecimento. Correção vence.

(c) Ler número de seguidores → AP. Mostrar "10.523 seguidores" quando são 10.525 durante uma partição é perfeitamente aceitável. Inconsistência irrelevante; indisponibilidade ("não foi possível carregar o perfil") é pior. Disponibilidade vence — é informação tolerante a atraso.

(d) Reservar o último ingresso → CP. Este é o caso clássico onde a inconsistência é inaceitável: vender o último ingresso para duas pessoas (overselling) é um problema real e caro. Na partição, é melhor recusar/esperar (o cliente vê "processando") do que vender duas vezes. Recursos escassos e únicos exigem CP. Correção vence.

O padrão: a pergunta decisiva é sempre a assimetria — "qual custa mais: dar uma resposta errada/velha, ou não dar resposta?". Onde o dado errado causa dano real (dinheiro, recurso escasso, autorização) → CP. Onde o dado velho é tolerável (contagens, feeds, informação social) → AP. E note que pode ser o mesmo sistema: a rede social é AP para curtidas/seguidores e talvez CP para algo como cobrança de anúncios.

Médio
Exercício 2 · Desmontar afirmações sobre CAP

Cada afirmação contém um erro conceitual sobre CAP. Identifique e corrija: (a) "Escolhemos um banco AP, então abrimos mão de consistência completamente"; (b) "Nosso sistema é CA porque temos rede confiável"; (c) "CAP nos obriga a escolher entre consistência e disponibilidade o tempo todo"; (d) "MongoDB é CP e Cassandra é AP, então MongoDB é mais consistente em tudo".

(a) "Banco AP, abrimos mão de consistência completamente" → erro. AP não significa ausência de consistência; significa consistência eventual — uma garantia específica de que, sem novas escritas, as réplicas convergem. Você abre mão de consistência forte durante a partição, não de toda consistência. Correção: "escolhemos AP, então durante partições priorizamos disponibilidade e aceitamos consistência eventual, com uma regra de reconciliação".

(b) "Somos CA porque temos rede confiável" → erro. Rede confiável não elimina partições (cabos rompem, switches falham, mesmo nas melhores redes). CA não é uma categoria real em sistemas distribuídos. Correção: "como nossas partições são raras, temos C e A simultaneamente quase sempre — mas ainda precisamos decidir se, na partição rara, paramos (CP) ou degradamos (AP)".

(c) "CAP nos obriga a escolher o tempo todo" → erro. O trade-off C vs A só se impõe durante a partição, que é rara. No tempo normal você tem as duas. Correção: "CAP nos obriga a decidir o comportamento durante partições; no resto do tempo temos consistência e disponibilidade". (O trade-off que vale o tempo todo é o latência-consistência do PACELC, que é outro.)

(d) "MongoDB é CP, logo mais consistente em tudo" → erro duplo. Primeiro, a classificação depende da configuração (read/write concern, em sistemas reais é ajustável por operação). Segundo, e mais importante, CP/AP é por operação, não uma propriedade absoluta — você pode configurar consistência diferente por consulta. Correção: "ambos permitem ajustar a consistência por operação; a classificação 'CP/AP' descreve o default ou um modo, não uma verdade absoluta sobre todas as operações". Comparar bancos por um rótulo CAP único é simplista.

Médio
Exercício 3 · Aplicar o PACELC (o ELC)

O CAP fala da partição; o PACELC adiciona o trade-off latência-consistência no tempo normal (ELC). Para cada cenário no funcionamento normal (sem partição), decida se você priorizaria latência (EL) ou consistência (EC), explicando o custo: (a) o feed de uma rede social; (b) o saldo exibido num app bancário; (c) o catálogo de produtos de uma loja; (d) a confirmação de que um pagamento foi processado.

(a) Feed de rede social → EL (latência). Ler da réplica mais próxima e responder rápido, mesmo que o feed esteja alguns segundos atrás, é a escolha certa — o usuário valoriza um feed instantâneo, e não percebe (nem se importa) se um post apareceu 3 segundos atrasado. Pagar latência de coordenação para garantir o feed perfeitamente atual seria desperdício. Custo aceito: dado levemente velho.

(b) Saldo bancário → EC (consistência). O saldo precisa refletir as últimas transações; mostrar um saldo velho leva o cliente a decisões erradas (acha que tem dinheiro que não tem). Vale pagar a latência de coordenação para mostrar o número correto. Custo aceito: resposta um pouco mais lenta, em troca de correção.

(c) Catálogo de produtos → EL (latência). Mostrar a descrição/preço do produto rapidamente, mesmo que uma atualização de preço de segundos atrás ainda não tenha propagado, é aceitável (e há outras salvaguardas no checkout). Velocidade de navegação importa mais que consistência perfeita do catálogo. Custo aceito: o preço pode estar segundos desatualizado (validado de novo no checkout, que é EC).

(d) Confirmação de pagamento → EC (consistência). Confirmar ao cliente que o pagamento foi processado exige ter certeza de que foi — uma confirmação baseada em dado velho/não-coordenado pode dizer "pago" quando não foi. Vale a latência de garantir a consistência. Custo aceito: confirmação um pouco mais lenta, em troca de ser verdadeira.

O insight: o ELC do PACELC é onde você vive todos os dias (a partição é rara; a coordenação é constante). O padrão se repete: informação tolerante a atraso (feed, catálogo) → EL, lê rápido da réplica próxima; verdade que orienta decisão/dinheiro (saldo, confirmação) → EC, paga a coordenação. E observe (c): o catálogo é EL, mas o checkout revalida com EC — a mesma loja, latência onde dá, consistência onde precisa, por operação.

Difícil
Exercício 4 · Projetar o comportamento sob partição — entrevista

Você está projetando o sistema de uma plataforma de caronas (estilo Uber). O entrevistador pergunta: "como diferentes partes do seu sistema se comportam durante uma partição de rede? Use CAP e PACELC para justificar." Estruture a resposta cobrindo pelo menos quatro operações distintas (ex.: mostrar motoristas próximos, aceitar uma corrida, processar o pagamento, exibir o histórico de viagens), classificando cada uma e explicando o porquê — demonstrando que você decide por operação, não por sistema.

Enquadramento: a chave da resposta é demonstrar que "o sistema" não tem uma classificação CAP única — cada operação posiciona-se no eixo conforme a assimetria de custos entre inconsistência e indisponibilidade para aquela operação. Vou percorrer as operações da mais tolerante à mais crítica.

1. Mostrar motoristas próximos no mapa → AP / EL. Esta é a operação mais tolerante. Mostrar a posição de um motorista alguns segundos atrasada, ou perder um motorista que acabou de entrar na área durante a partição, é irrelevante — o usuário só quer ver que há carros por perto. Indisponibilidade ("não foi possível carregar o mapa") é muito pior que a leve inconsistência. AP na partição, e EL no normal (lê rápido da réplica próxima; posições são naturalmente efêmeras).

2. Aceitar/atribuir uma corrida → CP (o ponto mais delicado). Aqui há um recurso que não pode ser duplo: um motorista, uma corrida. Se durante uma partição duas réplicas atribuírem o mesmo motorista a dois passageiros (ou o mesmo passageiro a dois motoristas), o resultado é caótico e custoso. Esta operação exige consistência forte na atribuição — é melhor o passageiro esperar um instante a mais ("buscando motorista...") do que sofrer uma atribuição dupla. CP: na dúvida, coordene ou recuse; não atribua sem certeza. (Análogo ao "último ingresso".)

3. Processar o pagamento → CP / EC. Como todo dinheiro: cobrar exatamente uma vez, o valor certo. Na partição, prefira esperar/recusar a arriscar cobrança dupla ou ausente. No normal, pague a latência de coordenação para garantir a consistência — uma confirmação de pagamento tem que ser verdadeira. CP na partição, EC no normal. Idempotência é essencial aqui (Cap 16).

4. Exibir histórico de viagens → AP / EL. A operação mais tolerante junto com o mapa. Mostrar o histórico com uma viagem recente faltando, durante uma partição, é aceitável — ela aparece depois. Indisponibilidade do histórico é pior que estar segundos atrás. AP na partição, EL no normal (lê da réplica próxima; histórico não precisa ser instantâneo).

O quadro completo (a tabela mental):

  • Motoristas no mapa: AP / EL (tolerante, efêmero)
  • Atribuir corrida: CP (recurso único, não pode duplicar)
  • Pagamento: CP / EC (dinheiro, exatamente uma vez)
  • Histórico: AP / EL (tolerante a atraso)

O que um bom candidato demonstra: que o sistema é uma mistura de comportamentos CAP/PACELC, decididos por operação a partir da assimetria de custos (a inconsistência custa mais ou menos que a indisponibilidade, nesta operação?); que reconhece o caso crítico (atribuição de recurso único = CP, como o último ingresso); e que separa o eixo da partição (CAP) do eixo do dia normal (PACELC/ELC). Frase de fechamento: "não existe 'o sistema é CP ou AP' — existe, para cada operação, a pergunta de qual erro dói mais quando a rede falhar. Onde o dado velho é tolerável (mapa, histórico), escolho disponibilidade e latência; onde duplicar um recurso ou errar o dinheiro é inaceitável (atribuição, pagamento), escolho consistência e pago em latência. O CAP não me dá a resposta — ele me força a fazer a pergunta para cada operação, antes da partição chegar."

Fim do capítulo 5
Próximo capítulo: a consistência em si. CAP trata "consistência" como uma coisa só (forte) versus sua ausência (eventual), mas entre os dois há um espectro rico — linearizável, sequencial, causal, read-your-writes, monotônica. Escolher o nível certo é mais sutil, e mais poderoso, que decidir CP ou AP. Vamos mapear esse espectro e aprender a escolher o nível mínimo que o problema exige — porque cada grau a mais de consistência custa latência e disponibilidade.
Parte II · Capítulo 6 · As leis

Consistência:
forte, eventual,
causal.

O CAP tratou consistência como uma coisa só — você a tem (forte) ou abre mão dela (eventual). É uma simplificação útil para entender a partição, mas péssima para projetar de verdade, porque entre "forte" e "eventual" existe um espectro rico de modelos intermediários, cada um com garantias precisas e custos diferentes. Saber que existe consistência causal, read-your-writes, monotônica — e escolher o nível mínimo que o seu problema exige — é onde o design de dados deixa de ser binário e vira preciso. Cada grau a mais de consistência que você pede custa latência e disponibilidade; pedir mais do que precisa é desperdício, pedir menos é bug.

Este capítulo abre o espectro que o CAP comprimiu. Vamos ver por que consistência é uma hierarquia (do mais forte ao mais fraco), o que linearizabilidade e consistência sequencial garantem (e cobram), a consistência causal como o ponto-doce que preserva a ordem que importa sem o custo da ordem total, as garantias de sessão (read-your-writes, leituras monotônicas) que resolvem a maioria dos problemas práticos de forma barata, por que "eventual" não significa "sem garantia nenhuma", e — o mais importante — como escolher o nível mínimo suficiente, já que cada grau a mais custa. Terminamos com o que fazer quando réplicas divergem e precisam convergir. Quando você sair daqui, "consistência" deixa de ser um botão liga-desliga e vira um dial que você ajusta, por operação, ao ponto exato que o problema pede.

6.1 A história — de uma memória só a muitas cópias discordantes

Contexto histórico

O problema da consistência nasceu antes dos sistemas distribuídos, dentro de uma única máquina com múltiplos processadores. Nos anos 1970, ao construir computadores com vários núcleos compartilhando memória, os engenheiros descobriram que "ler e escrever na memória" deixava de ser simples quando vários processadores faziam isso ao mesmo tempo: a ordem em que as escritas de um processador se tornavam visíveis aos outros importava, e podia variar. Em 1979, Leslie Lamport formalizou a consistência sequencial para dar sentido a isso — o primeiro modelo formal de "o que significa a memória estar consistente quando há concorrência".

Em 1990, Herlihy e Wing formalizaram a linearizabilidade — um modelo ainda mais forte, que adiciona a noção de tempo real: as operações parecem acontecer instantaneamente em algum ponto entre seu início e fim, respeitando a ordem temporal. Esses dois modelos, nascidos para multiprocessadores, viraram a base do vocabulário de consistência.

Quando os sistemas se distribuíram pela rede nos anos 1990-2000, o problema explodiu em escala e dificuldade. Agora não eram núcleos numa placa, mas máquinas em continentes diferentes, com a rede entre elas falhando (Cap 5). Garantir que todas vissem a mesma coisa ao mesmo tempo tornou-se proibitivamente caro. Surgiu uma família de modelos mais fracos mas práticos: a consistência causal (Ahamad, 1995), as garantias de sessão (Terry, 1994), e a consistência eventual (popularizada pelo Dynamo da Amazon, 2007), que trocavam garantias por disponibilidade e latência.

Em 2026, o panorama é de um espectro bem mapeado — uma hierarquia formal que vai de linearizável (o mais forte) a eventual (o mais fraco), com modelos intermediários precisamente definidos. E há uma tendência clara: a consistência ajustável (tunable), onde o mesmo sistema permite escolher o nível por operação, em vez de impor um nível global. Bancos modernos deixam você dizer, por consulta, "esta leitura pode ser eventual" ou "esta exige linearizabilidade". O design de consistência deixou de ser "escolha um banco forte ou fraco" e virou "escolha o nível certo para cada operação" — exatamente a granularidade que o Cap 5 introduziu para o CAP, agora aplicada ao espectro inteiro.

6.2 O espectro, não o binário

A primeira correção mental: consistência não é forte-ou-eventual, é uma hierarquia ordenada do mais forte ao mais fraco. Cada modelo mais forte implica os mais fracos (se você tem linearizabilidade, tem tudo abaixo de graça), e cada modelo proíbe certas anomalias que os mais fracos permitem.

a hierarquia da consistência (forte → fraco)
LINEARIZÁVEL      toda leitura vê a última escrita, em tempo real.
   │              Como se houvesse uma cópia só. Mais forte, mais caro.
   ▼
SEQUENCIAL        todos veem as operações na mesma ordem, que
   │              respeita a ordem de cada processo — mas não
   │              necessariamente a ordem do relógio (tempo real).
   ▼
CAUSAL            operações causalmente relacionadas são vistas
   │              na mesma ordem por todos; as independentes podem
   │              ser vistas em ordens diferentes. O "ponto-doce".
   ▼
GARANTIAS DE      por CLIENTE: você vê suas próprias escritas
SESSÃO            (read-your-writes), nunca vê dado mais velho que
   │              já viu (monotonic reads), etc. Baratas e úteis.
   ▼
EVENTUAL          sem novas escritas, todas as réplicas convergem.
                  Nenhuma garantia de ordem. Mais fraco, mais barato.

REGRA: cada nível acima IMPLICA os de baixo. Cada nível abaixo
       PERMITE anomalias que o de cima proíbe. Mais forte = mais
       coordenação = mais latência e menos disponibilidade (PACELC).
Consistência mede quais anomalias o sistema proíbe
A forma mais útil de pensar em modelos de consistência: cada um é uma promessa sobre quais comportamentos estranhos não podem acontecer. Linearizabilidade proíbe quase tudo estranho (você nunca vê dado velho). Eventual proíbe quase nada (você pode ver qualquer coisa, contanto que no fim convirja). Os modelos do meio proíbem algumas anomalias específicas e permitem outras — e a arte é escolher o modelo que proíbe exatamente as anomalias que quebram o seu problema, sem pagar para proibir anomalias que o seu problema toleraria. Você não quer "a consistência mais forte"; quer a mais fraca que ainda proíbe os comportamentos que seriam bugs no seu caso. Pedir consistência forte onde a causal bastaria é pagar latência e disponibilidade para proibir anomalias que nunca te incomodariam.

6.3 O topo: linearizável e sequencial

Os dois modelos mais fortes são frequentemente confundidos, mas a diferença entre eles é instrutiva — é a diferença entre respeitar o relógio e respeitar só a ordem de programa.

Linearizável (o mais forte)

Cada operação parece acontecer instantaneamente em algum ponto entre seu início e fim, respeitando o tempo real. Se a escrita X terminou antes da leitura Y começar (no relógio), Y vê X. O sistema se comporta como se houvesse uma única cópia dos dados. A ilusão perfeita de não ser distribuído — e a mais cara.

Sequencial (forte, mas sem relógio)

Todos os processos veem as operações na mesma ordem total, e essa ordem respeita a ordem de programa de cada processo — mas não precisa respeitar o tempo real. Uma escrita que aconteceu antes no relógio pode aparecer depois na ordem global, contanto que todos concordem na mesma ordem. Mais barato que linearizável, ainda forte.

A diferença que importa: linearizável respeita o "agora" global
A distinção sutil mas crucial: linearizabilidade respeita o tempo real, consistência sequencial não. Exemplo concreto: você posta um comentário (escrita) e, logo depois, liga para um amigo que abre a página (leitura). Sob linearizabilidade, o amigo garantidamente vê seu comentário, porque a escrita terminou antes da leitura começar no tempo real. Sob consistência sequencial, o amigo pode não ver — a ordem global é consistente, mas pode colocar a leitura "antes" da escrita, mesmo que no relógio tenha sido depois. Para a maioria das interações humanas, essa diferença importa (você espera que o amigo veja o que você acabou de postar), e é por isso que linearizabilidade é o padrão-ouro intuitivo. Mas ela é a mais cara: exige coordenação que respeite uma ordem temporal global, o que em sistemas distribuídos é extremamente difícil e lento (é o que sistemas como o Spanner gastam relógios atômicos para conseguir).

6.4 Consistência causal — o ponto-doce

Entre o forte (caro) e o eventual (fraco demais para muitos casos) há um modelo que captura quase toda a intuição humana de "ordem que faz sentido" a um custo muito menor: a consistência causal. A ideia: preservar a ordem das operações que estão causalmente relacionadas, e não se importar com a ordem das que são independentes.

Causal preserva "isto aconteceu por causa daquilo"
A intuição da consistência causal: se uma operação causou ou influenciou outra, todos devem vê-las nessa ordem; se duas operações são independentes (nenhuma soube da outra), todos podem vê-las em qualquer ordem, sem problema. O exemplo clássico: numa thread de comentários, se Ana pergunta "alguém viu o jogo?" e Bruno responde "foi incrível!", a resposta de Bruno é causalmente dependente da pergunta de Ana (ele a leu antes de responder). Consistência causal garante que ninguém veja a resposta de Bruno antes da pergunta de Ana — o que seria confuso e sem sentido ("foi incrível!" aparece, e só depois "alguém viu o jogo?"). Mas se Ana e Carla postam comentários independentes ao mesmo tempo, sem uma ter lido a outra, tanto faz a ordem em que aparecem para os outros. Causal proíbe exatamente a anomalia que confunde os humanos (efeito antes da causa) e tolera a que não importa (ordem entre eventos não relacionados). É por isso que é o ponto-doce: preserva o que a mente humana espera, sem o custo da ordem total e do tempo real.

O custo da consistência causal é bem menor que o da linearizabilidade porque ela não exige uma ordem global única nem coordenação com o relógio — só rastrear as relações de causalidade (quem viu o quê antes de escrever). Por isso ela pode ser combinada com alta disponibilidade: é o modelo mais forte que ainda permite um sistema permanecer disponível durante partições (diferente de linearizável e sequencial, que forçam indisponibilidade na partição).

6.5 As garantias de sessão — barato e suficiente

Aqui está o segredo mais prático do capítulo: a maioria dos problemas de consistência que de fato incomodam usuários são resolvidos por garantias fracas e baratas chamadas garantias de sessão — promessas feitas por cliente (por sessão), não globalmente. Elas valem mesmo sob consistência eventual, e custam quase nada perto da consistência forte.

Garantia de sessãoO que prometeO bug que evita
Read-your-writesVocê sempre vê suas próprias escritasPostar um comentário e ele "sumir" ao recarregar
Monotonic readsVocê nunca vê dado mais velho do que já viuVer 50 likes, recarregar, e ver 45 (o tempo "andar para trás")
Monotonic writesSuas escritas são aplicadas na ordem que você as fezEditar e depois apagar, mas a edição chegar depois
Writes-follow-readsUma escrita que reage a uma leitura vem depois delaResponder um post e a resposta aparecer antes do post
"Read-your-writes" resolve o bug de consistência mais comum (e barato)
O problema de consistência que mais frustra usuários é também um dos mais baratos de resolver: você posta algo, recarrega a página, e não vê o que acabou de postar — porque sua leitura foi para uma réplica que ainda não recebeu sua escrita. Isso parece um bug grave ("perdi meu comentário!") e gera suporte e desconfiança. A solução não é consistência forte global (cara); é a garantia de sessão read-your-writes, que só promete que vocêsuas próprias escritas — implementável de forma barata (ex.: roteando suas leituras para a réplica que recebeu sua escrita, ou para o primário, por um tempo). Você obtém a experiência que o usuário espera ("vejo o que postei") sem pagar pela consistência forte que garantiria que todos vissem instantaneamente. Esta é a lição central das garantias de sessão: elas resolvem os bugs de consistência percebidos pelos usuários a uma fração do custo da consistência forte, porque a maioria desses bugs é sobre a relação do usuário com seus próprios dados, não sobre a consistência global do sistema.

6.6 "Eventual" não é "sem nada"

A consistência eventual carrega uma fama injusta de ser "sem consistência, vale tudo". É um mal-entendido. Eventual é uma garantia específica e definida: na ausência de novas escritas, todas as réplicas convergem para o mesmo valor. Não é "qualquer coisa pode acontecer para sempre"; é "as réplicas podem divergir temporariamente, mas vão se reconciliar".

O que "eventual" garante e o que não garante
Eventual garante: convergência. Pare de escrever, e em algum momento (segundos, normalmente) todas as réplicas terão o mesmo valor. Eventual não garante: quando isso acontece (pode ser milissegundos ou, num caso ruim, mais), nem qual valor vence quando há escritas conflitantes (isso depende da regra de reconciliação — Cap 5, o merge do carrinho), nem qualquer ordem durante a divergência (você pode ver valores "andando para trás" sem as garantias de sessão). A consequência prática: consistência eventual pura permite anomalias desconfortáveis (ver dado velho depois de um novo), e por isso quase sempre se combina com garantias de sessão para tornar a experiência aceitável. "Eventual + read-your-writes + monotonic reads" é uma combinação comum e poderosa: você tem a disponibilidade e a baixa latência do eventual, mas sem os bugs percebidos mais irritantes. A lição: "eventual" raramente é usado puro; é uma base sobre a qual se adicionam as garantias de sessão que o problema exige.

6.7 O nível mínimo suficiente — a decisão de design

Com o espectro mapeado, a decisão de design se torna clara e oposta ao instinto: não escolha a consistência mais forte; escolha a mais fraca que ainda evita os bugs do seu problema. Cada grau a mais de consistência custa latência (coordenação) e disponibilidade (PACELC, Cap 5) — então pedir mais do que precisa é pagar por proteção contra anomalias que nunca te machucariam.

OperaçãoNível mínimo suficientePor quê
Transferência bancária / saldoLinearizável (ou forte)Errar a ordem ou ver saldo velho = dinheiro errado
Reservar recurso único (ingresso)LinearizávelDuas pessoas não podem reservar o mesmo
Thread de comentários / chatCausalResposta não pode aparecer antes da pergunta
Ver seu próprio perfil após editarRead-your-writesVocê precisa ver sua edição; os outros podem esperar
Contador de curtidas, visualizaçõesEventual (+ monotonic reads)Número aproximado serve; só não pode "andar para trás"
Feed de notícias, catálogoEventualAlguns segundos de atraso é irrelevante
A pergunta de design: qual anomalia quebraria isto?
Para escolher o nível mínimo, faça a pergunta ao contrário: em vez de "quão forte preciso?", pergunte "qual comportamento estranho quebraria esta operação?". Se a resposta é "ver dinheiro/estoque errado" → você precisa de linearizável. Se é "ver uma resposta antes da pergunta" → causal basta. Se é "não ver o que eu mesmo acabei de fazer" → read-your-writes resolve. Se é "o contador andar para trás" → monotonic reads. Se é "nada me incomodaria além de divergir para sempre" → eventual puro serve. Identifique a anomalia específica que seria um bug, e escolha o modelo mais fraco que a proíbe — porque qualquer modelo mais forte que isso é latência e disponibilidade pagas por proteção que você não usa. O design de consistência é uma busca pelo mínimo, não pelo máximo.

6.8 Quando réplicas divergem — a reconciliação

Sob qualquer modelo mais fraco que o forte, réplicas vão divergir temporariamente, e quando escritas conflitantes acontecem (dois clientes escrevem valores diferentes na mesma coisa, em réplicas diferentes, sem ver um ao outro), o sistema precisa de uma regra para reconciliar. Como o sistema decide quem vence define a corretude do seu modelo eventual.

  • Last-write-wins (LWW): o valor com timestamp mais recente vence. Simples, mas perigoso — relógios de máquinas diferentes não são sincronizados perfeitamente, e LWW descarta silenciosamente a escrita perdedora. Bom para dados onde perder uma escrita é tolerável; péssimo onde cada escrita importa.
  • Merge semântico: uma regra específica do domínio combina os valores (o merge do carrinho do Cap 5: une os itens). Preserva ambas as escritas de forma que faz sentido para aquele dado. Exige pensar na regra, mas não perde dados.
  • CRDTs (tipos de dados que convergem): estruturas de dados projetadas matematicamente para que escritas concorrentes sempre convirjam para o mesmo resultado, sem coordenação — um contador, um conjunto, um texto colaborativo que se reconciliam automaticamente e corretamente. A solução elegante para consistência eventual forte, quando o dado cabe num CRDT.
  • Deixar para o cliente: apresentar os valores conflitantes e deixar o usuário/aplicação resolver (como faziam versões antigas de sistemas de sincronização). Raramente bom para UX, mas às vezes o único caminho correto.
A regra de reconciliação É parte do seu modelo de consistência
Um erro comum é escolher "consistência eventual" e considerar a decisão tomada — esquecendo que como as réplicas reconciliam é metade do modelo. Eventual com last-write-wins e eventual com merge semântico são sistemas com comportamentos profundamente diferentes: o primeiro pode perder escritas silenciosamente; o segundo as preserva. A escolha da regra de reconciliação é onde a consistência eventual ganha ou perde corretude para o seu caso. Por isso, ao projetar algo eventualmente consistente, a pergunta não termina em "eventual" — continua em "e quando duas escritas conflitam, o que acontece, e esse resultado é aceitável para este dado?". Para um contador de curtidas, LWW perde curtidas (ruim) e um CRDT de contador as soma (certo). Para um carrinho, LWW perde itens e o merge os une (certo). A regra de reconciliação não é um detalhe de implementação — é a definição do que "consistente" significa para os seus dados.

6.9 Estudo de caso — os níveis de consistência de uma rede social

Um sistema, cada operação no seu nível
O cenário

Você projeta o backend de uma rede social. A tentação inicial é escolher "um" modelo de consistência para o sistema. O design maduro faz o oposto: percorre cada operação e atribui o nível mínimo que ela exige — porque cada grau a mais custa latência e disponibilidade, e a rede social tem operações que vão de "número aproximado serve" a "isto envolve dinheiro".

Passo 1 · As operações tolerantes (eventual)
onde eventual basta (a maioria)
Contador de curtidas/visualizações → EVENTUAL + monotonic reads
  Número aproximado serve ("1,2M curtidas"). A única anomalia
  que incomoda é o contador "andar para trás" ao recarregar —
  monotonic reads proíbe isso. Custo mínimo. Usa CRDT de
  contador para somar curtidas concorrentes sem perder nenhuma.

Feed de quem você segue → EVENTUAL
  Um post aparecer 2 segundos atrasado é irrelevante. Lê da
  réplica mais próxima, rápido (EL do PACELC). Sem coordenação.
Passo 2 · As operações que precisam de ordem (causal)
onde a causalidade importa
Thread de comentários / mensagens → CAUSAL
  A anomalia que quebra: ver uma resposta ANTES da mensagem
  que ela responde ("foi incrível!" antes de "viram o jogo?").
  Consistência causal proíbe exatamente isso, preservando a
  ordem de causa-e-efeito, sem o custo da ordem total/tempo real.
  Mensagens independentes podem aparecer em qualquer ordem.
Passo 3 · As operações pessoais (read-your-writes)
a relação do usuário com os próprios dados
Editar o próprio perfil / postar → READ-YOUR-WRITES
  A anomalia que frustra: editar a bio, recarregar, e ver a
  bio ANTIGA (sua leitura foi para uma réplica atrasada).
  Read-your-writes garante que VOCÊ vê SUAS escritas — os
  outros podem ver a atualização alguns segundos depois, tudo
  bem. Barato: roteia suas leituras para onde sua escrita está.
Passo 4 · As operações críticas (forte)

E o punhado de operações que exigem o topo do espectro: cobrança de anúncios e pagamentos (não pode cobrar errado nem duas vezes → linearizável + idempotência), e qualquer recurso único e escasso (ex.: reservar um nome de usuário — dois não podem registrar o mesmo handle → linearizável). Aqui se paga a latência e a possível indisponibilidade na partição, conscientemente, porque a anomalia (cobrança dupla, handle duplicado) é inaceitável. São a minoria das operações, mas a que justifica ter consistência forte disponível no sistema.

Resultado: a rede social não tem "um" modelo de consistência — tem um espectro de escolhas por operação, cada uma no nível mínimo suficiente. A vasta maioria das operações (feeds, curtidas, visualizações) roda em eventual, rápido e disponível; as que envolvem ordem percebida (comentários, mensagens) usam causal; as pessoais usam read-your-writes; e só o punhado crítico (dinheiro, handles únicos) paga por consistência forte. O ganho é enorme: o sistema é rápido e disponível onde pode (a maioria), e correto onde precisa (a minoria crítica), em vez de pagar o custo da consistência forte em tudo (lento e indisponível desnecessariamente) ou de usar eventual em tudo (bugs de cobrança e handles duplicados). Isto é o design de consistência maduro: não escolher um nível, mas ajustar o dial por operação, sempre buscando o mínimo que evita o bug daquela operação. Próximo capítulo: as leis matemáticas da escala — Amdahl, Little, Universal Scalability — que governam quanto seu sistema pode crescer e por que adicionar máquinas eventualmente para de ajudar (ou começa a atrapalhar).

6.10 Erros comuns

Erro 1 · Tratar consistência como binário

Pensar só em "forte ou eventual", ignorando o espectro rico no meio (causal, garantias de sessão). A maioria dos problemas reais é resolvida por modelos intermediários mais baratos. Conheça a hierarquia e escolha o ponto certo, não um dos dois extremos.

Erro 2 · Pedir mais consistência do que precisa

Escolher consistência forte "por segurança" onde causal ou eventual bastaria, pagando latência e indisponibilidade por proteção contra anomalias que nunca te machucariam. Escolha o nível mínimo que evita os bugs do seu problema — cada grau a mais custa.

Erro 3 · Achar que "eventual" é "sem garantia"

Tratar consistência eventual como caos onde tudo pode acontecer para sempre. Eventual é uma garantia específica: as réplicas convergem na ausência de novas escritas. E quase nunca é usada pura — combina-se com garantias de sessão para evitar os bugs percebidos. É consistência adiada, não ausência dela.

Erro 4 · Ignorar as garantias de sessão

Sofrer com bugs percebidos (não ver o que postou, contador andando para trás) e achar que a solução é consistência forte global (cara). Garantias de sessão (read-your-writes, monotonic reads) resolvem esses bugs por cliente, baratíssimas. A maioria das queixas de consistência é sobre a relação do usuário com os próprios dados.

Erro 5 · Esquecer a regra de reconciliação

Escolher "eventual" e parar, sem decidir como réplicas reconciliam escritas conflitantes. A regra (LWW, merge, CRDT) é metade do modelo: LWW pode perder escritas silenciosamente; merge/CRDT as preserva. A reconciliação define o que "consistente" significa para os seus dados.

Erro 6 · Confundir linearizável com sequencial

Achar que são a mesma coisa, ou que sequencial garante que você vê o que acabou de acontecer no tempo real. Só linearizável respeita o relógio; sequencial garante uma ordem global consistente, mas que pode não refletir o "agora". Para interações onde o tempo real importa (ver o que outro acabou de fazer), você precisa de linearizável.

Erro 7 · Last-write-wins onde cada escrita importa

Usar LWW para reconciliar dados onde perder uma escrita é um bug — contadores, conjuntos, qualquer coisa que acumula. LWW descarta o perdedor silenciosamente, e relógios não-sincronizados pioram. Use merge semântico ou CRDTs onde cada escrita precisa sobreviver; LWW só onde perder uma é tolerável.

Verifique seu entendimento
Usuários do seu app reclamam: eles editam a foto de perfil, a página recarrega, e por alguns segundos ainda mostra a foto antiga — depois corrige sozinha. Um engenheiro propõe migrar todo o sistema para consistência forte (linearizável) para resolver. Qual é a abordagem mais alinhada com um bom design de consistência?

6.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Qual anomalia quebra a operação?

Para cada operação, identifique a anomalia que seria um bug e o nível mínimo de consistência que a proíbe: (a) ver o saldo da sua conta bancária; (b) ver o contador de visualizações de um vídeo; (c) ler as respostas a um comentário numa thread; (d) ver sua própria mensagem após enviá-la num chat.

(a) Saldo bancário → a anomalia: ver um saldo desatualizado e tomar uma decisão errada (sacar dinheiro que não tem). Nível mínimo: linearizável/forte. O saldo precisa refletir a última transação no tempo real; qualquer atraso pode causar dano financeiro. Não há atalho aqui — paga-se a consistência forte.

(b) Contador de visualizações → a anomalia: o contador "andar para trás" (ver 1000, recarregar, ver 990). Nível mínimo: eventual + monotonic reads. O número exato não importa (1000 ou 1002, tanto faz); o que incomoda é vê-lo diminuir. Monotonic reads proíbe ver valor mais velho que já viu — barato. Use CRDT de contador para não perder views concorrentes.

(c) Respostas numa thread → a anomalia: ver uma resposta antes da mensagem que ela responde (efeito antes da causa). Nível mínimo: causal. A ordem causal (pergunta antes da resposta) precisa ser preservada; a ordem entre mensagens independentes não importa. Consistência causal é exatamente isso, sem o custo de linearizável.

(d) Sua própria mensagem após enviar → a anomalia: enviar e não ver a mensagem que você mesmo mandou. Nível mínimo: read-your-writes. Você precisa ver suas escritas; os outros podem ver com um pequeno atraso. A garantia de sessão read-your-writes resolve a um custo mínimo.

O insight: a pergunta "qual anomalia quebra isto?" leva direto ao nível mínimo. Note a variedade — quatro operações, quatro níveis diferentes, do mais forte (saldo) ao mais fraco (contador). Escolher um único nível para todas seria errar: forte para tudo é caro e lento; eventual para o saldo é um bug financeiro.

Médio
Exercício 2 · Linearizável vs sequencial vs causal

Explique, com um exemplo concreto para cada, a diferença observável entre os três modelos: o que um usuário poderia ver sob consistência causal que não veria sob sequencial, e o que veria sob sequencial que não veria sob linearizável. Por que essas diferenças importam (ou não) na prática?

Sequencial mas não linearizável (a diferença é o tempo real): Você posta "cheguei!" às 14h00. Seu amigo, num lugar diferente, abre a página às 14h01 (depois, no relógio real). Sob linearizável, ele garantidamente vê "cheguei!" (a escrita terminou antes da leitura começar no tempo real). Sob sequencial, ele pode não ver — a ordem global é consistente para todos, mas pode posicionar a leitura dele "antes" da sua escrita, mesmo que no relógio tenha sido depois. Importa quando: há comunicação fora do sistema (você liga para o amigo e diz "olha o que postei") e o tempo real cria uma expectativa. Não importa quando ninguém compara com o relógio externo.

Causal mas não sequencial (a diferença é a ordem total): Ana posta "vou viajar" e, independentemente, Bruno posta "comprei um carro" — duas escritas sem relação causal. Sob sequencial, todos os usuários veem essas duas na mesma ordem (digamos, sempre Ana antes de Bruno, para todos). Sob causal, usuários diferentes podem ver em ordens diferentes (uns veem Ana primeiro, outros Bruno primeiro) — porque não há relação causal entre elas, a ordem é livre. Importa quando: raramente, para eventos independentes — ninguém percebe ou se incomoda que dois posts não relacionados apareçam em ordens diferentes para pessoas diferentes. É justamente por isso que causal é o ponto-doce: ela relaxa exatamente a garantia (ordem total de eventos independentes) que não importa, mantendo a que importa (ordem causal).

O insight: à medida que você desce de linearizável → sequencial → causal, vai abrindo mão de garantias progressivamente menos importantes na prática (primeiro o tempo real, depois a ordem total de eventos não relacionados), e ganhando disponibilidade e latência em troca. A causal é tão útil porque a garantia que ela mantém (causa antes do efeito) é a que os humanos realmente percebem, e as que ela abre mão (tempo real, ordem total de independentes) são as que raramente notamos.

Médio
Exercício 3 · Escolher a regra de reconciliação

Para cada tipo de dado replicado com consistência eventual, escolha a regra de reconciliação apropriada (last-write-wins, merge semântico, CRDT, ou resolução pelo cliente) e explique o que daria errado com a escolha ingênua: (a) um contador de "likes"; (b) a configuração de tema (claro/escuro) do usuário; (c) um documento de texto editado colaborativamente; (d) o conjunto de tags de um post.

(a) Contador de likes → CRDT (contador). O ingênuo LWW seria desastroso: se duas réplicas recebem um like ao mesmo tempo e cada uma vira "valor = velho+1", o LWW mantém só um — você perdeu um like. Um CRDT de contador (que rastreia incrementos, não o valor final) soma corretamente as duas operações concorrentes. Cada like precisa sobreviver; LWW perde escritas.

(b) Tema do usuário (claro/escuro) → last-write-wins. Aqui LWW é apropriado: é um valor único que o próprio usuário define, e se ele mudou em dois dispositivos quase ao mesmo tempo, a última escolha vencer é exatamente o comportamento esperado. Não há nada para "preservar" — o valor mais recente é o desejado. Este é o caso onde LWW é a escolha certa, não a ingênua.

(c) Documento de texto colaborativo → CRDT (sequência/texto) ou OT. O ingênuo LWW perderia o trabalho de um dos editores inteiro (a última gravação sobrescreve a outra). Edição colaborativa exige estruturas que mesclam edições concorrentes preservando ambas — CRDTs de sequência ou Operational Transformation (a tecnologia por trás de editores colaborativos). Cada edição precisa ser preservada e integrada.

(d) Conjunto de tags → CRDT (conjunto, ex.: OR-Set). Se uma réplica adiciona a tag "viagem" e outra adiciona "férias" concorrentemente, LWW manteria só uma adição — perdendo uma tag. Um CRDT de conjunto une as operações: o resultado tem ambas as tags. (Cuidado com adição vs remoção concorrentes — daí o OR-Set, que resolve esse conflito de forma definida.)

O insight: a regra de reconciliação depende inteiramente da semântica do dado. Para valores únicos que o usuário substitui (tema), LWW é certo. Para qualquer coisa que acumula (contadores, conjuntos, texto), LWW perde dados e você precisa de um CRDT ou merge que preserve as operações concorrentes. A pergunta-chave: "perder uma das escritas concorrentes é aceitável?". Se sim, LWW. Se não, precisa de algo que preserve ambas.

Difícil
Exercício 4 · O dial de consistência de um sistema — entrevista

Você projeta o backend de um app de colaboração estilo Google Docs / Notion (documentos editados por múltiplas pessoas, com comentários, permissões e histórico). O entrevistador pergunta: "que modelo de consistência você usa?". Demonstre que a resposta não é um modelo único, percorrendo as operações principais e atribuindo a cada uma o nível mínimo, justificando pela anomalia que cada uma não pode tolerar.

Enquadramento: a pergunta "que modelo de consistência você usa?" tem uma resposta errada óbvia ("forte" ou "eventual") e uma certa: "depende da operação — eu ajusto o dial por operação, ao nível mínimo que evita o bug daquela operação". Vou percorrer as operações de um app de colaboração.

1. Edição colaborativa do conteúdo do documento → o caso mais delicado. Múltiplas pessoas editam o mesmo texto ao mesmo tempo. A anomalia que quebra: perder a edição de alguém, ou ver o documento incoerente. A solução não é um modelo de consistência clássico, mas estruturas de convergência: CRDTs de sequência ou Operational Transformation, que mesclam edições concorrentes preservando todas e convergindo para o mesmo documento em todas as réplicas. Combinado com baixa latência (a edição precisa aparecer instantaneamente para quem digita — read-your-writes local) e propagação rápida para os colaboradores. Não é "forte" nem "eventual" — é convergência sem perda via CRDT/OT.

2. Ver suas próprias edições → read-your-writes. Quando você digita, você vê imediatamente (localmente). Garantia de sessão básica — o pânico de "minha edição sumiu" não pode acontecer para o autor.

3. Comentários e suas respostas → causal. Como toda thread: uma resposta não pode aparecer antes do comentário que ela responde. Consistência causal preserva a ordem de causa-efeito sem o custo de ordem total.

4. Permissões / controle de acesso → forte (linearizável). Este é o ponto crítico que exige o topo do espectro. Se alguém é removido do acesso a um documento, essa mudança precisa ser fortemente consistente — não pode haver uma janela em que a pessoa removida ainda lê o documento porque a revogação não propagou. Permissão é segurança; a anomalia (acesso após revogação) é uma falha de segurança. Paga-se a consistência forte aqui, conscientemente. (Mesmo na partição, prefira negar acesso na dúvida — fail-closed.)

5. Lista de documentos / histórico de versões → eventual + read-your-writes. Ver a lista de documentos alguns segundos atrasada é tolerável; só você precisa ver um documento que acabou de criar (read-your-writes). Eventual com a garantia de sessão resolve.

O quadro: conteúdo do doc → CRDT/OT (convergência sem perda); ver as próprias edições → read-your-writes; comentários → causal; permissões → forte; lista/histórico → eventual + sessão. Um sistema, cinco níveis.

O que um bom candidato demonstra: que rejeita a premissa de "um modelo só"; que reconhece o caso especial da edição colaborativa (CRDT/OT, não um modelo clássico); que identifica a operação crítica que exige forte (permissões = segurança, fail-closed); e que usa o nível mínimo em cada caso. Frase de fechamento: "não há 'um' modelo de consistência para este app — há um dial que eu ajusto por operação. A maioria roda em eventual com garantias de sessão, rápido e disponível; a edição colaborativa usa CRDTs para convergir sem perder ninguém; e o punhado crítico — permissões acima de tudo — paga consistência forte, porque ali a anomalia é uma falha de segurança, não um aborrecimento. Escolho sempre o nível mais fraco que ainda proíbe o bug daquela operação específica."

Fim do capítulo 6
Próximo capítulo: as leis da escala. CAP e consistência governam a correção sob distribuição; agora vamos às leis matemáticas que governam a performance sob escala — a lei de Amdahl (por que a parte serial limita o paralelismo), a lei de Little (a relação entre fila, vazão e latência), e a Universal Scalability Law (por que adicionar máquinas eventualmente para de ajudar, e depois atrapalha). As forças que determinam quão longe seu sistema cresce antes de bater num teto.
Parte II · Capítulo 7 · As leis

As leis: Amdahl,
Little, Universal
Scalability.

"Está lento? Adiciona mais máquinas." Esse reflexo está errado com mais frequência do que se imagina, e há leis matemáticas que explicam exatamente por quê. A lei de Amdahl mostra que a parte serial do seu trabalho impõe um teto ao paralelismo. A lei de Little relaciona, de forma inescapável, fila, vazão e tempo de espera. E a Universal Scalability Law revela o fato mais contraintuitivo de todos: a partir de certo ponto, adicionar máquinas não só para de ajudar — começa a piorar a performance. Estas leis não são acadêmicas; são as forças físicas que determinam quão longe seu sistema cresce antes de bater num muro.

Os capítulos anteriores da Parte II trataram de correção sob distribuição (CAP, consistência); este trata de performance sob escala, e é governado por matemática implacável. Vamos ver a lei de Amdahl (por que a fração serial limita o ganho do paralelismo, com um teto que nenhuma quantidade de processadores ultrapassa), a lei de Little (a relação universal entre quantos itens estão no sistema, a taxa com que chegam e quanto tempo ficam — que explica por que filas explodem perto da saturação), e a Universal Scalability Law de Gunther (que estende Amdahl com os dois custos reais da escala: contenção e coerência, e prevê o ponto onde adicionar recursos passa a degradar). Terminamos transformando as três em ferramentas de diagnóstico. Quando você sair daqui, "vamos escalar horizontalmente" deixa de ser um reflexo e vira uma decisão que você sabe modelar — incluindo saber quando ela vai falhar.

7.1 A história — três tentativas de medir o limite

Contexto histórico

Em 1967, Gene Amdahl, um dos arquitetos dos mainframes da IBM, fez um argumento numa conferência que viraria lei. Os entusiastas do processamento paralelo prometiam que, com processadores suficientes, qualquer computação ficaria arbitrariamente rápida. Amdahl mostrou, com aritmética simples, que estavam errados: se uma fração do trabalho é inerentemente serial (não paralelizável), essa fração impõe um teto absoluto ao ganho, não importa quantos processadores você jogue no problema. A lei de Amdahl nasceu como um balde de água fria no otimismo paralelo.

A lei de Little é mais antiga e mais geral. Em 1954, e formalizada por John Little em 1961, ela emergiu da teoria de filas (a mesma de Erlang, do Cap 4) e estabeleceu uma relação tão simples e universal que parece quase mágica: o número médio de itens num sistema é igual à taxa de chegada multiplicada pelo tempo médio que cada item passa nele. Vale para qualquer sistema estável — uma fila de banco, requisições num servidor, carros numa estrada — independentemente da distribuição de chegadas. É uma das poucas verdades verdadeiramente universais da computação.

A peça que faltava veio de Neil Gunther, nos anos 1990-2000. Amdahl explicava por que o paralelismo tem teto, mas não explicava um fenômeno que os engenheiros observavam na prática e que os assustava: às vezes, adicionar mais máquinas diminuía a vazão — o sistema ficava mais lento com mais recursos. Gunther formalizou a Universal Scalability Law (USL), estendendo Amdahl com um segundo custo: além da fração serial (contenção), há o custo de manter os nós coerentes entre si, que cresce de forma quadrática e eventualmente domina, causando o "speedup retrógrado".

Em 2026, essas três leis são mais relevantes do que nunca, justamente porque a facilidade de "adicionar mais instâncias" na nuvem tornou o reflexo de escalar horizontalmente quase automático — e caro quando errado. Elas são o antídoto matemático para a ilusão de que escala é só uma questão de jogar mais hardware no problema. Amdahl diz que há um teto; Little diz por que as filas explodem; a USL diz que existe um ponto ótimo, depois do qual mais é pior. Juntas, elas transformam "vamos escalar" de um ato de fé num cálculo — e frequentemente revelam que o gargalo não se resolve com mais máquinas, mas com menos serialização e menos coordenação.

7.2 A lei de Amdahl — o teto do paralelismo

A lei de Amdahl responde: se eu paralelizar um trabalho em N processadores, quão mais rápido ele fica? A resposta depende de quanto do trabalho é serial (precisa rodar em sequência, não paralelizável) versus paralelo.

a lei de Amdahl
                    1
speedup(N) = ─────────────────
              σ + (1 − σ) / N

  σ  = fração SERIAL do trabalho (não paralelizável)
  N  = número de processadores

O TETO (quando N → infinito):
                    1
  speedup_máximo = ───
                    σ

Ou seja: se 5% do trabalho é serial (σ = 0,05), o speedup
MÁXIMO é 1/0,05 = 20×. Com INFINITOS processadores. Os
outros 95% paralelos não importam além disso — a fração
serial de 5% impõe o teto de 20×, e nada o ultrapassa.
Uma fração serial pequena impõe um teto surpreendentemente baixo
O choque da lei de Amdahl é quão pouca serialização é necessária para limitar drasticamente o ganho. Se apenas 5% do trabalho é serial, o speedup máximo é 20× — não importa se você usa 100, 1000 ou um milhão de processadores. Com 10% serial, o teto cai para 10×. A intuição falha aqui: parece que "só 5% serial" deveria permitir quase 100% do ganho paralelo, mas não — aquela fração serial vira um piso de tempo que nenhum paralelismo reduz, e ela domina o limite. A consequência de design é profunda: para escalar, atacar a fração serial rende muito mais que adicionar processadores. Reduzir o serial de 5% para 1% eleva o teto de 20× para 100×; adicionar a milésima máquina quando você já tem cem quase não muda nada. Antes de escalar horizontalmente, pergunte: qual é a parte serial do meu trabalho (o lock global, a etapa sequencial, o recurso único), e dá para reduzi-la? Frequentemente esse é o ganho real, e mais máquinas é o ganho ilusório.

7.3 A lei de Little — a relação inescapável

A lei de Little é talvez a relação mais útil e mais subutilizada da engenharia de sistemas. Ela diz, para qualquer sistema estável:

a lei de Little
  L = λ × W

  L = número médio de itens NO sistema (na fila + em serviço)
  λ = taxa de chegada (itens por segundo)
  W = tempo médio que cada item passa no sistema

Vale para QUALQUER sistema estável, sem suposição sobre a
distribuição de chegadas. É quase uma identidade contábil.

REARRANJANDO (a forma mais útil no design):
  W = L / λ        (tempo de espera = itens / vazão)

Exemplo: se há 100 requisições no sistema (L) e ele processa
50 por segundo (λ), então cada requisição passa, em média,
W = 100/50 = 2 segundos no sistema. Inescapável.
Little conecta as três coisas que você mede — e não dá para enganar
O poder da lei de Little é que ela amarra as três grandezas que importam num sistema sob carga — quantos itens estão dentro (L), a que taxa chegam/saem (λ), e quanto tempo cada um demora (W) — numa relação que sempre vale. Você não pode melhorar uma sem afetar as outras de forma previsível. Se a taxa de chegada (λ) sobe e a vazão não acompanha, L cresce (a fila aumenta) e W explode (cada item espera mais). Se você quer reduzir o tempo de resposta W mantendo a vazão λ, precisa reduzir L — menos itens no sistema, ou seja, menos concorrência simultânea. A lei é uma ferramenta de diagnóstico imediata: meça duas das três grandezas e você sabe a terceira. "Por que a latência subiu?" → L cresceu (mais itens enfileirados) ou λ caiu (vazão despencou). É a contabilidade básica de qualquer sistema com fila, e nenhuma arquitetura escapa dela.

7.4 O joelho da saturação — por que filas explodem

A lei de Little, combinada com a teoria de filas, explica o fenômeno mais importante e mais mal-compreendido do comportamento sob carga: a latência não cresce linearmente com a utilização — ela explode perto da saturação. Um sistema a 70% de utilização e o mesmo sistema a 95% não estão "um pouco diferentes"; estão em mundos diferentes.

o joelho: latência vs utilização
Tempo de resposta cresce com a utilização (ρ) assim:

  W ∝ 1 / (1 − ρ)      (aproximação para fila simples)

ρ = 50%  → fator ~2     (latência dobra vs sistema vazio)
ρ = 80%  → fator ~5
ρ = 90%  → fator ~10
ρ = 95%  → fator ~20
ρ = 99%  → fator ~100   ← o "joelho": latência dispara
ρ = 100% → ∞            (fila infinita, sistema colapsa)

A latência não sobe em rampa — ela vira uma PAREDE perto
de 100%. Os últimos 10% de utilização custam mais latência
que os primeiros 80%.
Não planeje para 100% de utilização — planeje para a folga
A consequência prática do joelho é uma das mais importantes do design de capacidade: um sistema rodando perto de 100% de utilização não é "eficiente", é instável. Naquele ponto, qualquer pequeno aumento de carga, ou qualquer variação natural no tráfego, empurra a latência para o céu e a fila para o infinito. É por isso que sistemas saudáveis operam com folga deliberada — tipicamente 60-70% de utilização no pico planejado — não por desperdício, mas porque a margem é o que mantém a latência sob controle e absorve as flutuações. O engenheiro ingênuo vê 70% de utilização e pensa "estamos desperdiçando 30%, vamos apertar"; o experiente sabe que aqueles 30% são o amortecedor que impede o joelho. Isto também explica por que a estimativa de capacidade (Cap 3) dimensiona pelo pico com folga, não pela média justa: você precisa estar longe do joelho mesmo no pior momento, porque perto dele o sistema não degrada graciosamente — ele despenca. A utilização alta demais não é economia; é um incidente esperando uma flutuação para acontecer.

7.5 A Universal Scalability Law — os dois custos da escala

Amdahl captura um custo da escala (a fração serial). A USL de Gunther captura dois, e por isso descreve sistemas reais com muito mais precisão. A escalabilidade real é limitada por duas forças que crescem conforme você adiciona nós:

a Universal Scalability Law
                       N
  C(N) = ──────────────────────────────
          1 + α(N − 1) + β·N·(N − 1)

  C(N) = capacidade (vazão) com N nós/processadores
  α (alfa) = CONTENÇÃO — espera por recursos compartilhados
             (cresce LINEARMENTE com N) → é a fração serial
  β (beta) = COERÊNCIA — custo de manter os nós consistentes
             entre si (cresce QUADRATICAMENTE, com N²)

Quando β = 0, a USL VIRA a lei de Amdahl. O β é o que
Amdahl não tinha — e é o que causa o desastre da escala.

O termo da contenção (α) é essencialmente a fração serial de Amdahl: o tempo que os nós passam esperando por um recurso compartilhado (um lock, o banco, uma fila). Ele cresce linearmente e impõe um teto, como em Amdahl. Mas o termo da coerência (β) é novo e mais perigoso: é o custo de manter todos os nós concordando sobre o estado — a sincronização, a troca de mensagens para coordenar. Como coordenar N nós envolve, no pior caso, comunicação entre cada par de nós, esse custo cresce com N² — e é isso que muda tudo.

7.6 Quando mais máquinas pioram — o speedup retrógrado

Aqui está a revelação mais contraintuitiva do capítulo, e a razão de a USL existir: por causa do termo quadrático da coerência (β), a curva de capacidade não só satura — ela vira para baixo. Existe um número ótimo de nós, e adicionar nós além dele reduz a vazão. Mais máquinas deixam o sistema mais lento.

O ponto onde adicionar nós passa a destruir vazão
A intuição: enquanto você adiciona nós, ganha capacidade de processamento (bom), mas também adiciona custo de coordenação (cada novo nó precisa se sincronizar com todos os outros). No começo, o ganho domina e a vazão sobe. Mas o custo de coordenação cresce quadraticamente (N²), enquanto o ganho cresce linearmente (N) — então, em algum ponto, o custo de coordenação ultrapassa o ganho, e cada nó adicional gasta mais tempo coordenando do que trabalhando. A partir daí, adicionar máquinas diminui a vazão total: é o speedup retrógrado. O sistema com 50 nós pode ser mais lento que o mesmo sistema com 30. Isso não é teoria — é observado em sistemas reais o tempo todo, e é a razão de tantas migrações para "mais instâncias" não entregarem o ganho esperado, ou até regredirem. A lição brutal: existe um teto, e depois dele um precipício. Escalar horizontalmente sem reduzir a coerência (β) não só tem retornos decrescentes — tem retornos negativos depois de um ponto. A pergunta nunca é só "quantas máquinas adiciono?", é "quanto de coordenação cada máquina nova impõe, e quando esse custo vai me ultrapassar?".

7.7 Os três Cs — o vocabulário da escalabilidade

A USL organiza a escalabilidade em torno de três forças, os "três Cs", que dão um vocabulário preciso para diagnosticar por que um sistema não escala:

O CO que éCresceComo reduzir
ConcorrênciaO trabalho paralelizável — o que permite escalarLinear (ideal)Maximizar: mais trabalho independente
Contenção (α)Espera por recursos compartilhados (locks, banco)Linear, impõe tetoReduzir serialização, eliminar gargalos compartilhados
Coerência (β)Custo de manter os nós consistentes entre siQuadrático (N²), causa retrocessoReduzir coordenação, particionar para isolar
Diagnostique pelos Cs: contenção impõe teto, coerência causa precipício
Os três Cs viram um diagnóstico prático quando seu sistema não escala como esperado. Se a vazão satura (para de crescer, mas não cai) ao adicionar nós, o vilão provável é a contenção (α) — há um recurso compartilhado (um lock, o banco primário, uma seção crítica) que serializa o trabalho e impõe o teto de Amdahl. A solução: encontrar e reduzir essa serialização. Mas se a vazão regride (cai ao adicionar nós), o vilão é a coerência (β) — os nós estão gastando tempo demais se sincronizando, e o custo quadrático tomou conta. A solução aqui é diferente e mais profunda: reduzir a necessidade de coordenação entre os nós, frequentemente particionando o sistema para que grupos de nós trabalhem isolados, sem precisar concordar uns com os outros (Cap 13). Esta distinção é a do diagnóstico: teto = contenção = reduza serialização; precipício = coerência = reduza coordenação. Adicionar máquinas não resolve nenhum dos dois — só reduzir o C culpado resolve.

7.8 As leis como ferramenta de design

Juntando as três, elas formam um conjunto de perguntas que mudam como você aborda a escala:

  • Antes de escalar horizontalmente (Amdahl): qual é a fração serial do meu trabalho? Reduzi-la pode render mais que adicionar máquinas. O lock global, a etapa sequencial, o recurso único — esses limitam o ganho independentemente de quantos nós você tem.
  • Ao dimensionar capacidade (Little + joelho): em que utilização meu sistema opera no pico? Se está perto de 100%, a latência vai explodir na primeira flutuação. Planeje folga (60-70%), não eficiência justa. E use L = λ×W para diagnosticar latência ("subiu porque a fila cresceu ou a vazão caiu?").
  • Ao escalar para muitos nós (USL): quanto de coordenação (coerência) cada nó novo impõe? Se for alta (β grande), existe um ponto ótimo, e além dele mais máquinas pioram. Reduza a coordenação (particione, isole) antes de adicionar nós.
  • O diagnóstico geral: satura → contenção (reduza serialização); regride → coerência (reduza coordenação). Em ambos os casos, mais máquinas não é a resposta — reduzir o C culpado é.
As leis explicam por que "adicionar máquinas" decepciona tão frequentemente
Há uma razão pela qual tantas histórias de escala terminam em decepção: "dobramos as instâncias e a vazão só subiu 20%", ou "adicionamos nós e ficou mais lento". As três leis explicam cada uma dessas decepções. A primeira é Amdahl: a fração serial impôs um teto, e você estava perto dele — os novos nós trabalharam, mas o serial os esperou. A segunda é a USL com coerência alta: você passou do ponto ótimo, e o custo de coordenação dos novos nós superou o ganho. Em nenhum dos casos o problema era "poucas máquinas" — era serialização (contenção) ou coordenação (coerência) demais, e adicionar máquinas só amplifica esses custos. Por isso o engenheiro maduro, diante de "está lento", não pergunta primeiro "quantas máquinas adiciono?"; pergunta "o que está serializando ou coordenando, e dá para reduzir?". Quase sempre, o ganho real está em reduzir os Cs, não em multiplicar os nós — e as leis são o que torna isso visível antes de você gastar com hardware que vai decepcionar.

7.9 Estudo de caso — a migração que ficou mais lenta

"Adicionamos nós e a vazão caiu"
O cenário

Um serviço processa pedidos e está no limite. A equipe escala horizontalmente: de 10 para 20 para 40 instâncias. Os resultados confundem todos: de 10 para 20, a vazão subiu pouco (não dobrou). De 20 para 40, a vazão caiu. Mais máquinas, menos trabalho feito. Em vez de adicionar ainda mais (o reflexo), a equipe aplica as leis para diagnosticar.

Passo 1 · O sintoma aponta o C culpado
lendo a curva pela USL
Vazão medida:
  10 nós → 9.000 pedidos/s
  20 nós → 11.000 pedidos/s   (subiu pouco: contenção?)
  40 nós → 8.000 pedidos/s    (CAIU: speedup retrógrado!)

Diagnóstico pela USL: a vazão REGREDIU ao dobrar de 20→40.
Isso não é teto de contenção (que só SATURARIA) — é o termo
de COERÊNCIA (β) dominando. Os nós estão gastando mais tempo
se coordenando do que processando. Há coordenação quadrática
escondida em algum lugar.
Passo 2 · Caçar a coordenação escondida
onde estava o β
Investigação: cada instância, ao processar um pedido,
atualizava um contador GLOBAL de estoque compartilhado,
com um lock distribuído para coordenar. Com 10 nós, a
disputa pelo lock era tolerável. Com 40 nós, cada nó
passava a maior parte do tempo DISPUTANDO o lock com os
outros 39 — coordenação par-a-par, custo ~N².

O β não estava no "processamento" (que é paralelo) — estava
no ponto de coordenação compartilhado que TODOS tocavam.
Passo 3 · Reduzir a coerência, não adicionar nós

A solução não foi mais máquinas (que pioraria) nem menos (que limitaria) — foi eliminar a coordenação quadrática. Em vez de um contador global com lock disputado por todos, o estoque foi particionado: cada instância recebeu uma "reserva" local de estoque para vender, sincronizando com o global só esporadicamente (quando a reserva acabava), não a cada pedido. A coordenação par-a-par a cada operação virou coordenação rara e isolada. O β despencou.

Passo 4 · O resultado com a coerência reduzida
a curva depois de matar o β
Depois do particionamento do estoque:
  10 nós → 9.500 pedidos/s
  20 nós → 18.000 pedidos/s   (quase dobrou: linear!)
  40 nós → 34.000 pedidos/s   (escala de novo!)

Com o β reduzido, a curva voltou a subir com os nós — o
speedup retrógrado desapareceu. O sistema agora escala
horizontalmente DE VERDADE, porque os nós não disputam
mais um ponto de coordenação a cada pedido.

Resultado: a migração que "ficou mais lenta" não tinha máquinas de menos — tinha coordenação de mais. A USL diagnosticou o que o reflexo ("adiciona mais nós") teria piorado: o termo de coerência quadrático, escondido num contador de estoque global que todos os nós disputavam. A correção não foi hardware, foi arquitetura — particionar o estoque para que os nós trabalhassem isolados, transformando coordenação constante em coordenação rara. E só então a escala horizontal passou a funcionar como prometido. Esta é a lição das leis: o gargalo de escala quase nunca é "poucas máquinas"; é serialização (contenção, que satura) ou coordenação (coerência, que regride), e a única saída é reduzir o C culpado — adicionar nós a um sistema com β alto é acelerar em direção ao precipício. Próximo capítulo: as falácias da computação distribuída — os oito mitos que todo iniciante acredita sobre a rede (que é confiável, que a latência é zero, que a banda é infinita) e que viram os bugs mais difíceis de todos quando a realidade os desmente.

7.10 Erros comuns

Erro 1 · Achar que paralelismo não tem teto

Acreditar que, com processadores suficientes, qualquer trabalho fica arbitrariamente rápido. A lei de Amdahl prova o contrário: a fração serial impõe um teto (1/σ) que nenhum número de nós ultrapassa. Reduzir o serial rende mais que adicionar máquinas — atacar 5% de serialização pode dobrar o teto.

Erro 2 · Planejar para utilização perto de 100%

Tratar 70% de utilização como "desperdício" e apertar o sistema para perto da capacidade máxima. O joelho da fila faz a latência explodir perto de 100% — os últimos 10% custam mais que os primeiros 80%. Folga (60-70% no pico) não é desperdício; é o amortecedor que impede o colapso na primeira flutuação.

Erro 3 · Adicionar máquinas como reflexo

Responder "está lento" com "adiciona instâncias", sem diagnosticar a causa. Se o gargalo é contenção (serialização), mais nós saturam; se é coerência (coordenação), mais nós pioram (speedup retrógrado). Adicionar máquinas amplifica os dois custos. Diagnostique o C culpado primeiro.

Erro 4 · Ignorar o speedup retrógrado

Assumir que mais nós, na pior das hipóteses, não ajudam — quando na verdade podem piorar. O termo de coerência (β, quadrático) faz a vazão regredir depois de um ponto ótimo. Existe um número de nós além do qual o sistema fica mais lento. Modele isso antes de escalar para muitos nós.

Erro 5 · Confundir saturação com regressão

Tratar "a vazão parou de crescer" e "a vazão caiu" como o mesmo problema. Saturação (platô) é contenção — reduza serialização. Regressão (queda) é coerência — reduza coordenação. São diagnósticos e soluções diferentes; confundi-los leva à correção errada.

Erro 6 · Esquecer a lei de Little ao diagnosticar latência

Investigar latência alta sem usar L = λ×W. A lei aponta direto: a latência (W) subiu porque a fila cresceu (L) ou a vazão caiu (λ). Ela conecta as três grandezas de forma inescapável — meça duas, descubra a terceira. Ignorá-la é diagnosticar no escuro.

Erro 7 · Criar pontos de coordenação global sem perceber

Introduzir um contador global, um lock distribuído, uma sequência compartilhada que todos os nós tocam — e criar um termo de coerência quadrático que destrói a escala. Cada ponto que todos os nós precisam coordenar é um β escondido. Particione para isolar; o que os nós não compartilham, não os faz regredir.

Verifique seu entendimento
Sua equipe dobra o número de instâncias de um serviço de 20 para 40, esperando dobrar a vazão. Em vez disso, a vazão cai. Qual é o diagnóstico mais provável e a direção da solução?

7.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Aplicar a lei de Amdahl

Um trabalho tem 10% de fração serial (σ = 0,10). Calcule: (a) o speedup com 10 processadores; (b) o speedup com 100 processadores; (c) o speedup máximo teórico (infinitos processadores); (d) que conclusão de design isso sugere sobre onde investir esforço.

Fórmula: speedup(N) = 1 / (σ + (1−σ)/N), com σ = 0,10.

(a) 10 processadores: 1 / (0,10 + 0,90/10) = 1 / (0,10 + 0,09) = 1 / 0,19 ≈ 5,3×. Note: 10 processadores, mas só 5,3× de ganho.

(b) 100 processadores: 1 / (0,10 + 0,90/100) = 1 / (0,10 + 0,009) = 1 / 0,109 ≈ 9,2×. Dez vezes mais processadores que (a), mas menos que o dobro do ganho.

(c) Máximo (N→∞): 1/σ = 1/0,10 = 10×. Esse é o teto absoluto. Nenhuma quantidade de processadores ultrapassa 10×.

(d) Conclusão de design: com 100 processadores você já está em 9,2×, quase no teto de 10× — adicionar mais processadores é quase inútil daqui em diante (de 100 para 1000 processadores, o ganho vai de 9,2× para ~9,9×, um ganho irrisório por 10× o hardware). O esforço deve ir para reduzir a fração serial: se você cortasse σ de 10% para 5%, o teto subiria de 10× para 20× — dobrar o teto reduzindo a serialização rende infinitamente mais que multiplicar processadores. A lição de Amdahl: depois de certo ponto, atacar o serial é a única alavanca real; mais hardware bate na parede do teto.

Médio
Exercício 2 · Diagnosticar com a lei de Little

Um serviço de API recebe 500 requisições por segundo (λ) e você observa que, em média, há 250 requisições "em voo" no sistema a qualquer momento (L). (a) Qual o tempo médio de resposta (W)? (b) Se o tráfego dobra para 1000 req/s mas a capacidade de processamento não muda, o que acontece com L e W? (c) Como a lei de Little explica o "joelho" da latência?

(a) Tempo de resposta W: da lei de Little, L = λ×W, então W = L/λ = 250/500 = 0,5 segundos. Cada requisição passa, em média, meio segundo no sistema.

(b) Tráfego dobra para 1000 req/s, capacidade igual: aqui a lei de Little revela o perigo. Se o sistema já estava processando perto da capacidade a 500 req/s, ele não consegue processar 1000 req/s — as requisições chegam mais rápido do que saem. O resultado: L (itens no sistema) cresce continuamente (a fila se acumula), e como W = L/λ, o tempo de resposta W também cresce. Se o sistema está saturado, L cresce sem limite e W explode — o sistema fica instável. A lei de Little não "conserta" isso; ela descreve a explosão: vazão de saída < taxa de chegada ⟹ fila infinita ⟹ latência infinita.

(c) Como Little explica o joelho: conforme a utilização se aproxima de 100%, o sistema processa quase tão rápido quanto recebe — mas qualquer flutuação faz a fila (L) saltar, porque não há folga para absorvê-la. E como W = L/λ, um salto em L é um salto em W. Perto da saturação, pequenos aumentos de carga causam grandes aumentos de L (a fila), e portanto grandes aumentos de W (latência) — é o joelho. A lei de Little conecta matematicamente "fila crescendo" (L) com "latência explodindo" (W): eles são proporcionais, e perto de 100% a fila cresce descontroladamente. É por isso que se opera com folga: longe da saturação, L fica pequeno e estável, e W também.

Médio
Exercício 3 · Contenção ou coerência?

Para cada sintoma de escala, diga se o vilão provável é contenção (α) ou coerência (β), e proponha a direção da solução: (a) ao adicionar nós, a vazão sobe cada vez menos e estabiliza num platô; (b) ao adicionar nós, a vazão sobe até um pico e depois começa a cair; (c) um serviço stateless de processamento de imagens escala quase linearmente; (d) um serviço que atualiza um ranking global compartilhado regride ao adicionar nós.

(a) Vazão estabiliza num platô → contenção (α). A saturação (parar de crescer sem cair) é a assinatura da contenção: há um recurso compartilhado serializando o trabalho (o banco primário, um lock, uma seção crítica) que impõe o teto de Amdahl. Solução: encontrar e reduzir essa serialização — otimizar o recurso compartilhado, replicá-lo, ou eliminar a dependência dele.

(b) Vazão sobe, atinge pico, depois cai → coerência (β). A regressão (cair depois de um pico) é a assinatura da coerência: o custo quadrático de coordenação entre os nós ultrapassou o ganho. Solução: reduzir a coordenação — particionar para isolar grupos de nós, eliminar o ponto de sincronização global que todos disputam.

(c) Stateless de imagens escala linearmente → nenhum dos dois (o caso ideal). Processamento de imagens stateless é "embaraçosamente paralelo": cada imagem é independente, não há recurso compartilhado disputado (contenção baixa) nem necessidade de os nós concordarem entre si (coerência baixa). Por isso escala quase linearmente. Este é o alvo: trabalho independente, sem contenção nem coerência. É o que o particionamento tenta aproximar.

(d) Ranking global compartilhado regride → coerência (β). Atualizar um ranking global exige que todos os nós coordenem sobre o estado compartilhado — coordenação par-a-par, custo N². Por isso regride. Solução: a mesma do estudo de caso — particionar (rankings parciais por nó, agregados esporadicamente) ou tornar a atualização assíncrona e aproximada, em vez de coordenada a cada operação.

O insight: a forma da curva é o diagnóstico. Platô = contenção = recurso compartilhado serializa = reduza serialização. Queda = coerência = nós coordenam demais = reduza coordenação (particione). E o caso ideal (c) mostra o alvo: trabalho independente, sem pontos compartilhados — é por isso que sistemas stateless e particionados escalam, e sistemas com estado global compartilhado não.

Difícil
Exercício 4 · O plano de escala guiado pelas leis — entrevista

Um serviço crítico está atingindo seu limite de capacidade, e a liderança quer "escalar para 10× a capacidade atual". O entrevistador pergunta: "como você aborda isso, e como garante que escalar vai realmente funcionar?". Estruture a resposta usando as três leis para guiar o diagnóstico e o plano, demonstrando que você não trata escala como "adicionar máquinas".

Enquadramento: "escalar para 10×" não é "adicionar 10× máquinas" — as leis mostram que isso pode entregar muito menos que 10× (Amdahl), ou até regredir (USL). A abordagem é diagnosticar o que limita a escala antes de adicionar recursos, porque adicionar máquinas a um gargalo de contenção ou coerência não dá 10×.

1. Medir a curva de escalabilidade atual (a base de tudo): antes de qualquer decisão, medir a vazão real em diferentes números de nós (5, 10, 20, 40) sob carga estável. A forma da curva diz o que está acontecendo: ela é linear (ótimo), satura num platô (contenção), ou regride (coerência)? Sem essa medição, qualquer plano é chute. (Esta é a aplicação prática da USL — ajustar α e β a dados reais.)

2. Diagnosticar pela lei de Amdahl (a fração serial): qual parte do trabalho é serial e não paraleliza? O lock global, a etapa sequencial obrigatória, o recurso único. Se a fração serial é, digamos, 10%, o teto é 10× — e atingir 10× exigiria infinitos nós (impossível). Conclusão: para chegar a 10×, provavelmente preciso reduzir a fração serial, não só adicionar nós. Identificar e atacar o serial é a primeira alavanca.

3. Diagnosticar pela USL (a coerência): há pontos de coordenação global que todos os nós disputam (contadores compartilhados, locks distribuídos, sequências únicas)? Se sim, eles criam o termo quadrático (β) que causa regressão — e escalar 10× os nós com β alto pode reduzir a vazão. Conclusão: preciso eliminar ou particionar esses pontos de coordenação antes de escalar, ou a escala vai falhar no precipício.

4. Dimensionar com a lei de Little e o joelho: a 10× a carga, qual a utilização-alvo? Planejar para 60-70% no pico, não 100% — porque perto da saturação a latência explode (joelho). Usar L = λ×W para dimensionar: a 10× λ, quanto L (concorrência) o sistema sustenta dentro do orçamento de latência W? Isso dá o número real de nós necessário, com folga.

5. O plano resultante (em ordem): primeiro reduzir a fração serial (Amdahl) e eliminar/particionar os pontos de coordenação (USL) — isso muda a forma da curva para perto do linear. Depois, adicionar nós, agora que cada nó adicional realmente entrega vazão. E dimensionar com folga (Little/joelho). Adicionar máquinas é o último passo, não o primeiro.

O que um bom candidato demonstra: que mede a curva antes de agir; que sabe que a fração serial (Amdahl) e a coordenação (USL) limitam a escala independentemente de hardware; que dimensiona com folga (joelho); e que coloca "adicionar máquinas" por último, depois de reduzir os Cs. Frase de fechamento: "escalar 10× não é comprar 10× máquinas — se eu fizer isso sobre um sistema com fração serial alta ou coordenação global, posso ganhar 3× ou até regredir. Primeiro eu meço a curva e diagnostico: o que serializa (contenção) e o que coordena (coerência)? Reduzo esses dois para endireitar a curva, e só então adiciono nós, com folga para ficar longe do joelho. As leis me dizem que o ganho real está em reduzir os Cs; o hardware é o último passo, não o primeiro."

Fim do capítulo 7
Próximo capítulo: as falácias da computação distribuída. As leis deste capítulo governam a performance; as falácias governam a correção sob a realidade brutal da rede. Os oito mitos que todo iniciante acredita — que a rede é confiável, que a latência é zero, que a banda é infinita, que a topologia não muda — e que, quando a realidade os desmente, produzem os bugs mais difíceis e mais caros dos sistemas distribuídos. Conhecê-los é a diferença entre projetar para a rede que você deseja e para a rede que existe.
Parte II · Capítulo 8 · As leis

As falácias da
computação
distribuída.

Há mais de trinta anos, engenheiros da Sun Microsystems perceberam que quase todo programador, ao construir seu primeiro sistema distribuído, comete o mesmo conjunto de suposições erradas sobre a rede — que ela é confiável, que a latência é zero, que a banda é infinita. Elas parecem inofensivas, e por isso são perigosas: o sistema funciona perfeitamente nos testes locais e depois falha de formas misteriosas em produção, porque a rede real desmente cada uma. As oito falácias são o catálogo desses enganos, e conhecê-las é a diferença entre projetar para a rede que você deseja e para a rede que existe.

Este capítulo fecha a Parte II com o complemento prático das leis: se Amdahl e a USL governam a performance sob escala, as falácias governam a correção sob a realidade brutal da rede. Vamos ver a raiz comum das oito (tratar uma chamada remota como se fosse local), percorrer cada falácia com o bug concreto que ela causa quando a realidade a desmente, e — o mais importante — derivar de cada uma uma prática defensiva. Você vai reconhecer que muitos dos conceitos das partes anteriores (timeouts, retries, idempotência, o CAP) existem precisamente porque essas falácias são falsas. Quando você sair daqui, terá uma checklist mental que passa sobre todo design distribuído: "estou assumindo, sem perceber, que a rede é confiável? que a latência é zero?" — e cada "sim" é um bug futuro identificado antes de nascer.

8.1 A história — os enganos que todos repetem

Contexto histórico

No início dos anos 1990, na Sun Microsystems — uma empresa no centro da revolução das redes e do Java —, os engenheiros notaram um padrão. Todo programador que construía seu primeiro sistema distribuído cometia os mesmos erros, partindo das mesmas suposições otimistas sobre a rede. As primeiras quatro suposições foram catalogadas por volta de 1991 por Bill Joy e Dave Lyon, e James Gosling (futuro criador do Java) as classificou como "as falácias da computação em rede".

Em 1994, Peter Deutsch, outro fellow da Sun, expandiu a lista para sete, adicionando as falácias sobre topologia, administração e custo de transporte. E em 1997, Gosling acrescentou a oitava — "a rede é homogênea". Juntas, ficaram conhecidas como as oito falácias da computação distribuída: um catálogo das suposições falsas que quase todo iniciante faz, e que se provam erradas no longo prazo, gerando os bugs mais difíceis e caros.

O insight por trás da lista era psicológico, não só técnico. As falácias não são erros de ignorância — são erros de modelo mental. O programador aprende a programar com chamadas de função locais, que são confiáveis, instantâneas, gratuitas e seguras. Quando passa a fazer chamadas remotas, carrega inconscientemente esse modelo mental local para um mundo onde ele não vale. As falácias são o que acontece quando você trata a rede como se fosse memória local.

Em 2026, três décadas depois, as falácias permanecem assustadoramente atuais — e algumas pessoas argumentam que a nuvem as tornou ainda mais traiçoeiras. As ferramentas modernas (frameworks de RPC, service meshes, abstrações de microsserviços) tornaram tão fácil fazer uma chamada remota parecer uma chamada local que esconderam a rede ainda mais fundo, fazendo as falácias mais fáceis de cometer. Um framework que faz servico.metodo() parecer idêntico a uma chamada local é conveniente — e é exatamente a armadilha que faz o programador esquecer que ali há uma rede que pode falhar, atrasar, e cobrar. As falácias não envelheceram; elas se esconderam melhor. Conhecê-las é reaprender a ver a rede que as abstrações escondem.

8.2 A raiz comum: tratar o remoto como local

Antes de percorrer as oito, vale ver o que as une. Todas as falácias brotam de uma única raiz: tratar uma chamada remota como se fosse uma chamada local. Uma chamada de função local é confiável (não "falha em chegar"), instantânea (latência desprezível), gratuita (sem custo de transporte), segura (mesma máquina), e o ambiente é homogêneo e controlado. Uma chamada de rede não é nada disso — mas o programador, treinado em chamadas locais, assume que é.

A abstração que mente é mais perigosa que a ausência de abstração
O perigo redobra quando uma abstração faz a chamada remota parecer local. Frameworks de RPC e service meshes modernos permitem escrever usuario = servicoUsuarios.buscar(id) — sintaticamente idêntico a uma chamada local, mas por baixo é uma viagem pela rede que pode falhar, demorar segundos, ou cair no meio. A conveniência é real, mas o custo é que o programador esquece que há uma rede ali. Ele não coloca timeout (porque chamadas locais não precisam), não trata falha (porque chamadas locais não falham assim), faz a chamada num loop (porque chamadas locais são grátis). A abstração que esconde a rede é uma faca de dois gumes: ela reduz o código que você escreve e aumenta os bugs que você não vê. Por isso o bom engenheiro distribuído mantém, conscientemente, um modelo mental onde toda chamada remota é visivelmente diferente de uma local — porque ela é, e fingir o contrário é convidar as oito falácias para o seu sistema.

8.3 Falácia 1 — "A rede é confiável"

A primeira e mais fundamental. A rede não é confiável: pacotes se perdem, conexões caem no meio, switches reiniciam, cabos se rompem, e — o pior — às vezes a mensagem chega mas a confirmação se perde, deixando você sem saber se a operação aconteceu.

A incerteza é pior que a falha: você não sabe se aconteceu
O aspecto mais traiçoeiro da rede não-confiável não é a falha limpa (a requisição não chegou — você reenviar). É a incerteza: você envia uma requisição, e não recebe resposta. O que aconteceu? Três possibilidades indistinguíveis: (a) a requisição não chegou — seguro reenviar; (b) a requisição chegou e foi processada, mas a resposta se perdeu no caminho de volta — reenviar vai processar duas vezes; (c) ainda está em trânsito, lenta — reenviar é prematuro. Do seu lado, esses três casos são idênticos: silêncio. É por isso que sistemas distribuídos são difíceis: a falha parcial cria estados ambíguos que não existem numa máquina só. E é por isso que a idempotência (Cap 16) é tão central — se reenviar é seguro mesmo que a primeira tenha funcionado, a incerteza deixa de importar. A prática defensiva que esta falácia exige: retries com idempotência. Toda operação remota importante precisa poder ser reenviada sem dano, porque você vai precisar reenviar sem saber se a primeira funcionou.

8.4 Falácia 2 — "Latência é zero"

Uma chamada local leva nanossegundos; uma chamada de rede leva de milissegundos (mesma região) a centenas de milissegundos (entre continentes — a velocidade da luz é uma lei, Cap 3). Assumir latência zero leva ao pecado das chamadas conversadoras (chatty): código que faz dezenas ou centenas de pequenas chamadas remotas onde uma só, maior, bastaria.

O código "conversador" que funciona local e morre remoto
O bug clássico desta falácia: um código que busca uma lista de 100 itens e, para cada item, faz uma chamada remota separada para buscar um detalhe — 100 chamadas de rede. Localmente (ou com a abstração escondendo a rede), funciona instantaneamente nos testes. Em produção, com 50ms de latência por chamada, são 5 segundos só de espera de rede — o sistema "trava" sem nenhum bug de lógica. É o problema N+1, e ele é uma manifestação direta de "latência é zero". A prática defensiva: projete para reduzir o número de viagens, não o tamanho de cada uma — agrupe as 100 chamadas em uma (busca em lote), traga mais dados de uma vez, evite o ping-pong. Em sistemas distribuídos, o número de idas e voltas (round-trips) frequentemente domina a latência muito mais que a quantidade de dados ou o trabalho de CPU. Otimizar round-trips é frequentemente a maior alavanca de performance, e ela só aparece quando você para de assumir que latência é zero.

8.5 Falácia 3 — "Banda é infinita"

A rede tem capacidade finita, e quando você tenta passar mais dados do que ela comporta, surgem congestionamento, enfileiramento e latência crescente (o joelho do Cap 7, agora na rede). Assumir banda infinita leva a transferir dados demais — payloads gordos, sincronizações completas onde deltas bastariam, mídia não comprimida.

Latência e banda são problemas diferentes — não confunda
Uma distinção que esta falácia obriga a fazer: latência (Falácia 2) e banda (Falácia 3) são limites independentes, e a solução de um não resolve o outro. Latência é o tempo de uma viagem (limitada pela distância/velocidade da luz); banda é quantos dados passam por segundo (limitada pela capacidade do canal). Um vídeo de 4K tem problema de banda (muitos dados), não de latência. Cem chamadas pequenas têm problema de latência (muitas viagens), não de banda. Confundi-los leva à correção errada: comprimir o payload (solução de banda) não ajuda um sistema conversador (problema de latência), e agrupar chamadas (solução de latência) não ajuda a transferir um arquivo gigante (problema de banda). A prática defensiva combina as duas, conforme o gargalo: para banda, comprima, mande deltas em vez de tudo, sirva mídia de CDN (Cap 13 do vol. anterior); para latência, reduza round-trips. Diagnostique qual dos dois é o seu gargalo antes de escolher a cura.

8.6 Falácia 4 — "A rede é segura"

A rede não é segura: o tráfego pode ser interceptado, modificado, forjado. Assumir segurança leva a mandar dados sensíveis sem criptografia, a confiar cegamente no que vem de outro serviço, a não autenticar as chamadas internas. É a falácia que o volume anterior combateu inteiro (TLS, secrets, validação de entrada).

"É tráfego interno" não é garantia de segurança
A forma moderna desta falácia é a suposição de que o tráfego dentro do seu data center ou da sua rede privada é seguro e dispensa proteção — "é só comunicação entre nossos serviços". Essa premissa, o "perímetro confiável", é cada vez mais reconhecida como perigosa: se um atacante penetra o perímetro (e perímetros são penetrados), ele se move livremente entre serviços que confiam uns nos outros sem verificação. A resposta moderna é o zero trust: nenhuma chamada é confiável por padrão, nem as internas — toda comunicação é criptografada (TLS mesmo entre serviços internos) e autenticada (cada serviço prova quem é). A prática defensiva que esta falácia exige: trate toda chamada de rede, interna ou externa, como potencialmente interceptável e forjável — criptografe em trânsito sempre, autentique sempre, valide toda entrada sempre, mesmo a que vem de "dentro". A rede nunca é segura, e "interna" não é uma exceção.

8.7 Falácias 5 e 6 — "Topologia não muda" e "Há um administrador"

Estas duas tratam da natureza dinâmica e descentralizada dos sistemas reais, que o iniciante assume estáticos e centralizados.

Falácia 5 · "Topologia não muda"

A suposição: os servidores e suas localizações são fixos. A realidade: na nuvem, instâncias nascem e morrem o tempo todo (autoscaling), endereços mudam, serviços se movem. Hard-codar endereços IP, assumir que um servidor específico sempre existe — tudo quebra quando a topologia muda. A defesa: service discovery dinâmico, nunca endereços fixos.

Falácia 6 · "Há um administrador"

A suposição: uma pessoa/equipe controla e conhece todo o sistema. A realidade: sistemas reais atravessam times, organizações, provedores de nuvem, serviços de terceiros — ninguém controla tudo. Você depende de sistemas que outros administram e mudam sem te avisar. A defesa: design resiliente a mudanças que você não controla, e contratos explícitos entre as partes.

A nuvem tornou estas duas mais verdadeiras, não menos
Curiosamente, as falácias 5 e 6 — sobre topologia mutável e administração descentralizada — ficaram mais relevantes com a nuvem, não menos. A elasticidade que torna a nuvem poderosa (instâncias que nascem e morrem conforme a carga) significa que a topologia muda constantemente, por design — assumir endereços fixos é quebrar na primeira vez que o autoscaler age. E a dependência de dezenas de serviços gerenciados de terceiros (o gateway de pagamento, o provedor de email, o banco gerenciado) significa que boa parte do seu sistema é administrada por gente que você não conhece e que muda as coisas sem te consultar. A prática defensiva moderna assume ambas como permanentes: descoberta dinâmica de serviços (nunca IPs fixos), tolerância a instâncias aparecendo e desaparecendo (statelessness, Cap 4), e resiliência às mudanças e falhas dos serviços de terceiros que você não controla (timeouts, circuit breakers, Cap 20). Você não administra o seu sistema inteiro — você administra a sua parte e se defende do resto.

8.8 Falácias 7 e 8 — "Custo de transporte é zero" e "A rede é homogênea"

As duas últimas, frequentemente esquecidas, mas com consequências reais de custo e compatibilidade.

  • Falácia 7 · "Custo de transporte é zero". Mover dados pela rede custa — em duas moedas. Custa dinheiro (o egresso do FinOps do volume anterior — transferir dados entre regiões ou para fora da nuvem é uma linha real e às vezes dominante da fatura). E custa recursos (serializar/desserializar dados, montar/desmontar pacotes consome CPU). Assumir transporte grátis leva a arquiteturas que cruzam a rede sem necessidade (chamadas entre regiões, dados que poderiam estar juntos espalhados) e a faturas de egresso surpreendentes. A defesa: minimizar o tráfego que cruza fronteiras caras, manter junto o que é acessado junto (localidade).
  • Falácia 8 · "A rede é homogênea". Os componentes do sistema não são todos iguais — diferentes linguagens, protocolos, versões, formatos, sistemas operacionais, capacidades. Assumir homogeneidade leva a quebras de interoperabilidade: um serviço que envia um formato que outro não entende, uma suposição de protocolo que não vale para todos. A defesa: padrões abertos e bem definidos para comunicação (formatos como JSON/Protobuf, contratos explícitos e versionados — Cap 1, a decisão irreversível do contrato), nunca assumir que o outro lado é igual a você.
A falácia 7 reaparece em cada fatura de nuvem
Das oito, a falácia 7 ("custo de transporte é zero") é a que mais silenciosamente machucou orçamentos na era da nuvem. O egresso — dados saindo de uma região, de uma zona, ou da nuvem — é cobrado, e frequentemente caro, de formas que não aparecem como uma linha óbvia até o fim do mês (foi um tema inteiro do FinOps no volume anterior). Arquiteturas que cruzam regiões alegremente, que replicam dados entre zonas sem pensar, ou que servem mídia do servidor de origem em vez de CDN, descobrem que o "custo zero" de mover dados é, na verdade, uma das maiores linhas da conta. A prática defensiva une design e FinOps: pense na localidade dos dados — mantenha junto o que é acessado junto, minimize o que cruza fronteiras de cobrança, e trate cada travessia de rede como tendo um preço (porque tem). A rede não é só não-confiável, lenta e limitada — ela também é cara, e ignorar isso é uma surpresa garantida na fatura.

8.9 Projetar para a rede real

As oito falácias, viradas do avesso, formam um conjunto de práticas defensivas — e você vai notar que quase todas já apareceram neste livro e no anterior, porque são a razão de existirem:

A falácia (falsa)A prática defensiva
A rede é confiávelRetries + idempotência; timeouts; circuit breakers (Cap 16, 20)
Latência é zeroReduzir round-trips; batching; evitar o N+1; async (Cap 4)
Banda é infinitaComprimir, mandar deltas, CDN; paginar (vol. anterior)
A rede é seguraTLS sempre, zero trust, autenticar e validar tudo (vol. anterior)
Topologia não mudaService discovery dinâmico; statelessness (Cap 4)
Há um administradorResiliência a terceiros; contratos explícitos
Custo de transporte é zeroLocalidade de dados; minimizar egresso (FinOps)
A rede é homogêneaPadrões abertos; contratos versionados (Cap 1)
As falácias são o "porquê" por trás de metade deste livro
Olhe a coluna da direita: timeouts, retries, idempotência, circuit breakers, batching, CDN, TLS, service discovery, contratos versionados. Cada uma dessas técnicas — espalhadas pelos dois volumes — existe como resposta a uma das oito falácias. Elas não são práticas arbitrárias que se acumulam; são as defesas que a rede real exige porque cada falácia é falsa. Isto dá uma forma poderosa de entender o design distribuído: ele é, em grande medida, o conjunto de respostas à pergunta "o que dá errado quando a rede não é o que eu gostaria que fosse?". Quando você encontra uma técnica nova de sistemas distribuídos, vale perguntar "contra qual falácia ela defende?" — quase sempre há uma, e entender a falácia que a motiva é entender por que a técnica existe e quando aplicá-la. As oito falácias não são uma curiosidade histórica; são o mapa das razões por trás de quase tudo que se faz em sistemas distribuídos.

8.10 Estudo de caso — o serviço que funcionava na demo

Cada falácia, um incidente em produção
O cenário

Uma equipe construiu um serviço de checkout que funcionava perfeitamente em desenvolvimento e na demo. Em produção, ele falhou de várias formas misteriosas nas primeiras semanas. Cada falha, investigada, revelou-se uma falácia da computação distribuída em ação — o sistema fora projetado para a rede ideal, e a rede real o desmentiu, uma falácia de cada vez.

Incidente 1 · "A rede é confiável" → cobrança dupla
a falácia 1 cobra seu preço
O checkout chamava o gateway de pagamento e, se não recebesse
resposta em tempo, reenviava. Mas a primeira chamada TINHA
funcionado — só a resposta se perdeu na volta (a incerteza
da falácia 1). O retry cobrou o cliente DE NOVO.

Correção: idempotência (Cap 16). Cada cobrança leva uma chave
única; o gateway reconhece o retry e não cobra duas vezes.
O retry agora é seguro apesar da rede não-confiável.
Incidente 2 · "Latência é zero" → checkout de 8 segundos
a falácia 2 trava a página
Para montar a tela de checkout, o código fazia uma chamada
remota por item do carrinho (preço, estoque, frete), em
série. Local: instantâneo. Produção: 40 itens × 60ms × 3
chamadas = ~7 segundos de espera de rede. O problema N+1.

Correção: batching. Uma chamada que traz tudo de todos os
itens de uma vez. 3 round-trips em vez de 120. Checkout
voltou a ser instantâneo. (Falácia 2: latência não é zero.)
Incidente 3 · "Topologia não muda" → erros após o autoscaling
a falácia 5 quebra no autoscaler
O serviço guardava o endereço IP do serviço de estoque numa
config. Funcionou até o autoscaler substituir a instância de
estoque por uma nova, com IP diferente. O checkout continuou
batendo no IP velho (morto) → erros em cascata.

Correção: service discovery (falácia 5). O checkout pergunta
"onde está o serviço de estoque AGORA?" a cada vez, em vez de
assumir um IP fixo. Topologia muda; o design agora aceita isso.
Incidente 4 · "Custo de transporte é zero" → a fatura

E o incidente que não derrubou nada, mas apareceu no fim do mês: o serviço de checkout numa região chamava o banco numa outra região a cada operação, cruzando a fronteira de cobrança de egresso milhares de vezes por minuto. Funcionalmente, ok. Financeiramente, uma conta de transferência de dados que ninguém previu — a falácia 7. Correção: colocar o serviço e o banco que ele usa na mesma região (localidade), eliminando a travessia cara. O custo de transporte não era zero.

Resultado: o serviço que "funcionava na demo" falhou em produção uma falácia de cada vez — cobrança dupla (rede confiável), checkout lento (latência zero), erros após autoscaling (topologia fixa), e uma fatura surpresa (transporte grátis). Nenhuma delas era um bug de lógica; todas eram suposições otimistas sobre a rede que a realidade desmentiu. E cada correção foi uma das práticas defensivas do capítulo: idempotência, batching, service discovery, localidade. A lição é que a demo mente: em desenvolvimento, a rede é praticamente ideal (local, confiável, instantânea, grátis), então as falácias não cobram seu preço — e o sistema parece pronto. É em produção, sob a rede real, que as suposições se revelam. O engenheiro maduro projeta para a rede real desde o começo, passando a checklist das oito falácias sobre o design, em vez de descobri-las uma a uma nos incidentes. Isto fecha a Parte II — as leis. Você conhece o que governa a correção (CAP, consistência) e a performance (Amdahl, Little, USL) e a realidade (as falácias) dos sistemas distribuídos. A Parte III desce ao concreto: os componentes — balanceadores, caches, filas, bancos, o log — as peças com que você constrói, e o trade-off de cada uma.

8.11 Erros comuns

Erro 1 · Não tratar a incerteza da rede não-confiável

Reenviar uma requisição sem resposta sem idempotência, causando processamento duplo (cobrança dupla) — porque a primeira pode ter funcionado e só a resposta se perdeu. A rede não-confiável cria a incerteza "não sei se aconteceu". A defesa: retries com idempotência, para reenviar ser sempre seguro.

Erro 2 · Código conversador (chatty / N+1)

Fazer muitas chamadas remotas pequenas onde uma maior bastaria, assumindo latência zero. Funciona local, trava em produção (o N+1). Em sistemas distribuídos, o número de round-trips domina a latência. A defesa: batching, reduzir idas e voltas, trazer mais de uma vez.

Erro 3 · Confundir problema de latência com problema de banda

Comprimir o payload (solução de banda) para um sistema conversador (problema de latência), ou vice-versa. São limites independentes com curas diferentes. Diagnostique qual é o gargalo — muitas viagens (latência) ou muitos dados (banda) — antes de escolher a solução.

Erro 4 · Confiar no tráfego "interno"

Assumir que a comunicação dentro da rede privada é segura e dispensar criptografia, autenticação e validação. Perímetros são penetrados, e dentro deles serviços confiam cegamente uns nos outros. A defesa é zero trust: criptografe, autentique e valide toda chamada, inclusive as internas.

Erro 5 · Hard-codar endereços e topologia

Fixar IPs e assumir que servidores específicos sempre existirão — e quebrar quando o autoscaling substitui instâncias. A topologia da nuvem muda por design. A defesa: service discovery dinâmico, statelessness, tolerância a instâncias nascendo e morrendo.

Erro 6 · Esquecer o custo de transporte

Cruzar regiões e zonas alegremente, espalhar dados acessados juntos, servir mídia do origin — e levar um susto de egresso na fatura. Transporte não é grátis (dinheiro e CPU). A defesa: localidade de dados, minimizar travessias de fronteiras de cobrança, manter junto o que é acessado junto.

Erro 7 · Achar que a demo prova que está pronto

Concluir que o sistema funciona porque rodou perfeitamente em desenvolvimento — onde a rede é praticamente ideal e as falácias não cobram seu preço. A demo mente; a produção tem a rede real. Passe a checklist das oito falácias sobre o design antes de produção, em vez de descobri-las nos incidentes.

Verifique seu entendimento
Seu serviço chama uma API de pagamento. Às vezes a chamada não retorna resposta (timeout), e seu código a reenvia automaticamente. Clientes começam a reclamar de cobranças duplicadas. Qual falácia foi cometida, e qual é a correção correta?

8.12 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Identificar a falácia

Para cada decisão de design problemática, identifique a falácia cometida e a prática defensiva correta: (a) o código hard-coda o IP de um serviço dependente; (b) um relatório busca cada linha do banco com uma chamada de rede separada; (c) dados sensíveis trafegam sem criptografia entre dois serviços internos; (d) a aplicação replica o banco inteiro entre três regiões a cada hora.

(a) Hard-coda o IP → falácia 5 ("topologia não muda"). O IP vai mudar (autoscaling, realocação), e o código quebra. Defesa: service discovery dinâmico — pergunte onde o serviço está a cada vez, não assuma um endereço fixo.

(b) Uma chamada de rede por linha → falácia 2 ("latência é zero"). É o N+1: funciona local, trava em produção com a latência acumulada. Defesa: batching — buscar todas as linhas em uma chamada (ou poucas), reduzindo round-trips.

(c) Dados sensíveis sem criptografia entre serviços internos → falácia 4 ("a rede é segura"). "Interno" não é seguro; o tráfego pode ser interceptado se o perímetro for penetrado. Defesa: TLS sempre, inclusive interno (zero trust); autenticar as chamadas entre serviços.

(d) Replica o banco inteiro entre regiões a cada hora → falácias 3 e 7 ("banda infinita" e "custo de transporte zero"). Transferir o banco inteiro consome banda enorme e gera custo de egresso entre regiões. Defesa: enviar apenas os deltas (o que mudou) em vez do banco inteiro (banda), e considerar se a replicação entre regiões é mesmo necessária dado o custo (transporte).

O insight: cada decisão problemática mapeia para uma falácia específica, e a falácia aponta direto para a defesa. Reconhecer "que suposição sobre a rede esse código está fazendo?" leva à correção. As falácias são uma checklist de diagnóstico.

Médio
Exercício 2 · Latência ou banda?

Para cada sintoma de lentidão na rede, diga se a causa raiz é um problema de latência ou de banda, e a solução apropriada (lembrando que a cura de um não resolve o outro): (a) baixar um relatório de 2 GB demora muito; (b) abrir o dashboard faz 200 chamadas pequenas e demora 6s; (c) um vídeo trava e bufferiza constantemente; (d) salvar um formulário simples demora 3s porque valida cada campo numa chamada separada.

(a) Relatório de 2 GB demora → banda. O problema é volume de dados, não número de viagens. Soluções de banda: comprimir o relatório, paginar/streaming (não baixar tudo de uma vez), enviar só o necessário, formatos mais compactos. Reduzir round-trips não ajudaria — já é uma transferência só, grande.

(b) Dashboard com 200 chamadas → latência. O problema é o número de viagens (cada uma paga a latência da rede), não o tamanho dos dados. Soluções de latência: batching (agrupar as 200 em uma ou poucas chamadas), reduzir round-trips. Comprimir cada chamadinha não ajudaria — o gargalo são as 200 idas e voltas, não os bytes.

(c) Vídeo travando/bufferizando → banda. O fluxo de dados do vídeo excede a banda disponível, então ele esvazia o buffer e trava. Soluções de banda: bitrate adaptativo (servir resolução menor quando a banda é limitada), CDN próxima do usuário, compressão melhor. É clássico problema de throughput de dados.

(d) Formulário valida cada campo numa chamada → latência. Mesmo problema do (b): muitas viagens pequenas. Cada validação é um round-trip; 10 campos = 10 idas e voltas em série. Solução de latência: validar tudo numa chamada só (batch), ou validar no cliente quando possível. Não é problema de banda (os dados são triviais), é de número de viagens.

O insight: a pergunta diagnóstica é "muitos dados (banda) ou muitas viagens (latência)?". Dados grandes/streaming → banda → comprima/adapte. Muitas chamadas pequenas → latência → agrupe/reduza round-trips. Aplicar a cura errada (comprimir o dashboard conversador, ou agrupar o download do relatório) não resolve, porque os dois limites são independentes.

Médio
Exercício 3 · Da falácia à defesa

Para cada falácia, descreva uma prática defensiva concreta que você aplicaria num sistema de microsserviços e por que ela neutraliza a suposição falsa: (a) "a rede é confiável"; (b) "topologia não muda"; (c) "há um administrador"; (d) "a rede é homogênea".

(a) "A rede é confiável" → timeouts + retries idempotentes + circuit breakers. Como a rede falha e cria incerteza, toda chamada entre serviços tem timeout (não espera para sempre), retries com idempotência (reenviar é seguro), e circuit breaker (Cap 20: para de tentar um serviço que está claramente fora, evitando cascata). Isso neutraliza a não-confiabilidade tratando a falha como esperada, não excepcional.

(b) "Topologia não muda" → service discovery. Em vez de cada serviço conhecer os endereços fixos dos outros, há um registro dinâmico ("o serviço de estoque está em tais instâncias agora"). Quando o autoscaler cria/destrói instâncias, o registro se atualiza, e os chamadores sempre encontram instâncias vivas. Neutraliza a topologia mutável tornando a localização uma consulta, não uma constante.

(c) "Há um administrador" → contratos explícitos e resiliência a dependências. Como times e terceiros diferentes controlam partes diferentes, cada serviço expõe um contrato de API versionado e estável (para que mudanças internas de uma equipe não quebrem as outras), e se defende das dependências que não controla (timeouts, fallbacks, degradação graciosa se um serviço de terceiro cai). Neutraliza a descentralização com fronteiras claras e defesa contra o que está do outro lado delas.

(d) "A rede é homogênea" → padrões abertos e contratos versionados. Como os serviços podem ser de linguagens, versões e formatos diferentes, a comunicação usa padrões neutros e bem definidos (JSON, Protobuf, gRPC) e esquemas versionados que evoluem sem quebrar (campos opcionais, compatibilidade para trás). Neutraliza a heterogeneidade não assumindo que o outro lado é igual a você — só que ele respeita o contrato.

O insight: cada defesa transforma uma suposição falsa em algo explicitamente tratado. Note que muitas dessas (timeouts, retries, circuit breakers, contratos versionados) são os temas dos próximos capítulos — porque a Parte IV inteira é, em boa medida, o conjunto de defesas contra as falácias.

Difícil
Exercício 4 · Revisar um design pelas oito falácias — entrevista

Você está revisando o design de um novo sistema distribuído de um colega. Use as oito falácias como uma checklist estruturada de revisão: para cada falácia, formule a pergunta que você faria sobre o design para verificar se ele caiu nela, e o que procuraria como sinal de alerta. Demonstre que as falácias são uma ferramenta de revisão sistemática, não só uma lista para memorizar.

Enquadramento: as oito falácias são uma das melhores checklists de revisão de design distribuído que existem, porque cada uma aponta para uma classe de bug que só aparece em produção. Vou transformar cada falácia numa pergunta de revisão concreta.

1. "A rede é confiável" → "O que acontece quando uma chamada entre serviços falha ou não responde?" Sinais de alerta: chamadas sem timeout, retries sem idempotência, nenhum tratamento para "não sei se a operação aconteceu". Procurar: toda operação importante é idempotente? há timeouts e circuit breakers?

2. "Latência é zero" → "Quantos round-trips de rede uma operação típica do usuário dispara?" Sinais de alerta: loops que fazem uma chamada remota por item (N+1), cadeias longas de chamadas em série entre serviços. Procurar: as chamadas são agrupadas (batch)? a latência acumulada da cadeia foi estimada?

3. "Banda é infinita" → "Quanto dado cada chamada transfere, e isso escala?" Sinais de alerta: transferir objetos enormes, sincronizações completas onde deltas bastariam, sem paginação. Procurar: payloads são enxutos? mídia vem de CDN? manda-se delta ou tudo?

4. "A rede é segura" → "O tráfego interno é criptografado e autenticado?" Sinais de alerta: "é interno, não precisa de TLS", serviços que confiam cegamente uns nos outros, entrada de outros serviços não validada. Procurar: TLS interno, autenticação entre serviços, validação de toda entrada (zero trust).

5. "Topologia não muda" → "Como os serviços se encontram, e o que acontece quando instâncias nascem/morrem?" Sinais de alerta: IPs hard-coded, suposição de que um servidor específico sempre existe. Procurar: service discovery, statelessness, tolerância a autoscaling.

6. "Há um administrador" → "De quais sistemas que não controlamos dependemos, e o que acontece quando eles mudam ou caem?" Sinais de alerta: dependência de terceiros sem fallback, suposição de que APIs externas nunca mudam. Procurar: contratos explícitos, degradação graciosa, resiliência a dependências externas.

7. "Custo de transporte é zero" → "Que tráfego cruza fronteiras de cobrança (regiões, zonas, saída da nuvem)?" Sinais de alerta: serviço numa região chamando banco em outra, replicação entre regiões sem necessidade clara. Procurar: localidade (o que é acessado junto está junto?), estimativa de egresso.

8. "A rede é homogênea" → "Como serviços de tecnologias diferentes se comunicam, e como o contrato evolui?" Sinais de alerta: formatos proprietários, esquemas sem versionamento, suposição de que todos os serviços são da mesma stack. Procurar: padrões abertos, esquemas versionados com compatibilidade.

O que um bom revisor demonstra: que usa as falácias como uma varredura sistemática (oito perguntas que cobrem as classes de bug distribuído), não como trivia; que para cada uma sabe o sinal de alerta concreto a procurar no design; e que conecta cada falácia à sua defesa. Frase de fechamento: "eu passo as oito falácias sobre todo design distribuído como uma checklist, porque cada uma é uma classe de bug que só aparece em produção, quando é caro. Para cada falácia eu faço uma pergunta — o que acontece quando isto falha/atrasa/muda/custa? — e o silêncio do design sobre qualquer uma delas é um bug que vamos descobrir num incidente. É mais barato encontrar as oito numa revisão de uma hora do que uma a uma nos próximos seis meses."

Fim do capítulo 8 · Fim da Parte II
Você concluiu as leis. Conhece o que governa a correção sob distribuição (CAP, consistência), a performance sob escala (Amdahl, Little, USL) e a realidade da rede (as oito falácias). Tem o aparato conceitual para raciocinar sobre qualquer sistema distribuído — o que é possível, o que custa, o que vai dar errado. A Parte III deixa a teoria e pega nas ferramentas: os componentes com que se constroem sistemas — balanceadores de carga, caches, filas, bancos, sharding, o log — cada um com seu propósito, seu trade-off, e seu lugar certo. Peça "continua" para a Parte III.
Parte III
Os componentes

As peças com que se constroem sistemas, e o trade-off de cada uma. Balanceadores que distribuem carga, caches que trocam frescor por velocidade, filas que desacoplam no tempo, bancos que guardam a verdade, o particionamento que quebra o que não cabe, e o log como a primitiva que unifica tudo. Seis capítulos sobre saber qual peça usar, quando, e o que ela cobra.

Balanceamento Cache Filas Bancos Sharding O log
Parte III · Capítulo 9 · Os componentes

Balanceamento
de carga.

O balanceador de carga é a peça mais onipresente e mais subestimada da arquitetura distribuída. "Ele distribui requisições entre servidores" é o que todo mundo sabe — e é só metade da história. O balanceador é também o que torna a escala horizontal possível, o que detecta e contorna servidores mortos, o que termina o TLS, o que roteia por conteúdo, e o que decide — através de um algoritmo aparentemente trivial — se sua frota fica equilibrada ou se um servidor afoga enquanto outro fica ocioso. Escolher mal o algoritmo, ou esquecer o estado, transforma o balanceador de solução em problema.

Este capítulo abre a Parte III — os componentes — com a peça que fica na frente de quase tudo. Vamos ver o papel real do balanceador (muito além de distribuir carga), a distinção fundamental entre balanceamento na camada de transporte (L4) e na de aplicação (L7) e quando usar cada uma, os algoritmos de distribuição (round-robin, least-connections, hashing) e por que o "óbvio" round-robin frequentemente é a escolha errada, os health checks que detectam servidores mortos, o problema do estado (que torna a distribuição muito mais difícil), o hashing consistente que resolve afinidade e cache, e a questão recursiva de quem balanceia o próprio balanceador. Terminamos com um caso onde o algoritmo errado afundou um servidor. Quando você sair daqui, o balanceador deixa de ser uma caixa preta "que distribui" e vira um componente cujas escolhas você entende e controla.

9.1 A história — do hardware caro ao software onipresente

Contexto histórico

No início da web comercial, nos anos 1990, escalar significava comprar um servidor maior (escala vertical, Cap 1). Quando isso bateu no teto, surgiu a necessidade de dividir a carga entre máquinas — e nasceram os primeiros balanceadores de carga, que eram appliances de hardware caríssimos, de empresas como a F5 e a Cisco. Eram caixas físicas dedicadas, que custavam dezenas de milhares de dólares e exigiam especialistas para configurar. Balancear carga era um luxo de quem tinha escala e orçamento.

A virada veio com o software. O HAProxy (2001) e o NGINX (2004) trouxeram balanceamento de carga de alta performance em software livre, rodando em hardware comum. De repente, qualquer um podia pôr um balanceador na frente de seus servidores sem comprar uma caixa de dezenas de milhares de dólares. O balanceamento deixou de ser luxo e virou prática padrão, e os algoritmos e técnicas que antes eram conhecimento de especialistas se democratizaram.

A nuvem deu o passo seguinte: o balanceador virou um serviço gerenciado. AWS ELB/ALB/NLB, Google Cloud Load Balancing, e equivalentes transformaram o balanceador numa caixa que você liga com um clique, que escala sozinha e que o provedor opera. Em paralelo, o mundo dos contêineres e microsserviços levou o balanceamento para dentro do sistema — service meshes (Istio, Linkerd) e proxies como o Envoy passaram a balancear o tráfego entre serviços, não só na borda.

Em 2026, o balanceamento de carga é tão onipresente que se tornou quase invisível — há balanceadores na borda (recebendo a internet), entre camadas, entre serviços, dentro do cluster Kubernetes. As tendências atuais incluem roteamento mais inteligente (decisões baseadas em utilização real do backend, não só contagem de conexões, como o "choice-of-2" do Netflix) e a integração profunda com service meshes. Mas os fundamentos — L4 vs L7, os algoritmos, os health checks, o problema do estado — permaneceram os mesmos por décadas, porque resolvem problemas que não mudam. Este capítulo é sobre esses fundamentos, que valem do balanceador de dois servidores ao de milhões de requisições por segundo.

9.2 Mais que distribuir carga — o papel real

Reduzir o balanceador a "distribui requisições" esconde a maioria das razões pelas quais ele é essencial. Seus papéis reais:

  • Habilitar a escala horizontal: sem um ponto único de entrada que distribui, você não pode ter múltiplos servidores atendendo o mesmo serviço. O balanceador é o que torna "adicionar mais instâncias" possível (Cap 1, escala horizontal).
  • Prover alta disponibilidade: ao detectar um servidor morto (health checks) e parar de mandar tráfego para ele, o balanceador contorna falhas automaticamente. Um servidor cai, os usuários nem percebem — é a peça central da resiliência.
  • Abstrair a topologia: os clientes falam com um endereço (o do balanceador), e os servidores atrás dele podem nascer, morrer e mudar livremente. É a defesa contra a falácia "topologia não muda" (Cap 8).
  • Terminar o TLS: descriptografar o HTTPS num ponto central, aliviando os servidores de aplicação desse trabalho e centralizando a gestão de certificados.
  • Rotear por conteúdo (L7): mandar /api para uns servidores, /static para outros, /ws para os de WebSocket — roteamento inteligente baseado no que a requisição pede.
O balanceador é o ponto onde a frota vira um serviço
A forma mais útil de pensar no balanceador: ele é a fronteira que transforma uma frota de máquinas (que nascem, morrem, escalam) num serviço único e estável (um endereço, sempre no ar). Do lado de fora, os clientes veem um serviço confiável; do lado de dentro, há um conjunto mutável de servidores descartáveis (o "gado, não bicho de estimação" do Cap 4). O balanceador é o que medeia entre esses dois mundos — ele absorve a volatilidade da frota e apresenta estabilidade ao cliente. Por isso ele é mais que um distribuidor: é o componente que desacopla os clientes da realidade bagunçada da infraestrutura, e é onde você implementa boa parte da resiliência (contornar falhas), da segurança (TLS, rate limiting) e do roteamento (por conteúdo) do sistema. Subestimá-lo como "só distribui" é perder de vista que ele é a interface entre o seu sistema e o mundo.

9.3 L4 vs L7 — transporte ou aplicação

A distinção mais fundamental entre tipos de balanceador é a camada em que operam, e ela define um trade-off claro entre velocidade e inteligência.

L4 — camada de transporte (rápido, cego)

Roteia por IP e porta (TCP/UDP), sem olhar o conteúdo da requisição. Apenas encaminha pacotes para um backend. Extremamente rápido e de baixa latência (não há nada para inspecionar), funciona com qualquer protocolo. Mas é cego: não pode rotear por URL, header ou cookie. Para throughput máximo e protocolos não-HTTP.

L7 — camada de aplicação (inteligente, mais lento)

Entende o protocolo de aplicação (HTTP). Termina a conexão, lê a requisição, e pode rotear por conteúdo (path, header, cookie), terminar TLS, comprimir, autenticar, limitar taxa. Muito mais poderoso e flexível, ao custo de mais latência (precisa parsear a requisição) e de só funcionar com o protocolo que entende.

O padrão moderno: L4 na borda, L7 atrás (duas camadas)
A escolha não precisa ser exclusiva, e a arquitetura comum em escala usa ambos, em camadas. Na borda, um balanceador L4 recebe a internet — rápido, robusto, lida com o volume bruto de conexões e oferece um endereço estável (VIP). Atrás dele, balanceadores L7 fazem o trabalho inteligente — roteamento por conteúdo, TLS, políticas. O L4 dá throughput e estabilidade; o L7, flexibilidade. Para a maioria dos sistemas menores, porém, a recomendação prática é começar com L7 para cargas HTTP: a flexibilidade (rotear por path, terminar TLS, autenticar) quase sempre compensa o pequeno custo de latência, e a maioria das aplicações web se beneficia mais da inteligência do L7 do que do throughput cru do L4. Use L4 quando você tem protocolos não-HTTP (gRPC bruto, jogos, proxy de banco) ou precisa de throughput máximo com latência mínima. A regra: L7 por padrão para HTTP; L4 quando a velocidade crua ou um protocolo não-HTTP exige.

9.4 Os algoritmos — e por que round-robin engana

Como o balanceador escolhe qual servidor recebe a próxima requisição? O algoritmo parece um detalhe, mas é o que determina se a carga fica equilibrada ou se um servidor afoga. Os principais:

AlgoritmoComo decideQuando usar
Round-robinReveza em ordem: 1, 2, 3, 1, 2, 3...Requisições uniformes, servidores iguais
Weighted round-robinReveza, mas servidores mais fortes recebem maisServidores de capacidades diferentes
Least connectionsManda para quem tem menos conexões ativasRequisições de duração muito variável
Least response timeManda para quem responde mais rápido agoraAdaptar à performance real em tempo real
IP hash / consistent hashMesmo cliente/chave → sempre o mesmo servidorAfinidade de sessão, cache (ver 9.6)
Power of two choices (P2C)Sorteia 2, manda para o menos carregado dos 2Bom equilíbrio com baixo custo, em escala
Round-robin é o default, mas falha quando as requisições variam
O round-robin é o algoritmo "óbvio" e um default razoável, mas tem um ponto cego perigoso: ele assume que todas as requisições custam o mesmo. Quando as requisições têm durações muito variáveis — algumas respondem em 10ms, outras seguram a conexão por 30 segundos —, o round-robin distribui contagem, não carga. Imagine: o servidor 1 recebe, por azar do rodízio, três requisições lentas seguidas e fica afogado; o servidor 2 recebe três rápidas e fica ocioso. O round-robin acha que está equilibrado (cada um recebeu o mesmo número), mas a carga real está desbalanceada. É por isso que least connections costuma ser melhor quando as durações variam: ele manda a próxima requisição para quem tem menos conexões abertas no momento — naturalmente evitando o servidor que está preso em requisições lentas. A lição: o algoritmo "óbvio" (round-robin) é certo para requisições uniformes, mas a maioria dos sistemas reais tem requisições de custo variável, e aí distribuir por contagem engana. Olhe a variância das suas requisições antes de escolher.

9.5 Health checks — detectar o morto

Um balanceador que manda tráfego para um servidor morto é pior que inútil — ele transforma uma falha de um servidor numa falha visível para os usuários. Os health checks são como o balanceador sabe quais servidores estão vivos e aptos a receber tráfego.

Health check ativo

O balanceador pergunta periodicamente a cada servidor "você está bem?" (ex.: requisita /healthz a cada poucos segundos). Se o servidor não responde ou responde erro N vezes, é removido da rotação. Detecta problemas proativamente, antes de mandar tráfego real para um servidor doente.

Health check passivo

O balanceador observa o tráfego real: se um servidor começa a dar erros ou timeouts nas requisições reais, é marcado como ruim e removido. Não gera tráfego extra, mas só detecta o problema depois de alguns usuários já terem sofrido com ele.

O health check raso mente: 200 OK não significa "saudável"
Um erro comum e perigoso é um health check raso demais: o /healthz retorna "200 OK" simplesmente porque o processo web está rodando — mesmo que o servidor não consiga falar com o banco, ou esteja sem memória, ou com o disco cheio. O balanceador acha que está saudável e manda tráfego, que falha. O health check precisa verificar a saúde real de servir: o servidor consegue alcançar suas dependências críticas (banco, cache)? tem recursos? Um bom /healthz testa o caminho que importa, não só "estou ligado". Mas há um equilíbrio delicado: um health check profundo demais (que testa todas as dependências) pode causar falhas em cascata — se o banco fica lento, todos os servidores falham o health check ao mesmo tempo, são todos removidos, e o serviço inteiro cai, mesmo podendo servir parte do tráfego. A arte é um health check que reflete a capacidade real de servir, sem amplificar uma falha de dependência numa queda total. Esta tensão (raso demais mente, profundo demais amplifica) é uma das sutilezas reais de operar balanceadores.

9.6 O problema do estado — onde a distribuição quebra

Distribuir requisições é fácil quando os servidores são stateless (Cap 4) — qualquer um atende qualquer requisição igualmente. O problema surge quando há estado: se o servidor A guarda a sessão do usuário na memória, e a próxima requisição desse usuário cai no servidor B (que não tem a sessão), o usuário é deslogado. O estado quebra a distribuição livre.

Sticky sessions (afinidade) — o remendo

Forçar o mesmo usuário a sempre cair no mesmo servidor (via cookie ou IP hash), para que ele encontre seu estado. Resolve o sintoma, mas reintroduz os problemas que a statelessness evita: se aquele servidor cai, o usuário perde o estado; e a distribuição fica desigual (um servidor com usuários pesados afoga). Um remendo, não uma solução.

Estado externo — a solução real

Tirar o estado dos servidores e pô-lo num store compartilhado (sessão no Redis, dados no banco, arquivos no object storage — Cap 4). Agora qualquer servidor atende qualquer requisição, porque nenhum guarda estado local. A distribuição volta a ser livre, e os servidores voltam a ser descartáveis. É a fundação da escala horizontal.

Sticky sessions é tratar o sintoma; statelessness é curar a doença
Quando alguém propõe sticky sessions para resolver "o usuário desloga ao trocar de servidor", vale reconhecer que isso é tratar o sintoma. A causa raiz é que o servidor guarda estado local, e sticky sessions apenas esconde esse problema garantindo que o usuário volte ao mesmo servidor — até esse servidor cair (e aí o usuário perde tudo), ou até a distribuição ficar desigual (sticky impede o balanceador de mover carga). A cura real é externalizar o estado (Cap 4): com a sessão no Redis, qualquer servidor serve qualquer usuário, sticky sessions se torna desnecessário, e você recupera a distribuição livre e a resiliência (um servidor cai, o usuário continua, porque seu estado nunca esteve naquele servidor). Sticky sessions tem usos legítimos (afinidade de cache, alguns protocolos), mas como solução para sessão de usuário é quase sempre o caminho errado — um sinal de que o estado deveria ter saído da máquina. Antes de configurar afinidade, pergunte: por que este servidor tem estado que deveria estar fora dele?

9.7 Hash consistente — afinidade sem o caos da realocação

Quando você precisa de afinidade (a mesma chave sempre no mesmo servidor — para cache local, ou um banco particionado), o hashing ingênuo tem um problema grave, e o hashing consistente o resolve.

Por que hash módulo N quebra, e hash consistente não
A forma ingênua de mapear chaves a servidores é servidor = hash(chave) % N, onde N é o número de servidores. Funciona — até N mudar. Quando você adiciona ou remove um servidor (N muda), quase todas as chaves remapeiam para servidores diferentes, porque o resto da divisão muda para quase tudo. Para um cache, isso é catastrófico: adicionar um servidor invalida quase todo o cache de uma vez (todas as chaves agora apontam para o lugar errado), e o sistema é inundado de cache misses. O hashing consistente resolve isso mapeando tanto servidores quanto chaves a pontos num "anel" — e quando um servidor entra ou sai, só as chaves próximas a ele no anel remapeiam, não todas. Adicionar um servidor a um anel de 10 remaneja ~10% das chaves, não ~90%. É a técnica essencial para qualquer afinidade que precise sobreviver a mudanças de topologia: caches distribuídos, bancos particionados, sticky sessions sem cookie. A lição: se você precisa de "mesma chave, mesmo servidor" e os servidores mudam, hash consistente é a ferramenta — o hash módulo N só funciona se N nunca muda, o que contradiz a falácia da topologia fixa (Cap 8).

Mas o hashing consistente tem um limite importante: ele equilibra chaves, não carga. Se uma única chave é muito mais popular que as outras (uma "hot key" — um post viral, um canal popular), todas as requisições dela vão para o mesmo servidor, que afoga, mesmo com o anel perfeitamente balanceado em número de chaves. O hash consistente distribui as chaves uniformemente, mas não pode dividir a carga de uma chave quente — esse é um problema separado (Cap 13, hot partitions) que nenhum algoritmo de hashing resolve sozinho.

9.8 Quem balanceia o balanceador?

Uma pergunta que expõe um risco escondido: se o balanceador é o ponto único de entrada, ele não é um ponto único de falha? Se ele cai, todo o serviço cai, por mais servidores que existam atrás dele. A resposta é que o próprio balanceador precisa ser redundante.

O balanceador não pode ser o ponto único de falha
O balanceador resolve o ponto único de falha dos servidores de aplicação — mas, se for único, ele mesmo vira o ponto único de falha. Um único balanceador caindo derruba tudo, independentemente de quantos servidores saudáveis existam atrás. As soluções: ter múltiplos balanceadores (em modo ativo-ativo ou ativo-passivo, com failover entre eles), e usar mecanismos de rede que distribuem o tráfego entre eles antes mesmo do balanceador (como DNS round-robin apontando para vários, ou IPs virtuais que migram entre balanceadores no failover, ou o anycast que roteia para o balanceador mais próximo vivo). Os balanceadores gerenciados da nuvem (ELB, etc.) já fazem isso por baixo — são internamente redundantes e escaláveis, e é uma das razões para usá-los em vez de operar o seu. A lição conceitual: resiliência não é eliminar pontos de falha individuais, é não deixar nenhum ponto cuja queda derrube o sistema — e isso inclui, recursivamente, a peça que você adicionou para resolver a resiliência dos outros. Todo ponto único no caminho, incluindo o balanceador, precisa de redundância.

9.9 Estudo de caso — o algoritmo que afundou um servidor

"Um servidor a 100%, os outros ociosos"
O cenário

Um serviço com 5 servidores atrás de um balanceador round-robin começa a apresentar latência alta e erros intermitentes, mesmo com a carga total bem abaixo da capacidade somada. O monitoramento mostra algo estranho: um ou dois servidores ficam a 100% de CPU enquanto os outros ficam quase ociosos. A carga "está distribuída" pelo round-robin, mas claramente não está equilibrada. O que está acontecendo?

Passo 1 · O round-robin distribui contagem, não carga
o problema do round-robin com requisições variáveis
O serviço tem dois tipos de requisição:
  - "rápida" (consulta simples): ~20ms, 95% do tráfego
  - "pesada" (relatório): ~30 SEGUNDOS, 5% do tráfego

Round-robin reveza: 1,2,3,4,5,1,2,3,4,5...
Cada servidor recebe o MESMO NÚMERO de requisições.
MAS: por azar do rodízio, várias "pesadas" caem no mesmo
servidor em sequência. Ele acumula 3-4 relatórios de 30s
rodando ao mesmo tempo → 100% CPU, travado. Enquanto isso,
o vizinho só pegou requisições rápidas → ocioso.

O round-robin acha que está justo (contagem igual). A CARGA
real está absurdamente desbalanceada (durações desiguais).
Passo 2 · Por que least connections resolve
least connections segue a carga real
Com LEAST CONNECTIONS, o balanceador manda a próxima
requisição para quem tem MENOS conexões abertas agora.

O servidor preso em 3 relatórios de 30s tem 3 conexões
abertas e longas → o balanceador PARA de mandar tráfego
para ele até elas terminarem, mandando para os ociosos.

Resultado: as requisições pesadas "ocupam" um servidor, e
o balanceador naturalmente desvia o resto para os livres.
A carga segue a OCUPAÇÃO real, não a contagem cega.
Passo 3 · A solução mais profunda — separar as cargas

Trocar para least connections aliviou o sintoma, mas havia uma solução melhor de arquitetura: as requisições "pesadas" (relatórios de 30s) não deveriam competir com as "rápidas" (20ms) pelo mesmo recurso. A solução robusta foi separar — rotear (via L7, por path) os relatórios para um pool de servidores dedicado (ou, melhor ainda, processá-los de forma assíncrona via fila, Cap 11), deixando os servidores de requisições rápidas livres para responder com baixa latência. As cargas com perfis tão diferentes foram isoladas, em vez de disputarem.

Passo 4 · O resultado
antes e depois
ANTES (round-robin, cargas misturadas):
  Servidores 1-2: 100% CPU (presos em relatórios)
  Servidores 3-5: 15% CPU (ociosos)
  Latência p99 das requisições rápidas: 8 SEGUNDOS
  (esperando atrás dos relatórios)

DEPOIS (least connections + relatórios isolados):
  Requisições rápidas: pool dedicado, p99 ~30ms
  Relatórios: pool/fila separado, processam sem afetar
  Utilização equilibrada, sem servidor afogado.

Resultado: o servidor afogado não era falta de capacidade — era o algoritmo errado para o perfil de carga. O round-robin, que distribui contagem, escondia um desequilíbrio brutal de carga porque as requisições tinham durações radicalmente diferentes (20ms vs 30s). Least connections, que segue a ocupação real, corrigiu o sintoma; e a separação das cargas por perfil (requisições rápidas vs relatórios pesados, via roteamento L7 ou fila assíncrona) corrigiu a causa. A lição central do capítulo: o algoritmo de balanceamento não é um detalhe — ele determina se a carga fica equilibrada, e o "óbvio" round-robin engana quando as requisições variam. E a lição mais profunda: cargas com perfis muito diferentes (latência baixa vs trabalho longo) não devem competir pelo mesmo recurso; separá-las (por pool ou por fila) é frequentemente melhor que qualquer algoritmo. Próximo capítulo: cache — o componente que troca frescor por velocidade, com os dois problemas mais difíceis da computação (invalidação e nomeação) escondidos dentro dele.

9.10 Erros comuns

Erro 1 · Round-robin para requisições de custo variável

Usar round-robin quando as requisições têm durações muito diferentes, e ver um servidor afogar enquanto outro fica ocioso. Round-robin distribui contagem, não carga. Para durações variáveis, use least connections, que segue a ocupação real. Olhe a variância das requisições antes de escolher o algoritmo.

Erro 2 · Health check raso demais

Um /healthz que retorna 200 só porque o processo subiu, mesmo sem conseguir falar com o banco — o balanceador manda tráfego que falha. O health check precisa refletir a capacidade real de servir (alcança dependências críticas?), sem ser tão profundo a ponto de uma lentidão do banco derrubar todos os servidores de uma vez.

Erro 3 · Sticky sessions para resolver sessão de usuário

Configurar afinidade para o usuário não deslogar ao trocar de servidor — tratando o sintoma. A causa é o estado local; sticky esconde, mas reintroduz fragilidade (servidor cai = estado perdido) e desequilíbrio. A cura é externalizar o estado (sessão no Redis), tornando qualquer servidor capaz de atender qualquer usuário.

Erro 4 · Hash módulo N onde a topologia muda

Usar hash(chave) % N para afinidade/cache, e ver quase todo o cache invalidar quando um servidor entra ou sai (N muda). Use hashing consistente, que remapeia só as chaves próximas ao servidor alterado (~1/N delas), não todas. Hash módulo só serve se N nunca muda — o que contradiz a realidade da nuvem.

Erro 5 · Esquecer que o balanceador é ponto único de falha

Resolver o ponto único dos servidores com um balanceador, e deixar o balanceador único — que, se cai, derruba tudo. O balanceador também precisa de redundância (múltiplos, failover, anycast/IP virtual). Os gerenciados da nuvem já fazem isso; o seu próprio, não, a menos que você projete.

Erro 6 · Achar que hash consistente resolve hot keys

Esperar que o hashing consistente equilibre a carga quando uma única chave é muito mais popular que as outras. Ele equilibra chaves, não carga por chave: uma hot key concentra toda a sua carga num servidor, mesmo com o anel balanceado. Hot keys são um problema separado (Cap 13), que hashing não resolve.

Erro 7 · Misturar cargas de perfis muito diferentes

Deixar requisições rápidas (20ms) e trabalho pesado (30s) competirem pelo mesmo pool de servidores — e as rápidas ficarem presas atrás das pesadas. Nenhum algoritmo conserta isso totalmente. Separe as cargas por pool (roteamento L7) ou tire o trabalho pesado para uma fila assíncrona (Cap 11). Perfis diferentes não devem disputar o mesmo recurso.

Verifique seu entendimento
Seu serviço tem 5 servidores idênticos atrás de um balanceador round-robin. As requisições variam muito: a maioria responde em 20ms, mas algumas (relatórios) levam 30 segundos. Você observa que alguns servidores ficam a 100% de CPU enquanto outros ficam ociosos. Qual é o diagnóstico e a melhor primeira correção?

9.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Escolher o algoritmo

Para cada cenário, escolha o algoritmo de balanceamento mais apropriado e justifique: (a) servidores idênticos, requisições uniformes e curtas; (b) requisições de duração muito variável; (c) um cache distribuído onde a mesma chave deve sempre ir ao mesmo nó; (d) servidores de capacidades diferentes (alguns mais potentes).

(a) Idênticos, uniformes e curtos → round-robin. Este é o caso onde o round-robin brilha: requisições de custo parecido distribuídas em rodízio ficam naturalmente equilibradas, e o algoritmo é o mais simples e barato. Não há variância para enganá-lo.

(b) Duração muito variável → least connections. Como vimos, round-robin engana aqui (distribui contagem, não carga). Least connections segue a ocupação real, desviando do servidor preso em requisições longas. (Least response time é uma alternativa ainda mais adaptativa.)

(c) Cache distribuído, mesma chave → mesmo nó → hashing consistente. Você precisa de afinidade (a chave sempre no mesmo nó, para o cache funcionar) que sobreviva a mudanças de topologia. Hashing consistente garante isso remapeando só ~1/N das chaves quando um nó entra/sai, em vez de invalidar tudo (que o hash módulo N faria).

(d) Capacidades diferentes → weighted round-robin. Atribua pesos proporcionais à capacidade: o servidor 3× mais potente recebe 3× mais requisições. Round-robin simples sobrecarregaria os fracos e subutilizaria os fortes. (Weighted least connections combina os dois se as durações também variam.)

O insight: não há algoritmo universalmente melhor — a escolha depende de duas perguntas: as requisições têm custo uniforme ou variável? e há necessidade de afinidade (mesma chave/cliente → mesmo servidor)? Uniforme sem afinidade → round-robin. Variável → least connections. Afinidade → hashing consistente. Capacidades desiguais → adicione pesos.

Médio
Exercício 2 · O health check certo

Você precisa projetar o endpoint /healthz de um serviço que depende de um banco de dados e de um cache Redis. Discuta: (a) o que um health check raso demais verificaria e por que isso é perigoso; (b) o que um health check profundo demais verificaria e qual o risco; (c) qual é o equilíbrio certo, e como evitar que uma lentidão do banco derrube toda a frota.

(a) Raso demais: retornar 200 só porque o processo web está rodando, sem verificar nada além disso. Perigo: o servidor pode estar incapaz de servir de verdade (banco inalcançável, sem memória, disco cheio) e ainda assim passar no health check — o balanceador manda tráfego que falha, transformando um problema invisível em erros para usuários. O health check mente sobre a saúde real.

(b) Profundo demais: verificar, a cada health check, conexão ativa com o banco, com o Redis, e talvez fazer queries de teste. Risco: falha em cascata. Se o banco fica lento ou momentaneamente indisponível, todos os servidores falham o health check ao mesmo tempo, são todos removidos da rotação, e o serviço inteiro cai — mesmo que pudesse servir as requisições que não dependem do banco, ou se recuperar em segundos. Uma falha de dependência vira uma queda total amplificada.

(c) O equilíbrio: o health check deve refletir "este servidor consegue servir tráfego útil?", o que tipicamente inclui verificar as dependências críticas — mas com salvaguardas contra a cascata:

  • Verificar a dependência crítica (ex.: consegue alcançar o banco), mas com cache do resultado e timeout curto, para não martelar o banco a cada health check.
  • Considerar separar "liveness" (o processo está vivo? — raso, reinicia se falhar) de "readiness" (está pronto para receber tráfego? — verifica dependências). O Kubernetes formaliza essa distinção.
  • Para evitar a cascata: não remover todos os servidores de uma vez por uma falha compartilhada. Algumas implementações têm um "modo pânico" — se uma fração grande demais dos backends fica unhealthy ao mesmo tempo, o balanceador ignora os health checks e manda tráfego para todos mesmo assim (melhor servir degradado que cair totalmente), assumindo que uma falha tão ampla é provavelmente da dependência, não dos servidores.

O insight: a tensão raso-vs-profundo não tem resposta única; o objetivo é um check que reflita a capacidade real de servir sem amplificar uma falha de dependência numa queda total. Verifique o crítico, com timeouts e cache, separando liveness de readiness, e tenha uma salvaguarda para o caso de "todos unhealthy ao mesmo tempo" (que é quase sempre a dependência, não os servidores).

Médio
Exercício 3 · O custo de adicionar um nó ao cache

Você tem um cache distribuído em 10 servidores, mapeado por hash(chave) % 10. O tráfego cresceu e você adiciona um 11º servidor, mudando para % 11. (a) O que acontece com o cache existente? (b) Qual o impacto imediato no sistema? (c) Como o hashing consistente teria evitado isso, e qual seria o impacto com ele?

(a) O que acontece com o cache: ao mudar de % 10 para % 11, o resto da divisão muda para quase todas as chaves. Uma chave que antes ia para o servidor hash % 10 = 3 agora vai para hash % 11 = 7 (ou outro) — e isso vale para a esmagadora maioria das chaves. Na prática, quase todo o cache é invalidado de uma vez: as chaves agora apontam para servidores diferentes, que não têm os valores cacheados.

(b) Impacto imediato: uma tempestade de cache misses. Como quase nada está no servidor "certo", quase toda requisição vira um miss, que cai no banco (a fonte da verdade). O banco, que normalmente via só uma fração do tráfego (os misses), de repente recebe quase todo o tráfego de uma vez — e pode ser sobrecarregado ou cair. Adicionar um servidor de cache "para aliviar" pode, ironicamente, derrubar o sistema momentaneamente. É um incidente clássico e contraintuitivo.

(c) Com hashing consistente: servidores e chaves são mapeados a um anel. Ao adicionar o 11º servidor, ele assume apenas as chaves de um segmento do anel — aproximadamente 1/11 das chaves (as que ficam "perto" dele) remapeiam; as outras ~10/11 continuam apontando para os mesmos servidores e seus caches permanecem válidos. O impacto: em vez de ~100% de invalidação e uma tempestade de misses, só ~9% das chaves remapeiam, o banco vê um aumento pequeno e absorvível de misses, e o sistema continua estável. Esta é precisamente a razão de o hashing consistente existir: permitir mudar a topologia (adicionar/remover nós) sem invalidar quase todo o cache. O hash módulo N só é seguro se N nunca muda — e N quase sempre muda.

Difícil
Exercício 4 · Projetar a camada de balanceamento — entrevista

Você projeta a camada de entrada de uma aplicação web que serve uma API REST (/api), arquivos estáticos (/static), e conexões WebSocket de longa duração (/ws), com tráfego global e necessidade de alta disponibilidade. O entrevistador pede: "desenhe a estratégia de balanceamento de carga". Estruture a resposta cobrindo L4 vs L7, roteamento, os diferentes perfis de carga, o estado, e a resiliência do próprio balanceador.

Enquadramento: o desafio aqui é que as três cargas (API, estáticos, WebSocket) têm perfis radicalmente diferentes, e a chave é não tratá-las igual. Vou estruturar por camadas, do global ao específico.

1. Camada global (tráfego global + HA): como o tráfego é global, começar com balanceamento geográfico — DNS-based ou anycast que roteia cada usuário para a região mais próxima (menor latência, a velocidade da luz do Cap 3). Isso também dá resiliência regional: se uma região cai, o tráfego vai para outra. (GSLB — global server load balancing.)

2. Borda de cada região — L4 + L7: na entrada de cada região, um balanceador L4 recebe a internet (rápido, estável, lida com o volume bruto de conexões e dá um VIP estável). Atrás dele, um balanceador L7 faz o roteamento inteligente por conteúdo — porque preciso rotear por path. (Em sistemas menores, começaria direto no L7.)

3. Roteamento por conteúdo (L7) — separar os perfis: esta é a decisão central, porque as três cargas são muito diferentes:

  • /static → idealmente nem chega aos servidores: servir de CDN (Cap 13 do vol. anterior), tirando esse tráfego da frota. O que chegar, vai para um pool simples com round-robin (arquivos são uniformes).
  • /api → pool de servidores de API stateless, com least connections (requisições REST podem variar em custo). Stateless é essencial para distribuir livremente.
  • /ws (WebSocket, conexões longas) → este é o caso especial. Conexões WebSocket são longas e stateful por natureza (a conexão fica aberta). Precisam de um pool dedicado, com algoritmo que considere conexões abertas (least connections), e o balanceador precisa suportar conexões persistentes (não fechar a conexão longa). Separá-las da API é crucial — uma conexão WebSocket de horas não pode "ocupar um slot" de round-robin de API.

4. O estado: os servidores de API devem ser stateless (sessão no Redis, Cap 4), para distribuição livre e resiliência. Os de WebSocket têm estado inerente (a conexão), mas o estado da aplicação (quem está conectado, salas) deve ficar num store compartilhado, para que a queda de um servidor WebSocket não perca o estado lógico, só force reconexão.

5. Resiliência do balanceador: cada balanceador precisa ser redundante (múltiplos, com failover) — senão ele vira o ponto único de falha. Usar os balanceadores gerenciados da nuvem ajuda (são internamente redundantes). Health checks ativos em todos os pools, que reflitam capacidade real de servir sem amplificar falhas de dependência.

O que um bom candidato demonstra: que separa os três perfis de carga em vez de tratá-los igual (a decisão central); que conhece L4 vs L7 e usa cada um onde cabe; que tira estáticos para CDN e mantém a API stateless; que reconhece o caso especial do WebSocket (conexões longas); e que não esquece a redundância do próprio balanceador e o roteamento global. Frase de fechamento: "a chave aqui é que API, estáticos e WebSocket são três cargas com perfis opostos — uma rápida e stateless, uma cacheável e melhor servida de CDN, uma longa e stateful. O erro seria balanceá-las juntas com um algoritmo só. Eu separo por path no L7, dou a cada uma o pool e o algoritmo certos, tiro o que pode para a CDN, mantenho a API stateless, e garanto que nem o balanceador nem nenhuma camada seja um ponto único de falha — porque um balanceador que cai derruba tudo o que está atrás dele."

Fim do capítulo 9
Próximo capítulo: cache. Se o balanceador distribui a carga, o cache a reduz — guardando resultados para não recalculá-los, trocando frescor por velocidade (o espaço-por-tempo do Cap 4). É um dos componentes mais poderosos e mais perigosos, porque esconde os dois problemas notoriamente difíceis da computação: a invalidação (saber quando o dado em cache ficou velho) e a coerência entre camadas. Vamos às camadas de cache, às estratégias, e aos problemas que fazem o cache morder quem o usa sem cuidado.
Parte III · Capítulo 10 · Os componentes

Cache: camadas,
invalidação, os
problemas difíceis.

O cache é a otimização de maior alavancagem que existe — guardar um resultado para não recalculá-lo pode acelerar um sistema em ordens de magnitude por quase nada. E é, ao mesmo tempo, uma das fontes mais traiçoeiras de bugs, porque esconde os dois problemas que a lenda diz serem os mais difíceis da computação: saber quando o dado guardado ficou velho (invalidação) e onde, em qual das muitas camadas, cada dado deve viver. Um cache mal pensado não é uma otimização — é uma máquina de servir dados errados, rápido.

Este capítulo trata do componente que reduz a carga em vez de distribuí-la. Vamos ver a essência do cache (o trade-off espaço-por-tempo e frescor-por-velocidade do Cap 4), as muitas camadas onde ele existe (navegador, CDN, aplicação, banco), as estratégias de leitura e escrita (cache-aside, write-through, write-back) e o que cada uma cobra, o problema notoriamente difícil da invalidação (e por que TTL é a resposta padrão e imperfeita), o cache stampede que derruba sistemas quando uma chave popular expira, a questão de qual camada é "dona" do frescor de cada dado (a fonte de quase todo bug de cache), e quando simplesmente não se deve cachear. Quando você sair daqui, o cache deixa de ser "guarda o resultado e pronto" e vira um componente cujos modos de falha você antecipa — porque o cache que você não entende é o que vai te servir dados errados em produção.

10.1 A história — a hierarquia que sempre existiu

Contexto histórico

Cache não é uma invenção da web — é um princípio tão antigo quanto a computação, nascido da diferença brutal de velocidade entre níveis de memória (Cap 3). Desde os anos 1960, os processadores tinham caches de hardware: pequenas memórias ultrarrápidas (L1, L2) que guardavam os dados mais usados, porque buscar na memória principal era ordens de magnitude mais lento. A ideia central já estava lá: guardar perto e rápido o que você vai usar de novo, aceitando o custo de espaço e a complexidade de manter a cópia válida.

Esse mesmo princípio subiu por todas as camadas do software. O sistema operacional cacheia páginas de disco na memória. O banco de dados cacheia resultados de queries e páginas de índice. E quando a web cresceu, o cache se multiplicou: o navegador cacheia recursos, os proxies e CDNs cacheiam conteúdo na borda, as aplicações cacheiam dados em memória (Memcached, 2003; Redis, 2009). O cache deixou de ser um detalhe de hardware e virou uma decisão de arquitetura em cada nível.

E em algum momento dessa história, uma frase virou folclore da engenharia, atribuída a Phil Karlton: "existem apenas duas coisas difíceis em ciência da computação: invalidação de cache e nomear coisas". A piada captura uma verdade profunda: a parte fácil do cache é guardar; a parte difícil — a que causa os bugs — é saber quando o que você guardou deixou de ser verdade, e ter que invalidá-lo no momento certo.

Em 2026, o cache está em todo lugar e mais necessário do que nunca — é frequentemente a otimização de maior retorno num sistema, capaz de absorver a maior parte da carga de leitura (Cap 2, read-heavy). Mas as armadilhas permaneceram idênticas por décadas, porque são fundamentais, não tecnológicas: a invalidação continua difícil, o cache stampede continua derrubando sistemas, e a confusão sobre qual camada é dona do frescor continua sendo a fonte número um de bugs de dado velho. Este capítulo é sobre colher o enorme benefício do cache enquanto se evita cada uma dessas armadilhas conhecidas — porque elas são conhecidas, e ainda assim cada geração as redescobre da forma difícil.

10.2 A essência — espaço e frescor por velocidade

No fundo, todo cache faz a mesma troca, que você já nomeou no Cap 4: gasta espaço (memória para guardar o resultado) e frescor (o risco de o dado guardado ficar velho) para comprar velocidade (não recalcular nem rebuscar). Entender o cache como essa troca dupla é o que evita usá-lo onde ele não compensa.

Cache funciona por causa de uma assimetria: muitos leem o que poucos escrevem
O cache só compensa quando há uma assimetria favorável entre leitura e escrita: o mesmo dado é lido muitas vezes entre as raras vezes em que muda. Um produto de catálogo é lido milhares de vezes por dia e atualizado uma vez por semana — cachear é uma vitória enorme (uma busca ao banco serve milhares de leituras). Mas um dado que muda toda vez que é lido não tem o que cachear (você nunca reusa o valor guardado), e um dado lido raramente não vale o espaço. É por isso que o cache é a arma natural dos sistemas read-heavy (Cap 2): eles têm exatamente a assimetria que o cache explora. A pergunta antes de cachear qualquer coisa: "este dado é lido muito mais do que muda?". Se sim, o cache rende muito; se não — se muda quase tanto quanto é lido —, o cache gasta espaço e adiciona o risco de frescor sem o benefício de reúso. Cachear o que não tem assimetria é pagar o custo do cache sem colher o ganho.

10.3 As camadas de cache — onde o dado pode morar

Numa requisição web típica, um dado pode ser cacheado em várias camadas, cada uma mais perto do usuário e mais rápida que a anterior. Conhecer as camadas é entender onde cada dado deveria ser cacheado:

CamadaO que cacheiaInvalidação
Navegador (cliente)Recursos estáticos, respostas (via headers HTTP)TTL nos headers; difícil de forçar antes
CDN (borda)Estáticos e conteúdo cacheável, perto do usuárioTTL + purge explícito
Aplicação (Redis/Memcached)Dados, resultados computados, sessõesControle total: delete explícito + TTL
Banco de dadosQueries, páginas de índice (interno)Gerenciado pelo banco
Memória do processo (local)O mais rápido, mas não compartilhado entre instânciasPor instância — cuidado com inconsistência
Para cada dado, pergunte: qual camada é dona do seu frescor?
A pergunta que organiza o caching em camadas, e que previne a maioria dos bugs: para cada pedaço de dado, qual camada deve ser a dona da sua validade?. Um asset estático com hash no nome (ex.: app.a1b2c3.js) pode viver no navegador por um ano imutável — o conteúdo nunca muda (se mudar, o hash muda, o nome muda). Uma resposta de API por usuário pertence ao Redis, com invalidação explícita quando o dado muda. Uma contagem ao vivo num dashboard não pertence a nenhum cache durável — sirva dinamicamente. O erro mais comum e mais difícil de debugar acontece quando duas camadas acham que são donas do mesmo valor: o Redis tem uma versão, a CDN tem outra, o navegador uma terceira, e o usuário vê uma delas imprevisivelmente. Decidir explicitamente, por dado, qual camada é a fonte de frescor — e fazer as outras camadas respeitarem essa decisão — é a disciplina que separa o caching que funciona do que serve dados velhos misteriosos.

10.4 As estratégias — como o cache e a fonte se relacionam

Há padrões bem definidos de como o cache interage com a fonte da verdade (o banco), na leitura e na escrita. Cada um faz um trade-off diferente entre simplicidade, frescor e latência:

EstratégiaComo funcionaTrade-off
Cache-aside (lazy)App lê o cache; no miss, busca no banco e popula o cacheSimples, comum; risco de stampede e dado velho na janela do TTL
Read-throughO cache busca no banco sozinho no miss (a app só fala com o cache)Código mais limpo; o cache vira ponto de falha
Write-throughToda escrita vai ao cache E ao banco, juntosCache sempre fresco; escrita mais lenta (escreve em dois lugares)
Write-back (write-behind)Escreve no cache; persiste no banco depois, asyncEscrita rápida; RISCO DE PERDA se o cache cai antes de persistir
Cache-aside é o default; write-back é o perigo
O padrão mais comum, e um bom default, é o cache-aside: a aplicação olha o cache, e se não acha (miss), busca no banco e guarda no cache para a próxima. Simples e eficaz para o caso read-heavy. Seu trade-off: durante a janela do TTL, o dado pode estar velho (se o banco mudou e o cache não soube), e há risco de stampede (próxima seção). No outro extremo, o write-back é o mais perigoso: ele escreve só no cache e persiste no banco depois, assincronamente — o que torna a escrita rapidíssima, mas significa que, se o cache cair antes de persistir, você perde dados. Write-back é tentador pela velocidade de escrita, mas usá-lo para dados que importam (qualquer coisa que você não pode perder — pagamentos, pedidos) sem um journal durável é um desastre esperando o cache reiniciar. A regra: cache-aside para a maioria; write-through quando precisa de frescor forte na escrita (e tolera a latência); write-back só para dados descartáveis ou com buffer durável. Cada estratégia move o trade-off entre velocidade de escrita, frescor e risco de perda — escolha sabendo qual você está aceitando.

10.5 A invalidação — o problema difícil

Aqui está o coração da dificuldade. Cachear é fácil; o difícil é saber quando o que você cacheou deixou de ser verdade e precisa ser removido ou atualizado. Errar a invalidação produz os dois piores resultados: dado velho (o cache continua servindo o valor antigo) ou churn excessivo (você invalida demais e perde o benefício do cache). As abordagens:

  • TTL (time-to-live): o dado expira automaticamente após um tempo fixo. Simples, sem coordenação — mas força uma escolha desconfortável: TTL longo = dado velho por mais tempo; TTL curto = mais misses, menos benefício. É a resposta padrão, e imperfeita.
  • Invalidação explícita: quando o dado muda no banco, você ativamente remove (ou atualiza) a entrada do cache. Mantém o cache fresco, mas exige que todo caminho de escrita lembre de invalidar — e uma invalidação esquecida é um bug sutil de dado velho.
  • Invalidação por evento (CDC): mudanças no banco emitem eventos que disparam a invalidação automaticamente, desacoplando a escrita da invalidação. Mais robusto, mais complexo.
  • Versionamento de chave: a chave do cache inclui uma versão; mudar o dado incrementa a versão, e as entradas antigas viram lixo que expira. Evita race conditions, ao custo de memória com versões velhas.
Delete, não update — e o padrão híbrido que vence na prática
Duas lições práticas sobre invalidação. Primeiro: ao invalidar, prefira deletar a entrada do cache em vez de atualizá-la com o novo valor. Por quê? Atualizar o cache na escrita cria uma race condition sutil: a thread A lê o valor antigo do banco, a thread B atualiza o banco e o cache para o novo valor, e então A — atrasada — escreve o valor antigo que leu por cima do cache, deixando o cache velho enquanto o banco está novo. Deletar evita isso: depois do delete, a próxima leitura simplesmente faz miss e busca o valor fresco do banco. Segundo: o padrão mais comum e robusto em produção é o híbrido TTL + invalidação explícita — você invalida explicitamente quando o dado muda (para frescor imediato no caminho crítico), e mantém um TTL como rede de segurança (para o caso de uma invalidação ser esquecida ou se perder). A invalidação explícita dá frescor; o TTL garante que, no pior caso, o dado velho não fica para sempre. Confiar só em TTL serve dado velho demais; confiar só em invalidação explícita quebra quando você esquece um caminho. O híbrido cobre os dois.

10.6 O cache stampede — quando a popularidade derruba

Um dos modos de falha mais perigosos e contraintuitivos do cache: o cache stampede (ou thundering herd). Acontece quando uma entrada popular expira e, no mesmo instante, centenas ou milhares de requisições concorrentes fazem miss ao mesmo tempo — e todas correm para o banco simultaneamente para recalcular o mesmo valor, sobrecarregando a fonte que o cache deveria proteger.

a anatomia do stampede
Uma chave popular (ex.: a home page) é lida 10.000 vezes/s,
servida do cache. Tudo bem — o banco não vê nada.

t=0: o TTL da chave expira.
t=0: as próximas 10.000 requisições/s TODAS fazem miss.
t=0: as 10.000 correm para o banco AO MESMO TEMPO para
     recalcular o MESMO valor.
t=0: o banco, que via ~0 dessa chave, recebe 10.000
     queries idênticas de uma vez → sobrecarrega → cai.

O cache protegia o banco — até o momento da expiração, quando
a proteção sumiu de uma vez e o banco levou o golpe inteiro.
Ironia: quanto MAIS popular a chave, PIOR o stampede.
As defesas contra o stampede: que só um recalcule, e que ninguém expire junto
O stampede tem soluções conhecidas, todas baseadas em duas ideias: impedir que muitos recalculem o mesmo valor ao mesmo tempo, e evitar que muitas chaves expirem no mesmo instante. As principais:

1. Locking / single-flight: quando uma chave expira, apenas uma requisição recalcula (pega um lock); as outras esperam esse recálculo e reusam o resultado, em vez de todas baterem no banco. Uma busca ao banco serve todas as 10.000.

2. TTL com jitter: adicionar um valor aleatório ao TTL de cada chave, para que chaves populares não expirem todas no mesmo segundo. Espalha as expirações no tempo, evitando o pico sincronizado.

3. Refresh-ahead (recálculo antecipado): recalcular a chave em segundo plano pouco antes de ela expirar, para que ela nunca chegue a estar ausente — o valor é renovado sem nunca haver um miss. Variante elegante: a expiração probabilística, onde cada requisição tem uma chance crescente de disparar o refresh conforme o TTL se aproxima, espalhando o recálculo no tempo.

4. Stale-while-revalidate: servir o valor velho imediatamente (sem esperar) enquanto um refresh assíncrono busca o novo em segundo plano. O usuário recebe resposta instantânea (possivelmente um pouco velha), e a próxima já é fresca. Troca frescor por nunca-bloquear.

A escolha depende do dado, mas a lição é universal: uma chave popular sem proteção contra stampede é uma bomba-relógio que dispara no momento exato em que ela expira — e quanto mais popular, maior a explosão.

10.7 Quem é dono do frescor — a fonte de quase todo bug

Vale aprofundar o ponto mais importante e mais ignorado do capítulo, porque é a causa raiz da maioria absoluta dos bugs de "por que estou vendo dado velho?". Em um sistema com múltiplas camadas de cache, o bug nasce quando mais de uma camada acha que é dona do frescor do mesmo dado.

O bug do dado velho: três camadas, três versões, e ninguém no comando
Imagine um dado servido por uma API. Ele pode ser cacheado no navegador (via header), na CDN (na borda), e no Redis (na aplicação). Você atualiza o dado no banco e invalida o Redis — mas esquece a CDN, que continua servindo a versão velha por horas; e o navegador do usuário, com seu próprio cache, serve uma terceira versão. O usuário vê dado velho, você jura que invalidou, e o bug é quase impossível de reproduzir porque depende de qual camada respondeu. A causa raiz não é uma camada com bug — é a ausência de uma decisão clara sobre qual camada é dona do frescor. A disciplina que previne isso: para cada dado, decidir explicitamente uma camada como fonte de frescor, e configurar as outras para não cachear (ou cachear por muito pouco tempo, ou respeitar a invalidação da camada dona). Dado que muda e precisa ser fresco → não deixe a CDN nem o navegador cachearem por muito tempo; deixe o Redis (que você controla) ser o dono, com invalidação explícita. Dado imutável (asset com hash) → deixe o navegador ser dono, por um ano. O princípio: uma fonte de frescor por dado, e todas as outras camadas subordinadas a ela. Caching em camadas sem essa clareza não é otimização — é uma loteria de versões.

10.8 Quando não cachear

O cache é poderoso, mas não é grátis nem universal. Há casos onde cachear é o erro — e reconhecê-los é tão importante quanto saber cachear:

  • Dado que muda quase toda leitura: sem a assimetria leitura/escrita, não há reúso. Você gasta espaço e adiciona risco de frescor sem ganho (10.2).
  • Dado que precisa ser sempre exato e atual: um saldo bancário, um estoque no checkout — onde dado velho causa dano (Cap 6, consistência forte). Cachear aqui é introduzir a possibilidade de erro num lugar que não a tolera.
  • Dado lido raramente: se quase nunca é acessado, o cache fica frio (expira antes de ser reusado) e só gasta memória. Cache rende para o que é quente.
  • Quando a complexidade não compensa: cada cache adiciona invalidação, stampede, coerência — complexidade real. Para um ganho marginal, essa complexidade pode não valer. Cache é uma resposta a um gargalo medido (Cap 3), não um enfeite automático.
Não cacheie por reflexo; cacheie o quente e caro, medido
O reflexo de "está lento, põe um cache" leva a cachear coisas que não precisam, herdando a complexidade do cache (invalidação, stampede, coerência) sem o benefício. A disciplina: cachear é uma decisão deliberada para dados que são simultaneamente quentes (lidos muito), caros (custosos de buscar/computar) e relativamente estáveis (mudam pouco em relação às leituras). Esse é o ponto-doce onde o cache rende muito mais do que custa. Fora dele — dado frio, ou barato de buscar, ou que muda toda hora — o cache adiciona risco e complexidade por pouco ganho. Como tudo em design, o cache é um trade-off, não um bem absoluto: você troca frescor e espaço e complexidade por velocidade, e essa troca só compensa onde a assimetria favorece. Antes de cachear, meça (este dado é mesmo o gargalo?) e qualifique (é quente, caro e estável?). Cachear o que não precisa é adicionar os problemas difíceis da computação ao seu sistema de graça.

10.9 Estudo de caso — o cache que derrubou o banco

"Adicionamos cache e o sistema ficou menos estável"
O cenário

Um site de notícias adicionou um cache (cache-aside com TTL) na frente do banco para aguentar o tráfego. Funcionou — a carga no banco despencou. Mas o sistema ficou menos estável: a cada poucos minutos, picos súbitos de latência e, ocasionalmente, o banco caía por alguns segundos. Contraintuitivo: o cache, que deveria proteger o banco, parecia estar causando os incidentes. O que estava acontecendo?

Passo 1 · O stampede da home page
diagnosticando os picos
A home page era a chave mais quente: ~20.000 req/s, servida
do cache com TTL de 60s. Por 60 segundos: banco vê ~0.

A cada 60s, no instante da expiração:
  - as 20.000 req/s simultâneas fazem miss de uma vez
  - as 20.000 correm para o banco para recalcular a home
  - o banco recebe 20.000 queries idênticas num piscar
  - latência explode, banco às vezes cai

O padrão dos picos "a cada poucos minutos" batia EXATAMENTE
com a expiração das chaves quentes. Era cache stampede.
Passo 2 · As chaves expiravam todas juntas
o agravante: expiração sincronizada
Pior: muitas chaves quentes foram populadas ao MESMO tempo
(quando o cache foi ligado, ou após um restart) — todas com
TTL de 60s. Então elas expiravam todas JUNTAS, a cada 60s.

Não era só o stampede de UMA chave: era o stampede de
DEZENAS de chaves quentes simultaneamente, sincronizadas
no mesmo segundo. Um tsunami periódico no banco.
Passo 3 · As três defesas aplicadas
as correções, combinadas
1. SINGLE-FLIGHT (lock): quando uma chave quente expira,
   só UMA requisição recalcula; as outras esperam e reusam.
   20.000 misses → 1 query ao banco, não 20.000.

2. TTL COM JITTER: TTL = 60s ± aleatório(0-15s). As chaves
   param de expirar todas no mesmo segundo — as expirações
   se espalham no tempo, sem o tsunami sincronizado.

3. STALE-WHILE-REVALIDATE para a home: servir a versão
   velha (segundos de atraso são invisíveis numa home de
   notícias) enquanto um refresh assíncrono busca a nova.
   A chave quente NUNCA fica ausente → nunca há miss em massa.
Passo 4 · O resultado

Com as três defesas, o banco parou de ver os tsunamis periódicos. O single-flight colapsou os 20.000 misses simultâneos em uma query; o jitter desincronizou as expirações; e o stale-while-revalidate fez as chaves mais quentes nunca ficarem ausentes. A carga no banco virou suave e previsível, e os picos de latência sumiram. O cache finalmente protegia o banco em vez de periodicamente desabrigá-lo.

Resultado: o cache que "deixou o sistema menos estável" não estava com defeito — estava sem proteção contra stampede, o modo de falha clássico onde a expiração de chaves quentes desabriga o banco de uma vez. A ironia central: o cache reduz a carga no banco até o momento da expiração, quando toda a carga reprimida explode de uma vez sobre uma fonte que não a vê há minutos. As defesas — single-flight (um recalcula, não todos), TTL com jitter (não expirar junto) e stale-while-revalidate (nunca ficar ausente) — atacam as duas raízes do stampede: muitos recalculando o mesmo valor, e muitas chaves expirando ao mesmo tempo. A lição: adicionar um cache não é só "guardar o resultado"; é assumir a responsabilidade pelos seus modos de falha, e o stampede é o mais traiçoeiro porque ele só aparece sob carga real, no instante exato da expiração — invisível na demo, devastador em produção. Próximo capítulo: filas e mensageria — o componente que desacopla no tempo (o síncrono-vs-assíncrono do Cap 4 tornado concreto), absorvendo picos e conectando partes do sistema que não precisam esperar umas pelas outras.

10.10 Erros comuns

Erro 1 · Cachear sem proteção contra stampede

Pôr uma chave popular no cache com TTL e nada mais — e ver o banco levar um tsunami de misses simultâneos toda vez que ela expira. Quanto mais popular a chave, pior. Use single-flight (um recalcula), TTL com jitter (não expirar junto) ou stale-while-revalidate (nunca ficar ausente) para chaves quentes.

Erro 2 · Duas camadas donas do mesmo frescor

Cachear o mesmo dado no navegador, na CDN e no Redis sem decidir qual é dono da validade — e servir versões diferentes imprevisivelmente. A causa raiz dos bugs de dado velho. Para cada dado, eleja uma camada como fonte de frescor e subordine as outras a ela.

Erro 3 · Atualizar o cache em vez de deletar

Na escrita, escrever o novo valor no cache — criando uma race condition onde uma thread atrasada sobrescreve com o valor velho. Prefira deletar a entrada: a próxima leitura faz miss e busca o valor fresco do banco, sem corrida. Delete, não update.

Erro 4 · Confiar só em TTL para dado que precisa ser fresco

Usar só TTL para um dado que muda e precisa refletir a mudança rápido — e servir dado velho durante toda a janela do TTL. Para frescor, use invalidação explícita na escrita, com TTL como rede de segurança (o híbrido). Só-TTL serve dado velho; só-invalidação quebra quando você esquece um caminho.

Erro 5 · Write-back para dados que importam

Usar write-back (escreve no cache, persiste depois) para dados que você não pode perder — e perdê-los quando o cache reinicia antes de persistir. Write-back é rápido mas arriscado; use-o só para dados descartáveis ou com buffer durável. Para dados críticos, write-through ou cache-aside.

Erro 6 · Cachear o que muda quase toda leitura

Cachear dados sem a assimetria leitura/escrita — que mudam quase tão rápido quanto são lidos. Você gasta espaço e adiciona risco de frescor sem reúso. Cache rende para o que é lido muito mais do que muda; sem essa assimetria, não cacheie.

Erro 7 · Cachear dado que precisa ser exato

Cachear um saldo, um estoque de checkout, ou qualquer dado onde velho causa dano — introduzindo a possibilidade de erro num lugar que exige consistência forte (Cap 6). Alguns dados não devem ser cacheados de forma durável; sirva-os da fonte da verdade, sempre atuais.

Verifique seu entendimento
Você adicionou um cache (cache-aside, TTL de 60s) na frente do banco para a sua página mais popular, que recebe 15.000 requisições por segundo. A carga no banco caiu — mas agora, a cada minuto, há um pico súbito de latência e às vezes o banco fica sobrecarregado. Qual é a causa e a melhor combinação de soluções?

10.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Cachear ou não?

Para cada dado, decida se vale cachear e por quê (considere a assimetria leitura/escrita e a tolerância a dado velho): (a) o catálogo de produtos de uma loja; (b) o saldo da conta de um usuário no momento do checkout; (c) o resultado de uma query analítica cara que roda sobre dados do dia anterior; (d) a posição em tempo real de um veículo num app de entrega.

(a) Catálogo de produtos → cachear, sim (caso ideal). Forte assimetria: lido milhares de vezes, atualizado raramente. E tolera dado levemente velho (um preço atualizado aparecer alguns segundos depois é aceitável, com revalidação no checkout). Cache-aside com TTL é perfeito aqui — alta taxa de acerto, ganho enorme.

(b) Saldo no checkout → NÃO cachear. Precisa ser exato e atual (Cap 6, consistência forte) — um saldo velho pode autorizar uma compra que não deveria. O dano de servir velho é real e financeiro. Sirva da fonte da verdade, sempre atual. Este é o caso clássico de "não cacheie o que precisa ser exato".

(c) Query analítica cara sobre dados de ontem → cachear, sim (caso excelente). É caro de computar (a query é pesada) e estável (os dados de ontem não mudam mais). Computar uma vez e servir do cache o dia todo é uma vitória enorme. Como o dado de origem é imutável (ontem já passou), nem há problema de invalidação — só um TTL longo ou recálculo diário. Caro + estável = cache ideal.

(d) Posição em tempo real de um veículo → NÃO cachear (ou cache de segundos). Muda constantemente (quase toda leitura tem um valor novo) — não há assimetria, não há o que reusar. Cachear por mais que um instante serve uma posição velha, que é o oposto do que "tempo real" pede. Sirva dinamicamente, ou com um cache de pouquíssimos segundos no máximo.

O insight: as duas perguntas decidem — há assimetria (lido muito mais do que muda)? e dado velho causa dano? Catálogo e query analítica têm assimetria e toleram atraso → cachear. Saldo não tolera velho (dano) → não cachear. Posição não tem assimetria (muda sempre) → não cachear. Cache não é universal; é a ferramenta certa para o dado quente, caro e tolerante a atraso.

Médio
Exercício 2 · Diagnosticar o dado velho

Um usuário relata que atualizou sua foto de perfil, mas alguns visitantes ainda veem a foto antiga por horas, enquanto outros já veem a nova — de forma imprevisível. O dado é cacheado no navegador (header), numa CDN, e no Redis da aplicação. Diagnostique a causa raiz e descreva como você corrigiria, decidindo a "dona do frescor".

O diagnóstico: o comportamento "uns veem a nova, outros a velha, imprevisivelmente" é a assinatura clássica de múltiplas camadas donas do mesmo frescor. A foto está cacheada em três lugares (navegador, CDN, Redis), e provavelmente a atualização invalidou só uma (o Redis), deixando a CDN servindo a versão velha por horas (seu TTL) e os navegadores com suas próprias cópias velhas. Qual versão um visitante vê depende de qual camada respondeu para ele — daí a imprevisibilidade. Não há "um bug"; há a ausência de uma decisão sobre quem é dono do frescor.

A correção — decidir a dona do frescor:

  • Opção A (recomendada): tornar a URL da imagem imutável e versionada. Em vez de /foto/user123.jpg (mesma URL, conteúdo que muda — o pesadelo de invalidação), use /foto/user123-v2.jpg ou /foto/abc123hash.jpg (a URL muda quando a foto muda). Agora cada versão tem uma URL única e imutável: o navegador e a CDN podem cachear por um ano sem risco, porque o conteúdo daquela URL nunca muda. Quando a foto muda, a URL muda, e todos buscam a nova naturalmente. O "dono do frescor" vira o nome — invalidação por mudança de URL, a técnica mais robusta (é como assets com hash funcionam).
  • Opção B (se a URL precisa ser estável): eleger uma camada dona e subordinar as outras. Decidir que o Redis é o dono, configurar a CDN e o navegador para não cachear a foto (ou cachear por muito pouco tempo), e invalidar o Redis na atualização. Funciona, mas perde o benefício de cache na borda — por isso a Opção A é melhor para conteúdo imutável-versionável como fotos.

O insight: o bug não se resolve "invalidando melhor" cada camada — isso é frágil (sempre se esquece uma). Resolve-se eliminando a ambiguidade sobre quem é dono. Para conteúdo que pode ser versionado por URL (imagens, assets), a melhor solução é tornar a URL imutável: o frescor passa a ser garantido pelo nome, e todas as camadas podem cachear agressivamente sem nunca servir velho.

Médio
Exercício 3 · Escolher a estratégia de escrita

Para cada caso, escolha entre cache-aside, write-through e write-back, justificando pelo trade-off (frescor, latência de escrita, risco de perda): (a) o catálogo de produtos (lido muito, escrito pouco); (b) um contador de visualizações que recebe milhares de incrementos por segundo; (c) os dados de um pedido de compra que está sendo finalizado; (d) o perfil de um usuário que ele mesmo edita ocasionalmente.

(a) Catálogo de produtos → cache-aside. Read-heavy clássico: a app lê do cache, no miss busca no banco e popula. As escritas (atualizar um produto) são raras e podem invalidar a entrada (delete). Simples e eficaz; o pequeno risco de dado velho na janela do TTL é tolerável para um catálogo. Não precisa da complexidade do write-through.

(b) Contador de visualizações (milhares de incrementos/s) → write-back. Aqui o write-back brilha: incrementar no cache (rápido, em memória) e persistir no banco em lote, periodicamente, em vez de um INSERT por view. O volume de escrita é alto demais para escrever no banco a cada incremento. O risco de perda (perder alguns incrementos se o cache cai) é tolerável para um contador de views (um número aproximado serve, Cap 6). Este é o caso onde o risco do write-back é aceitável.

(c) Pedido de compra sendo finalizado → write-through (ou nem cachear na escrita). Dados de um pedido em finalização são críticos e não podem ser perdidos. Write-back está fora de questão (risco de perda inaceitável). Write-through (escreve no cache e no banco juntos) garante que o banco — a fonte durável — sempre tem o dado, ao custo de escrita mais lenta, que é aceitável aqui (o volume de checkouts é moderado e a durabilidade é inegociável). Dado crítico exige durabilidade na escrita.

(d) Perfil editado ocasionalmente → cache-aside com invalidação na escrita. Lido com frequência, escrito raramente (o usuário edita de vez em quando). Cache-aside para as leituras; ao editar, invalidar (deletar) a entrada do cache, com TTL de rede de segurança. O usuário precisa ver sua própria edição (read-your-writes, Cap 6) — o delete na escrita garante que a próxima leitura busca o valor fresco.

O insight: a escolha depende de três fatores — frescor exigido, volume de escrita, e tolerância a perda. Read-heavy estável → cache-aside. Escrita altíssima e perda tolerável → write-back. Dado crítico (não pode perder) → write-through. E sempre invalide por delete na escrita para o read-your-writes.

Difícil
Exercício 4 · Projetar a estratégia de cache de um sistema — entrevista

Você projeta a camada de cache de uma plataforma de notícias com tráfego enorme: home page muito acessada, artigos individuais, contadores de visualização, e dados personalizados por usuário (recomendações). O entrevistador pede: "desenhe a estratégia de cache, cobrindo as camadas, a invalidação, e a proteção contra os modos de falha". Estruture a resposta tratando cada tipo de dado conforme suas características.

Enquadramento: o erro seria uma estratégia de cache única. Cada tipo de dado da plataforma tem características diferentes (frescor exigido, taxa de leitura/escrita, personalização) e merece uma abordagem própria. Vou percorrer cada um, decidindo camada, estratégia, invalidação e proteção.

1. Artigos publicados → CDN + cache de borda, agressivo. Um artigo publicado é praticamente imutável (raramente editado) e lido massivamente. É o caso ideal de cache na borda: servir da CDN, perto do usuário, com TTL longo. Para edições raras, invalidação explícita (purge da CDN) ou — melhor — versionar a URL ao editar significativamente. A CDN é a dona do frescor; o origin quase não é tocado. Tira o maior volume de tráfego da infraestrutura.

2. Home page → cache curto + proteção pesada contra stampede. A home muda com frequência (novas manchetes) mas é a chave mais quente de todas. Cache com TTL curto (segundos a um minuto), mas com todas as defesas de stampede, porque é a chave que mais sofreria: single-flight (um recalcula), TTL com jitter, e stale-while-revalidate (servir a home de segundos atrás enquanto renova — atraso invisível numa home de notícias). A home nunca pode ficar ausente sob 20k req/s.

3. Contadores de visualização → write-back no Redis. Milhares de incrementos por segundo, valor aproximado aceitável (Cap 6). Incrementar no Redis (rápido), persistir no banco em lote periodicamente. Risco de perder alguns incrementos num crash é tolerável para um contador. Não escrever no banco a cada view (esmagaria o banco — write-heavy, Cap 2).

4. Recomendações personalizadas por usuário → Redis por usuário, cache-aside, TTL moderado. Personalizadas (não cacheáveis na CDN, que serve a todos igual — cachear na borda vazaria as recomendações de um usuário para outro!). Cache-aside no Redis, chaveado por usuário, com TTL moderado (as recomendações não precisam ser instantâneas). Importante: nunca na CDN nem no navegador compartilhável, por serem dados por usuário.

5. A disciplina transversal — quem é dono do frescor: para cada dado, uma camada dona. Artigos → CDN. Home → cache de app com proteção. Contadores → Redis. Recomendações → Redis por usuário. E configurar as outras camadas para não interferir (ex.: recomendações com headers private, no-store para a CDN não cacheá-las).

O que um bom candidato demonstra: que trata cada tipo de dado conforme suas características em vez de uma estratégia única; que tira o conteúdo imutável e massivo (artigos) para a CDN; que protege a chave mais quente (home) contra stampede com todas as defesas; que escolhe write-back só onde a perda é tolerável (contadores); que reconhece o perigo de cachear dado personalizado na borda compartilhada; e que decide explicitamente a dona do frescor de cada dado. Frase de fechamento: "não há uma estratégia de cache — há uma por tipo de dado. O imutável e massivo (artigos) vai para a CDN; a chave quente e mutável (home) fica num cache curto blindado contra stampede; o contador de alta escrita usa write-back porque tolera perda; e o personalizado fica no Redis por usuário, nunca na borda compartilhada. A regra que costura tudo é uma só: para cada dado, uma camada é dona do frescor, e as outras se subordinam — porque o pior bug de cache não é lentidão, é servir dado errado, rápido."

Fim do capítulo 10
Próximo capítulo: filas e mensageria. Se o cache reduz a carga de leitura, a fila desacopla as partes do sistema no tempo — torna concreto o síncrono-vs-assíncrono do Cap 4. Ela absorve picos (o produtor despeja, o consumidor processa no seu ritmo), conecta serviços que não precisam esperar uns pelos outros, e traz seus próprios problemas: garantia de entrega, ordem, e o que fazer com o que falha. O componente que transforma "faça agora e espere" em "registre para fazer depois".
Parte III · Capítulo 11 · Os componentes

Filas e
mensageria.

A fila é o componente que torna concreto o desacoplamento no tempo do Capítulo 4. Ela permite que uma parte do sistema diga "faça isto depois" e siga em frente, enquanto outra parte processa no seu próprio ritmo — absorvendo picos, isolando falhas, conectando serviços que não precisam esperar uns pelos outros. É uma das peças mais poderosas da arquitetura distribuída. E também uma das que mais traem o iniciante, porque ela troca a simplicidade da chamada síncrona ("faço e sei o resultado") pela complexidade do assíncrono: garantia de entrega, ordem, e o que fazer com o que falha — problemas que a chamada direta nunca teve.

Este capítulo trata do componente que desacopla no tempo. Vamos ver o que exatamente a fila resolve (absorver picos, desacoplar, isolar falha), a distinção entre uma fila tradicional e um log (que muda o que é possível), as garantias de entrega (at-most-once, at-least-once, exactly-once) e por que "pelo menos uma vez com consumidor idempotente" é a escolha prática quase universal, o problema da ordem (mais sutil do que parece, e por que ordem total é cara), o que fazer com mensagens que falham (a dead letter queue), o backpressure que impede o produtor de afogar o consumidor, e como escolher entre as ferramentas (Kafka, RabbitMQ, SQS). Quando você sair daqui, a fila deixa de ser "um cano onde jogo mensagens" e vira um componente cujas garantias você escolhe conscientemente — porque a garantia errada é um bug que só aparece sob falha.

11.1 A história — do middleware corporativo ao streaming

Contexto histórico

A ideia de comunicação assíncrona por mensagens é antiga. Nos anos 1980-90, o "message-oriented middleware" (MOM) já conectava sistemas corporativos que não podiam se acoplar diretamente — o IBM MQ (1993) sendo o exemplo canônico, movendo mensagens entre mainframes de bancos e seguradoras com garantias de entrega. A motivação já era a mesma de hoje: deixar sistemas conversarem sem precisarem estar ambos disponíveis no mesmo instante, e sem um afogar o outro.

Os anos 2000 trouxeram a padronização e a democratização. O protocolo AMQP e implementações como o RabbitMQ (2007) levaram mensageria flexível e de código aberto para qualquer um — com roteamento sofisticado (exchanges, bindings) que tornava o broker "inteligente". Era o paradigma do smart broker, dumb consumer: o broker fazia o trabalho pesado de rotear, filtrar e garantir entrega; o consumidor só recebia.

A virada conceitual veio em 2011 com o Apache Kafka, criado no LinkedIn. Kafka inverteu o paradigma: dumb broker, smart consumer. Em vez de uma fila que entrega e descarta, Kafka é um log distribuído — uma sequência append-only de eventos, retida por tempo configurável, onde os consumidores controlam sua própria posição (offset) e podem reler. Essa diferença aparentemente técnica abriu possibilidades enormes: múltiplos consumidores independentes da mesma stream, replay de eventos, e a fundação do event sourcing (Cap 18). Kafka não era uma fila melhor; era uma coisa diferente que também servia de fila.

Em 2026, o ecossistema é rico e a escolha é por adequação, não por moda. Há os serviços gerenciados da nuvem (SQS, Pub/Sub) que tiram a operação das suas mãos; o RabbitMQ continua sendo a navalha suíça para filas de trabalho e roteamento flexível (e, para muitos times, o default sensato); o Kafka domina o streaming de alto volume e o replay (processando trilhões de mensagens por dia nas maiores instalações); e o Redis Streams oferece uma opção leve. As tendências são a integração com arquiteturas orientadas a eventos e o tooling de confiabilidade (dead letter queues, observabilidade de lag). Mas os conceitos fundamentais — entrega, ordem, falha, backpressure — são os mesmos do IBM MQ dos anos 90, porque os problemas de comunicação assíncrona não mudam. Este capítulo é sobre esses fundamentos.

11.2 O que a fila resolve

Antes de escolher uma fila, é preciso entender por que usá-la. A fila resolve quatro problemas distintos, e quase sempre você a adota por causa de pelo menos um deles:

  • Absorver picos (buffering): o produtor pode despejar mensagens numa rajada que o consumidor não daria conta em tempo real; a fila as segura e o consumidor processa no seu ritmo. O pico vira backlog, não queda. É o amortecedor entre a chegada e o processamento.
  • Desacoplar no tempo: o produtor não espera o consumidor (Cap 4, assíncrono). Ele registra "faça isto" e segue; o trabalho acontece depois. A resposta percebida fica rápida, e as partes não precisam estar disponíveis no mesmo instante.
  • Isolar falhas: se o consumidor cai, as mensagens ficam na fila esperando; quando ele volta, processa o backlog. A falha do consumidor não derruba o produtor nem perde trabalho — a fila é o ponto durável que sobrevive à queda de quem processa.
  • Distribuir trabalho: múltiplos consumidores puxam da mesma fila, processando em paralelo. Adicionar consumidores escala o processamento, e a fila distribui naturalmente entre eles.
A fila transforma "faça agora e espere" em "registre para fazer depois"
A mudança mental que a fila introduz: uma operação que era síncrona ("processe o pagamento e me diga se deu certo, eu espero") vira assíncrona ("registre que há um pagamento a processar; eu sigo, e o resultado vem depois"). Essa transformação é o que dá todos os benefícios — o usuário não espera, o pico é absorvido, a falha do processador não trava o checkout. Mas é também a fonte de toda a complexidade do capítulo: "o resultado vem depois" levanta perguntas que a chamada síncrona nunca teve. Vem com certeza (e se a mensagem se perder)? Vem uma vez só (e se for processada em duplicidade)? Na ordem certa? E se nunca conseguir ser processada? Cada benefício da fila tem como contrapartida uma dessas perguntas, e o resto do capítulo é sobre respondê-las. A fila não é grátis: ela troca a simplicidade do "sei o resultado agora" pelos problemas do "o resultado virá, de algum jeito, depois".

11.3 Fila vs log — a distinção que muda tudo

Há duas famílias de sistemas de mensageria, com modelos fundamentalmente diferentes, e confundi-las leva a escolher a ferramenta errada. A diferença está no que acontece com a mensagem depois de consumida.

Fila tradicional (RabbitMQ, SQS)

A mensagem é entregue a um consumidor e, após confirmada (ack), é removida. A fila é um cano: mensagens entram, são processadas, e somem. Modelo de trabalho a fazer — cada tarefa é feita uma vez por um worker. O broker é "inteligente" (roteia, distribui).

Log (Kafka, Redis Streams)

As mensagens são append-only num log e retidas por um tempo configurável, mesmo após consumidas. Vários consumidores independentes leem a mesma stream, cada um com sua posição (offset), e podem reler. Modelo de histórico de eventos — o que aconteceu, disponível para reprocessar. O consumidor é "inteligente" (controla o que lê).

O log permite reler e ter muitos consumidores; a fila, não
A diferença prática mais importante: num log, as mensagens não somem após consumidas — ficam retidas, o que permite duas coisas que uma fila tradicional não faz. Primeiro, replay: se um consumidor processou tudo errado por uma semana (um bug), você aponta um novo consumidor para o offset de uma semana atrás e reprocessa tudo. Numa fila, as mensagens já foram removidas — o trabalho perdido é irrecuperável. Segundo, múltiplos consumidores independentes: o serviço de analytics, o de notificações e o de auditoria podem todos ler a mesma stream de eventos, cada um no seu ritmo, sem interferir. Numa fila, uma mensagem entregue a um consumidor some para os outros. Por isso o log é a base do event sourcing (Cap 18) e de arquiteturas onde o mesmo evento alimenta vários sistemas. A regra de escolha: precisa de uma fila de trabalho (cada tarefa feita uma vez)? Fila tradicional. Precisa de replay, ou de vários consumidores na mesma stream, ou de um histórico de eventos? Log. Escolher fila quando precisava de log significa descobrir, tarde, que não pode reprocessar o que deu errado.

11.4 As garantias de entrega — o trade-off central

Quando a rede não é confiável (Cap 8), entregar uma mensagem "uma vez exata" é surpreendentemente difícil. Há três níveis de garantia, e a escolha entre eles é uma das decisões mais importantes ao usar uma fila:

GarantiaO que prometeO risco
At-most-once (no máximo uma)Entrega 0 ou 1 vez; nunca duplicaPode perder mensagens
At-least-once (pelo menos uma)Entrega 1 ou mais vezes; nunca perdePode duplicar mensagens
Exactly-once (exatamente uma)Entrega 1 vez exataCaro e complexo; garantias limitadas na prática
"Pelo menos uma vez + consumidor idempotente" é a resposta prática
A escolha quase universal em produção, e por boas razões: at-least-once com consumidores idempotentes. Veja por quê. At-most-once (pode perder) é inaceitável para quase tudo que importa — você não quer "perder" um pagamento ou um pedido. Exactly-once é o sonho, mas é caro, complexo, e na prática tem garantias limitadas (frequentemente é "exactly-once" só dentro de um sistema específico, sob condições específicas — e a falácia da rede, Cap 8, torna o "exatamente uma vez" de ponta a ponta quase impossível de garantir de verdade). Sobra o at-least-once: a mensagem nunca se perde, mas pode chegar duplicada (porque, sob incerteza de rede, reenviar é a única forma de não perder — Cap 8). E o duplicado é um problema solucionável: se o consumidor é idempotente (Cap 16) — processar a mesma mensagem duas vezes tem o mesmo efeito que processá-la uma —, a duplicação deixa de importar. Você obtém o efeito prático do exactly-once (cada coisa acontece uma vez, em efeito) com a robustez do at-least-once (nada se perde), pagando o preço de tornar os consumidores idempotentes. Esta combinação é tão dominante que "use at-least-once e torne o consumidor idempotente" é quase um reflexo de design — e o capítulo 16 é inteiro sobre como tornar algo idempotente.

11.5 O problema da ordem

"As mensagens chegam na ordem em que foram enviadas?" — uma pergunta que parece trivial e cuja resposta honesta é "depende, e garantir ordem total é caro". A ordem é mais sutil do que o iniciante imagina.

Ordem total é cara; ordem por chave é o que você quase sempre quer
Garantir que todas as mensagens sejam processadas na ordem global exata em que foram enviadas (ordem total) é caro, porque exige serializar tudo por um único ponto — o que mata o paralelismo (você não pode ter vários consumidores processando em paralelo se a ordem global precisa ser respeitada; é o gargalo serial de Amdahl, Cap 7). Mas — e aqui está a chave — você quase nunca precisa de ordem total. O que você precisa é de ordem dentro de cada entidade relacionada: as mensagens de um mesmo usuário, ou de um mesmo pedido, na ordem certa; entre usuários ou pedidos diferentes, a ordem não importa. Essa é a ordem por chave de partição: o sistema garante ordem dentro de uma partição (definida por uma chave — o user ID, o order ID), mas permite que partições diferentes sejam processadas em paralelo e em qualquer ordem entre si. É exatamente o que o Kafka faz: ordem dentro da partição, paralelismo entre partições. Você usa o user ID como chave de partição, e todas as mensagens daquele usuário ficam ordenadas, enquanto usuários diferentes processam em paralelo. A lição: não peça ordem total (cara, serial); identifique a chave dentro da qual a ordem importa, e particione por ela — ordem onde precisa, paralelismo onde pode. É a mesma sabedoria do Cap 7: o serial (ordem total) limita; particionar (ordem por chave) libera.

11.6 O que falha — a dead letter queue

Uma pergunta que o iniciante esquece e que vira incidente: o que acontece com uma mensagem que o consumidor não consegue processar? Talvez esteja malformada, ou dispare um bug, ou referencie algo que não existe. Se você simplesmente retenta para sempre, essa mensagem "venenosa" (poison message) trava a fila e consome recursos infinitamente.

A poison message que trava a fila — e a DLQ que a isola
O cenário: uma mensagem malformada chega à fila. O consumidor tenta processá-la, falha, e a mensagem volta para a fila (at-least-once: não perde). O consumidor tenta de novo, falha de novo, devolve de novo — para sempre. Pior, se a ordem é preservada, essa mensagem venenosa pode bloquear todas as mensagens atrás dela, travando a fila inteira por causa de um único item ruim. A solução é a dead letter queue (DLQ): depois de N tentativas falhas, a mensagem é movida para uma fila separada — a DLQ — em vez de retentada infinitamente. Isso faz três coisas essenciais: tira a mensagem venenosa do caminho (a fila principal volta a fluir), preserva a mensagem (ela não é perdida, está na DLQ para inspeção), e sinaliza um problema (alguém olha a DLQ, descobre o bug, corrige, e reprocessa). A DLQ é uma das peças mais subestimadas de uma fila — sem ela, uma única mensagem ruim pode derrubar o processamento inteiro, e você perde a visibilidade do que está falhando. Toda fila de produção precisa de uma DLQ com monitoramento; "o que acontece com o que falha?" não é um detalhe, é parte do design. E cuidado com loops: a própria DLQ e o reprocessamento precisam de salvaguardas (circuit breakers, Cap 20) para a mensagem ruim não voltar eternamente.

11.7 Backpressure — quando o produtor é rápido demais

A fila absorve picos, mas não é infinita. Se o produtor produz consistentemente mais rápido do que o consumidor consome, a fila cresce sem limite — o backlog se acumula, a memória/disco enche, e a latência das mensagens (o tempo entre entrar e ser processada) explode. O backpressure é o mecanismo que comunica "vá mais devagar" de volta ao produtor.

A fila não é mágica: se o consumo não acompanha a produção, o backlog explode
Um erro conceitual comum é tratar a fila como capacidade infinita — "joga na fila que ela aguenta". Ela aguenta picos temporários (é para isso que serve, absorver rajadas), mas não um desequilíbrio sustentado. É a lei de Little (Cap 7) de novo: se a taxa de chegada excede a de processamento por tempo suficiente, o número de itens na fila (L) cresce indefinidamente, e o tempo de espera (W) com ele. A fila adia o problema, não o resolve — eventualmente ela enche (esgota memória/disco) ou as mensagens ficam tão atrasadas que são inúteis (um email de confirmação que chega 6 horas depois). O backpressure é a resposta: quando a fila cresce demais, o sistema empurra a pressão de volta — desacelera ou rejeita os produtores (com erros que dizem "tente mais tarde"), em vez de aceitar trabalho que não consegue processar. Sem backpressure, um produtor rápido demais transforma a fila num buraco que enche até estourar. A lição: a fila absorve a variância (picos sobre uma média sustentável), não um déficit permanente de capacidade — para isso, ou você escala os consumidores, ou aplica backpressure para não aceitar mais do que consegue fazer. Dimensionar o consumo para a média da carga, com a fila absorvendo os picos, e backpressure para o pior caso, é o design correto.

11.8 Escolher a ferramenta

Com os conceitos no lugar, a escolha entre as ferramentas se torna uma questão de adequação ao problema, não de moda:

Você precisa de…Ferramenta típicaPor quê
Fila de trabalho simples, roteamento flexível, request-replyRabbitMQBroker inteligente, roteamento rico; default sensato para a maioria
Fila gerenciada, na nuvem (AWS), zero operaçãoSQSSem broker para operar; standard (at-least-once) ou FIFO (ordenado)
Streaming de alto volume, replay, muitos consumidores na mesma stream, event sourcingKafkaÉ um log: retenção, replay, consumer groups, ordem por partição
Mensageria leve, já usa RedisRedis StreamsSimples, leve; menos garantias de durabilidade que os outros
Comece com o mais simples que resolve; o boring vence
A sabedoria de escolha aqui ecoa o "choose boring technology" do Cap 1: comece com a ferramenta mais simples que atende seu requisito, e adicione complexidade só quando o requisito a exigir. Para a maioria dos times e casos — filas de trabalho, jobs em background, processamento assíncrono, pub/sub modesto —, uma fila tradicional (RabbitMQ gerenciado, ou SQS se você está na AWS) é a resposta certa, e Kafka seria complexidade prematura. Kafka é magnífico quando você precisa do que ele oferece: alto volume de streaming, replay de eventos, múltiplos consumidores independentes na mesma stream, event sourcing. Adotá-lo sem esses requisitos é comprar a operação de um sistema distribuído complexo (mesmo gerenciado, é mais peças para entender) para resolver um problema que uma fila simples resolveria. A pergunta não é "qual é a ferramenta mais poderosa?" (é Kafka, quase sempre) — é "qual é a mais simples que atende meu requisito real?". Se você não precisa de replay nem de múltiplos consumidores na mesma stream, provavelmente não precisa de um log; uma fila basta, e é mais simples de operar e raciocinar.

11.9 Estudo de caso — o pico de Black Friday que não derrubou nada

Como uma fila transformou um pico fatal em backlog gerenciável
O cenário

Um e-commerce processava pedidos de forma síncrona: ao finalizar a compra, o checkout esperava o sistema cobrar o cartão, baixar o estoque, gerar a nota fiscal, e disparar os emails — tudo antes de responder ao cliente. Funcionava no dia a dia. Na primeira Black Friday, o pico de pedidos derrubou o sistema: o processamento síncrono não dava conta da rajada, os checkouts travavam, e os clientes viam erros. A solução foi rearquitetar o fluxo em torno de uma fila.

Passo 1 · Separar o que precisa ser síncrono do que não precisa
o que o cliente precisa esperar?
SÍNCRONO (o cliente espera, tem que ser confiável agora):
  - Validar o pedido e reservar o estoque (não pode oversell)
  - Registrar o pedido de forma durável

ASSÍNCRONO (pode acontecer depois, o cliente não espera):
  - Cobrar o cartão (segundos depois é ok)
  - Gerar a nota fiscal
  - Enviar emails de confirmação
  - Atualizar analytics, recomendações

Insight: o cliente só precisa saber "seu pedido foi recebido".
O resto pode virar mensagens numa fila, processadas depois.
Passo 2 · A fila absorve o pico
o pico vira backlog, não queda
Agora o checkout: valida, reserva estoque, registra o pedido,
publica uma mensagem "pedido X criado" na fila, e responde
ao cliente "pedido recebido!" — rápido. FIM da parte síncrona.

Na Black Friday, 10x o pico normal:
  - ANTES: o processamento síncrono não dava conta → travava
  - AGORA: o checkout (leve) aguenta; o excesso de trabalho
    pesado (cobrança, NF, emails) vira BACKLOG na fila, que
    os consumidores processam no seu ritmo. O pico de pedidos
    vira uma fila mais longa, não um sistema caído.

Os emails chegam alguns minutos depois no pico — aceitável.
Ninguém vê erro. O pico foi ABSORVIDO, não sofrido.
Passo 3 · As garantias certas para cada mensagem
entrega, ordem, idempotência
ENTREGA: at-least-once (não pode PERDER uma cobrança).
  → consumidores IDEMPOTENTES: cobrar duas vezes a mesma
    mensagem (duplicada pela rede) tem o efeito de cobrar uma
    (chave de idempotência por pedido, Cap 16). Resolve o
    risco do at-least-once.

ORDEM: por chave de PEDIDO. As mensagens de um mesmo pedido
  (criar → cobrar → faturar) em ordem; pedidos diferentes em
  paralelo. Particiona por order_id. Ordem onde importa,
  paralelismo onde pode.

FALHA: cobrança que falha N vezes (cartão recusado, bug) →
  DLQ. Não trava a fila, não se perde, alguém investiga.
Passo 4 · O backpressure e o resultado

E uma salvaguarda: se o backlog crescesse demais (consumo não acompanhando por muito tempo, não só um pico), o sistema escalaria os consumidores automaticamente; e se mesmo assim não desse conta, aplicaria backpressure no que fosse adiável. O resultado da Black Friday seguinte: o checkout respondeu rápido o tempo todo, o pico de pedidos virou um backlog de processamento que se resolveu em minutos, os emails chegaram com um pequeno atraso (invisível para a experiência), e nada caiu. O mesmo pico que antes derrubava o sistema agora era apenas uma fila temporariamente mais longa.

Resultado: a fila transformou um pico fatal em backlog gerenciável, mas o segredo não foi só "adicionar uma fila" — foi a sequência completa de decisões. Separar o síncrono (o que o cliente precisa esperar: reservar estoque, registrar o pedido) do assíncrono (o que pode esperar: cobrar, faturar, notificar) reduziu o caminho crítico ao mínimo. A fila então absorveu o pico do trabalho pesado, transformando uma rajada que derrubava o sistema numa fila mais longa que se drena no tempo. E as garantias certas tornaram isso correto, não só rápido: at-least-once com consumidores idempotentes (não perde cobrança, duplicação não importa), ordem por chave de pedido (ordem onde importa, paralelismo onde pode), DLQ para o que falha (não trava, não perde), e backpressure como rede de segurança. Cada conceito do capítulo apareceu, e cada um era necessário. A lição central: a fila é poderosa, mas seu poder vem de usá-la com as garantias certas — at-least-once + idempotência, ordem por chave, DLQ, backpressure — não de simplesmente jogar mensagens num cano. Próximo capítulo: bancos de dados — SQL e NoSQL, o componente que guarda a verdade, e a decisão sobre o modelo de dados que é, de todas, a mais difícil de reverter (Cap 1, porta de mão única).

11.10 Erros comuns

Erro 1 · Esquecer a idempotência com at-least-once

Usar a entrega at-least-once (a escolha certa) sem tornar os consumidores idempotentes — e processar mensagens duplicadas como se fossem novas (cobrar duas vezes). At-least-once vai duplicar sob falha de rede; o consumidor idempotente é o que torna isso seguro. As duas decisões são inseparáveis.

Erro 2 · Buscar exactly-once de verdade

Gastar enorme complexidade tentando garantir entrega "exatamente uma vez" de ponta a ponta — que é caro e, pela falácia da rede, quase impossível de verdade. A resposta prática é at-least-once + consumidor idempotente, que dá o efeito do exactly-once com a robustez do at-least-once. Não persiga o exactly-once; torne o consumidor idempotente.

Erro 3 · Exigir ordem total quando precisa de ordem por chave

Pedir que todas as mensagens sejam processadas na ordem global exata — matando o paralelismo (serializa tudo, gargalo de Amdahl). Você quase sempre só precisa de ordem dentro de cada entidade (um usuário, um pedido). Particione por essa chave: ordem onde importa, paralelismo entre chaves.

Erro 4 · Não ter dead letter queue

Deixar mensagens que falham serem retentadas para sempre — uma poison message trava a fila, consome recursos, e (se a ordem é preservada) bloqueia tudo atrás dela. Toda fila de produção precisa de DLQ: após N tentativas, a mensagem vai para uma fila separada, monitorada, sem travar a principal nem se perder.

Erro 5 · Tratar a fila como infinita

Achar que "joga na fila que ela aguenta" sem limite — e ver o backlog crescer até estourar memória/disco ou tornar as mensagens inúteis pelo atraso. A fila absorve picos (variância), não um déficit sustentado de capacidade (lei de Little). Escale os consumidores ou aplique backpressure quando o consumo não acompanha a produção.

Erro 6 · Escolher log quando precisa de fila (ou vice-versa)

Usar Kafka (um log) para uma fila de trabalho simples (complexidade prematura), ou usar uma fila tradicional quando precisa de replay/múltiplos consumidores (e descobrir tarde que não pode reprocessar). Pergunte: precisa reler ou ter vários consumidores na mesma stream? Log. Só fazer cada tarefa uma vez? Fila.

Erro 7 · Mover para assíncrono o que precisa ser síncrono

Enfileirar tudo "para ser rápido", inclusive o que o usuário precisa confirmar na hora (reservar o estoque, validar o pedido). Nem tudo pode ser adiado — algumas coisas o cliente precisa saber agora. Separe o síncrono crítico (o caminho mínimo) do assíncrono adiável; só este vai para a fila.

Verifique seu entendimento
Você usa uma fila com entrega at-least-once para processar pagamentos. Sob falhas de rede, ocasionalmente a mesma mensagem de pagamento é entregue duas vezes ao consumidor. Como evitar cobrar o cliente em duplicidade, sem abrir mão da garantia de não perder pagamentos?

11.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Síncrono ou assíncrono?

Para cada operação no fluxo de "publicar um post numa rede social", decida se deve ser síncrona (o usuário espera) ou assíncrona (vai para uma fila), justificando: (a) salvar o texto do post; (b) gerar miniaturas das imagens anexadas; (c) notificar os seguidores; (d) atualizar o índice de busca; (e) verificar se o conteúdo viola as regras (moderação).

(a) Salvar o texto → síncrono. O usuário precisa de confirmação imediata de que o post foi criado, e ver seu próprio post (read-your-writes, Cap 6). É o caminho crítico mínimo: registrar o post de forma durável e responder "publicado!". Rápido e síncrono.

(b) Gerar miniaturas → assíncrono. Transcodificar imagens é trabalho pesado que o usuário não precisa esperar — o post pode aparecer e as miniaturas chegarem segundos depois. Vai para a fila, processado por workers. Fazer isso síncrono travaria o checkout do post no pico.

(c) Notificar seguidores → assíncrono. O fan-out para seguidores (potencialmente milhões, Cap 21) é pesado e não precisa ser instantâneo — as notificações chegando alguns segundos depois é aceitável. Fila, sem dúvida; fazer síncrono seria impossível em escala.

(d) Atualizar índice de busca → assíncrono. O post ficar pesquisável alguns segundos depois de publicado é perfeitamente aceitável. Vai para a fila; a indexação acontece em background. Não é caminho crítico.

(e) Moderação → depende da política (caso interessante). Se a moderação é bloqueante (o post não aparece até ser aprovado), é síncrona ou quase — mas isso adiciona latência e pode ser inviável em escala. O padrão comum é assíncrono com publicação otimista: o post aparece imediatamente, a moderação roda em background (fila), e remove o conteúdo se violar. Para conteúdo de alto risco, pode-se moderar antes (síncrono); para o geral, depois (assíncrono). A decisão é de produto/risco, não só técnica.

O insight: o caminho crítico síncrono deve ser o mínimo que o usuário precisa confirmar agora (salvar o post). Todo o resto — trabalho pesado, fan-out, indexação — vai para filas, processado depois. Isso mantém a resposta rápida e absorve picos. A moderação é o caso de fronteira, decidido pelo risco do conteúdo.

Médio
Exercício 2 · Escolher a garantia e a ordem

Para cada cenário, escolha a garantia de entrega apropriada e diga se (e como) a ordem importa: (a) métricas de telemetria de servidores; (b) eventos de débito/crédito numa conta bancária; (c) mensagens de um chat entre dois usuários; (d) cliques para um sistema de analytics.

(a) Telemetria de servidores → at-most-once aceitável; ordem não importa muito. Perder uma métrica ocasional entre milhares é tolerável (a próxima amostra vem logo) — at-most-once (não duplica, pode perder) é aceitável e mais barato. A ordem exata entre métricas raramente importa (você agrega). Volume altíssimo, criticidade baixa: a garantia mais leve serve.

(b) Débito/crédito bancário → at-least-once + idempotência; ordem por conta CRÍTICA. Não pode perder uma transação (at-least-once), e não pode duplicar (consumidor idempotente). E a ordem importa muito, por conta: aplicar um débito antes ou depois de um crédito pode mudar se uma transação é aprovada (saldo). Particione por conta: ordem estrita dentro de cada conta, contas diferentes em paralelo. Este é o caso mais exigente.

(c) Chat entre dois usuários → at-least-once + idempotência; ordem por conversa. Não pode perder mensagens (at-least-once), duplicada é confusa (idempotência por ID de mensagem). Ordem importa dentro da conversa (uma resposta depois da pergunta — consistência causal, Cap 6) — particione por ID da conversa. Entre conversas diferentes, a ordem não importa.

(d) Cliques para analytics → at-least-once (ou at-most-once); ordem geralmente não importa. Dependendo da precisão exigida: se você tolera perder alguns cliques, at-most-once; se quer contagem precisa, at-least-once com deduplicação. A ordem global raramente importa para analytics agregado (você conta, não sequencia). Volume alto; a garantia depende de quão exata a análise precisa ser.

O insight: a garantia escala com a criticidade (telemetria tolera perda; dinheiro não tolera perda nem duplicação), e a ordem quase nunca é total — é por chave (conta, conversa) onde importa, e irrelevante onde os eventos são independentes. Identifique a chave de ordenação e a criticidade, e a escolha se segue.

Médio
Exercício 3 · Diagnosticar o backlog crescente

Você monitora uma fila e observa que o backlog (número de mensagens esperando) cresce continuamente ao longo do dia, e a latência das mensagens (tempo entre entrar e ser processada) passou de segundos para horas. (a) O que está acontecendo, em termos da lei de Little? (b) Por que adicionar mais espaço de fila não resolve? (c) Quais são as soluções reais?

(a) Em termos da lei de Little (Cap 7): o backlog crescer continuamente significa que a taxa de chegada (λ, mensagens entrando) está consistentemente maior que a taxa de processamento (a vazão dos consumidores). Pela lei de Little, L = λ × W: se λ excede a capacidade de processamento de forma sustentada, o número de mensagens na fila (L) cresce sem limite, e o tempo de espera (W) cresce junto — daí a latência ir de segundos para horas. Não é um pico (que a fila absorveria e drenaria); é um déficit sustentado de capacidade de consumo.

(b) Por que mais espaço de fila não resolve: aumentar o tamanho da fila só adia o estouro — se a produção excede o consumo continuamente, qualquer fila finita eventualmente enche, e enquanto isso a latência só piora (mais mensagens esperando = espera mais longa). A fila não é o gargalo; o consumo é. Dar mais espaço a um problema de capacidade é como comprar uma caixa de correio maior para uma correspondência que você nunca abre — o acúmulo continua.

(c) As soluções reais:

  • Escalar os consumidores: a solução mais direta — adicionar mais workers processando em paralelo, aumentando a vazão até superar a taxa de chegada e começar a drenar o backlog. (Idealmente, autoscaling baseado no tamanho do backlog.)
  • Acelerar cada consumidor: otimizar o processamento de cada mensagem (mais rápido = mais vazão por worker). Às vezes o gargalo é uma dependência lenta (o banco, uma API externa) que cada consumidor espera.
  • Backpressure / reduzir a entrada: se não dá para acelerar o consumo o suficiente, reduzir a produção — rejeitar ou desacelerar produtores, priorizar mensagens importantes, descartar as descartáveis. Aceitar menos do que não consegue processar.
  • Reexaminar o trabalho: talvez parte do que está na fila não precise ser feito, ou possa ser agrupado (batch) para eficiência.

O insight: backlog crescente é sempre um desequilíbrio entre produção e consumo sustentado, não um problema de tamanho de fila. A fila revela o déficit (acumulando), mas a cura é do lado do consumo (escalar/acelerar) ou da entrada (backpressure) — nunca de dar mais espaço ao acúmulo.

Difícil
Exercício 4 · Projetar o processamento assíncrono de um sistema — entrevista

Você projeta o backend de uma plataforma de processamento de vídeo: usuários fazem upload de vídeos, que precisam ser transcodificados para várias resoluções, ter miniaturas geradas, ser analisados por moderação, e então publicados. O entrevistador pede: "desenhe o processamento assíncrono com filas". Estruture a resposta cobrindo o que é síncrono vs assíncrono, as garantias, a ordem, o tratamento de falhas, e o backpressure.

Enquadramento: processamento de vídeo é o caso paradigmático de fila — trabalho pesado, demorado, que não pode bloquear o upload. A chave é decompor o pipeline em etapas, decidir as garantias de cada uma, e tratar falha e backpressure desde o início.

1. Síncrono (o mínimo que o usuário espera): receber o upload do vídeo, armazená-lo de forma durável (object storage, Cap 4), registrar o vídeo como "processando", e responder ao usuário "upload recebido, processando". Rápido. Tudo o mais é assíncrono — o usuário não espera a transcodificação (que leva minutos).

2. O pipeline assíncrono (etapas em fila): ao registrar o vídeo, publicar uma mensagem que dispara o pipeline. As etapas:

  • Transcodificação para múltiplas resoluções (240p a 4K) — o trabalho mais pesado, paralelizável (cada resolução pode ser um job). Workers dedicados, possivelmente com hardware específico (GPU).
  • Geração de miniaturas — mais leve, paralelo à transcodificação.
  • Moderação — analisar o conteúdo; pode ser bloqueante para publicação (o vídeo só vai ao ar se passar).
  • Publicação — quando transcodificação e moderação terminam, marcar como publicado e notificar o usuário.

3. Garantias de entrega: at-least-once com workers idempotentes. Não pode perder um vídeo a processar; e se uma mensagem de transcodificação for entregue duas vezes (falha de rede), o worker idempotente reconhece que aquela resolução daquele vídeo já foi feita e não refaz (chave: vídeo_id + resolução). Transcodificar duas vezes desperdiça recursos; a idempotência evita.

4. Ordem: dentro de um vídeo, há dependências (a publicação só depois da transcodificação e moderação) — isso é orquestração de pipeline, não só ordem de fila (pode ser modelado com etapas que disparam a seguinte ao concluir, ou um orquestrador — Saga, Cap 17). Entre vídeos diferentes, total paralelismo (não há ordem entre vídeos de usuários diferentes). Particionar/paralelizar por vídeo.

5. Tratamento de falhas (DLQ): um vídeo corrompido ou um formato não suportado fará a transcodificação falhar repetidamente — uma poison message. Após N tentativas, vai para a DLQ, o vídeo é marcado como "falha no processamento" (o usuário é avisado), e a fila principal não trava. A DLQ é monitorada para detectar padrões (um codec novo quebrando tudo?).

6. Backpressure e escala: a transcodificação é cara e o volume varia muito (viraliza um evento, todos sobem vídeos). Os workers escalam com o backlog (autoscaling pelo tamanho da fila). Se mesmo assim o backlog cresce demais, backpressure: o sistema continua aceitando uploads (não pode rejeitar o usuário facilmente), mas comunica tempos de processamento maiores, e prioriza (vídeos de criadores premium primeiro?). O upload (leve) raramente precisa de backpressure; a transcodificação (pesada) é onde o gargalo está.

O que um bom candidato demonstra: que reduz o síncrono ao mínimo (upload + registro); que decompõe o pipeline em etapas com filas; que escolhe at-least-once + idempotência (transcodificar duas vezes desperdiça); que reconhece as dependências dentro de um vídeo (orquestração) vs o paralelismo entre vídeos; que projeta a DLQ para vídeos que falham; e que pensa no backpressure no ponto caro (transcodificação). Frase de fechamento: "o upload é síncrono e mínimo — recebo, guardo, respondo. Todo o trabalho pesado vira um pipeline de filas: transcodificação, miniaturas, moderação, publicação, com workers idempotentes (at-least-once não pode me fazer transcodificar duas vezes) e paralelismo entre vídeos. Vídeos que falham vão para uma DLQ monitorada em vez de travar a fila, e os workers de transcodificação escalam com o backlog, com backpressure se o pico for extremo. A fila é o que transforma 'processar vídeo' de uma operação que travaria o upload numa que acontece em background, na escala que o pico exigir."

Fim do capítulo 11
Próximo capítulo: bancos de dados — SQL e NoSQL. Se a fila move dados no tempo, o banco os guarda: é o componente que abriga a verdade do sistema. E a escolha do modelo de dados é, de todas as decisões de design, a mais difícil de reverter (Cap 1, a porta de mão única por excelência). Vamos ao relacional e suas garantias (ACID), às famílias NoSQL e o que cada uma troca, e — acima de tudo — a como escolher o modelo certo para o problema, em vez de pelo hype.
Parte III · Capítulo 12 · Os componentes

Bancos de dados:
SQL e NoSQL.

O banco de dados é onde mora a verdade do sistema, e a escolha do seu modelo de dados é, de todas as decisões de design, a mais difícil de reverter — a porta de mão única por excelência (Cap 1). Trocar de framework é trabalhoso; trocar o modelo de dados de um sistema em produção, com terabytes de dados e dezenas de serviços dependentes, é uma cirurgia de risco que pode levar anos. Por isso essa escolha merece mais cuidado que qualquer outra, e merece ser feita pelo problema — não pelo hype que, por uma década, prometeu que o NoSQL substituiria o SQL, e errou.

Este capítulo trata do componente que guarda a verdade. Vamos ver por que essa é a decisão mais irreversível do design, o modelo relacional e o que o ACID garante (e cobra), as quatro famílias NoSQL e o que cada uma troca, o princípio que organiza o NoSQL (modelar pelo padrão de acesso, não pelos dados), por que escalar SQL horizontalmente é genuinamente difícil (e o NoSQL nasceu para isso), os bancos NewSQL que tentam ter os dois mundos, e a realidade de 2026 — persistência poliglota, onde se usa o banco certo para cada trabalho. Quando você sair daqui, "SQL ou NoSQL?" deixa de ser uma guerra de torcida e vira uma escolha de engenharia: qual modelo de dados o seu problema pede, sabendo o que cada um garante e o que cobra.

12.1 A história — o relacional, a revolta, e o retorno do rei

Contexto histórico

Em 1970, Edgar Codd, da IBM, publicou o modelo relacional — uma forma matematicamente fundamentada de organizar dados em tabelas com relações, consultável por uma linguagem declarativa. Foi uma revolução: em vez de programar como navegar pelos dados, você descrevia o que queria, e o banco descobria como buscar. O SQL nasceu desse modelo e, por quatro décadas, reinou quase absoluto — Oracle, depois MySQL e PostgreSQL tornaram o relacional a fundação padrão de praticamente todo sistema. O ACID, as transações, os joins, o esquema rígido: era assim que se guardavam dados, ponto.

Então veio a escala da web. Nos anos 2000, gigantes como Google e Amazon enfrentaram volumes que um único banco relacional não suportava, e o esquema rígido atrapalhava a velocidade de desenvolvimento. Surgiram os papers seminais — o BigTable do Google (2006), o Dynamo da Amazon (2007) — e, com o lançamento do MongoDB (2009) e do Cassandra, a revolta NoSQL: bancos não-relacionais que trocavam o esquema rígido e o ACID por flexibilidade e escala horizontal. A promessa, gritada alto na época, era que o NoSQL substituiria o relacional, agora visto como um dinossauro que não escalava.

Não foi o que aconteceu. O NoSQL encontrou seu nicho (escala massiva, esquema flexível, padrões de acesso específicos), mas o relacional não morreu — porque transações, consistência forte e queries complexas continuaram sendo exatamente o que muitos sistemas precisavam, e o NoSQL as sacrificava. A guerra de "um vai matar o outro" se revelou falsa: os dois coexistiam, cada um bom para coisas diferentes.

Em 2026, o pêndulo voltou a um equilíbrio maduro, e com uma reviravolta: o relacional recuperou o lugar de default. O PostgreSQL é, há anos, o banco mais usado e mais querido — "o default se moveu de volta para o Postgres", como observam os especialistas, sem invalidar o NoSQL para o que ele faz bem. E as fronteiras borraram: o Postgres ganhou suporte robusto a JSON (documentos dentro do relacional), busca vetorial, e mais; o MongoDB ganhou transações ACID; e os bancos NewSQL (Spanner, CockroachDB, TiDB) passaram a oferecer SQL com escala horizontal de NoSQL. A pergunta de 2026 raramente é "SQL ou NoSQL"; é "qual combinação, e onde cada workload traça a linha". Este capítulo é sobre traçar essa linha com critério — porque a decisão é irreversível, e o hype, como a história mostrou, é um péssimo guia.

12.2 A decisão mais irreversível

Antes dos detalhes técnicos, vale internalizar por que essa escolha pesa mais que as outras. No Cap 1, distinguimos decisões reversíveis (portas de mão dupla) de irreversíveis (mão única). O modelo de dados é a mão única mais estreita de todas.

Trocar o modelo de dados é a cirurgia mais cara que existe
Por que mudar de banco — especialmente entre modelos (relacional ↔ documento) — é tão mais difícil que outras mudanças? Porque o modelo de dados permeia tudo. Ele molda como você escreve o código (as queries, os acessos), como os dados se relacionam, quais operações são fáceis e quais são caras, e — crucialmente — uma vez que você tem terabytes de dados naquela forma, e dezenas de serviços que assumem aquele modelo, migrar significa transformar todos os dados, reescrever todos os acessos, e fazer isso sem downtime e sem perder nada, num sistema vivo. Empresas levam anos em migrações de banco, com equipes dedicadas, e muitas adiam indefinidamente porque o risco é alto demais. Compare com trocar um framework web (semanas) ou um provedor de email (dias). O modelo de dados é a fundação sobre a qual tudo o mais é construído, e refazer a fundação de um prédio habitado é a obra mais arriscada. A consequência prática: essa é a decisão onde vale gastar mais tempo pensando antes, onde o "escolha o boring/comprovado" (Cap 1) pesa mais, e onde errar custa mais caro. Não é uma decisão para seguir o hype — é uma para entender profundamente o problema e escolher o que vai servi-lo por anos.

12.3 O relacional e o ACID — o que ele garante

O modelo relacional organiza dados em tabelas com esquema fixo, ligadas por relações (chaves estrangeiras), e consultáveis com joins. Seu maior trunfo técnico são as transações ACID — quatro garantias que tornam o relacional confiável para dados que não podem errar:

A
Atomicidade
A transação acontece inteira ou nada. Transferir dinheiro (debitar A, creditar B) ou faz as duas ou nenhuma — nunca debita sem creditar, mesmo se o sistema cai no meio.
C
Consistência
A transação leva o banco de um estado válido a outro válido, respeitando as regras (restrições, chaves). Nunca deixa o banco num estado que viola suas próprias regras. (É o C de ACID — diferente do C de CAP, Cap 5.)
I
Isolamento
Transações concorrentes não interferem umas nas outras — cada uma vê o banco como se estivesse sozinha. Duas pessoas comprando o último item ao mesmo tempo não corrompem o estoque.
D
Durabilidade
Uma vez confirmada, a transação é permanente — sobrevive a quedas, faltas de energia, reinícios. O que foi commitado não se perde.
ACID é o que torna o relacional insubstituível para dinheiro e estoque
O valor do ACID fica claro num exemplo: uma transferência bancária precisa debitar uma conta e creditar outra como uma única operação indivisível. Sem atomicidade, um crash no meio deixaria o dinheiro debitado e não creditado (sumiu) — inaceitável. Sem isolamento, duas transações concorrentes na mesma conta poderiam se sobrepor e corromper o saldo. O ACID dá essas garantias de graça para o desenvolvedor — você escreve a transação, e o banco garante atomicidade, isolamento e durabilidade. É por isso que praticamente todo sistema financeiro, de inventário e regulado roda sobre um relacional: o ACID é exatamente o que esses domínios exigem, e reimplementá-lo por cima de um banco que não o tem é difícil e propenso a erros. Quando o seu problema tem dados que não podem errar — transações multi-tabela que precisam ser atômicas, consistência forte inegociável (Cap 6) —, o relacional não é uma opção entre várias; é o default forte, porque ele resolve esse problema de um jeito que décadas de engenharia tornaram confiável. Stripe, PayPal, os ledgers financeiros do mundo: todos sobre relacional, pelo ACID.

12.4 As famílias NoSQL — quatro modelos, quatro propósitos

"NoSQL" não é uma coisa — é um guarda-chuva sobre quatro famílias com modelos de dados muito diferentes, cada uma otimizada para um padrão de acesso específico. Tratá-las como intercambiáveis é o primeiro erro.

FamíliaModeloBrilha emExemplos
Key-valueChave → valor (o mais simples)Lookups por chave ultrarrápidos, cache, sessõesRedis, DynamoDB
DocumentoDocumentos JSON aninhados, esquema flexívelDados semiestruturados, evolução rápida, agregadosMongoDB, CouchDB
Wide-column (colunar)Linhas com colunas dinâmicas, particionávelEscrita massiva, séries temporais, escala enormeCassandra, HBase
GrafoNós e arestas (relações como cidadãs de 1ª classe)Relacionamentos complexos, travessias (redes sociais)Neo4j, Neptune
Cada família é uma ferramenta especializada, não um SQL genérico mais rápido
O erro mental que causa más escolhas de NoSQL é pensar nele como "um banco mais rápido e flexível que o SQL". Não é — cada família NoSQL é uma ferramenta especializada para um padrão de acesso, e fora desse padrão ela é pior que o relacional, não melhor. Um key-value é imbatível para "me dê o valor desta chave" e inútil para "me dê todos os pedidos acima de R$100 do último mês" (ele não consulta por valor, só por chave). Um banco de documento é ótimo quando seus dados são agregados naturais (um pedido com seus itens dentro, lido junto) e ruim quando você precisa de joins complexos entre entidades. Um wide-column absorve escrita massiva e escala horizontalmente como poucos, mas exige que você modele em torno de queries conhecidas de antemão. Um banco de grafo torna trivial "amigos dos amigos que curtiram X" — uma query que no relacional seria um pesadelo de joins — e é overkill para dados sem relações complexas. A pergunta nunca é "NoSQL é mais rápido?"; é "qual é o meu padrão de acesso dominante, e qual família foi construída para ele?". Escolher a família errada para o seu acesso é pior que ficar no relacional.

12.5 Modelar pelo acesso, não pelos dados

Há uma inversão mental fundamental entre o relacional e o NoSQL, e não entendê-la é a causa de quase todo projeto NoSQL malsucedido. No relacional, você modela os dados (normaliza em tabelas) e depois consulta como quiser. No NoSQL, você modela em torno das queries que vai fazer.

Relacional — modele os dados, consulte depois

Você normaliza: cada fato num lugar só, sem duplicação, ligado por chaves. Depois, consulta de qualquer jeito com joins — o modelo de dados é agnóstico às queries. Flexível para queries imprevistas; o custo é que joins complexos podem ser caros, e a normalização espalha o dado.

NoSQL — modele as queries primeiro

Você parte das queries que vai fazer e modela os dados para servi-las rápido — frequentemente desnormalizando (duplicando dados, juntando o que é lido junto num só documento/linha). Rápido para os acessos previstos; péssimo para os imprevistos. Errar as queries de antemão é caro de corrigir.

No NoSQL, você precisa saber as queries ANTES de modelar
Esta é a diferença que mais pega os times de surpresa. No relacional, você pode modelar os dados de forma limpa (normalizada) e descobrir as queries depois — se amanhã você precisar de um relatório novo, escreve uma query nova com joins, e funciona (talvez devagar, mas funciona). No NoSQL, isso não vale: como você modela em torno das queries conhecidas, uma query que você não previu pode ser impossível ou proibitivamente cara no modelo que você escolheu. Se você modelou um key-value por user_id e amanhã precisa buscar "todos os usuários de São Paulo", não há como — o modelo não suporta essa query, e adicioná-la exige remodelar (ou manter um índice/cópia separada). Isso torna o NoSQL menos perdoador: ele recompensa quem conhece bem seus padrões de acesso e pune quem não os conhece. A consequência prática: o NoSQL é uma escolha melhor quando seus padrões de acesso são conhecidos e estáveis (você sabe exatamente como vai consultar, e isso não muda muito); o relacional é mais seguro quando os padrões são imprevisíveis ou evoluem (você vai consultar de formas que ainda não imagina). Modelar pelo acesso é poderoso quando você conhece o acesso — e uma armadilha quando não conhece.

12.6 Por que escalar SQL horizontalmente custa

O NoSQL nasceu da dor de escalar o relacional, e vale entender por que essa dor é real. O relacional escala verticalmente com facilidade (máquina maior), mas escalar horizontalmente (entre máquinas) é genuinamente difícil — e a razão revela uma tensão profunda.

Joins e transações ACID brigam com a distribuição
As duas coisas que tornam o relacional poderoso — joins e transações ACID — são exatamente as que resistem à distribuição. Quando você fragmenta (sharding, Cap 13) um banco relacional entre máquinas (usuários A-M numa, N-Z noutra), dois problemas surgem. Primeiro, os joins entre shards: uma query que precisa juntar dados que estão em máquinas diferentes vira uma operação distribuída lenta e complexa, ou simplesmente não é possível. Segundo, as transações ACID entre shards: garantir atomicidade e isolamento de uma transação que toca dados em múltiplas máquinas exige protocolos de coordenação caros (two-phase commit) que são lentos e frágeis (e esbarram no CAP, Cap 5). O modelo relacional foi projetado assumindo um nó único (ou um cluster fortemente acoplado), e suas garantias mais fortes pressupõem que todos os dados estão coordenados. O NoSQL fez a troca oposta: abriu mão de joins entre entidades e de transações multi-documento, e por isso consegue particionar livremente — sem joins nem transações cruzando partições, cada partição é independente (shared-nothing), e adicionar nós escala linearmente. Não é que o NoSQL seja "mais escalável" por magia; é que ele sacrificou exatamente as features que impedem a distribuição. A escala horizontal do NoSQL é o ACID e os joins que você abriu mão, transformados em capacidade de particionar. Como sempre, não há mágica — há um trade-off.

12.7 NewSQL — querer os dois mundos

Se o relacional tem ACID mas custa a escalar, e o NoSQL escala mas sacrifica o ACID, a pergunta óbvia é: dá para ter os dois? Os bancos NewSQL (ou "distributed SQL") são a tentativa de responder sim.

NewSQL: SQL e ACID com escala horizontal — a um custo de complexidade
Bancos como Google Spanner, CockroachDB, TiDB e YugabyteDB oferecem o que antes parecia impossível: a interface SQL e as transações ACID do relacional, com a escala horizontal distribuída do NoSQL. Você escreve SQL familiar, tem transações que funcionam entre nós, e o banco distribui os dados e sobrevive a falhas de data center automaticamente. Parece o melhor dos mundos — e para muitos casos é. Mas, como tudo em design, há um trade-off escondido: garantir ACID de verdade entre nós distribuídos exige coordenação (Spanner famosamente usa relógios atômicos para conseguir consistência forte global), o que adiciona latência (o ELC do PACELC, Cap 5 — consistência forte distribuída custa tempo) e complexidade operacional (são sistemas distribuídos sofisticados de operar e entender, mesmo gerenciados). NewSQL não é "ACID e escala de graça"; é "ACID e escala, pagando em latência de coordenação e complexidade". Para quem genuinamente precisa de transações fortes e escala que um único nó não comporta — alguns sistemas financeiros globais, por exemplo — é uma resposta excelente. Para quem cabe num Postgres robusto (a maioria, pela estimativa do Cap 3), é complexidade que ainda não se justifica. A existência do NewSQL não significa "use NewSQL"; significa que o trade-off histórico (ACID ou escala) foi parcialmente dissolvido, ao custo de um novo trade-off (latência e complexidade da coordenação distribuída).

12.8 Persistência poliglota — o banco certo para cada trabalho

A lição final, e a realidade de 2026: a pergunta madura não é "SQL ou NoSQL para o meu sistema?", mas "qual banco para cada parte do meu sistema?". Sistemas reais frequentemente usam vários bancos, cada um onde brilha — a persistência poliglota.

Um sistema, vários bancos — cada um no seu lugar
Um sistema real de escala raramente usa um banco só, porque suas diferentes partes têm necessidades diferentes. Um e-commerce pode usar: relacional (Postgres) para pedidos e pagamentos (precisa de ACID, transações, consistência forte); key-value (Redis) para sessões e cache (lookups rápidos, descartável); documento (MongoDB) para o catálogo de produtos (esquema flexível, cada produto com atributos diferentes); busca (Elasticsearch) para a busca de produtos (full-text, relevância); wide-column (Cassandra) para o histórico de eventos/cliques (escrita massiva); e talvez grafo (Neo4j) para recomendações ("quem comprou isto também comprou"). Cada um faz o que faz melhor, e nenhum tenta fazer tudo. Isto é persistência poliglota, e é a resposta madura à guerra SQL-vs-NoSQL: não é uma escolha única para o sistema; é a ferramenta certa para cada trabalho dentro dele. O custo, claro, é operar e entender múltiplos bancos (complexidade, Cap 1) — então não se exagera: começa-se com um (geralmente o relacional, o default versátil), e adiciona-se outro só quando uma necessidade específica e medida o justifica. Mas a mentalidade é clara: o banco não é uma religião que você escolhe para a vida; é uma ferramenta que você seleciona por tarefa. A pergunta "SQL ou NoSQL?" é mal formulada — a boa pergunta é "qual banco para cada padrão de acesso do meu sistema?".

12.9 Estudo de caso — a startup que escolheu NoSQL pelo hype

Quando "vai escalar" custou um ano de dor
O cenário

Uma startup começou seu produto — uma plataforma B2B com clientes, projetos, faturas e relatórios — escolhendo um banco de documentos (NoSQL), pela crença de que "NoSQL escala melhor" e "é mais moderno e flexível". A decisão pareceu sábia na época. Um ano depois, ela era a maior fonte de dor da engenharia. O que deu errado revela tudo sobre escolher pelo hype.

Passo 1 · Os dados eram relacionais
o modelo brigava com os dados
Os dados do negócio eram FORTEMENTE relacionais:
  - clientes TÊM projetos
  - projetos TÊM faturas
  - faturas REFERENCIAM itens, impostos, pagamentos
  - relatórios CRUZAM tudo isso

No banco de documentos, sem joins, eles tinham duas opções
ruins: ou DUPLICAR dados em vários documentos (e lidar com
a inconsistência quando algo mudava em um lugar e não no
outro), ou fazer "joins na aplicação" (buscar documento A,
extrair IDs, buscar documentos B, juntar no código) — lento,
verboso, e propenso a erros. Cada relatório virava um
pesadelo de código que um JOIN de SQL resolveria numa linha.
Passo 2 · As queries imprevistas
o NoSQL pune a query que você não previu
O modelo foi feito em torno das queries conhecidas no dia 1.
Mas o produto evoluiu, e os clientes pediam relatórios novos:
  "faturas vencidas por região e setor do cliente"
  "projetos sem atividade nos últimos 30 dias"

Cada query nova que não cabia no modelo original exigia ou
remodelar os documentos (migração custosa) ou varreduras
completas e lentas. No relacional, seriam queries SQL ad-hoc
com WHERE e JOIN — triviais. No documento, eram obras.
Passo 3 · A escala que nunca veio (e não era o gargalo)

E a ironia final: a escala que justificara a escolha nunca foi um problema. A plataforma era B2B, com alguns milhares de clientes empresariais — um volume que (pela estimativa do Cap 3) um único Postgres engoliria com folga de anos. Eles pagaram o custo do NoSQL (sem joins, sem ACID multi-documento, modelar pelas queries) para resolver um problema de escala que não tinham e provavelmente nunca teriam. Escolheram a ferramenta para o cenário de sucesso fantasiado, não para o problema real — exatamente o erro do Cap 3.

Passo 4 · A migração dolorosa de volta
a porta de mão única, atravessada ao contrário
A correção: migrar para PostgreSQL. Mas agora havia um ano
de dados em documentos, e dezenas de partes do código
assumindo o modelo de documento. A migração levou MESES:
  - transformar todos os documentos em tabelas normalizadas
  - reescrever todo o acesso a dados
  - rodar os dois bancos em paralelo durante a transição
  - validar que nada se perdeu

Foi a cirurgia cara da seção 12.2, na prática. Tudo porque
a decisão irreversível foi tomada pelo hype, não pelo problema.

Resultado: a startup pagou meses de migração e um ano de produtividade reduzida por uma decisão de banco tomada pelo hype ("NoSQL escala e é moderno") em vez de pelo problema. Os erros se acumularam: os dados eram relacionais (e o documento brigava com eles, sem joins nem ACID), as queries evoluíam de forma imprevisível (e o NoSQL pune o que não foi modelado de antemão), e a escala que justificou tudo nunca existiu (o volume cabia num Postgres com folga). Cada um desses era previsível com as perguntas certas: os dados são relacionais? as queries são conhecidas e estáveis? a escala realmente excede um nó? Se tivessem feito essas perguntas — e a estimativa do Cap 3 —, teriam escolhido o relacional, o default que servia o problema real, e economizado o ano de dor. A lição é dupla: a escolha de banco é a decisão mais irreversível (12.2), então merece o máximo cuidado; e ela deve ser feita pelo problema (os dados são relacionais? as queries são previsíveis? a escala excede um nó?), nunca pelo hype — porque o hype prometeu por uma década que o NoSQL substituiria o SQL, e quem acreditou pagou a conta. Próximo capítulo: sharding e particionamento — o que você faz quando os dados de fato não cabem mais num nó, e os problemas (hot partitions, rebalanceamento) que vêm com quebrar a verdade em pedaços.

12.10 Erros comuns

Erro 1 · Escolher o banco pelo hype

Adotar NoSQL (ou qualquer banco) porque é "moderno" ou "escala", sem perguntar se o seu problema o pede. A escolha é irreversível e cara de corrigir; faça-a pelo problema (dados relacionais? queries previsíveis? escala real?), não pela moda. O hype prometeu que NoSQL mataria SQL — e errou por uma década.

Erro 2 · NoSQL para dados relacionais

Usar um banco de documentos para dados fortemente relacionados (entidades que se referenciam, relatórios que cruzam tudo) — e sofrer com joins na aplicação, duplicação inconsistente, e código verboso. Dados relacionais pedem um banco relacional; sem joins, você reimplementa joins pior, no código.

Erro 3 · NoSQL com queries imprevisíveis

Escolher NoSQL (que se modela em torno de queries conhecidas) quando seus padrões de acesso ainda evoluem — e descobrir que cada query nova exige remodelar ou varrer tudo. NoSQL recompensa acesso conhecido e estável; para queries imprevisíveis, o relacional (que consulta de qualquer jeito) é mais seguro.

Erro 4 · Tratar "NoSQL" como uma coisa só

Falar de "NoSQL" como se fosse um banco, ignorando que são quatro famílias (key-value, documento, wide-column, grafo) com modelos e propósitos opostos. Cada uma é especializada para um padrão de acesso. Escolher "NoSQL" sem escolher a família certa para o seu acesso é meio caminho para o erro.

Erro 5 · Evitar o relacional por medo de não escalar

Descartar o Postgres "porque não escala" sem estimar (Cap 3) se você vai mesmo exceder um nó — quando a maioria dos sistemas cabe num relacional robusto por anos. O relacional escala verticalmente muito longe, e há réplicas e sharding quando preciso. Não troque ACID e joins por uma escala que talvez nunca venha.

Erro 6 · Achar que NewSQL é ACID e escala de graça

Adotar NewSQL achando que resolve o trade-off sem custo. Ele dá ACID com escala horizontal, mas paga em latência de coordenação (consistência forte distribuída custa tempo, PACELC) e complexidade operacional. Use-o quando precisa genuinamente dos dois; se cabe num Postgres, é complexidade prematura.

Erro 7 · Forçar um banco só para tudo

Insistir que todo o sistema use um único banco, mesmo onde ele não é o ideal — em vez de usar o banco certo para cada parte (persistência poliglota). Pedidos no relacional, cache no Redis, busca no Elasticsearch: cada um onde brilha. (Com moderação — cada banco a mais é complexidade operacional.)

Verifique seu entendimento
Uma equipe vai construir uma plataforma de gestão (clientes, projetos, faturas, relatórios que cruzam essas entidades) para alguns milhares de empresas. Um engenheiro propõe usar um banco de documentos NoSQL "porque escala melhor e é mais flexível". Qual é a análise mais sólida?

12.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Qual banco para qual dado?

Para cada caso, escolha a família de banco mais apropriada (relacional, key-value, documento, wide-column, ou grafo) e justifique pelo padrão de acesso: (a) sessões de usuário que expiram; (b) transações financeiras de um banco; (c) o catálogo de produtos de um e-commerce, onde cada categoria tem atributos diferentes; (d) a rede de "quem segue quem" de uma rede social, para sugestões de amizade.

(a) Sessões que expiram → key-value (Redis). O acesso é puro lookup por chave (session_id → dados da sessão), os dados são descartáveis e temporários, e a velocidade importa (toda requisição checa a sessão). Key-value é imbatível para isso, com TTL nativo para a expiração. Relacional seria overkill; nenhum join é necessário.

(b) Transações financeiras → relacional (PostgreSQL). Dinheiro exige ACID (atomicidade da transferência, isolamento, durabilidade), consistência forte (Cap 6), e provavelmente queries/relatórios complexos. É o caso canônico do relacional — todo grande fintech roda o core financeiro em relacional, pelo ACID. Não há atalho aqui.

(c) Catálogo com atributos variáveis por categoria → documento (MongoDB) — ou relacional com JSONB. O esquema flexível brilha aqui: um livro tem autor e ISBN, uma camiseta tem tamanho e cor — atributos diferentes que um esquema rígido acomodaria mal. Um banco de documentos guarda cada produto com sua estrutura própria. (Nota 2026: o Postgres com JSONB também faz isso bem, mantendo o relacional para o resto — frequentemente a escolha mais simples.)

(d) Rede de "quem segue quem" para sugestões → grafo (Neo4j). As relações são o dado, e a query típica ("amigos dos amigos", "caminhos entre pessoas") é uma travessia de grafo — trivial num banco de grafo, um pesadelo de joins recursivos no relacional. Quando os relacionamentos e suas travessias são o foco, o grafo é a ferramenta construída para isso.

O insight: a família certa segue o padrão de acesso dominante. Lookup por chave → key-value. ACID e relações → relacional. Esquema flexível por item → documento. Travessia de relações → grafo. (E wide-column para escrita massiva/séries temporais.) Note que (c) admite o relacional moderno (JSONB) — em 2026, o relacional frequentemente cobre casos que antes exigiam NoSQL.

Médio
Exercício 2 · O custo de modelar pelo acesso

Você está modelando um sistema de blog num banco de documentos NoSQL. A query principal é "mostrar um post com seus comentários" e você modela cada post como um documento com os comentários embutidos dentro. (a) Por que esse modelo é ótimo para a query principal? (b) Que queries esse modelo torna difíceis? (c) O que isso ensina sobre quando NoSQL é a escolha certa?

(a) Por que é ótimo para a query principal: "mostrar um post com seus comentários" é resolvido com uma única leitura — você busca o documento do post, e os comentários já estão dentro dele. Nenhum join, nenhuma segunda query. É o caso onde o modelo de documento brilha: quando o dado é lido como um agregado (o post e seus comentários sempre juntos), embuti-los num documento torna a leitura trivial e rápida. Você modelou exatamente para a query dominante.

(b) Que queries ficam difíceis:

  • "Todos os comentários de um usuário específico, em todos os posts": os comentários estão espalhados dentro dos documentos de cada post — não há como buscá-los por autor sem varrer todos os posts. A query que não respeita o agregado é cara.
  • "Os 10 comentários mais recentes do site inteiro": mesmo problema — comentários não são entidades de primeira classe, estão presos dentro dos posts. Ordená-los globalmente exige varrer tudo.
  • "Editar um comentário que aparece em vários lugares": se houvesse duplicação, manter a consistência seria manual.

(c) O que ensina sobre quando NoSQL é certo: o modelo de documento é excelente quando o seu acesso respeita os agregados naturais dos dados (ler o post com seus comentários juntos) e ruim quando você precisa consultar através dos agregados (comentários por autor, cruzando posts). Logo, NoSQL de documento é a escolha certa quando: (1) seus dados têm agregados naturais claros, (2) você os acessa principalmente por esses agregados, e (3) esses padrões são estáveis. Se você precisa consultar os dados de muitos ângulos diferentes e imprevisíveis (como comentários por autor, por data global, por post), o relacional — onde comentários seriam uma tabela consultável de qualquer jeito — é mais flexível. A pergunta-chave: meu acesso respeita um agregado fixo, ou eu preciso fatiar os dados de muitas formas?

Médio
Exercício 3 · O trade-off da escala

Explique, para um colega, por que bancos relacionais são difíceis de escalar horizontalmente e bancos NoSQL não — e por que isso não significa que "NoSQL é melhor". Use os conceitos de joins, transações ACID, e o que cada um sacrifica.

Por que o relacional custa a escalar horizontalmente: as duas features que tornam o relacional poderoso — joins e transações ACID — pressupõem que os dados estão coordenados, idealmente num lugar só. Quando você fragmenta o banco entre máquinas (sharding):

  • Joins entre shards ficam lentos ou impossíveis: juntar dados que estão em máquinas diferentes exige uma operação distribuída custosa. O join, que num nó único é trivial, vira um problema de coordenação de rede.
  • Transações ACID entre shards exigem coordenação cara (two-phase commit) para garantir atomicidade e isolamento através de máquinas — lento, frágil, e esbarrando no CAP (Cap 5).

Por que o NoSQL escala facilmente: o NoSQL fez a troca oposta — ele abriu mão de joins entre entidades e de transações multi-documento. Sem essas duas coisas cruzando partições, cada partição é independente (arquitetura shared-nothing): nenhuma operação precisa coordenar entre máquinas, então adicionar máquinas simplesmente adiciona capacidade, de forma quase linear. O NoSQL escala porque removeu exatamente o que impedia a distribuição.

Por que isso NÃO significa "NoSQL é melhor": porque a escalabilidade do NoSQL é o ACID e os joins que ele sacrificou, transformados em capacidade de particionar. Você não ganhou escala de graça — você pagou com as garantias e a flexibilidade de query do relacional. Se o seu problema precisa de joins e transações (dados relacionais, dinheiro), o NoSQL não é "melhor", é inadequado — ele removeu justamente o que você precisa. Se o seu problema não precisa deles e precisa de escala massiva, o NoSQL é melhor — ele removeu o que você não usava. "Melhor" depende inteiramente do que o seu problema exige. Não há banco superior; há o trade-off (ACID/joins vs escala fácil) e a escolha de qual lado o seu problema precisa.

O fecho: e em 2026, o NewSQL tenta dar os dois (ACID + escala) pagando em latência de coordenação e complexidade — mostrando que nem o trade-off é fixo, mas que sempre há um preço. Nunca há mágica; sempre há troca.

Difícil
Exercício 4 · A decisão de banco de um sistema — entrevista

Você projeta um sistema de e-commerce de médio-grande porte. O entrevistador pergunta: "que banco(s) de dados você usaria, e por quê?". Demonstre que você decide pelo problema e por padrão de acesso (não pelo hype), considerando persistência poliglota onde fizer sentido, e justificando cada escolha. Cubra ao menos: pedidos/pagamentos, catálogo, sessões/carrinho, busca, e histórico de eventos.

Enquadramento: a resposta forte rejeita "um banco para tudo" e também rejeita "vários bancos porque é legal" — ela escolhe pelo padrão de acesso de cada parte, começando simples e adicionando bancos só onde uma necessidade real justifica. Vou por partes do sistema.

1. Pedidos e pagamentos → relacional (PostgreSQL). O coração. Aqui é inegociável: precisa de ACID (um pedido com seus itens, pagamento e baixa de estoque tem que ser atômico), consistência forte (Cap 6 — não pode vender o último item duas vezes, não pode cobrar errado), e dados fortemente relacionais (pedidos ↔ clientes ↔ itens ↔ pagamentos). É o caso canônico do relacional. Todo o core transacional do e-commerce vive aqui.

2. Catálogo de produtos → relacional com JSONB, ou documento. Produtos têm atributos variáveis por categoria (livro vs eletrônico). Em 2026, eu provavelmente manteria no Postgres com colunas JSONB para os atributos flexíveis — assim não adiciono um banco novo e mantenho a opção de relacionar produtos a pedidos. Se o catálogo fosse gigantesco e muito dinâmico, um banco de documentos (MongoDB) seria justificável. Decisão pela simplicidade: um banco a menos se o Postgres resolve.

3. Sessões e carrinho → key-value (Redis). Acesso por chave (session/user → carrinho), velocidade crítica (toda página acessa), e dados semi-descartáveis com TTL. Key-value é perfeito. O carrinho pode ser AP/eventual (Cap 5 — o caso clássico do Dynamo): melhor disponível e ocasionalmente impreciso que indisponível. Redis brilha aqui.

4. Busca de produtos → motor de busca (Elasticsearch/OpenSearch). Busca full-text com relevância, filtros facetados, tolerância a erros de digitação — coisas que um LIKE de SQL faz mal. Um motor de busca dedicado, alimentado pelo catálogo (sincronizado via eventos), serve a caixa de busca. É uma necessidade especializada real que justifica um sistema dedicado.

5. Histórico de eventos/cliques → wide-column (Cassandra) ou um log (Kafka → data lake). Escrita massiva (cada clique, visualização), leitura esparsa (analytics), volume enorme. Wide-column absorve a escrita e escala horizontalmente; ou um pipeline via Kafka para um data lake. É write-heavy (Cap 2), o oposto do core transacional.

A disciplina transversal: começar com o Postgres fazendo o máximo (pedidos, pagamentos, catálogo via JSONB) e adicionar cada banco especializado (Redis, Elasticsearch, Cassandra) só quando a necessidade específica e medida o justifica — não todos no dia 1. Cada banco a mais é complexidade operacional (Cap 1); a persistência poliglota é poderosa com moderação.

O que um bom candidato demonstra: que decide cada parte pelo padrão de acesso (ACID/relacional para pedidos, key-value para sessões, busca dedicada para busca, write-heavy para eventos); que conhece a opção moderna (JSONB no Postgres) para evitar bancos desnecessários; que reconhece o carrinho como caso AP; e que aplica moderação (começar com poucos, adicionar sob necessidade). Frase de fechamento: "eu não escolho 'um banco' para o e-commerce — escolho pelo padrão de acesso de cada parte. O core transacional (pedidos, pagamentos) é relacional, sem negociação, pelo ACID. Sessões e carrinho são key-value pela velocidade e descartabilidade. Busca é um motor dedicado. Eventos são write-heavy, num banco que absorve escrita. Mas começo com o Postgres fazendo o máximo e só adiciono cada banco especializado quando uma necessidade real e medida aparece — porque cada banco a mais é uma operação a mais, e a decisão de banco é a mais cara de reverter. Decido pelo problema, com moderação, nunca pelo hype."

Fim do capítulo 12
Próximo capítulo: sharding e particionamento. O relacional escala verticalmente longe, mas há um ponto onde os dados não cabem mais num nó — e aí você precisa quebrá-los entre máquinas. Vamos ver como particionar (por hash, por intervalo), os problemas que isso cria (as hot partitions que um único dado quente causa, o rebalanceamento quando se adiciona um nó), e por que o particionamento é uma faca de dois gumes: necessário em escala, mas fonte de complexidade que você adia o máximo possível.
Parte III · Capítulo 13 · Os componentes

Sharding e
particionamento.

Há um ponto em que os dados simplesmente não cabem mais num nó — nem o maior servidor do mundo guarda tudo, ou aguenta toda a escrita. Aí você precisa quebrar a verdade em pedaços e espalhá-los entre máquinas. O sharding é o que torna a escala possível além de um nó, e é também uma das fontes mais profundas de complexidade da arquitetura distribuída: ele cria hot partitions que afogam uma máquina enquanto as outras dormem, rebalanceamentos arriscados quando você adiciona um nó, e queries entre shards que eram triviais e viram pesadelos. Por isso a primeira lição do capítulo é também a mais importante: adie o sharding o máximo que puder.

Este capítulo trata de quebrar os dados entre máquinas. Vamos separar três termos sempre confundidos (particionamento, sharding, replicação), entender por que o sharding é o último recurso (e o que tentar antes), as estratégias de particionamento (por hash, por intervalo, por geografia) e o que cada uma troca, a escolha da shard key — a decisão mais consequente e mais difícil de reverter dentro do sharding —, as hot partitions que uma chave mal escolhida (ou um dado popular) causa, o rebalanceamento custoso de adicionar nós (e como o hashing consistente o alivia), e as queries e transações entre shards que o sharding torna difíceis. Quando você sair daqui, o sharding deixa de ser "só dividir os dados" e vira uma decisão de último recurso cujos custos você conhece — e cuja chave você escolhe com o cuidado que uma decisão irreversível merece.

13.1 A história — quando um nó deixou de bastar

Contexto histórico

Por décadas, a resposta para "meu banco não aguenta" foi simples: compre um servidor maior (escala vertical, Cap 1). Funcionou notavelmente bem — e ainda funciona, mais do que se imagina. Mas, no início dos anos 2000, os gigantes da web bateram num teto que nenhum cheque resolvia: nem o maior mainframe guardava todos os dados do Google, nem aguentava toda a escrita do Facebook. Pela primeira vez em massa, os dados de um sistema simplesmente não cabiam numa máquina, por maior que fosse.

A solução foi quebrar os dados em pedaços (shards) e espalhá-los entre muitas máquinas. No começo, isso era feito na mão, na camada da aplicação — empresas escreviam código complexo e frágil para decidir em qual servidor cada dado vivia, rotear as queries, e lidar com o caos quando precisavam adicionar máquinas. Era trabalhoso, propenso a erros, e cada empresa reinventava a roda. O sharding era um mal necessário que poucos faziam bem.

Os papers seminais mudaram isso. O Dynamo da Amazon (2007) popularizou o consistent hashing para distribuir dados e minimizar o caos de rebalanceamento. O BigTable do Google e o Cassandra trouxeram o particionamento para dentro do banco. Aos poucos, a inteligência de "onde cada dado vive, como rotear, como rebalancear" migrou da aplicação frágil para o banco, que passou a fazer isso automaticamente.

Em 2026, a boa notícia é que você raramente implementa sharding sozinho — os bancos distribuídos modernos (Cassandra, MongoDB, DynamoDB, os NewSQL) cuidam da colocação, do roteamento e do rebalanceamento por baixo, frequentemente sem a aplicação nem perceber. Mas — e este é o ponto — entender o que acontece por baixo continua essencial, porque as decisões que você ainda toma (qual é a shard key, principalmente) determinam se o sistema escala lindamente ou sofre de hot partitions e rebalanceamentos infernais. O banco automatiza o como; você ainda decide o quê (a chave), e essa decisão é tão consequente e tão irreversível que erra-la é remodelar tudo. Este capítulo é sobre tomá-la bem — e, antes disso, sobre saber quando você realmente precisa shardar, porque a resposta é "mais tarde do que você pensa".

13.2 Partição, sharding, réplica — três coisas diferentes

Três termos são usados de forma intercambiável e não deveriam ser, porque resolvem problemas distintos. Esclarecê-los é o primeiro passo:

TermoO que fazResolve
ParticionamentoDivide os dados em pedaços, tipicamente dentro de uma instânciaQueries varrem menos dados; manutenção mais fácil
ShardingDistribui os pedaços entre múltiplas instâncias/máquinasDados/escrita que não cabem num nó (escala além de um nó)
ReplicaçãoCopia os mesmos dados em várias máquinasDisponibilidade e escala de leitura — NÃO o tamanho
Réplica escala leitura e disponibilidade; sharding escala tamanho e escrita
A confusão mais consequente é entre replicação e sharding, porque elas resolvem problemas opostos. Replicação copia os mesmos dados em várias máquinas: ótimo para disponibilidade (uma cai, as outras servem) e para escalar leitura (distribuir SELECTs entre réplicas), mas não aumenta o tamanho máximo do dataset — cada réplica tem tudo, então você continua limitado ao que cabe num nó. Sharding divide os dados em pedaços diferentes em máquinas diferentes: cada shard tem só uma fatia, então o dataset total pode exceder qualquer nó único, e a escrita se distribui (cada shard recebe só a sua parte). A regra: se o seu problema é "muita leitura" ou "preciso de alta disponibilidade", a resposta é replicação (e é mais simples). Se o seu problema é "os dados não cabem num nó" ou "a escrita afoga um nó só", aí sim é sharding (e é mais complexo). E elas se combinam: na prática, cada shard é também replicado (para que a falha de uma máquina não perca aquela fatia dos dados). Confundir os dois leve a shardar quando uma réplica bastaria — adicionando complexidade enorme para um problema que a replicação, muito mais simples, resolveria.

13.3 O sharding é o último recurso

A lição mais importante do capítulo, e a que mais se ignora: o sharding adiciona tanta complexidade que você deve adiá-lo o máximo possível, esgotando todas as alternativas antes. "Não sharde cedo" é quase um mandamento.

Antes de shardar, esgote: vertical, índices, cache, réplicas
O sharding traz consigo um cortejo de problemas — hot partitions, rebalanceamento arriscado, queries e transações entre shards que viram pesadelos, complexidade operacional permanente. Por isso ele é um último recurso, e há uma escada de alternativas a subir primeiro, cada uma muito mais simples: (1) Escala vertical — uma máquina maior resolve mais do que se imagina, e os servidores modernos comportam datasets enormes; frequentemente o "preciso shardar" é "não tentei um servidor maior". (2) Índices e otimização de queries — muitas vezes o banco não está lento por tamanho, mas por queries mal indexadas; corrigir isso evita o sharding inteiro. (3) Cache (Cap 10) — absorver a carga de leitura tira a pressão do banco, adiando o limite. (4) Réplicas de leitura — se o problema é leitura, réplicas distribuem-na sem a complexidade do sharding. Só depois de esgotar essa escada — quando os dados genuinamente não cabem num nó, ou a escrita genuinamente afoga o nó primário, e nenhuma das alternativas resolve — é que o sharding se justifica. E mesmo então, as implementações mais bem-sucedidas frequentemente shardam menos do que planejavam, porque otimizaram o resto primeiro. A pergunta antes de shardar não é "como sharbo?", é "eu realmente preciso, ou ainda há um degrau da escada por subir?". Shardar cedo é importar toda a complexidade da distribuição para resolver um problema que uma máquina maior ou um índice resolveria.

13.4 As estratégias de particionamento

Quando o sharding se justifica, a primeira decisão é como dividir os dados. As três estratégias principais fazem trade-offs diferentes entre distribuição uniforme e eficiência de range queries:

EstratégiaComo divideBom paraO risco
Por hashhash(chave) decide o shard — distribui aleatoriamenteDistribuição uniforme, evita hotspotsRange queries ruins (dados próximos espalhados)
Por intervalo (range)Faixas contíguas (A-M, N-Z; jan-mar, abr-jun)Range scans, séries temporaisHotspots se os valores são enviesados
Por geografiaPor região/data center (US, EU, APAC)Localidade, residência de dados (leis)Desbalanceio se uma região domina
Hash distribui mas perde a ordem; range mantém a ordem mas arrisca hotspots
As duas estratégias principais — hash e range — fazem um trade-off direto e oposto. O particionamento por hash aplica uma função hash à chave e usa o resultado para escolher o shard: isso espalha os dados uniformemente (chaves parecidas vão para shards diferentes), o que é ótimo para distribuir a carga e evitar hotspots — mas destrói a localidade: dados que você quer ler juntos (pedidos de janeiro, usuários de A a C) ficam espalhados por todos os shards, tornando range queries ("todos os pedidos do último mês") ineficientes, porque precisam consultar todos os shards. O particionamento por intervalo faz o oposto: mantém dados próximos juntos (jan-mar num shard), o que torna range queries eficientes (estão num shard só) e é ideal para séries temporais — mas arrisca hotspots: se os dados são enviesados (todo mundo escreve "agora", então o shard do mês atual recebe toda a escrita), um shard afoga enquanto os outros (meses passados) dormem. A escolha depende do seu acesso: precisa de range queries e dados ordenados? Range (cuidando do hotspot). Precisa de distribuição uniforme e acessa por chave? Hash (perdendo as range queries). E a geografia entra quando há localidade ou leis de residência de dados (LGPD, GDPR — os dados de europeus precisam ficar na Europa). Como sempre, não há estratégia universalmente melhor — há a que serve o seu padrão de acesso.

13.5 A escolha da shard key — a decisão dentro da decisão

De todas as decisões do sharding, a escolha da shard key (a chave que decide em qual shard cada dado vai) é a mais consequente — porque uma chave ruim cria problemas que você não consegue corrigir sem resharding (migrar todos os dados, a cirurgia cara do Cap 12). É uma decisão irreversível dentro de uma decisão irreversível.

Os três critérios de uma boa shard key
Uma shard key precisa satisfazer três propriedades, e falhar em qualquer uma causa dor:

1. Alta cardinalidade: deve ter muitos valores distintos, para que os dados possam se espalhar por muitos shards. Uma coluna booleana (só dois valores) é uma shard key terrível — você nunca teria mais que dois shards. Quanto mais valores distintos, mais granular a distribuição possível.

2. Distribuição uniforme: os valores devem aparecer com frequência aproximadamente igual. Se 80% dos seus usuários estão num país, "país" é uma chave péssima — um shard receberia 80% da carga (uma hot partition garantida). A chave precisa espalhar a carga, não concentrá-la.

3. Alinhamento com as queries: a maioria das suas queries deve incluir a shard key. Se você consulta por user_id em 90% das vezes, user_id é uma forte candidata — porque cada query vai direto ao shard certo. Se suas queries não incluem a shard key, cada uma precisa consultar todos os shards (scatter-gather), matando o benefício.

O problema é que essas três propriedades às vezes conflitam, e equilibrá-las é a arte. A consequência de errar: como mudar a shard key exige reshardar (migrar tudo), essa é uma decisão que você toma uma vez e carrega por anos. Trate-a com o cuidado de uma porta de mão única — porque é.

13.6 As hot partitions — quando um shard afoga

O problema mais comum e mais traiçoeiro do sharding: uma hot partition, onde um shard recebe muito mais carga que os outros — afogando enquanto o resto da frota fica ocioso. Pode vir de uma chave mal distribuída (13.5) ou de algo que nenhum algoritmo resolve: um dado intrinsecamente popular.

O hashing consistente equilibra chaves, não a carga de uma chave quente
Há dois tipos de hot partition, com causas e curas diferentes. O primeiro é distribuição desigual de chaves — uma shard key ruim (80% num país) que concentra muitas chaves num shard; a cura é escolher uma chave melhor (alta cardinalidade, uniforme), ou usar hashing para espalhar. Mas o segundo tipo é mais insidioso e nenhum algoritmo de particionamento resolve: uma única chave intrinsecamente quente — um post que viralizou, um produto em promoção relâmpago, a conta de uma celebridade, um canal popular. Toda a carga daquela chave vai, por definição, para o shard que a contém, porque ela é uma chave e não pode ser dividida pela função de hash. O hashing consistente (Cap 9) distribui as chaves uniformemente pelos shards, mas não pode pegar a carga de uma chave e espalhá-la — uma chave mora num shard, e se ela é mil vezes mais popular que as outras, aquele shard afoga. É por isso que vimos no Cap 9 que "consistent hashing não resolve hot keys": é o mesmo problema. As curas para a chave quente única são outras: replicar a chave quente para vários shards (lê-se de qualquer um), cachear agressivamente a chave quente na frente (Cap 10 — a maioria das leituras nem chega ao shard), ou dividir a chave artificialmente (adicionar um sufixo aleatório para espalhá-la em sub-chaves, agregando na leitura — "split key"). A lição: distribuição desigual de chaves se resolve com chave melhor; chave única quente se resolve com replicação, cache ou split — e confundir os dois leva a tentar (em vão) resolver uma celebridade viral com uma função de hash.

13.7 O rebalanceamento — o custo de crescer

Você shardou, o sistema cresceu, e agora precisa adicionar mais shards. Aqui mora um dos problemas operacionais mais difíceis do sharding: rebalancear — mover dados para os novos shards — é uma operação lenta, arriscada e cara, porque envolve migrar grandes volumes de dados num sistema vivo.

Hash módulo N quebra no rebalanceamento; hashing consistente o suaviza
O rebalanceamento expõe, de novo, o problema que vimos com cache no Cap 9. Se você shardou com shard = hash(chave) % N, adicionar um shard (mudar N) força quase todos os dados a remapear para shards diferentes — o que significa mover quase o dataset inteiro entre máquinas, uma operação catastroficamente cara e demorada. O hashing consistente resolve isso da mesma forma que resolveu para cache: ao mapear shards e chaves a um anel, adicionar um shard só remapeia as chaves próximas a ele (~1/N dos dados), não todas — minimizando dramaticamente o volume movido. Como o rebalanceamento é uma das partes mais difíceis e arriscadas de operar um banco shardado, minimizar os dados movidos reduz o tempo de migração, o risco, e a carga de fundo que o rebalanceamento impõe ao sistema vivo. É por isso que bancos como a Cassandra usam hashing consistente (com "virtual nodes" para distribuir ainda melhor) por baixo. Mesmo assim, rebalancear nunca é grátis nem instantâneo — move-se dados reais por uma rede real (a falácia do custo de transporte, Cap 8). A boa notícia de 2026 é que os bancos distribuídos modernos fazem o rebalanceamento automaticamente e de forma incremental, escondendo boa parte dessa dor — mas entender que ela existe explica por que adicionar capacidade a um sistema shardado nunca é tão simples quanto "ligar mais uma máquina".

13.8 As queries entre shards — o que o sharding quebra

O sharding tem um custo que aparece em toda query que não respeita a partição: operações que num banco único eram triviais — joins, agregações, transações que tocam vários dados — viram difíceis, lentas, ou impossíveis quando os dados estão espalhados por máquinas diferentes.

A query que cruza shards é lenta; a transação que cruza shards é um pesadelo
Quando os dados estão num shard só (porque a query inclui a shard key — critério 3 da seção 13.5), tudo é rápido: a query vai direto ao shard certo. O problema é a operação que cruza shards. Uma query entre shards (ex.: "todos os pedidos acima de R$1000 no sistema inteiro", quando o sharding é por usuário) precisa consultar todos os shards e juntar os resultados — o padrão "scatter-gather", que é lento (espera o shard mais lento, a cauda do Cap 4) e fica mais caro conforme você adiciona shards. Pior ainda é a transação entre shards: garantir atomicidade (ACID, Cap 12) de uma operação que modifica dados em máquinas diferentes exige coordenação distribuída cara (two-phase commit) que é lenta e frágil, e muitos sistemas shardados simplesmente não a oferecem — você não pode ter uma transação atômica entre shards. Isto reforça o critério de alinhamento da shard key: você escolhe a chave de modo que a esmagadora maioria das queries e transações fique dentro de um shard, e aceita que as poucas que cruzam serão lentas ou tratadas de outra forma (assincronamente, via Saga — Cap 17, ou num data warehouse separado para analytics). A lição: o sharding torna barato o acesso que respeita a partição e caro o que a cruza — então projete para que quase tudo respeite a partição, e isole o que precisa cruzar. Quando muitas operações cruzam shards, ou a shard key está errada, ou o sharding não era a solução certa.

13.9 Estudo de caso — a shard key que afogou a celebridade

Quando uma conta popular derruba um shard
O cenário

Uma rede social shardou seu banco de posts e atividades por user_id — uma escolha razoável: alta cardinalidade, alinhada com a maioria das queries ("os posts deste usuário"). Funcionou bem por um tempo. Então uma celebridade com 50 milhões de seguidores postou algo, e o shard que continha a conta dela afogou — latência disparou, erros, e o shard ficou indisponível, enquanto os outros shards estavam tranquilos. A shard key era boa pela maioria dos critérios. O que deu errado?

Passo 1 · A chave quente única que nenhum hash resolve
o problema da celebridade
user_id é uma boa shard key em geral:
  - alta cardinalidade (milhões de usuários) ✓
  - distribui bem os usuários COMUNS ✓
  - alinhada com as queries por usuário ✓

MAS: a conta da celebridade é UMA chave (um user_id) que
recebe MILHARES de vezes mais tráfego que qualquer outra.
Toda a atividade dela — 50M seguidores lendo, comentando,
curtindo — bate no shard que contém aquele user_id.

O hashing consistente espalha as CHAVES, mas a carga de UMA
chave vai toda para UM shard. Nenhum algoritmo de
particionamento divide a carga de um único user_id.
→ aquele shard afoga; os outros dormem. Hot partition.
Passo 2 · Por que reshardar não resolveria

O reflexo seria "a shard key está errada, vamos reshardar". Mas qualquer shard key que mantenha os dados de um usuário juntos (necessário para as queries por usuário) terá o mesmo problema: a celebridade sempre será uma chave quente, em qualquer shard onde ela caia. O problema não é a distribuição das chaves (que está boa) — é uma chave individualmente quente, um tipo de hot partition que a escolha de chave não resolve. Reshardar moveria o problema, não o eliminaria.

Passo 3 · As curas reais para a chave quente
tratar a chave quente, não a distribuição
1. CACHE agressivo da chave quente (Cap 10): os posts da
   celebridade são lidos por milhões — cachear na frente faz
   a esmagadora maioria das leituras NEM CHEGAR ao shard.
   (Com proteção de stampede! Cap 10.) Resolve a maior parte.

2. REPLICAR a chave quente para vários nós: os dados da
   celebridade ficam em réplicas extras; as leituras se
   distribuem entre elas. A leitura de uma chave quente deixa
   de bater num nó só.

3. SPLIT da chave (para escrita): adicionar um sufixo
   (user_id + bucket aleatório) espalha a ESCRITA da
   atividade dela em várias sub-partições, agregadas na
   leitura. A carga de escrita da chave quente se divide.

4. Tratar contas grandes como CASO ESPECIAL na arquitetura
   (o "celebrity problem" — sistemas reais têm um caminho
   separado para contas enormes).
Passo 4 · O resultado

Com o cache agressivo (a maioria das leituras da celebridade servida da memória, sem tocar o shard), a réplica extra da chave quente, e o tratamento de contas enormes como caso especial, o shard parou de afogar. A leitura massiva da celebridade foi absorvida pelo cache e pelas réplicas; a escrita de atividade foi distribuída. O shard voltou a respirar, e o sistema aguentou a próxima celebridade viral sem incidente — porque agora tinha um caminho para a chave quente, em vez de tratá-la como um usuário comum.

Resultado: a shard key (user_id) não estava errada — ela era boa pela cardinalidade, pela distribuição dos usuários comuns e pelo alinhamento com as queries. O problema era um tipo diferente de hot partition: uma chave individualmente quente (a celebridade), que nenhum algoritmo de particionamento resolve, porque a carga de uma única chave mora num único shard. As curas não foram de particionamento (reshardar não ajudaria) — foram de cache (a maioria das leituras nem chega ao shard), replicação da chave quente, e tratamento especial de contas enormes. A lição central: há dois problemas de hot partition que se confundem mas exigem soluções opostas — distribuição desigual de chaves (cura: chave melhor) e chave individualmente quente (cura: cache/réplica/split). Diagnosticar qual você tem é o que leva à solução certa; tratar uma celebridade viral com uma função de hash é tratar o problema errado. Próximo capítulo: o log como primitiva — a estrutura append-only que apareceu por toda a Parte III (no Kafka do Cap 11, no consistent hashing, na replicação) e que, vista como conceito unificador, é uma das ideias mais poderosas dos sistemas distribuídos: a fonte da verdade de onde tudo o mais deriva.

13.10 Erros comuns

Erro 1 · Shardar cedo demais

Importar toda a complexidade do sharding (hot partitions, rebalanceamento, cross-shard) antes de esgotar as alternativas — quando uma máquina maior, índices, cache ou réplicas resolveriam. Sharding é último recurso; suba a escada (vertical → índices → cache → réplicas) primeiro. Frequentemente "preciso shardar" é "não otimizei o resto".

Erro 2 · Shardar quando precisava replicar

Usar sharding (complexo) para um problema de leitura ou disponibilidade, que a replicação (simples) resolveria. Réplica escala leitura e disponibilidade; sharding escala tamanho e escrita. Se os dados cabem num nó mas há muita leitura, replique — não sharde.

Erro 3 · Shard key de baixa cardinalidade ou enviesada

Escolher uma chave com poucos valores (booleano) ou distribuição desigual (80% num país) — criando hot partitions garantidas e limitando o número de shards. A chave precisa de alta cardinalidade e distribuição uniforme. E como mudá-la exige reshardar, errar custa caro.

Erro 4 · Shard key desalinhada com as queries

Escolher uma chave que a maioria das queries não inclui — forçando cada query a consultar todos os shards (scatter-gather lento). A chave deve estar nas queries dominantes, para que cada uma vá direto ao shard certo. Desalinhamento mata o benefício do sharding.

Erro 5 · Tentar resolver chave quente única com particionamento

Reshardar ou trocar a função de hash para resolver uma chave intrinsecamente popular (celebridade, post viral) — em vão, porque a carga de uma chave mora num shard, em qualquer esquema. Chave quente única se resolve com cache, replicação da chave, ou split — não com particionamento.

Erro 6 · Hash módulo N para sharding

Usar hash % N para distribuir dados, e enfrentar a migração de quase todo o dataset ao adicionar um shard (N muda). Use hashing consistente, que remapeia só ~1/N dos dados. O rebalanceamento já é caro e arriscado; não o torne catastrófico com módulo N.

Erro 7 · Projetar para muitas queries entre shards

Escolher um sharding em que muitas operações cruzam shards (joins, agregações, transações distribuídas) — lentas (scatter-gather) ou impossíveis (transação entre shards). Projete para que quase tudo respeite a partição (via shard key alinhada), e isole o que precisa cruzar (async, Saga, ou warehouse separado).

Verifique seu entendimento
Você shardou um banco de redes sociais por user_id (boa cardinalidade, alinhada com as queries). Tudo funciona até uma celebridade com milhões de seguidores postar — e o shard que contém a conta dela afoga, enquanto os outros ficam ociosos. Qual é o diagnóstico e a solução correta?

13.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Sharding, réplica ou nem um nem outro?

Para cada problema, diga se a solução é sharding, replicação, ou outra coisa (vertical, índice, cache), justificando: (a) o banco está lento numa query específica, mas o dataset é pequeno; (b) há muita leitura e você quer alta disponibilidade, mas os dados cabem num nó; (c) os dados cresceram a ponto de não caberem no maior servidor disponível; (d) a escrita está afogando o nó primário, mesmo com a maior máquina.

(a) Lento numa query, dataset pequeno → índice/otimização, não sharding. Se o dataset é pequeno, o problema não é tamanho — é provavelmente uma query mal indexada ou ineficiente. Adicionar um índice na coluna consultada, ou reescrever a query, resolve. Shardar aqui seria importar enorme complexidade para um problema de otimização. Sempre verifique índices antes de pensar em distribuir.

(b) Muita leitura + alta disponibilidade, dados cabem num nó → replicação. Este é o caso clássico de réplica: copie os dados em várias máquinas. As réplicas distribuem a carga de leitura (cada uma serve SELECTs) e dão disponibilidade (uma cai, as outras servem). Não sharde — os dados cabem num nó, então dividi-los não traz benefício, só complexidade. Replicação é a resposta simples.

(c) Dados não cabem no maior servidor → sharding. Aqui o sharding se justifica de verdade: nenhuma máquina única comporta o dataset, então você precisa dividi-lo entre máquinas. Replicação não ajudaria (cada réplica precisaria de tudo, e tudo não cabe). Este é o caso legítimo de sharding — quando o tamanho excede um nó. (Mas confirme que esgotou a escala vertical primeiro.)

(d) Escrita afoga o primário, mesmo na maior máquina → sharding. O outro caso legítimo de sharding: quando a escrita (não a leitura) excede o que um nó aguenta. Réplicas não ajudam na escrita (todas as escritas vão ao primário). Sharding distribui a escrita: cada shard recebe só a sua fatia, então a carga de escrita total se divide entre as máquinas. Tamanho ou escrita excedendo um nó = os dois casos legítimos de sharding.

O insight: sharding é para tamanho ou escrita que excedem um nó. Leitura e disponibilidade → replicação. Lentidão sem problema de tamanho → índices/otimização. A maioria dos "preciso shardar" é na verdade um dos outros três — por isso o sharding é último recurso.

Médio
Exercício 2 · Avaliar shard keys

Para um sistema de pedidos de e-commerce, avalie cada candidata a shard key pelos três critérios (cardinalidade, distribuição, alinhamento com queries) e diga se é boa ou ruim: (a) status_do_pedido (pendente/pago/enviado/entregue); (b) data_do_pedido; (c) customer_id; (d) order_id (UUID aleatório).

(a) status_do_pedido → péssima. Cardinalidade baixíssima (4 valores) — você nunca teria mais que 4 shards. Distribuição enviesada (a maioria dos pedidos antigos está "entregue", então um shard fica gigante). E os status mudam (um pedido vai de pendente a entregue), o que moveria o dado entre shards. Viola todos os critérios. Nunca use um campo de baixa cardinalidade e mutável como shard key.

(b) data_do_pedido → ruim para distribuição, mas útil para range (cuidado com hotspot). Cardinalidade ok (muitas datas), mas a distribuição é o problema: por intervalo, o shard do período atual recebe toda a escrita nova (todo pedido é "agora"), enquanto os shards de meses passados ficam ociosos — hotspot de escrita clássico do range partitioning. É bom se você faz muitas range queries por data (relatórios temporais), mas precisa lidar com o hotspot. Por hash da data, perde o benefício de range. Use com cuidado, ciente do trade-off.

(c) customer_id → boa (com a ressalva da chave quente). Cardinalidade alta (muitos clientes), distribuição razoável (se nenhum cliente domina), e alinhada com a query comum "os pedidos deste cliente" (vai direto a um shard). É uma forte candidata. A ressalva: um cliente gigante (um marketplace dentro do site, uma conta enorme) seria uma chave quente — o "celebrity problem", tratável com as curas da seção 13.6. Mas para a maioria dos casos, é uma boa escolha.

(d) order_id (UUID aleatório) → ótima distribuição, mas desalinhada com queries por cliente. Cardinalidade altíssima e distribuição perfeitamente uniforme (UUID aleatório espalha lindamente, sem hotspots). MAS: o alinhamento é o problema — se a maioria das suas queries é "os pedidos deste cliente", elas não incluem o order_id, então cada uma teria que consultar todos os shards (scatter-gather). Ótima para distribuição, ruim para o padrão de acesso por cliente. Seria boa se você acessasse sempre por order_id individual.

O insight: nenhum critério sozinho basta. order_id distribui perfeitamente mas desalinha; customer_id equilibra os três (a melhor escolha geral, com a ressalva da chave quente); data serve range queries mas arrisca hotspot; status falha em tudo. A arte é equilibrar os três critérios para o seu padrão de acesso dominante — e customer_id geralmente vence para um e-commerce centrado no cliente.

Médio
Exercício 3 · Diagnosticar a hot partition

Dois sistemas têm hot partitions. Para cada um, diagnostique o tipo de hot partition e a cura apropriada: (a) um sistema shardado por região geográfica, onde 70% dos usuários estão num único país e aquele shard está sempre sobrecarregado; (b) um sistema shardado por ID de produto, onde tudo vai bem até uma promoção relâmpago de um produto específico, cujo shard então afoga.

(a) 70% dos usuários num país → hot partition por distribuição desigual de chaves. Este é o primeiro tipo: a shard key (região) distribui mal porque os valores são enviesados — um shard (o país dominante) recebe a maioria das chaves. Cura: escolher uma chave melhor distribuída. Em vez de shardar puramente por região, shardar por hash do user_id dentro da região (combinando geografia para residência de dados com hash para distribuição uniforme — o "geography + hash" composto). Assim os usuários do país dominante se espalham por vários shards, em vez de se concentrarem num. O problema é a distribuição das chaves, e a cura é uma chave/esquema que distribua melhor.

(b) Promoção relâmpago de um produto → hot partition por chave individualmente quente. Este é o segundo tipo, e é diferente: a distribuição das chaves está boa (produtos espalhados), mas uma chave específica (o produto em promoção) fica intrinsecamente quente por um período. Nenhuma mudança de shard key resolve — em qualquer esquema, aquele produto em promoção concentra a carga no shard onde mora. Cura: as da chave quente única — cachear agressivamente o produto em promoção (a maioria das visualizações nem chega ao shard, Cap 10), replicar a chave quente para distribuir a leitura, e possivelmente preparar a arquitetura para "eventos de pico previsíveis" (uma promoção é conhecida de antemão — pode-se pré-aquecer caches e réplicas). O problema é uma chave, e a cura é cache/réplica, não particionamento.

O insight: o diagnóstico é tudo. (a) é distribuição desigual de chaves — muitas chaves concentradas num shard pela chave enviesada; a cura é distribuir melhor (chave/esquema melhor). (b) é uma chave individualmente quente — uma chave concentra a carga; a cura é cache/réplica/split, e particionamento não ajuda. Confundir os dois leva à cura errada: tentar reshardar a promoção (inútil) ou cachear o país inteiro (não é o ponto). Identifique se o problema é a distribuição das chaves ou uma chave quente, e a cura se segue.

Difícil
Exercício 4 · A decisão de shardar — entrevista

Você lidera um sistema cujo banco relacional está ficando sobrecarregado conforme a empresa cresce. A liderança pergunta: "devemos shardar?". Estruture como você abordaria essa decisão — o que investigaria antes, como decidiria se o sharding é necessário, e, se for, como escolheria a estratégia e a shard key. Demonstre que você trata o sharding como último recurso e que entende seus custos.

Enquadramento: a resposta forte não pula para "como shardar" — ela primeiro questiona se shardar, porque o sharding é a fonte de complexidade que você adia o máximo possível. Vou estruturar como uma escada de investigação antes de qualquer decisão.

1. Diagnosticar o gargalo real (antes de tudo): "sobrecarregado" não é diagnóstico. Preciso medir: o problema é tamanho (dados não cabem), escrita (afogando o primário), leitura (muitos SELECTs), ou queries lentas (mal otimizadas)? Cada um tem uma solução diferente, e só dois (tamanho, escrita) levam a sharding. Sem esse diagnóstico, qualquer decisão é chute.

2. Subir a escada de alternativas (o sharding é o último degrau):

  • Otimização de queries e índices: muitas vezes "sobrecarregado" é queries mal indexadas. O ganho mais barato — verificar primeiro.
  • Escala vertical: uma máquina maior? Os servidores modernos comportam datasets enormes; frequentemente resolve por anos.
  • Cache (Cap 10): se é leitura, cachear absorve a maior parte, tirando a pressão do banco.
  • Réplicas de leitura: se é leitura e disponibilidade, réplicas resolvem sem a complexidade do sharding.

Só se nenhuma dessas resolver — e o gargalo for genuinamente tamanho ou escrita excedendo um nó — é que o sharding se justifica.

3. Se o sharding for inevitável — escolher a estratégia: pela natureza do acesso. Acesso por chave individual e distribuição uniforme → hash. Range queries / séries temporais → range (cuidando do hotspot do período atual). Residência de dados (LGPD/GDPR) → geográfica (ou composta: geografia + hash). A estratégia segue o padrão de acesso dominante.

4. Escolher a shard key — a decisão mais consequente: aplicar os três critérios: alta cardinalidade (muitos valores), distribuição uniforme (nenhum valor domina), e alinhamento com as queries (a maioria das queries inclui a chave, para irem direto a um shard). E pensar nas chaves quentes prováveis (contas/produtos gigantes — o celebrity problem) e em como tratá-las (cache, réplica). Lembrar que mudar a chave depois exige reshardar — então é uma decisão de mão única, tomada uma vez.

5. Planejar os custos permanentes: aceitar e planejar para o que o sharding traz — queries entre shards (projetar para que a maioria respeite a partição, isolar as que cruzam em async/Saga/warehouse), rebalanceamento (usar um banco com hashing consistente e rebalanceamento automático), e a complexidade operacional. Idealmente, usar um banco distribuído gerenciado que faça o sharding por baixo, em vez de implementá-lo na aplicação.

O que um bom candidato demonstra: que diagnostica o gargalo antes de decidir; que trata o sharding como último recurso, subindo a escada de alternativas mais simples primeiro; que, se shardar, escolhe estratégia e chave pelos critérios certos; que antecipa as chaves quentes; e que planeja para os custos permanentes (cross-shard, rebalanceamento). Frase de fechamento: "minha primeira resposta a 'devemos shardar?' é 'provavelmente ainda não' — porque o sharding é a maior fonte de complexidade permanente que existe, e a maioria dos 'estamos sobrecarregados' se resolve com um índice, uma máquina maior, cache ou réplicas. Só sharbo quando o gargalo é genuinamente tamanho ou escrita excedendo um nó, e nada mais resolve. E quando sharbo, escolho a shard key com o cuidado de uma decisão irreversível — alta cardinalidade, distribuição uniforme, alinhada com as queries — porque mudá-la depois é migrar todos os dados. O sharding resolve um problema real, mas cobra para sempre; eu o adio até ele ser inevitável, e então o faço com cuidado."

Fim do capítulo 13
Próximo capítulo: o log como primitiva. Você já viu o log aparecer por toda a Parte III — o Kafka do Cap 11 é um log, a replicação se faz por log, o event sourcing se apoia nele. Agora vamos olhá-lo como o conceito unificador que é: uma sequência append-only, ordenada, imutável, que se revela uma das ideias mais poderosas dos sistemas distribuídos — a fonte da verdade de onde réplicas, índices, caches e estados derivam. O capítulo que fecha a Parte III mostrando que muitas peças que parecem diferentes são, no fundo, a mesma ideia.
Parte III · Capítulo 14 · Os componentes

O log como
primitiva.

O log apareceu por toda esta parte sem se anunciar. O Kafka do Capítulo 11 é um log. A replicação de bancos se faz por log. O event sourcing se apoia num log. E não é coincidência: o log — uma sequência append-only, ordenada e imutável de registros — é uma das ideias mais profundas e unificadoras dos sistemas distribuídos. Olhada de perto, ela revela que muitas peças que parecem diferentes (filas, réplicas, índices, caches) são, no fundo, a mesma coisa: derivações de um log. Este capítulo fecha a Parte III mostrando que sob a diversidade dos componentes há uma primitiva só.

Este é o capítulo conceitual que costura os anteriores. Vamos definir o log com precisão (e ver que não é o "log de aplicação" que você pensa), reconhecê-lo nos lugares onde ele sempre esteve escondido (o write-ahead log dos bancos, a replicação), entender a dualidade profunda entre log e estado (um estado é o resultado de aplicar um log; um log é a sequência de mudanças de um estado), ver como a replicação inteira se reduz a "distribuir o mesmo log para todos", como índices, caches e visões podem todos ser derivados de um log, o que significa tratar o log como a fonte única da verdade (a base do event sourcing, Cap 18), e os limites honestos dessa ideia. Quando você sair daqui, terá uma lente nova: por baixo de filas, bancos, réplicas e caches, você vai enxergar o mesmo log — e essa unificação é uma das ideias mais poderosas que um projetista de sistemas pode ter.

14.1 A história — a ideia que estava escondida à vista

Contexto histórico

O log é antigo — tão antigo quanto os bancos de dados confiáveis. Desde os anos 1970-80, todo banco relacional sério usa, por baixo, um write-ahead log (WAL): antes de modificar os dados, o banco escreve a mudança num log sequencial. Por quê? Porque se o sistema cai no meio de uma operação, o log permite refazer (redo) ou desfazer (undo) as mudanças, garantindo a durabilidade e a atomicidade do ACID (Cap 12). O log era a tecnologia invisível que tornava os bancos confiáveis — mas era visto como um detalhe de implementação, não como uma ideia central.

Em paralelo, a teoria dos sistemas distribuídos descobria a mesma estrutura por outro caminho. Leslie Lamport e outros, trabalhando em como fazer várias máquinas concordarem (consenso), formularam a replicação de máquina de estados (state machine replication): se você tem várias máquinas que começam no mesmo estado e aplicam a mesma sequência de operações na mesma ordem, elas terminam no mesmo estado. E "a mesma sequência de operações na mesma ordem" é, precisamente, um log. Os algoritmos de consenso (Paxos, depois Raft) são, no coração, formas de fazer várias máquinas concordarem sobre um log.

Essas duas tradições — o WAL dos bancos e a replicação de máquina de estados da teoria distribuída — eram a mesma ideia, descoberta independentemente. Mas foi preciso alguém conectar os pontos e gritar a unificação. Em 2013, Jay Kreps, um dos criadores do Kafka no LinkedIn, escreveu um ensaio que virou clássico — "The Log: What every software engineer should know about real-time data's unifying abstraction" — argumentando que o log não era um detalhe, mas a abstração fundamental por trás de bancos, filas, replicação, e integração de dados. O Kafka nasceu dessa visão: não como "uma fila melhor", mas como um log distribuído, primeira classe.

Em 2026, "pensar em logs" é uma das mentalidades mais valiosas de um arquiteto de sistemas. O insight de Kreps amadureceu numa forma de ver a infraestrutura inteira: bancos são logs com índices derivados; filas são logs consumidos; replicação é distribuir um log; event sourcing é guardar o log como verdade. Sistemas modernos de dados (Kafka, e arquiteturas inteiras construídas sobre ele) tratam o log como a espinha dorsal da qual todo o resto deriva. Este capítulo é sobre essa lente — não uma tecnologia específica, mas uma forma de ver que, uma vez adquirida, reorganiza seu entendimento de tudo que veio antes na Parte III. O log estava à vista o tempo todo; falta aprender a vê-lo.

14.2 O que é o log (e o que não é)

Primeiro, desfazer uma confusão de nome. O "log" deste capítulo não é o log de aplicação que você lê para debugar ("ERROR: conexão falhou às 14h32"). É uma estrutura de dados precisa, com propriedades específicas:

O log: append-only, ordenado, imutável
O log, como primitiva, é a estrutura de armazenamento mais simples que existe: uma sequência de registros com três propriedades. Append-only: você só adiciona ao final; nunca modifica nem remove o que já está lá. Ordenado: os registros têm uma ordem total — cada um tem uma posição (offset), e a ordem é a ordem em que foram adicionados (o tempo lógico). Imutável: uma vez escrito, um registro nunca muda. É só isso — uma lista crescente de coisas que aconteceram, em ordem, que você lê da esquerda para a direita. A simplicidade é a força: porque é só append, escrever é rápido (sequencial no disco, o mais rápido que existe); porque é ordenado, todos que o leem veem os eventos na mesma ordem (a base da consistência); porque é imutável, ele é um registro confiável do que aconteceu, que nunca mente sobre o passado. Compare com uma tabela de banco, que você atualiza no lugar (perdendo o histórico) — o log nunca esquece, ele só acumula. Essa estrutura trivial, veremos, é a fundação da qual estruturas muito mais complexas (bancos, réplicas, índices) podem ser derivadas. A potência do log vem justamente de sua simplicidade: pouca coisa pode dar errado numa lista que só cresce, em ordem.

14.3 O log que sempre esteve lá

Uma vez que você sabe o que procurar, o log aparece em toda parte na infraestrutura — frequentemente como a tecnologia invisível que faz as coisas funcionarem:

  • O write-ahead log do banco: antes de mudar os dados, o banco anota a mudança num log. É como ele garante durabilidade (o log sobrevive ao crash) e atomicidade (refaz/desfaz a partir do log). Todo banco ACID tem um log no coração.
  • A replicação: bancos replicam enviando seu log de mudanças (o WAL, ou um binlog) para as réplicas, que o aplicam para chegar ao mesmo estado. Replicar é compartilhar um log.
  • O Kafka (Cap 11): não é "uma fila" — é literalmente um log distribuído, exposto como produto. Os consumidores leem o log na sua posição (offset).
  • O consenso (Raft, Paxos): esses algoritmos existem para fazer várias máquinas concordarem sobre um log de operações — o coração de sistemas de coordenação como ZooKeeper e etcd.
  • O event sourcing (Cap 18): guardar as mudanças (eventos) num log como a verdade, em vez de só o estado atual.
  • O blockchain: é um log append-only, imutável e distribuído (com garantias criptográficas) — o log levado ao extremo da descentralização.
Quando você vê o log em todo lugar, a infraestrutura simplifica
Há um momento, ao aprender essa lente, em que a infraestrutura inteira parece se reorganizar: você percebe que bancos, filas, sistemas de replicação, ferramentas de consenso e plataformas de streaming — que pareciam coisas distintas — são todos variações da mesma ideia, o log, com diferentes coisas construídas em cima. Isso não é só elegância intelectual; tem valor prático. Quando você reconhece que dois sistemas são "logs por baixo", você entende como conectá-los (o log de um pode alimentar o outro), como raciocinar sobre suas garantias (a ordem e a imutabilidade do log se propagam), e como simplificar arquiteturas (em vez de N sistemas trocando dados de N² formas, todos leem do mesmo log central). Foi exatamente essa percepção que levou à arquitetura "log-centric" que o Kreps descreveu: um log central como a espinha dorsal da organização, do qual todos os sistemas (bancos, índices de busca, caches, data warehouses) derivam, em vez de se integrarem aos pares. A lente do log não é trivia — é uma ferramenta de simplificação que transforma "muitos sistemas diferentes conversando" em "muitos consumidores de um log comum".

14.4 A dualidade log-estado — a ideia central

Aqui está a percepção que dá ao log seu poder, e que vale parar para absorver: um log e um estado são duas formas de ver a mesma coisa. Eles são duais — você converte um no outro.

Do log ao estado: aplicar (replay)

Pegue um log de mudanças (depositou R$100, sacou R$30, depositou R$50) e aplique-as em ordem, partindo do zero. O resultado é o estado atual (saldo = R$120). O estado é o log "dobrado" (folded) — o resultado de aplicar todas as mudanças. Reprocessar o log do início reconstrói o estado.

Do estado ao log: capturar mudanças

Inversamente, um estado que muda ao longo do tempo gera um log: cada modificação é um registro ("saldo mudou de 100 para 120"). Observar as mudanças de um estado produz o log delas. É o que o WAL e o Change Data Capture (CDC) fazem — transformar mutações de estado num log de mudanças.

O estado é o log dobrado; o log é o histórico do estado
A dualidade, dita de forma precisa: o estado é o resultado de aplicar (somar, "dobrar") todas as entradas do log; o log é o histórico completo das mudanças que produziram o estado. Eles contêm a mesma informação, vista de dois ângulos — o log é o "filme" (toda a sequência de mudanças), o estado é o "quadro atual" (o resultado acumulado). E essa equivalência tem consequências profundas. Se você guarda o log, você pode sempre reconstruir o estado (reaplicando-o) — e mais: pode reconstruir qualquer estado passado (aplicando só até um ponto), criar visões diferentes do mesmo log (aplicando-o de formas diferentes), e auditar tudo que aconteceu (o log nunca esquece). Se você guarda só o estado (como um banco tradicional que atualiza no lugar), você perde o histórico — sabe o saldo atual, mas não como chegou nele. É por isso que guardar o log como fonte da verdade (event sourcing, Cap 18) é tão poderoso: o log contém mais informação que o estado, e dele você deriva o estado quando quiser, mas do estado você não recupera o log. A escolha de guardar o log em vez de só o estado é a escolha de nunca perder a história — e de poder derivar dela tudo que precisar.

14.5 Replicação como log — fazer N máquinas concordarem

A dualidade log-estado resolve, de forma elegante, um dos problemas mais difíceis dos sistemas distribuídos: como fazer várias máquinas terem o mesmo estado. A resposta, via log, é surpreendentemente simples de enunciar.

Mesma sequência de operações, mesma ordem → mesmo estado
O problema de replicação é: tenho N máquinas que deveriam ter dados idênticos; como garanto que tenham? A resposta da replicação de máquina de estados é genial em sua simplicidade: se todas as máquinas começam no mesmo estado inicial e aplicam exatamente a mesma sequência de operações, na exata mesma ordem, elas garantidamente terminam no mesmo estado (assumindo operações determinísticas). E "a mesma sequência de operações na mesma ordem" é precisamente um log compartilhado. Então o problema de "fazer N máquinas concordarem sobre o estado" se reduz ao problema de "fazer N máquinas concordarem sobre um log" — distribuir a mesma sequência ordenada de mudanças para todas. Isso é uma redução poderosa, porque concordar sobre um log (uma lista ordenada de coisas) é um problema mais simples e bem-estudado (é o que os algoritmos de consenso como Raft resolvem) do que sincronizar estados arbitrários diretamente. É por isso que praticamente toda replicação funciona assim: o primário gera um log de mudanças, e as réplicas aplicam esse log em ordem para espelhar o estado. A consistência entre réplicas vira a consistência do log que elas compartilham. Resolver a replicação é resolver a distribuição confiável e ordenada de um log — e a ordem e imutabilidade do log são exatamente o que garante que todas cheguem ao mesmo lugar.

14.6 Derivar tudo do log — índices, caches, visões

Se o estado é uma derivação do log, então qualquer estado é — e isso inclui índices, caches, visões materializadas, resultados de busca. Todos podem ser vistos como diferentes "dobras" do mesmo log de mudanças.

Um log, muitas visões derivadas — cada consumidor dobra o log à sua maneira
Aqui a ideia floresce numa arquitetura. Se você tem um log central de tudo que acontece (pedidos criados, atualizados, cancelados), então cada sistema que precisa de uma visão desses dados pode ser construído como um consumidor que lê o log e o "dobra" à sua maneira: o banco relacional aplica o log para manter o estado atual dos pedidos; o índice de busca (Elasticsearch) aplica o log para manter os pedidos pesquisáveis; o cache aplica o log para manter os pedidos quentes em memória; o data warehouse aplica o log para análise; um serviço de notificações lê o log para avisar os usuários. Todos derivam do mesmo log, cada um produzindo uma visão diferente otimizada para seu uso. Isso tem três vantagens enormes. Primeiro, consistência: todas as visões derivam da mesma fonte ordenada, então não divergem arbitrariamente (a confusão de "quem é dono do frescor" do Cap 10 se dissolve — o log é o dono). Segundo, reconstrução: se um índice corrompe, você o reconstrói reaplicando o log do início. Terceiro, evolução: para adicionar uma visão nova (um novo índice, um novo serviço), você simplesmente adiciona um consumidor que lê o log desde o começo — sem tocar nos outros. É a arquitetura log-centric: o log é a verdade, e tudo o mais é uma projeção derivada e descartável dele. Índices, caches e bancos deixam de ser fontes independentes que precisam ser sincronizadas, e viram views de um log comum — reconstruíveis, consistentes, e independentemente evoluíveis.

14.7 O log como fonte da verdade

Levando a ideia ao limite: e se o log não for um detalhe interno (como o WAL), mas a fonte primária da verdade do sistema — o registro autoritativo, do qual o estado é apenas uma derivação? Essa inversão é o coração do event sourcing (Cap 18), e vale entender o que ela troca.

O tradicional: o estado é a verdade

O banco guarda o estado atual (saldo = R$120), atualizando-o no lugar. O log (WAL) é um detalhe interno, descartado após servir. Você sabe o agora, mas perdeu o histórico — não sabe como chegou a R$120, e não pode reconstruir um saldo passado nem auditar a sequência.

Log como verdade: as mudanças são a verdade

O log de mudanças (eventos: +100, -30, +50) é a fonte autoritativa, guardada para sempre. O estado (saldo) é uma derivação, recalculável a qualquer momento reaplicando o log. Você tem o histórico completo, auditoria, reconstrução de qualquer estado passado, e a liberdade de derivar novas visões.

Guardar o log como verdade ganha história e auditoria — e custa espaço e complexidade
Tratar o log como a fonte da verdade (em vez de só o estado) tem benefícios reais que decorrem da dualidade: você nunca perde a história (o log é imutável e completo), pode auditar tudo que aconteceu (crucial para finanças, compliance), pode reconstruir qualquer estado passado ("qual era o saldo em março?"), pode corrigir bugs reprocessando o log com a lógica corrigida, e pode derivar visões novas a qualquer momento. Mas — como tudo em design — há um custo. Guardar todas as mudanças para sempre custa espaço (o log só cresce; mitiga-se com snapshots periódicos do estado, para não reaplicar desde o início dos tempos). Reconstruir o estado a partir do log custa computação (o espaço-tempo do Cap 4). E pensar em termos de eventos imutáveis em vez de estado mutável é uma mudança de modelo mental que adiciona complexidade conceitual e exige cuidado (eventos mal modelados são difíceis de mudar, pois o passado é imutável). Por isso o log-como-verdade não é o default universal — é uma escolha poderosa para domínios onde a história e a auditoria importam (finanças, sistemas regulados, qualquer coisa onde "como chegamos aqui" é tão valioso quanto "onde estamos"), e complexidade desnecessária onde só o estado atual interessa. O capítulo 18 aprofunda quando vale. Aqui, o ponto é entender a opção: o log pode ser não o detalhe interno, mas a verdade — e essa escolha troca espaço e complexidade por história e flexibilidade.

14.8 Os limites do log

Como toda ideia poderosa, o log não é uma bala de prata, e tratá-lo como solução universal leva a forçá-lo onde não cabe. Seus limites honestos:

  • Consultar o estado atual exige derivá-lo. Um log é ótimo para "o que aconteceu", mas péssimo para "qual o estado agora?" diretamente — você precisa dobrá-lo (ou manter uma visão derivada). Para acesso ao estado atual, você ainda precisa de um banco/índice derivado; o log sozinho não responde "qual o saldo?" sem reprocessamento.
  • O log só cresce. Append-only significa crescimento perpétuo. Sem compactação, snapshots ou retenção limitada, o log fica grande demais para reprocessar do início. Gerenciar esse crescimento (snapshots, compaction) é trabalho real.
  • A ordem total tem custo. Um log estritamente ordenado é, por natureza, um ponto de serialização (Cap 7, Amdahl) — daí o particionamento (Cap 13): logs particionados (como no Kafka) dão ordem por partição, não total, para escalar. Ordem global num log é cara.
  • Eventos são imutáveis — e isso corta dos dois lados. A imutabilidade dá auditoria e confiabilidade, mas também significa que um evento mal modelado é difícil de corrigir (o passado não muda; você só adiciona eventos corretivos). O esquema dos eventos é uma decisão de mão única (Cap 1).
O log é uma lente, não uma religião
A lente do log é uma das mais valiosas que um arquiteto pode adquirir, mas como toda lente, ela mostra algumas coisas e esconde outras. Ver bancos, filas e réplicas como logs esclarece profundamente — revela a unidade sob a diversidade, simplifica integrações, fundamenta a replicação e o event sourcing. Mas nem todo problema é mais bem resolvido como um log: muitos sistemas só precisam do estado atual, mutável, num banco tradicional, e impor o log-como-verdade a eles é complexidade gratuita. A maturidade é usar a lente para entender (reconhecer o log que está lá, raciocinar sobre garantias, conectar sistemas) sem fazer dela um dogma que força tudo a ser um log explícito. O melhor uso do capítulo não é "transforme tudo em event sourcing"; é "entenda que sob a Parte III inteira — as filas, os bancos, as réplicas, os caches — há uma primitiva comum, e essa compreensão te torna um projetista melhor, porque você vê as conexões que antes pareciam coincidências". O log é a ideia que unifica a Parte III; usá-la como lente de entendimento é o ganho, e saber quando não torná-la explícita é a maturidade.

14.9 Estudo de caso — a integração que virou um log

De N² integrações frágeis a um log central
O cenário

Uma empresa tinha crescido para ter muitos sistemas que precisavam dos mesmos dados de pedidos: o banco transacional, o índice de busca, o data warehouse de analytics, o serviço de email, o sistema de recomendações, o cache. Cada um se integrava com os outros aos pares — o banco notificava a busca, que notificava o cache, que... Era uma teia de integrações ponto-a-ponto, frágil, inconsistente (cada sistema com uma versão ligeiramente diferente dos dados), e impossível de evoluir (adicionar um sistema novo exigia integrá-lo com vários). A solução foi repensar tudo através da lente do log.

Passo 1 · O problema das N² integrações
a teia frágil
Cada sistema integrava com cada outro que precisava dos dados:
  banco ──> busca, cache, warehouse, email, recomendações
  busca ──> cache, ...
  warehouse <── banco, busca, ...
  ...

Com N sistemas, ~N² conexões. Cada uma com seu formato, sua
lógica de sincronização, seus bugs. Adicionar o sistema N+1
exigia integrá-lo com os N existentes. Inconsistências em
toda parte (cada sistema com uma versão diferente do dado).
Frágil, caro de manter, impossível de evoluir.
Passo 2 · O insight: um log central
de N² para N
Insight (a lente do log): em vez de N² integrações aos pares,
um LOG CENTRAL de eventos de pedido, do qual todos derivam.

  pedidos ──> [ LOG CENTRAL de eventos ] <── (fonte da verdade)
                       │
       ┌───────┬───────┼───────┬───────┐
       ▼       ▼       ▼       ▼       ▼
     banco   busca   cache  warehouse email  (consumidores)

Cada sistema vira um CONSUMIDOR que lê o log e o "dobra" à sua
maneira. N conexões (cada sistema ao log), não N². Adicionar
um sistema novo = adicionar UM consumidor ao log.
Passo 3 · As propriedades do log resolvem os problemas
o que cada propriedade do log entrega
ORDENADO: todos os consumidores veem os eventos na MESMA
  ordem → as visões não divergem arbitrariamente. A
  inconsistência entre sistemas some (todos dobram o mesmo
  log ordenado).

IMUTÁVEL + RETIDO: um consumidor novo (ou um que corrompeu)
  reconstrói sua visão relendo o log do início. Recuperação
  e onboarding triviais.

DESACOPLADO: produtores escrevem no log sem saber dos
  consumidores; consumidores leem sem saber dos produtores
  (Cap 11). Adicionar/remover sistemas não afeta os outros.
Passo 4 · O resultado

A teia de N² integrações frágeis virou N consumidores de um log central (na prática, um Kafka). As inconsistências sumiram (todos derivam da mesma fonte ordenada). Adicionar um sistema novo virou trivial (mais um consumidor lendo o log). Reconstruir uma visão corrompida virou reler o log. E a empresa ganhou algo que não tinha: um registro completo e ordenado de tudo que aconteceu com os pedidos, do qual qualquer visão futura pode ser derivada. A arquitetura log-centric do Kreps, na prática.

Resultado: a lente do log transformou um problema de integração caótico (N² conexões frágeis e inconsistentes) numa arquitetura limpa (N consumidores de um log central). Cada propriedade do log resolveu um problema: a ordem eliminou as inconsistências (todos dobram o mesmo log na mesma ordem), a imutabilidade e retenção tornaram triviais a recuperação e o onboarding (releia o log), e o desacoplamento (Cap 11) tornou a evolução fácil (mais um consumidor). Esta é a ideia central do capítulo em ação: reconhecer que os muitos sistemas precisavam, no fundo, das mesmas mudanças ordenadas — um log — e que derivar todos de um log central era infinitamente mais simples que integrá-los aos pares. A lente do log não é trivia acadêmica; é a percepção que transforma arquiteturas. Isto fecha a Parte III — os componentes. Você conheceu as peças (balanceadores, caches, filas, bancos, sharding) e agora a primitiva que muitas delas compartilham (o log). A Parte IV sobe um nível: dos componentes isolados aos padrões que os combinam — síncrono vs assíncrono, idempotência, transações distribuídas e Saga, CQRS e event sourcing, rate limiting, resiliência. As formas comprovadas de juntar as peças em sistemas que funcionam sob falha.

14.10 Erros comuns

Erro 1 · Confundir o log-primitiva com o log de aplicação

Achar que "o log" é o arquivo de texto que você lê para debugar ("ERROR às 14h32"). O log-primitiva é uma estrutura de dados precisa: sequência append-only, ordenada, imutável. São coisas diferentes; o segundo é a fundação de bancos, filas e replicação, não um registro de diagnóstico.

Erro 2 · Não enxergar o log que já está lá

Tratar bancos, filas, réplicas e Kafka como coisas totalmente distintas, perdendo a unidade. Reconhecer que todos têm um log por baixo (WAL, binlog, o próprio Kafka, o log de consenso) esclarece garantias, simplifica integrações, e revela como conectá-los. A lente do log unifica o que parece disperso.

Erro 3 · Guardar só o estado quando a história importa

Atualizar o estado no lugar (banco tradicional) num domínio onde o histórico e a auditoria importam (finanças, compliance) — e perder para sempre como se chegou ao estado atual. Onde "como chegamos aqui" tem valor, considere guardar o log de mudanças como verdade (event sourcing, Cap 18): do log você deriva o estado, mas do estado não recupera o log.

Erro 4 · Esperar que o log responda "qual o estado agora?"

Usar um log para consultar o estado atual diretamente — ele é ótimo para "o que aconteceu", péssimo para "qual o saldo agora?" sem reprocessar. Para acesso ao estado atual, você precisa de uma visão derivada (banco/índice). O log é a verdade; o estado é uma projeção que você ainda precisa manter para consultar.

Erro 5 · Ignorar o crescimento perpétuo do log

Tratar o log append-only como se pudesse crescer para sempre sem consequência — e descobrir que reprocessá-lo do início levou horas, ou que ele esgotou o armazenamento. Gerencie o crescimento: snapshots periódicos do estado (para não reaplicar desde o início), compactação, retenção limitada onde a história antiga não importa.

Erro 6 · Exigir ordem total onde a parcial bastaria

Insistir num log globalmente ordenado (um ponto de serialização, Amdahl/Cap 7) quando ordem por partição bastaria — limitando a escala. Logs particionados (Kafka) dão ordem dentro da partição, não total, justamente para escalar. Peça a ordem que você precisa (geralmente por chave), não a total.

Erro 7 · Transformar tudo em log por dogma

Aplicar event sourcing e log-como-verdade a todo sistema porque a ideia é elegante — adicionando complexidade onde só o estado atual mutável bastaria. O log é uma lente para entender e uma opção para casos onde a história importa, não um dogma universal. Use-o para esclarecer; torne-o explícito só onde compensa.

Verifique seu entendimento
Sua empresa tem muitos sistemas (banco, busca, cache, data warehouse, notificações) que precisam dos mesmos dados de pedidos, integrados aos pares — uma teia frágil e inconsistente, onde adicionar um sistema novo exige integrá-lo com todos os outros. Aplicando a "lente do log", qual é a abordagem mais sólida?

14.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Encontrar o log escondido

Para cada sistema, identifique onde está o log (a sequência append-only, ordenada, imutável) por baixo, e o que ele garante: (a) a replicação de um banco PostgreSQL para uma réplica; (b) o Kafka; (c) um sistema de event sourcing de transações bancárias; (d) o ZooKeeper/etcd coordenando um cluster.

(a) Replicação do PostgreSQL → o write-ahead log (WAL). O primário escreve cada mudança no WAL (um log ordenado de modificações), e envia esse log para a réplica, que o aplica em ordem para chegar ao mesmo estado. O log garante que a réplica espelha o primário (mesma sequência de operações → mesmo estado) e a durabilidade (o WAL sobrevive a crashes).

(b) Kafka → ele é o log. Kafka não tem um log escondido — ele é, literalmente, um log distribuído exposto como produto. Tópicos são logs particionados; cada partição é uma sequência append-only ordenada; consumidores leem por offset. Garante ordem (por partição), retenção (replay) e desacoplamento (produtores/consumidores independentes).

(c) Event sourcing bancário → o log de eventos é a fonte da verdade. Aqui o log não é interno — é a verdade primária. Cada transação (depósito, saque) é um evento imutável append-only no log; o saldo (estado) é derivado aplicando os eventos. Garante o histórico completo, auditoria (crucial para banco), e reconstrução de qualquer saldo passado.

(d) ZooKeeper/etcd → o log de consenso (replicado via Raft/ZAB). Esses sistemas de coordenação mantêm um log replicado de operações, sobre o qual os nós chegam a consenso (Raft no etcd, ZAB no ZooKeeper). Todos os nós aplicam o mesmo log na mesma ordem → mesmo estado de coordenação. Garante que o cluster concorde sobre o estado (quem é líder, que locks existem) de forma consistente, mesmo com falhas.

O insight: em todos os quatro, há um log append-only ordenado por baixo — às vezes escondido (WAL do Postgres, log de consenso do etcd), às vezes explícito (Kafka, event sourcing). Reconhecê-lo revela que esses sistemas aparentemente distintos compartilham a mesma primitiva, e que suas garantias (consistência entre réplicas/nós, durabilidade, auditoria) vêm das propriedades do log (ordem, imutabilidade).

Médio
Exercício 2 · A dualidade log-estado na prática

Considere uma conta bancária. (a) Mostre como um log de eventos (depósitos e saques) "dobra" no estado (saldo). (b) Explique o que você ganha guardando o log em vez de só o saldo. (c) Explique o que você perde e como mitigar (espaço, performance de leitura do saldo).

(a) O log dobrando no estado:

log → estado
LOG (a verdade):          aplicando em ordem (dobrando):
  +100 (depósito)            saldo: 0 → 100
  -30  (saque)               saldo: 100 → 70
  +50  (depósito)            saldo: 70 → 120

ESTADO = soma de todos os eventos = R$120
(o estado é o log "dobrado" — o resultado acumulado)

(b) O que você ganha guardando o log:

  • Histórico completo: você sabe não só que o saldo é R$120, mas exatamente como chegou lá — cada transação.
  • Auditoria: para um banco, isso é essencial (e legal) — toda mudança é rastreável e imutável.
  • Estados passados: "qual era o saldo depois do saque?" → aplique o log até ali (R$70). Impossível se você guardasse só o saldo atual.
  • Correção de bugs: se a lógica de cálculo tinha um bug, corrija-a e reaplique o log — o estado se recalcula corretamente.
  • Novas visões: quer um relatório de "média de saques por mês"? Derive do log, sem ter previsto isso antes.

(c) O que você perde e como mitigar:

  • Espaço: o log cresce para sempre (toda transação fica guardada). Mitigação: snapshots — periodicamente, guarde o estado calculado (saldo em tal data) para não precisar reaplicar desde o primeiro evento; reaplique só a partir do último snapshot.
  • Performance de ler o saldo: calcular o saldo dobrando todo o log a cada leitura é caro. Mitigação: manter uma visão derivada do saldo atual (num banco/cache), atualizada a cada novo evento — você lê o saldo dessa visão (rápido), e o log permanece a verdade para auditoria/reconstrução. (Isto é, em essência, o CQRS do Cap 18: separar a escrita-no-log da leitura-da-visão.)

O insight: a dualidade dá a escolha — guardar o log (mais informação: história, auditoria, flexibilidade) ou só o estado (menos espaço, leitura direta). O log-como-verdade ganha a história ao custo de espaço e da necessidade de derivar o estado para leitura — mitigados por snapshots e visões derivadas. Para um banco (onde auditoria é lei), o ganho compensa; para um contador de likes, provavelmente não.

Médio
Exercício 3 · Derivar visões de um log

Um log central registra eventos de "pedido criado", "pedido pago", "pedido enviado", "pedido cancelado". Descreva como cada um destes sistemas seria construído como um consumidor que "dobra" esse log à sua maneira, e o que ganha em consistência: (a) um banco com o estado atual dos pedidos; (b) um índice de busca; (c) um dashboard de métricas de vendas; (d) um sistema de notificação ao cliente.

(a) Banco com estado atual dos pedidos: consome o log e mantém uma tabela com o estado corrente de cada pedido — "pedido criado" insere uma linha (status: criado), "pedido pago" atualiza para pago, "enviado" para enviado, "cancelado" para cancelado. Dobra o log no estado atual. É a visão que responde "qual o status deste pedido agora?".

(b) Índice de busca: consome o mesmo log e mantém os pedidos indexados para busca — cada evento atualiza o documento pesquisável do pedido (texto, status, datas). Dobra o log numa estrutura otimizada para busca. Responde "encontre pedidos que contêm X / com status Y".

(c) Dashboard de métricas: consome o log e agrega — conta pedidos criados por dia, soma o valor dos pagos, calcula taxa de cancelamento. Dobra o log em agregados estatísticos. Responde "quanto vendemos esta semana?". (Note: ele dobra o mesmo log de forma totalmente diferente do banco — agregando em vez de manter estado por pedido.)

(d) Sistema de notificação: consome o log e reage a eventos específicos — em "pedido pago", envia email de confirmação; em "pedido enviado", envia o código de rastreio; em "cancelado", envia aviso de cancelamento. Não mantém um estado derivado durável; usa o log como gatilho de ações. Responde a "o que fazer quando algo acontece".

O que ganham em consistência: como todos derivam do mesmo log ordenado, eles não divergem arbitrariamente — o banco, a busca e o dashboard refletem a mesma sequência de eventos, então concordam sobre o que aconteceu (a inconsistência entre sistemas, o pesadelo das integrações aos pares, desaparece). Se um deles corromper, reconstrói relendo o log. E adicionar um quinto sistema (ex.: detecção de fraude) é só adicionar um consumidor que dobra o log à sua maneira — sem tocar nos outros quatro.

O insight: o mesmo log, dobrado de quatro formas diferentes, produz quatro visões otimizadas para usos diferentes — todas consistentes entre si por derivarem da mesma fonte ordenada. É a arquitetura log-centric: uma verdade, muitas projeções.

Difícil
Exercício 4 · Quando o log é (e não é) a resposta — entrevista

Um colega, empolgado com a "lente do log", propõe rearquitetar todos os sistemas da empresa em torno de event sourcing e um log central, porque "tudo é um log". O entrevistador pergunta: "isso é uma boa ideia? Quando a arquitetura log-centric / event sourcing brilha, e quando ela é complexidade desnecessária?". Demonstre que você entende tanto o poder quanto os limites da ideia.

Enquadramento: a resposta forte abraça o poder da lente do log como ferramenta de entendimento e integração, mas rejeita o dogma de "transforme tudo em event sourcing". A chave é distinguir usar o log para esclarecer e integrar (quase sempre bom) de tornar o log a verdade primária de tudo via event sourcing (bom só em casos específicos).

Onde a lente do log brilha (quase sempre útil):

  • Integração de dados: o caso do estudo deste capítulo — N sistemas que precisam dos mesmos dados, integrados aos pares (N² frágil). Um log central como espinha dorsal é uma vitória clara: consistência, desacoplamento, evolução fácil. Isto quase sempre vale em organizações com muitos sistemas.
  • Replicação e consistência: entender que replicação é distribuir um log esclarece garantias e é a base de como os bancos já funcionam. Útil como modelo mental.
  • Streaming e processamento em tempo real: onde os dados são naturalmente um fluxo de eventos, o log é o ajuste natural.

Onde o event sourcing (log como verdade) brilha (casos específicos):

  • Domínios onde a história e a auditoria são essenciais: finanças, contabilidade, sistemas regulados, saúde — onde "como chegamos a este estado" é tão importante quanto o estado, e a auditoria é exigência legal. Aqui o log-como-verdade entrega valor real.
  • Onde reconstruir estados passados ou derivar visões novas é valioso: sistemas que evoluem muito, onde poder reprocessar o histórico com lógica nova compensa.

Onde é complexidade desnecessária:

  • Sistemas que só precisam do estado atual: um CRUD simples, um cadastro, um catálogo — onde ninguém precisa do histórico nem da auditoria. Event sourcing aqui adiciona a complexidade de pensar em eventos imutáveis, gerenciar o crescimento do log, derivar o estado para leitura — tudo para nenhum benefício. Um banco tradicional que guarda o estado é mais simples e correto.
  • Equipes sem experiência com a mentalidade de eventos: event sourcing exige uma mudança de modelo mental (eventos imutáveis, esquema de mão única, derivação de estado) que tem curva de aprendizado real e pega armadilhas (eventos mal modelados são difíceis de corrigir).

O veredito sobre a proposta do colega: "rearquitetar tudo" é o erro do dogma. Adotar um log central para integração entre os muitos sistemas? Provavelmente excelente. Transformar todo sistema em event sourcing? Não — só aqueles onde a história/auditoria justificam a complexidade. A lente do log é para entender e integrar; o event sourcing é uma escolha pontual.

O que um bom candidato demonstra: que distingue a lente do log (entender/integrar — amplamente útil) do event sourcing (log como verdade — pontualmente útil); que conhece os casos onde cada um brilha e onde vira complexidade gratuita; e que resiste ao dogma "tudo é um log, logo tudo deve ser event sourcing". Frase de fechamento: "a lente do log é uma das mais valiosas que existem — para entender replicação, para integrar muitos sistemas via um log central, ela é transformadora, e eu a usaria amplamente. Mas 'tudo é um log' é uma verdade conceitual, não um mandato arquitetural. Tornar o log a verdade primária de cada sistema, via event sourcing, só compensa onde a história e a auditoria importam de verdade — finanças, sistemas regulados. Para um CRUD que só precisa do estado atual, event sourcing é complexidade pura. Eu uso a lente para enxergar e para integrar; reservo o event sourcing para os domínios que pedem o histórico como verdade. O poder da ideia está em saber quando aplicá-la — e quando não."

Fim do capítulo 14 · Fim da Parte III
Você concluiu os componentes. Conhece as peças — balanceadores que distribuem, caches que aceleram, filas que desacoplam, bancos que guardam, o sharding que divide — e a primitiva que muitas compartilham: o log. Tem o vocabulário das ferramentas com que se constroem sistemas. A Parte IV combina essas peças em padrões: as formas comprovadas de juntá-las em sistemas que funcionam sob a falha que as Partes I e II garantiram que viria. Síncrono vs assíncrono, idempotência, transações distribuídas e Saga, CQRS e event sourcing, rate limiting, resiliência. Peça "continua" para a Parte IV.
Parte IV
Os padrões

As formas comprovadas de combinar as peças em sistemas que funcionam sob falha. Comunicação síncrona e assíncrona e arquiteturas de eventos, idempotência e garantias de entrega, transações distribuídas e Saga, CQRS e event sourcing, rate limiting e backpressure, e a resiliência de timeouts, retries e circuit breakers. Seis capítulos sobre juntar os componentes em algo que sobrevive ao mundo real.

Sync vs Async Idempotência Saga CQRS · Event Sourcing Rate limiting Resiliência
Parte IV · Capítulo 15 · Os padrões

Síncrono vs
assíncrono, e os
eventos.

Como as partes de um sistema conversam é, possivelmente, a decisão de arquitetura mais definidora que existe — mais do que qual banco, qual linguagem, qual nuvem. Síncrono (uma parte chama outra e espera) ou assíncrono (uma parte emite um evento e segue): essa escolha molda o acoplamento, a resiliência, a latência percebida, e a própria forma como você raciocina sobre o sistema. O Capítulo 4 deu o vocabulário; este mostra como ele se torna arquitetura — e por que a resposta madura quase nunca é "tudo síncrono" ou "tudo assíncrono", mas saber exatamente onde cada um pertence.

Este capítulo abre a Parte IV — os padrões — com o eixo que mais define um sistema. Vamos ver por que síncrono vs assíncrono é a escolha mais consequente da comunicação, o que a comunicação síncrona (request-response) garante e cobra, o que a assíncrona (eventos, mensagens) ganha e custa, a distinção entre comando ("faça isto") e evento ("isto aconteceu") que muda como os serviços se acoplam, os dois estilos de coordenar um fluxo distribuído (coreografia descentralizada vs orquestração centralizada) e quando usar cada um, a arquitetura orientada a eventos como um todo, e o padrão híbrido que combina o melhor dos dois. Quando você sair daqui, "como meus serviços conversam?" deixa de ser um detalhe de implementação e vira a decisão de design que você sabe ser a mais definidora — tomada conscientemente, parte a parte.

15.1 A história — do RPC ao evento

Contexto histórico

No começo, fazer máquinas conversarem pela rede foi modelado para parecer com o que os programadores já conheciam: a chamada de função. Nos anos 1980-90, o RPC (Remote Procedure Call) e suas encarnações (CORBA, RMI, depois SOAP) deixavam você chamar um método numa máquina remota como se fosse local — síncrono, bloqueante, request-response. Era intuitivo, e foi o paradigma dominante. Também era, como vimos no Cap 8, uma armadilha: esconder a rede atrás de uma chamada que parece local convida todas as falácias da computação distribuída.

Os anos 2000 trouxeram o REST, que simplificou o RPC sobre HTTP e dominou a integração de sistemas web — mas continuou fundamentalmente síncrono: o cliente chama, espera, recebe. Para a maioria das interações (um app pedindo dados a um servidor), isso bastava e basta. O modelo request-response é simples de entender e de programar, e por isso continua sendo o default da comunicação.

Mas conforme os sistemas se decompuseram em muitos serviços (a era dos microsserviços, anos 2010), o acoplamento síncrono começou a doer. Quando o serviço A chama B que chama C que chama D, todos síncronos, uma lentidão em D trava A — as falhas e a latência se propagam em cascata (Cap 8). Surgiu, com força, a comunicação assíncrona orientada a eventos: em vez de A chamar B, A emite um evento ("pedido criado") que B (e outros) consomem quando podem, via um broker de mensagens (Cap 11). O acoplamento temporal se dissolvia.

Em 2026, o consenso amadureceu para além da falsa dicotomia. Não é "síncrono é antigo, assíncrono é o futuro" — é que cada um serve a propósitos diferentes, e os melhores sistemas usam ambos, conscientemente. O padrão emergente é o híbrido: comunicação síncrona na borda voltada ao cliente (uma API request-response, baixa latência percebida), que imediatamente dispara workflows assíncronos orientados a eventos no processamento interno (resiliência, desacoplamento). "Sync na borda, async por dentro." A escolha entre síncrono e assíncrono deixou de ser uma decisão única para o sistema e virou uma decisão por interação — exatamente como a granularidade que vimos no CAP (Cap 5) e na consistência (Cap 6). Este capítulo é sobre fazer essa escolha bem, em cada ponto onde as partes do seu sistema conversam.

15.2 O eixo que define tudo

Antes dos detalhes, vale firmar por que essa escolha pesa tanto. A forma de comunicação não é um detalhe de transporte — ela determina propriedades fundamentais do sistema inteiro:

PropriedadeSíncronoAssíncrono
Acoplamento temporalAlto — ambos online ao mesmo tempoBaixo — desacoplados no tempo
Resiliência a falhaFalha se propaga em cascataFalha isolada (a mensagem espera)
Latência percebidaO chamador espera o trabalho todoResposta imediata; trabalho depois
SimplicidadeSimples de entender e debugarComplexo (entrega, ordem, rastreio)
Absorver picosPico afoga (sem buffer)Fila absorve o pico
A escolha de comunicação é a escolha de acoplamento
O que faz síncrono vs assíncrono ser tão definidor é que, no fundo, é uma escolha sobre acoplamento (Cap 4) — e o acoplamento determina quase tudo o mais. Comunicação síncrona cria acoplamento temporal: o chamador e o chamado precisam estar ambos disponíveis e responsivos no mesmo instante, o que significa que a indisponibilidade ou lentidão de um afeta diretamente o outro (a cascata). Comunicação assíncrona quebra esse acoplamento temporal: o emissor do evento não precisa que o consumidor esteja disponível agora — a mensagem espera. Essa diferença de acoplamento se propaga para todas as outras propriedades: o sistema síncrono é mais simples mas mais frágil (acoplado), o assíncrono é mais resiliente mas mais complexo (desacoplado). É a mesma tensão acoplamento-vs-simplicidade do Cap 4, agora na comunicação. E como o acoplamento molda como o sistema falha, escala e evolui, escolher a forma de comunicação é, na prática, escolher essas propriedades. Não é um detalhe de "como mando os bytes"; é "quão acopladas minhas partes são" — a decisão arquitetural mais consequente.

15.3 A comunicação síncrona — simples e acoplada

Na comunicação síncrona (request-response), o chamador faz uma requisição e bloqueia — espera — até o chamado terminar e responder. É o modelo do REST, do gRPC, do GraphQL. Sua grande virtude é a simplicidade; seu grande risco é o acoplamento.

A cascata de falhas: a corrente síncrona arrebenta no elo mais fraco
O perigo característico do síncrono em sistemas de muitos serviços é a cascata de falhas. Quando o serviço A chama B síncronamente, e B chama C, e C chama D — todos esperando —, o sistema vira uma corrente: A só responde quando D responde. Se D fica lento, C fica lento esperando D, B fica lento esperando C, A fica lento esperando B — a lentidão se propaga para trás por toda a cadeia. Pior, se D cai, e não há proteção, as requisições se acumulam esperando timeouts, esgotam recursos (threads, conexões), e a falha de um serviço no fim da cadeia derruba todos os anteriores — a falha em cascata que pode levar o sistema inteiro abaixo a partir de um único elo. É por isso que a comunicação síncrona em cadeias longas é frágil: ela acopla a disponibilidade e a latência de todos os elos, e a corrente é tão forte quanto o elo mais fraco. As defesas existem (timeouts, circuit breakers — Cap 20), mas são justamente isso: defesas contra a fragilidade inerente do acoplamento síncrono. O síncrono é simples de raciocinar (a resposta vem na hora, no lugar), mas essa simplicidade tem como contrapartida a fragilidade da cadeia acoplada.

Apesar do risco, o síncrono é o default certo para muitas interações: quando o chamador genuinamente precisa da resposta para continuar (uma busca, uma leitura, uma validação que decide o próximo passo), e a cadeia é curta. Para essas, a simplicidade do request-response supera o risco de acoplamento — não force assíncrono onde você precisa da resposta agora (Cap 4, o erro de "assíncrono para ser rápido").

15.4 A comunicação assíncrona — resiliente e complexa

Na comunicação assíncrona, o emissor envia uma mensagem ou emite um evento e segue em frente — não espera o processamento (Cap 4). O trabalho acontece depois, via um broker (Cap 11). Ganha-se desacoplamento e resiliência; paga-se em complexidade.

O que o assíncrono ganha

Desacoplamento temporal: o consumidor pode estar offline; a mensagem espera. Resiliência: a falha de um consumidor não afeta o emissor — sem cascata. Absorção de picos: a fila amortece rajadas (Cap 11). Escala por backlog: escala-se pela profundidade da fila, não pelo QPS instantâneo. Múltiplos consumidores: um evento pode alimentar vários sistemas (Cap 14).

O que o assíncrono cobra

Complexidade: entrega, ordem, duplicação, falha — todos os problemas do Cap 11. Consistência eventual: o resultado vem depois, então o sistema é eventualmente consistente (Cap 6). Rastreabilidade difícil: seguir uma operação que atravessa eventos por vários serviços é muito mais difícil de debugar que uma chamada síncrona. Não há resposta imediata: "deu certo?" não é respondível na hora.

Async resiliência e escala ao custo de raciocínio e rastreio
O trade-off do assíncrono é nítido: você ganha as propriedades de um sistema desacoplado (resiliência a falhas, absorção de picos, escala flexível, evolução fácil) e paga com a dificuldade de raciocinar sobre ele. Num sistema síncrono, seguir o que acontece é direto — você lê a cadeia de chamadas, vê a resposta vir, debuga o caminho. Num sistema assíncrono, uma operação do usuário pode disparar um evento que dispara outro que dispara outro, por serviços diferentes, em momentos diferentes — e quando algo dá errado, reconstruir "o que aconteceu e por quê" é um quebra-cabeça espalhado no tempo e no espaço. É por isso que sistemas assíncronos exigem investimento pesado em observabilidade (tracing distribuído, correlação de eventos) — sem isso, você tem resiliência mas não consegue debugar. A regra: o assíncrono vale quando você precisa do desacoplamento (resiliência, picos, múltiplos consumidores) e está disposto a pagar com a complexidade de rastreio (e a investir em observabilidade para mitigá-la). Não escolha assíncrono pela velocidade (não é mais rápido, Cap 4) nem pela moda — escolha-o pelo desacoplamento, sabendo que está trocando a facilidade de raciocínio pela resiliência.

15.5 Comando vs evento — a distinção que muda o acoplamento

Dentro da comunicação, há uma distinção sutil mas profunda entre dois tipos de mensagem, e ela determina quem conhece quem — o grau de acoplamento entre os serviços:

Comando — "faça isto"

Uma instrução direcionada a um destinatário específico: "ProcesseOPagamento", "EnvieOEmail". O emissor sabe quem deve agir e o que deve ser feito. Acoplamento maior: o emissor conhece o receptor e espera uma ação específica. Há um destinatário lógico.

Evento — "isto aconteceu"

Um fato sobre algo que ocorreu: "PagamentoProcessado", "PedidoCriado". O emissor anuncia sem saber nem se importar quem (se alguém) vai reagir. Acoplamento mínimo: o emissor não conhece os consumidores. Pode haver zero, um ou muitos reagindo.

Eventos desacoplam porque o emissor não conhece os consumidores
A diferença entre comando e evento é a diferença entre "eu te digo o que fazer" e "eu anuncio o que aconteceu". E essa diferença é a chave do acoplamento. Com comandos, o emissor sabe quem deve agir — ele direciona a instrução a um serviço específico, o que cria acoplamento (o emissor conhece o receptor e sua interface). Com eventos, o emissor apenas publica um fato ("o pedido foi criado") sem saber quem vai consumi-lo — pode ser ninguém hoje, e três serviços novos amanhã, e o emissor nunca muda. Esse é o desacoplamento máximo: adicionar um consumidor novo (um serviço de fraude que reage a "pedido criado") não exige tocar no emissor; ele só passa a escutar o evento que já era publicado. É por isso que arquiteturas orientadas a eventos são tão extensíveis (Cap 14, a derivação de visões): o produtor de eventos não sabe nem precisa saber quem deriva o quê. A regra prática: use comandos quando você precisa que um serviço específico faça algo específico (e aceita o acoplamento), e eventos quando você quer anunciar um fato e deixar que qualquer interessado reaja, agora ou no futuro (e quer o desacoplamento). Pensar em "que fato aconteceu?" (evento) em vez de "quem eu mando fazer o quê?" (comando) é a virada mental que torna sistemas extensíveis.

15.6 Coreografia vs orquestração — coordenar o fluxo

Quando uma operação de negócio precisa de vários serviços (criar pedido → cobrar → reservar estoque → enviar), surge a pergunta: quem coordena essa sequência? Há dois estilos opostos, e a escolha entre eles é um trade-off real.

Coreografia — descentralizada

Não há maestro. Cada serviço reage a eventos e emite os seus, e o fluxo emerge dessas reações: "pedido criado" → o serviço de pagamento reage e emite "pago" → o de estoque reage e emite "reservado"... O fluxo está espalhado, implícito nas reações. Máximo desacoplamento; difícil de ver e debugar o fluxo todo.

Orquestração — centralizada

Um orquestrador central conhece o fluxo e comanda cada passo: "pagamento, cobre" → (espera) → "estoque, reserve" → (espera) → "envio, despache". O fluxo é explícito, num lugar. Mais fácil de ver, monitorar e tratar erros; mas o orquestrador é um ponto central de lógica (e potencial gargalo).

Coreografia desacopla mas espalha o fluxo; orquestração centraliza mas clarifica
O trade-off entre coreografia e orquestração é entre desacoplamento e visibilidade. A coreografia (cada serviço reage a eventos, sem maestro) dá o máximo desacoplamento — nenhum serviço conhece o fluxo todo, cada um só reage ao que lhe interessa, e adicionar um passo é adicionar um reator. Mas o preço é que o fluxo de negócio fica espalhado e implícito: não há um lugar que diga "este é o processo de pedido"; ele emerge das reações distribuídas, o que torna difícil entender, monitorar e debugar o processo como um todo (quando algo trava no meio, onde está?). A orquestração (um orquestrador central comanda os passos) inverte: o fluxo fica explícito e centralizado — há um lugar que define e controla o processo inteiro, fácil de ver, monitorar, e tratar erros (o orquestrador sabe em que passo travou). Mas o preço é o acoplamento ao orquestrador (todos os serviços dependem dele) e o fato de ele concentrar a lógica. A escolha: para fluxos simples e altamente desacoplados, ou onde a extensibilidade importa mais que a visibilidade, coreografia. Para fluxos de negócio complexos onde entender, monitorar e tratar erros do processo inteiro é crucial (a maioria dos fluxos transacionais sérios), a orquestração costuma vencer — sua observabilidade superior compensa o orquestrador central. É a tensão de sempre: desacoplamento (coreografia) vs controle e clareza (orquestração).

15.7 A arquitetura orientada a eventos

Juntando as peças, a arquitetura orientada a eventos (event-driven architecture, EDA) é o estilo onde os serviços se comunicam primariamente emitindo e reagindo a eventos, em vez de se chamarem diretamente. Ela leva ao extremo o desacoplamento — com os ganhos e os custos que já vimos.

EDA é poderosa e exige disciplina — a falha raramente está no código
A arquitetura orientada a eventos entrega o que promete: serviços profundamente desacoplados, que escalam independentemente, evoluem sem se quebrar, e absorvem picos naturalmente — um sistema onde adicionar uma capacidade nova é adicionar um consumidor de eventos, sem tocar no resto (Cap 14). É genuinamente poderosa para sistemas complexos e em evolução. Mas há uma verdade incômoda observada repetidamente: equipes inteligentes e bem financiadas falham em implementar EDA corretamente, e a falha raramente está no código — está na governança e na disciplina arquitetural. Sem disciplina, a EDA degenera num emaranhado de eventos onde ninguém entende o fluxo completo, eventos mal modelados criam acoplamento implícito, a falta de observabilidade torna o debugging impossível, e a consistência eventual surpreende quem não a projetou. A EDA não é "mais fácil" — ela troca a complexidade explícita do acoplamento síncrono pela complexidade implícita e espalhada do desacoplamento assíncrono, e essa segunda complexidade exige investimento sério em observabilidade (tracing, monitoramento de eventos), em modelagem cuidadosa dos eventos (que são contratos de mão única, Cap 1), e em governança (quem pode emitir o quê, como os eventos evoluem). Adotar EDA sem essa disciplina é trocar problemas conhecidos por problemas mais difíceis de ver. Ela brilha quando você precisa do desacoplamento e tem a maturidade para operá-la — não como um default para todo sistema.

15.8 O padrão híbrido — o melhor dos dois

A resposta madura de 2026 não é escolher um lado, mas combinar os dois onde cada um pertence. O padrão dominante: síncrono na borda voltada ao cliente, assíncrono no processamento interno.

Sync na borda, async por dentro: baixa latência percebida e resiliência interna
O padrão híbrido — às vezes chamado "sync-over-async" — resolve a falsa dicotomia combinando os dois onde cada um brilha. Na borda voltada ao cliente, comunicação síncrona: o cliente faz uma requisição (request-response) e recebe uma resposta rápida — "seu pedido foi recebido!" — com baixa latência percebida e a simplicidade que o cliente espera. Mas essa resposta síncrona não faz todo o trabalho; ela apenas valida o essencial e dispara um workflow assíncrono interno (emite um evento "pedido criado"), e retorna. No processamento interno, comunicação assíncrona e orientada a eventos: cobrar, faturar, notificar, indexar — tudo acontece em segundo plano, desacoplado, resiliente a falhas, absorvendo picos. O cliente teve a experiência rápida e simples do síncrono; o sistema teve a resiliência e a escala do assíncrono. É exatamente o fluxo do estudo de caso da Black Friday (Cap 11): a parte síncrona é mínima (reservar, registrar, responder), e o trabalho pesado vira eventos assíncronos. A lição que fecha o capítulo: a melhor arquitetura de comunicação raramente é "tudo síncrono" (frágil, não escala) ou "tudo assíncrono" (complexo na borda, ruim para o cliente que quer resposta) — é colocar o limite no lugar certo: síncrono onde o cliente espera uma resposta agora, assíncrono onde o trabalho pode (e deve) ser desacoplado. Desenhar esse limite com clareza é a arte.

15.9 Estudo de caso — a cadeia síncrona que caiu em cascata

Como um serviço lento derrubou o sistema inteiro
O cenário

Um sistema de checkout fora decomposto em microsserviços que se chamavam síncronamente: o serviço de checkout chamava o de pagamento, que chamava o de estoque, que chamava o de notificação, que chamava um provedor de email externo — tudo numa cadeia, cada um esperando o próximo. Funcionava bem até um dia em que o provedor de email externo ficou lento. Em minutos, o sistema de checkout inteiro caiu — ninguém conseguia comprar. Como a lentidão de um email derrubou as compras?

Passo 1 · A anatomia da cascata
a corrente síncrona arrebentando
A cadeia síncrona:
  checkout → pagamento → estoque → notificação → email(externo)
     (cada um ESPERA o próximo responder)

O email externo ficou lento (5s por chamada). Então:
  - notificação ficou presa esperando o email (5s)
  - estoque ficou preso esperando notificação (5s)
  - pagamento ficou preso esperando estoque (5s)
  - checkout ficou preso esperando pagamento (5s)

E as requisições se acumularam: cada checkout segurava uma
thread por 5s esperando a cadeia. As threads esgotaram. Novos
checkouts não conseguiam nem começar. O sistema TODO caiu —
por causa de um provedor de EMAIL lento no fim da cadeia.
Passo 2 · O erro de design: tudo síncrono e acoplado

O problema raiz era de arquitetura de comunicação: coisas que não precisavam ser síncronas (notificar, enviar email) estavam na cadeia síncrona crítica do checkout. Enviar um email de confirmação não tem nada a ver com a capacidade de completar uma compra — mas, por estar na corrente síncrona, sua lentidão se propagou para trás e derrubou tudo. As coisas estavam acopladas no tempo quando não deviam estar.

Passo 3 · O redesenho: o limite síncrono/assíncrono certo
movendo o limite
SÍNCRONO (a cadeia crítica mínima — o que decide a compra):
  checkout → pagamento → estoque
  (reservar estoque e cobrar PRECISAM ser confirmados agora;
   o cliente espera saber se a compra deu certo)
  → responde ao cliente: "compra confirmada!"

ASSÍNCRONO (o que NÃO decide a compra → vira evento):
  emite "pedido confirmado" → fila
     ├─> serviço de notificação (consome quando puder)
     ├─> serviço de email (consome; se o provedor está lento,
     │   a fila segura, com retry — NÃO afeta o checkout)
     ├─> serviço de fatura
     └─> analytics

Agora o email lento afeta SÓ a fila de email (que espera e
retenta). O checkout responde rápido e NUNCA cai por causa
de um email. O limite foi posto no lugar certo.
Passo 4 · O resultado

Com o email e as notificações movidos para fora da cadeia síncrona — virando eventos assíncronos —, a lentidão do provedor de email deixou de tocar o checkout. Quando o email externo ficava lento, sua fila simplesmente crescia e retentava, invisível para o cliente, que continuava comprando normalmente. O sistema ficou resiliente exatamente onde antes era frágil: a falha de um componente não-crítico no fim da cadeia não derrubava mais o componente crítico no início. E o checkout ficou mais rápido, porque parou de esperar o trabalho que não precisava esperar.

Resultado: a cascata não foi causada por um bug — foi causada pela arquitetura de comunicação errada: coisas não-críticas (email, notificação) acopladas síncronamente na cadeia crítica do checkout, onde a lentidão de uma se propagava para trás e derrubava tudo. A correção não foi adicionar mais máquinas nem otimizar o email — foi mover o limite entre síncrono e assíncrono para o lugar certo: síncrono só para o que decide a compra (pagamento, estoque — o cliente precisa saber agora), assíncrono para tudo o mais (email, notificação, fatura — pode acontecer depois, desacoplado). É o padrão híbrido do capítulo, e a lição central: a escolha síncrono vs assíncrono, feita por interação, é o que separa um sistema que cai em cascata de um que degrada graciosamente. Pôr o trabalho não-crítico na corrente síncrona é convidar a cascata; tirá-lo para eventos assíncronos é a resiliência. Próximo capítulo: idempotência e garantias de entrega — porque assim que você move para o assíncrono (e para os retries que a rede não-confiável exige), surge o problema de processar a mesma coisa duas vezes, e a idempotência é o que torna isso seguro. O conceito que apareceu no Cap 11 e no Cap 8, agora estudado a fundo.

15.10 Erros comuns

Erro 1 · Trabalho não-crítico na cadeia síncrona

Acoplar síncronamente coisas que não decidem a operação (email, notificação, analytics) na cadeia crítica — e ver a lentidão de um componente não-crítico no fim derrubar o crítico no início (cascata). Mantenha na cadeia síncrona só o que o cliente precisa confirmar agora; o resto vira evento assíncrono.

Erro 2 · Cadeias síncronas longas sem proteção

Encadear muitos serviços síncronos (A→B→C→D) sem timeouts nem circuit breakers — criando uma corrente onde a lentidão ou queda de um elo se propaga e derruba todos. Cadeias síncronas longas são frágeis por natureza; encurte-as, ou proteja cada elo (Cap 20), ou desacople com eventos.

Erro 3 · Assíncrono pela velocidade, não pelo desacoplamento

Tornar interações assíncronas "para serem rápidas" — quando o assíncrono não acelera o trabalho (Cap 4), só desacopla no tempo, ao custo de complexidade. Escolha assíncrono pelo desacoplamento (resiliência, picos, múltiplos consumidores), não por uma velocidade que ele não dá.

Erro 4 · Forçar assíncrono onde precisa da resposta agora

Tornar assíncrona uma interação em que o chamador genuinamente precisa da resposta para continuar (uma validação, uma leitura) — herdando a complexidade do async sem motivo, e deixando o cliente sem a resposta que espera. Use síncrono onde a resposta é necessária na hora; nem tudo deve ser desacoplado.

Erro 5 · Confundir comando com evento

Modelar tudo como comando direcionado ("ServiçoX, faça Y") quando um evento ("Y aconteceu") desacoplaria — e criar acoplamento desnecessário onde o emissor conhece todos os receptores. Para anunciar fatos que muitos podem querer, use eventos (o emissor não conhece os consumidores); reserve comandos para quando um serviço específico deve fazer algo específico.

Erro 6 · Coreografia para fluxos complexos que exigem visibilidade

Usar coreografia (eventos descentralizados) para um fluxo de negócio complexo e crítico — e não conseguir ver, monitorar nem debugar o processo inteiro, espalhado em reações implícitas. Para fluxos complexos onde entender e tratar erros do processo todo importa, prefira orquestração (fluxo explícito e centralizado), apesar do orquestrador central.

Erro 7 · EDA sem observabilidade nem disciplina

Adotar arquitetura orientada a eventos pelo desacoplamento, sem investir em observabilidade (tracing, correlação) nem em governança (modelagem de eventos, evolução) — e acabar com um emaranhado impossível de debugar. A EDA troca complexidade explícita por implícita; exige disciplina e observabilidade sérias. Sem elas, é trocar problemas conhecidos por piores.

Verifique seu entendimento
Seu sistema de checkout chama, numa cadeia síncrona, os serviços de pagamento → estoque → notificação → email (provedor externo). Quando o provedor de email fica lento, o checkout inteiro cai e ninguém consegue comprar. Qual é a correção mais sólida?

15.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Síncrono ou assíncrono?

Para cada interação, decida se deve ser síncrona ou assíncrona, justificando pela necessidade de resposta imediata e pelo acoplamento: (a) um app buscando o perfil de um usuário para exibir; (b) processar o upload de um vídeo após o envio; (c) validar um cupom de desconto no checkout; (d) atualizar o índice de busca quando um produto muda.

(a) Buscar o perfil para exibir → síncrono. O app precisa dos dados agora para renderizar a tela — não há nada para fazer sem eles. É uma leitura onde a resposta é necessária imediatamente. Request-response simples. Tornar isso assíncrono não faria sentido (o que o app mostraria enquanto espera?).

(b) Processar o upload de vídeo → assíncrono. Transcodificar um vídeo leva minutos; o usuário não pode (nem deve) esperar bloqueado. Responda "upload recebido, processando" (síncrono mínimo) e processe em background via fila (assíncrono). Caso clássico de desacoplar trabalho pesado (Cap 11).

(c) Validar um cupom no checkout → síncrono. O resultado da validação decide o próximo passo — se o cupom é válido, aplica o desconto; se não, avisa o usuário. O checkout precisa da resposta para continuar. Síncrono, porque a resposta é necessária na hora para decidir. (Forçar assíncrono aqui deixaria o usuário sem saber se o cupom valeu.)

(d) Atualizar o índice de busca → assíncrono. O produto ficar pesquisável alguns segundos depois de mudar é perfeitamente aceitável, e a indexação não deve bloquear nem afetar a atualização do produto. Emita um evento "produto atualizado" e deixe o serviço de busca consumir e reindexar em background. Desacoplado, eventualmente consistente, sem cascata.

O insight: a pergunta decisiva é "o chamador precisa da resposta agora para continuar?". Se sim (exibir perfil, validar cupom) → síncrono. Se não (processar vídeo, indexar) → assíncrono, desacoplando e ganhando resiliência. Note que (b) combina: síncrono mínimo (confirma o upload) + assíncrono (processa) — o padrão híbrido em miniatura.

Médio
Exercício 2 · Comando ou evento?

Para cada mensagem, diga se é melhor modelada como comando ("faça isto") ou evento ("isto aconteceu"), e explique o impacto no acoplamento e na extensibilidade: (a) "cobrar o cartão do cliente"; (b) "o pedido foi pago"; (c) "enviar email de boas-vindas"; (d) "o usuário se cadastrou".

(a) "Cobrar o cartão" → comando. É uma instrução direcionada a um serviço específico (o de pagamento) que deve realizar uma ação específica e crítica. Você quer que exatamente um serviço execute isso, e provavelmente quer saber o resultado. Comando é apropriado — o acoplamento (o emissor conhece o serviço de pagamento) é justificado, porque há de fato um destinatário e uma ação determinada.

(b) "O pedido foi pago" → evento. É um fato que aconteceu, e muitos serviços podem querer reagir: o estoque (reservar), o de fatura (gerar NF), o de notificação (avisar), analytics, fidelidade (dar pontos). O emissor não deve saber nem se importar quem reage. Evento desacopla e torna extensível: adicionar um serviço de fidelidade que reage a "pedido pago" não exige tocar em quem emite o evento.

(c) "Enviar email de boas-vindas" → comando (ou consequência de um evento). Por si, é uma instrução a um serviço específico (email) fazer uma ação específica — um comando. Mas note o padrão melhor: em vez de mandar o comando "envie email" de quem cadastrou, é mais desacoplado emitir o evento "usuário se cadastrou" (item d) e deixar o serviço de email reagir a ele enviando o boas-vindas. Assim quem cadastra não precisa saber que existe email. O comando funciona, mas o evento desacopla melhor.

(d) "O usuário se cadastrou" → evento. Um fato, do qual muitas reações podem decorrer: enviar email de boas-vindas, criar perfil padrão, notificar vendas, adicionar a uma lista, dar um bônus. Emitir o evento e deixar cada interessado reagir é o desacoplamento máximo — e a extensibilidade: amanhã, um serviço novo de onboarding reage ao mesmo evento sem que ninguém mude.

O insight: comandos (a, c) são instruções direcionadas — use quando um serviço específico deve fazer algo específico, e o acoplamento é aceitável. Eventos (b, d) são fatos anunciados — use quando você quer que qualquer interessado reaja, agora ou no futuro, com desacoplamento e extensibilidade. A virada de design: preferir "anunciar o fato" (evento) a "mandar fazer" (comando) onde possível, porque eventos tornam o sistema extensível sem tocar no emissor.

Médio
Exercício 3 · Coreografia ou orquestração?

Para cada fluxo de negócio, escolha entre coreografia (descentralizada, por eventos) e orquestração (centralizada, por um orquestrador), justificando pelo trade-off desacoplamento vs visibilidade: (a) um fluxo simples onde "usuário se cadastrou" dispara email, criação de perfil e analytics, independentes entre si; (b) um fluxo de pedido complexo (pagar → reservar estoque → faturar → enviar) com necessidade de tratar falhas em cada passo e reverter; (c) reações independentes a um evento (notificar vários sistemas que algo mudou); (d) um processo de aprovação de empréstimo com múltiplas etapas, decisões condicionais e auditoria.

(a) Cadastro dispara ações independentes → coreografia. As ações (email, perfil, analytics) são independentes entre si — nenhuma depende da outra, todas só reagem ao fato "usuário se cadastrou". Coreografia é perfeita: emite o evento, cada serviço reage por conta própria. Não há um "fluxo" complexo para orquestrar; são reações paralelas e desacopladas. Orquestrar isso seria overhead desnecessário.

(b) Pedido complexo com falhas e reversão → orquestração. Aqui há um fluxo de verdade, com passos dependentes (estoque só depois do pagamento), tratamento de falha em cada um, e necessidade de reverter (se o envio falha, estornar o pagamento — uma Saga, Cap 17). Esse processo precisa ser visível, monitorável e controlável como um todo — o orquestrador sabe em que passo está, o que falhou, e como compensar. Coreografia espalharia essa lógica complexa por reações implícitas, tornando impossível ver e tratar o fluxo. Orquestração vence pela visibilidade e controle.

(c) Reações independentes a uma mudança → coreografia. Como (a): vários sistemas reagindo independentemente ao mesmo fato. Emita o evento, cada um consome. Não há sequência nem dependência — é fan-out de reações, o caso ideal de coreografia. Desacoplamento máximo, e adicionar um sistema é adicionar um consumidor.

(d) Aprovação de empréstimo com etapas, decisões e auditoria → orquestração. Um processo de negócio complexo, com decisões condicionais (se score < X, rejeita; se Y, pede mais dados), múltiplas etapas dependentes, e auditoria (precisa registrar o caminho da decisão). Isso clama por orquestração: um lugar central que define e executa o fluxo, fácil de ver, modificar, monitorar e auditar. Coreografia tornaria o processo de aprovação um emaranhado impossível de entender e auditar.

O insight: coreografia para reações independentes a fatos (fan-out simples, desacoplado — a, c); orquestração para fluxos de negócio complexos com passos dependentes, tratamento de falha, decisões e necessidade de visibilidade/auditoria (b, d). A pergunta-chave: é um conjunto de reações paralelas independentes (coreografia) ou um processo sequencial complexo que precisa ser visto e controlado como um todo (orquestração)? Quanto mais complexo o fluxo e mais importante a visibilidade, mais a orquestração compensa seu ponto central.

Difícil
Exercício 4 · Desenhar a comunicação de um sistema — entrevista

Você projeta um sistema de reservas de restaurante: o usuário reserva uma mesa, e isso envolve verificar disponibilidade, confirmar a reserva, cobrar uma taxa, notificar o restaurante, enviar confirmação ao usuário, e atualizar a disponibilidade. O entrevistador pede: "desenhe a arquitetura de comunicação — o que é síncrono, o que é assíncrono, e como você coordena o fluxo". Demonstre que você decide por interação e aplica o padrão híbrido.

Enquadramento: a chave é separar o que o usuário precisa confirmar agora (síncrono, na borda) do que pode acontecer depois (assíncrono, eventos), e escolher como coordenar o fluxo. Vou desenhar pelo padrão híbrido.

1. A parte síncrona (a borda crítica — o que decide a reserva): quando o usuário reserva, ele precisa saber imediatamente se conseguiu a mesa. Então o caminho síncrono é o mínimo que decide isso:

  • Verificar disponibilidade da mesa no horário (síncrono — precisa saber agora se há mesa).
  • Confirmar/reservar a mesa de forma consistente (síncrono e cuidadoso — não pode dar a mesma mesa a dois usuários; é um recurso escasso, consistência forte/CP, Cap 5, como o "último ingresso").
  • Responder ao usuário: "mesa reservada!" — rápido. Fim da parte síncrona crítica.

A cobrança da taxa pode ser síncrona (se você quer confirmar o pagamento antes de garantir) ou parte do fluxo — decisão de produto. Se a reserva exige pagamento garantido, cobra-se síncronamente; se a taxa é só uma garantia, pode ser assíncrona com a reserva já confirmada.

2. A parte assíncrona (o que não decide a reserva → eventos): uma vez reservada e respondida ao usuário, emite-se um evento "reserva confirmada", e os seguintes reagem assincronamente:

  • Notificar o restaurante (consome o evento; se o sistema do restaurante está fora, a fila espera e retenta — não afeta o usuário).
  • Enviar email/SMS de confirmação ao usuário (consome o evento; provedor lento não derruba a reserva — a lição do estudo de caso).
  • Atualizar a disponibilidade em sistemas derivados/cache (consome o evento).
  • Analytics, programa de fidelidade, etc.

Nenhum desses precisa bloquear a confirmação ao usuário, e nenhum, se falhar, deve derrubar a reserva. São eventos.

3. Comando vs evento: "reserva confirmada" é um evento (um fato, do qual muitos reagem — desacoplado, extensível). Já dentro da parte síncrona, "reservar a mesa" é mais um comando direcionado ao serviço de reservas (ação específica, crítica).

4. Coordenação — coreografia ou orquestração? O pós-reserva (notificar, emailar, atualizar) são reações independentes ao evento "reserva confirmada" → coreografia serve bem (cada serviço reage por conta). Mas se houvesse um fluxo com reversão (ex.: se a cobrança falha depois, cancelar a reserva), aí a orquestração (ou uma Saga, Cap 17) seria melhor para coordenar a compensação. Para o caso simples descrito, coreografia dos eventos pós-confirmação basta.

5. O recurso escasso (a sutileza): reservar uma mesa é como o "último ingresso" — dois usuários não podem reservar a mesma mesa no mesmo horário. Isso exige consistência forte na confirmação (síncrono, CP, possivelmente um lock ou transação), e é a única parte que não pode ser eventual. O resto pode ser assíncrono; isto não.

O que um bom candidato demonstra: que separa o síncrono crítico (verificar e reservar — o usuário espera, e é recurso escasso) do assíncrono (notificar, emailar, atualizar — eventos); que reconhece a reserva como recurso escasso exigindo consistência forte; que usa eventos para o desacoplamento pós-confirmação; e que escolhe a coordenação pelo fluxo (coreografia para reações independentes, orquestração/Saga se houvesse reversão). Frase de fechamento: "eu desenho pelo limite: síncrono só onde o usuário precisa da resposta agora e o recurso é escasso — verificar e reservar a mesa, com consistência forte para não dar a mesma mesa a dois. Tudo o que não decide a reserva — notificar o restaurante, emailar, atualizar disponibilidade — vira evento assíncrono, para que nada disso, ao falhar ou ficar lento, derrube a reserva. É o padrão híbrido: síncrono na borda crítica, assíncrono por dentro, com o limite posto exatamente onde o trabalho deixa de ser necessário para a resposta ao cliente."

Fim do capítulo 15
Próximo capítulo: idempotência e garantias de entrega. Assim que você abraça o assíncrono e os retries que a rede não-confiável exige (Cap 8), surge inevitavelmente o problema de processar a mesma coisa duas vezes — a mensagem duplicada, o retry da requisição que talvez já funcionou. A idempotência é o que torna "processar de novo" seguro, e é a peça que sustenta o at-least-once do Cap 11. Vamos estudá-la a fundo: o que é, como conseguir, e por que ela é a fundação silenciosa de quase todo sistema distribuído confiável.
Parte IV · Capítulo 16 · Os padrões

Idempotência e
garantias de
entrega.

A idempotência é a fundação silenciosa de quase todo sistema distribuído confiável. Ela quase nunca aparece nos diagramas, raramente é mencionada nas demos, e é exatamente o que separa um sistema que cobra o cliente uma vez de um que o cobra três. A razão é simples e inescapável: a rede não é confiável (Cap 8), então as coisas vão ser tentadas de novo — pela mensagem reentregue, pelo retry da requisição que talvez já funcionou —, e a única forma de tornar "tentar de novo" seguro é fazer com que tentar de novo não cause dano. Isso é idempotência, e este capítulo mostra por que ela é a peça que sustenta tudo que a Parte IV constrói.

Este capítulo aprofunda o conceito que apareceu no Cap 8 (a incerteza da rede) e no Cap 11 (at-least-once). Vamos definir idempotência com precisão, ver por que duplicatas são inevitáveis (não um bug, mas uma consequência da rede), distinguir operações naturalmente idempotentes das que não são, entender a chave de idempotência (quem a gera, qual o escopo, como), a implementação no servidor (a tabela de deduplicação numa transação), a idempotência em camadas (e por que cada camada importa), e desmascarar a ilusão do "exactly-once" (que é matematicamente impossível na entrega, mas alcançável no efeito). Quando você sair daqui, a idempotência deixa de ser uma palavra que você acena e vira uma técnica que você implementa — porque sem ela, todo o at-least-once, todos os retries, toda a resiliência da Parte IV produzem duplicatas, e duplicatas em pagamentos, pedidos e dados são bugs caros.

16.1 A história — o problema dos dois generais

Contexto histórico

A raiz teórica da idempotência é um dos resultados mais elegantes e desanimadores da ciência da computação: o problema dos dois generais. Imagine dois generais em colinas opostas, precisando coordenar um ataque, comunicando-se por mensageiros que atravessam o vale inimigo — onde podem ser capturados. O general A manda "ataquem ao amanhecer". Mas ele não sabe se a mensagem chegou. Então B manda uma confirmação. Mas B não sabe se a confirmação chegou. Então A confirma a confirmação... E prova-se que nenhum número de mensagens jamais garante que ambos tenham certeza — porque a última mensagem sempre pode ter se perdido. A certeza mútua sobre um canal não-confiável é impossível.

Esse resultado, formalizado nos anos 1970 (Akkoyunlu e outros, 1975), tem uma consequência direta para sistemas: a entrega "exatamente uma vez" é impossível sobre uma rede não-confiável. Quando você envia uma requisição e não recebe resposta, você está exatamente na posição do general: não sabe se ela chegou e a resposta se perdeu, ou se ela nem chegou (a incerteza do Cap 8). E não há protocolo que resolva isso com certeza.

A engenharia, diante de uma impossibilidade teórica, fez o que sempre faz: contornou-a. Se você não pode garantir entrega exatamente uma vez, você garante entrega pelo menos uma vez (reenviando até confirmar) e torna o processamento idempotente (processar de novo não causa dano). Assim, mesmo que a mensagem chegue várias vezes, o efeito é o de uma vez só. A impossibilidade da entrega exata foi contornada pela idempotência do efeito — uma das jogadas mais importantes do design de sistemas.

Em 2026, a idempotência é uma prática madura e onipresente, ainda que invisível. A API da Stripe popularizou a "Idempotency-Key" como padrão: o cliente gera uma chave única por intenção de pagamento, e o servidor garante que a mesma chave produza o mesmo resultado, não uma segunda cobrança. Esse padrão se espalhou — hoje, toda API séria que faz mudanças (pagamentos, pedidos) oferece idempotência por chave, e todo consumidor de fila bem projetado deduplica por ID. A idempotência virou a resposta padrão à pergunta "e se isto for executado duas vezes?" — uma pergunta que, pela rede não-confiável, você sempre tem que fazer. Este capítulo é sobre fazer essa pergunta e respondê-la corretamente, porque ignorá-la é garantir duplicatas.

16.2 O que é idempotência

A definição é simples e vale memorizar: uma operação é idempotente se executá-la múltiplas vezes tem o mesmo efeito que executá-la uma vez. Não importa quantas vezes você a repita, o estado final é o mesmo de uma execução única.

Idempotente: o efeito de N execuções é o de uma só
A idempotência é uma propriedade do efeito, não da execução. Uma operação idempotente pode ser executada uma vez, ou dez, ou mil — e o resultado no mundo é idêntico ao de uma execução. Pense em apertar o botão do andar num elevador: apertar uma vez ou cinco vezes leva ao mesmo andar (idempotente). Compare com adicionar uma colher de açúcar ao café: cinco vezes ≠ uma vez (não idempotente). Em sistemas, "definir o saldo para R$100" é idempotente (fazê-lo de novo não muda nada — já é R$100); "adicionar R$100 ao saldo" não é (cada execução soma mais R$100). Essa distinção é o coração de tudo: se uma operação é idempotente, então repeti-la é seguro, e a rede não-confiável (que vai repeti-la) deixa de ser um problema. A idempotência transforma a pergunta angustiante "minha requisição chegou?" (que, pelo problema dos dois generais, você não pode responder) na pergunta tranquila "e daí se chegou duas vezes?" (resposta: nenhum dano). Ela não resolve a incerteza da rede — ela a torna irrelevante. Por isso é a fundação: você para de precisar saber se algo aconteceu, porque fazê-lo de novo é inofensivo.

16.3 Por que duplicatas são inevitáveis

Um erro de iniciante é tratar duplicatas como um bug raro a ser evitado. Elas não são um bug — são uma consequência inevitável da rede não-confiável, e qualquer sistema distribuído as terá. Entender por que é entender por que a idempotência não é opcional.

a duplicata que a rede garante
O cenário inescapável (o do Cap 8):

  1. Cliente envia "cobrar R$100" ao servidor
  2. Servidor processa: cobra os R$100 ✓
  3. Servidor envia a resposta "ok, cobrado"
  4. A RESPOSTA SE PERDE na rede (timeout)
  5. Cliente não recebe nada. Do ponto de vista dele: falhou?
  6. Cliente faz a coisa CERTA: RETENTA "cobrar R$100"
  7. Sem idempotência: servidor cobra OUTROS R$100 ✗✗

O cliente agiu corretamente (retentar sob silêncio é certo).
O servidor processou corretamente as duas vezes. E mesmo
assim: cobrança DUPLA. Ninguém errou — a REDE causou.
O retry é correto — então o servidor é que precisa lidar com a duplicata
A lição crucial deste cenário: a duplicata não vem de um erro: o cliente deve retentar quando não recebe resposta (não retentar significaria perder operações legítimas sempre que uma resposta se perde). O retry é o comportamento correto diante da incerteza da rede. Logo, a responsabilidade de lidar com a duplicata não pode ser "o cliente não deveria retentar" — ele deve. A responsabilidade recai sobre o servidor (ou consumidor): ele precisa reconhecer que a segunda "cobrança" é uma repetição da primeira, e não cobrá-la de novo. Isto inverte a intuição: você não previne duplicatas impedindo retries (impossível e errado); você as torna inofensivas tornando o processamento idempotente. E como toda comunicação sobre uma rede não-confiável tem esse cenário (e toda fila at-least-once o tem por design, Cap 11), todo sistema distribuído que faz mudanças importantes precisa de idempotência em algum ponto. Não é uma otimização para sistemas grandes; é um requisito de correção para qualquer sistema que processe operações com efeito (pagamentos, pedidos, mudanças de estado) sobre uma rede. Duplicatas são tão certas quanto a falibilidade da rede — e a idempotência é a única defesa.

16.4 Naturalmente idempotente ou não

Algumas operações já são idempotentes por natureza; outras precisam ser tornadas idempotentes com trabalho extra. Reconhecer a diferença diz quanto esforço cada operação exige.

Naturalmente idempotente

Leituras (GET): ler N vezes não muda nada. Definir um valor absoluto ("status = enviado", "saldo = 100"): repetir leva ao mesmo estado. Deletar por ID ("remova o item X"): removido uma vez, removê-lo de novo não muda nada. PUT de um recurso completo. Repetir é seguro de graça.

NÃO naturalmente idempotente

Incrementos relativos ("saldo += 100", "estoque -= 1"): cada execução muda o resultado. Criar um recurso (POST /pedidos): repetir cria pedidos duplicados. Enviar (email, SMS, push): cada envio é um efeito real no mundo. Estas precisam ser tornadas idempotentes.

Prefira operações absolutas a relativas; e dê IDs às criações
Há duas lições de design aqui. Primeira: quando possível, prefira operações absolutas a relativas. "Defina o estoque para 42" é idempotente; "decremente o estoque em 1" não é. "Marque como enviado" é idempotente; "avance para o próximo estado" não é. Modelar suas operações como "ponha o estado em X" em vez de "mude o estado por delta" ganha idempotência de graça, porque repetir um "ponha em X" é inofensivo. Nem sempre dá (alguns problemas são inerentemente incrementais), mas onde dá, é a escolha mais robusta. Segunda: para criações (POST, que naturalmente duplica — dez chamadas criam dez pedidos), a idempotência vem de dar à operação uma identidade: um ID gerado pelo cliente que torna "criar o pedido 123" repetível (o servidor reconhece que o pedido 123 já existe e não cria outro). É a chave de idempotência da próxima seção. E para operações inerentemente não-idempotentes com efeitos externos reais (enviar um email de verdade), a solução é registrar que o efeito já ocorreu (a tabela de deduplicação) para não repeti-lo. A regra geral: leituras e definições absolutas são idempotentes de graça; criações e incrementos precisam de uma chave ou registro para se tornarem seguros de repetir.

16.5 A chave de idempotência — quem, qual escopo, como

A técnica central para tornar operações idempotentes é a chave de idempotência: um identificador único que marca uma operação lógica, permitindo ao servidor reconhecer repetições. As perguntas que a definem: quem a gera, qual o seu escopo, e como.

O cliente gera a chave, porque só ele sabe a intenção
A responsabilidade de gerar a chave de idempotência quase sempre pertence ao cliente, e a razão é profunda: só o cliente sabe a intenção. Quando o cliente retenta após um timeout, ele sabe que é a mesma operação (ele está retentando porque não obteve resposta, não porque quer uma operação nova) — então ele manda a mesma chave. O servidor, sozinho, não pode distinguir "o cliente quer pagar de novo" de "o cliente está retentando o mesmo pagamento" — duas requisições idênticas podem ser intenções diferentes ou a mesma. A chave gerada pelo cliente resolve isso: mesma chave = mesma intenção lógica (é um retry); chave nova = intenção nova (é uma operação diferente). É o modelo da Stripe: o cliente gera um identificador único por tentativa de pagamento e o envia no header Idempotency-Key; o servidor promete "me dê a mesma chave, receba a mesma resposta". E há um detalhe crítico de determinismo: o cliente deve gerar a chave antes da primeira tentativa e reusá-la em todos os retries — se ele gerasse uma chave nova a cada retry, a deduplicação falharia (o servidor veria chaves diferentes e processaria como operações distintas). A chave nasce com a intenção e sobrevive aos retries.
O escopo da chave: única dentro de um limite que faça sentido
A chave precisa ser única dentro de um escopo apropriado — tipicamente "por cliente + por tipo de operação + por intenção lógica". Uma chave global demais (reusada entre operações diferentes) causaria deduplicação falsa (operações genuinamente distintas vistas como duplicatas); uma chave estreita demais falharia em reconhecer o retry. Sobre como gerar: usa-se um identificador único — UUID aleatório é o comum, mas há nuances. Um UUIDv7 ou ULID (que embutem um timestamp ordenável) trazem vantagens: permitem ordenação temporal e detectar chaves "velhas demais" (úteis para expiração — chaves não devem viver para sempre, ou a tabela de dedup cresce infinitamente). Alternativamente, a chave pode ser derivada do conteúdo (um hash dos parâmetros de negócio) quando faz sentido que "a mesma operação lógica" seja definida pelos dados. E em fluxos com vários passos, uma hierarquia de chaves (uma operation_id geral que se traduz em sub-chaves para cada suboperação) mantém a cadeia inteira idempotente. O ponto: a chave é o mecanismo, mas seu escopo e ciclo de vida (expiração) são decisões de design que precisam ser pensadas, não automáticas.

16.6 A implementação no servidor — a tabela numa transação

Do lado do servidor, a idempotência se implementa com uma tabela de deduplicação e um detalhe crítico: a verificação e a operação de negócio devem acontecer na mesma transação, ou uma janela de corrida reabre o problema.

idempotência no servidor, corretamente
Ao receber uma requisição com idempotency-key:

  INÍCIO DA TRANSAÇÃO (atômica, Cap 12)
    1. A chave já existe na tabela de dedup?
       - SIM: retorne a resposta ARMAZENADA (não reprocesse)
       - NÃO: continue
    2. Execute a operação de negócio (cobrar, criar pedido)
    3. Grave a chave + a resposta na tabela de dedup
  FIM DA TRANSAÇÃO (commit atômico)

  → A próxima requisição com a MESMA chave cai no passo 1-SIM
    e recebe a mesma resposta, sem reprocessar. Idempotente.

CRÍTICO: passos 1, 2 e 3 numa ÚNICA transação. Se a checagem
da chave e a operação não forem atômicas, dois retries
concorrentes podem ambos passar pelo passo 1 (nenhum vê o
outro ainda) e ambos cobrarem. A atomicidade fecha essa
janela: a constraint de unicidade da chave + a transação
garantem que só UM processa.
A verificação e a operação precisam ser atômicas — senão a corrida reabre o furo
O detalhe que separa uma implementação correta de uma sutilmente quebrada: a checagem da chave (passo 1) e a operação de negócio (passo 2-3) precisam acontecer atomicamente, numa única transação. Por quê? Imagine dois retries chegando concorrentemente (o cliente retentou, e ambas as tentativas chegam quase juntas). Se a checagem e a operação não forem atômicas: a tentativa A checa a chave (não existe), a tentativa B também checa (ainda não existe, A não gravou) — ambas passam pela checagem e ambas cobram. A duplicata que você queria evitar acontece, agora por uma corrida em vez da rede. A solução é a atomicidade: ao fazer a checagem, a operação e a gravação da chave numa única transação (com uma constraint de unicidade na chave), o banco garante que apenas uma das tentativas concorrentes consegue inserir a chave e processar; a outra falha na constraint (ou espera) e recebe a resposta já gravada. A idempotência não é só "checar se já vi a chave"; é checar-e-processar-e-gravar atomicamente, para que retries concorrentes não escorreguem pela janela. Implementações que checam fora da transação têm um furo que só aparece sob concorrência — e portanto só em produção, sob carga, da pior forma.

16.7 Idempotência em camadas

A idempotência raramente é uma única barreira — ela é mais robusta como uma garantia em camadas, onde diferentes níveis do sistema deduplicam diferentes tipos de retry. Entender as camadas evita deixar um furo.

Cada camada deduplica um tipo de retry; remover uma reabre uma janela
Considere uma operação que passa por várias camadas: o cliente chama uma API, que escreve num banco, e dispara um evento que um consumidor processa, que por sua vez chama uma API externa. Cada fronteira é uma oportunidade de duplicata, e a idempotência precisa estar em cada uma: a camada HTTP (um middleware com a idempotency-key) deduplica os retries de rede do cliente; a camada de banco (a tabela de dedup transacional) deduplica retries concorrentes em processo; o consumidor de eventos deduplica as reentregas at-least-once da fila (Cap 11); e a chamada à API externa usa a chave de idempotência dela para deduplicar após o commit. A lição: idempotência é uma garantia em camadas, e se você remove qualquer uma das camadas, abre uma janela onde um retry causa um efeito duplicado. Um sistema que deduplica na API mas não no consumidor de eventos ainda pode processar um evento duas vezes; um que deduplica no consumidor mas chama uma API externa não-idempotente ainda pode disparar o efeito externo duas vezes. Projetar idempotência é mapear todas as fronteiras onde um retry pode ocorrer e garantir deduplicação em cada uma — porque a corrente da idempotência, como a corrente síncrona do Cap 15, é tão forte quanto seu elo mais fraco.

16.8 A ilusão do exactly-once

Vale enfrentar de frente o termo que confunde todo mundo: "exactly-once". Ele é vendido por muitos sistemas, desejado por todos os engenheiros, e matematicamente impossível na entrega — mas alcançável onde importa.

Exactly-once na entrega é impossível; exactly-once no efeito é a idempotência
A confusão se desfaz distinguindo entrega de efeito. Entrega exactly-once — garantir que uma mensagem seja entregue à outra ponta exatamente uma vez — é impossível sobre uma rede não-confiável, pelo problema dos dois generais (16.1): qualquer confirmação pode se perder, então o emissor nunca tem certeza, e ou ele reenviar (arriscando duplicata, at-least-once) ou não (arriscando perda, at-most-once). Não há meio-termo garantido. Quando um sistema diz "exactly-once", ele está fazendo uma de duas coisas: ou é exactly-once dentro de um escopo limitado e específico (ex.: dentro de um único cluster Kafka, com produtores idempotentes e consumidores transacionais — real, mas com complexidade e custo de performance, e não de ponta a ponta), ou é, na verdade, at-least-once + processamento idempotente apresentado como exactly-once. E esta segunda é a resposta prática para quase todos: você aceita que a entrega é at-least-once (a mensagem pode chegar várias vezes), e torna o processamento idempotente (processá-la várias vezes tem o efeito de uma) — obtendo exactly-once no efeito, que é o que você realmente queria. O cliente não se importa se a mensagem foi entregue uma ou três vezes; ele se importa que foi cobrado uma vez. A idempotência entrega exatamente isso. A maturidade é parar de perseguir o exactly-once na entrega (impossível, ou caro e limitado) e implementar o exactly-once no efeito via idempotência (possível, simples, robusto). "Exactly-once é uma ilusão; a idempotência é o caminho prático para o efeito que você queria."

16.9 Estudo de caso — a cobrança dupla que não deveria acontecer

De cobranças duplicadas a um pagamento idempotente
O cenário

Um sistema de pagamentos começou a receber reclamações de clientes cobrados duas (às vezes três) vezes pela mesma compra. Os logs mostravam que o servidor de pagamento havia, de fato, processado múltiplas cobranças idênticas em segundos. Não havia bug na lógica de cobrança — cada cobrança, isolada, estava correta. O problema estava na ausência de idempotência diante de uma rede não-confiável.

Passo 1 · Diagnosticar a origem das duplicatas
de onde vinham as cobranças extras
Duas fontes, ambas a rede não-confiável (Cap 8):

1. RETRY DO APP: o app móvel cobrava, mas em conexões ruins
   a resposta se perdia. O app, sem confirmação, retentava
   automaticamente. Servidor cobrava de novo.

2. RETRY DA FILA: internamente, a cobrança era processada
   por um consumidor de fila at-least-once (Cap 11). Quando
   um worker caía após cobrar mas antes de confirmar (ack),
   a fila reentregava a mensagem. Outro worker cobrava de novo.

Em ambos: ninguém errou. O app retentou certo, a fila
reentregou por design. Faltava IDEMPOTÊNCIA para tornar os
retries inofensivos.
Passo 2 · A chave de idempotência (no app)
o cliente gera a chave por intenção
O app passou a gerar uma idempotency-key ÚNICA por TENTATIVA
DE PAGAMENTO (não por requisição):

  - usuário aperta "pagar" → app gera key = UUID, UMA vez
  - app envia "cobrar R$100" + Idempotency-Key: abc-123
  - se não recebe resposta, RETENTA com a MESMA key abc-123
    (não gera nova! a intenção é a mesma)

Agora o servidor pode reconhecer: "já vi a key abc-123 →
isto é um retry da mesma intenção, não uma cobrança nova".
Passo 3 · A deduplicação atômica (no servidor)
checar-cobrar-gravar numa transação
O servidor, ao receber a key:

  TRANSAÇÃO:
    - key abc-123 já está na tabela de dedup?
      SIM → retorna a resposta gravada (NÃO cobra de novo)
      NÃO → cobra os R$100, grava (abc-123 → resultado)
  COMMIT

Tudo atômico (constraint de unicidade na key). Dois retries
concorrentes: só um insere a key e cobra; o outro recebe a
resposta já gravada. Sem janela de corrida.

E o consumidor de fila aplicou o MESMO: dedup pelo ID da
mensagem antes de cobrar (a camada da fila, Cap 11).
Passo 4 · O resultado

Com a chave de idempotência gerada pelo app (por tentativa de pagamento, reusada nos retries) e a deduplicação atômica no servidor (e no consumidor de fila), as cobranças duplicadas cessaram. Os retries continuaram acontecendo — a rede continuou não-confiável, o app continuou retentando, a fila continuou reentregando — mas agora cada tentativa de pagamento resultava em exatamente uma cobrança, não importa quantas vezes a mensagem trafegasse. O exactly-once no efeito, via at-least-once + idempotência. Os clientes pararam de ser cobrados em duplicidade, e a confiança no sistema se restaurou.

Resultado: as cobranças duplicadas não vinham de um bug de lógica — vinham da ausência de idempotência diante da rede não-confiável, que garante retries (o app retentando, a fila reentregando). A correção foi a idempotência em duas camadas: a chave gerada pelo cliente por intenção de pagamento (reusada em todos os retries, o detalhe do determinismo) e a deduplicação atômica no servidor e no consumidor de fila (checar-cobrar-gravar numa transação, fechando a janela de corrida). A lição central reúne o capítulo: duplicatas são inevitáveis (a rede as causa, os retries corretos as produzem), então a defesa não é evitar retries (impossível e errado) mas torná-los inofensivos via idempotência; a chave nasce no cliente com a intenção e sobrevive aos retries; a deduplicação no servidor deve ser atômica; e o resultado é exactly-once no efeito — o que o cliente realmente queria — apesar da impossibilidade do exactly-once na entrega. Próximo capítulo: transações distribuídas e Saga — porque quando uma operação precisa ser atômica entre vários serviços (cobrar num, reservar noutro), e o ACID de um banco não alcança (Cap 12), surge o problema de manter a consistência sem transações distribuídas, e a Saga é a resposta, construída — claro — sobre passos idempotentes.

16.10 Erros comuns

Erro 1 · Tratar duplicatas como bug raro a evitar

Achar que duplicatas são um caso raro a prevenir, em vez de uma consequência inevitável da rede não-confiável (retries corretos + reentregas at-least-once). Todo sistema distribuído com retries terá duplicatas; a defesa não é evitá-las, é torná-las inofensivas com idempotência. Projete assumindo que tudo será processado mais de uma vez.

Erro 2 · Gerar uma chave de idempotência nova a cada retry

O cliente gerar uma chave nova a cada tentativa — fazendo o servidor ver chaves diferentes e processar cada retry como operação nova (a deduplicação nunca dispara). A chave deve nascer com a intenção, antes da primeira tentativa, e ser reusada em todos os retries. Determinismo da chave é essencial.

Erro 3 · Checar a chave fora da transação

Verificar se a chave já existe separadamente da operação de negócio — abrindo uma janela onde dois retries concorrentes passam ambos pela checagem e ambos processam. A checagem, a operação e a gravação da chave devem ser atômicas (uma transação, constraint de unicidade). Senão a corrida reabre a duplicata.

Erro 4 · Idempotência só numa camada

Deduplicar na API mas não no consumidor de eventos (ou vice-versa) — deixando uma fronteira onde o retry ainda duplica. Idempotência é uma garantia em camadas (HTTP, banco, fila, API externa); cada fronteira de retry precisa deduplicar. Um elo sem idempotência reabre a janela inteira.

Erro 5 · Perseguir exactly-once na entrega

Gastar enorme complexidade tentando garantir entrega "exatamente uma vez" — que é matematicamente impossível sobre rede não-confiável (dois generais). A resposta prática é at-least-once + processamento idempotente, que dá exactly-once no efeito (o que importa). Não persiga a entrega exata; torne o efeito idempotente.

Erro 6 · Usar operações relativas onde absolutas serviriam

Modelar mudanças como incrementos relativos ("saldo += 100") quando absolutos ("saldo = 100") serviriam — perdendo a idempotência natural das operações absolutas. Onde possível, prefira "ponha o estado em X" a "mude por delta": repetir um absoluto é inofensivo, repetir um relativo acumula.

Erro 7 · Tabela de dedup que cresce para sempre

Guardar chaves de idempotência indefinidamente, deixando a tabela de deduplicação crescer sem limite. As chaves precisam de um ciclo de vida (expiração após um tempo razoável em que retries ainda podem chegar). Chaves com timestamp (UUIDv7/ULID) ajudam a identificar e expirar as velhas. Idempotência tem custo de armazenamento que precisa ser gerenciado.

Verifique seu entendimento
Seu app de pagamentos cobra clientes em duplicidade: quando a conexão é ruim e a resposta se perde, o app retenta a cobrança, e o servidor cobra de novo. Você adiciona uma chave de idempotência. Qual implementação está correta?

16.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Idempotente ou não?

Para cada operação, diga se é naturalmente idempotente, e se não, como torná-la idempotente: (a) GET /usuario/42; (b) UPDATE pedido SET status = 'enviado' WHERE id = 7; (c) UPDATE conta SET saldo = saldo - 50; (d) POST /pedidos (cria um pedido); (e) enviar um email de confirmação.

(a) GET /usuario/42 → naturalmente idempotente. Leituras não mudam nada; ler N vezes tem o mesmo efeito que ler uma. Seguro de repetir de graça, sem trabalho extra.

(b) SET status = 'enviado' → naturalmente idempotente. É uma definição absoluta: pôr o status em "enviado" várias vezes leva ao mesmo estado (já está enviado). Repetir é inofensivo. Este é o padrão a preferir (absoluto, não relativo).

(c) saldo = saldo - 50 → NÃO idempotente. É um decremento relativo: cada execução subtrai mais 50. Repetir corrompe o saldo. Como tornar idempotente: ou usar uma chave de idempotência (registrar que a operação "transação X" já foi aplicada, e não reaplicar), ou modelar como absoluto onde possível, ou registrar a transação como um evento único deduplicável.

(d) POST /pedidos → NÃO idempotente. Criar duplica: dez chamadas criam dez pedidos. Como tornar idempotente: dar uma chave de idempotência (gerada pelo cliente) à criação — o servidor reconhece "já criei o pedido com a chave abc" e retorna o pedido existente em vez de criar outro. Dar identidade à criação a torna repetível.

(e) Enviar email → NÃO idempotente (efeito externo real). Cada envio é um email real na caixa do usuário; enviar duas vezes = dois emails. Como tornar idempotente: registrar que o email "para esta operação / com este ID" já foi enviado (uma tabela de deduplicação de envios), e checar antes de enviar. Efeitos externos reais precisam de um registro de "já fiz isto" para não repetir.

O insight: leituras (a) e definições absolutas (b) são idempotentes de graça. Incrementos relativos (c), criações (d) e efeitos externos (e) não são — e precisam de uma chave ou registro que torne "já fiz isto" reconhecível. A regra: prefira absoluto a relativo; dê identidade às criações; registre os efeitos externos.

Médio
Exercício 2 · A janela de corrida

Um engenheiro implementa idempotência assim: primeiro faz uma query para verificar se a chave existe; se não existir, processa a operação e depois insere a chave numa segunda operação. (a) Que problema isso tem? (b) Em que situação ele se manifesta? (c) Como corrigir?

(a) O problema: a checagem da chave e a inserção são operações separadas, não atômicas. Entre verificar "a chave não existe" e gravá-la, há uma janela onde outra requisição concorrente pode fazer a mesma checagem e também ver "não existe". Ambas então processam a operação (cobram), e ambas inserem a chave. A deduplicação falha exatamente quando mais importa: sob retries concorrentes.

(b) Quando se manifesta: sob concorrência — quando dois retries da mesma operação chegam quase simultaneamente. Isso é comum justamente no cenário de idempotência: o cliente retentou porque não obteve resposta, e por azar a requisição original (lenta) e o retry chegam ao servidor ao mesmo tempo. As duas correm pela janela. Como é um problema de timing, ele é intermitente e não aparece em testes sequenciais — só em produção, sob carga, de forma difícil de reproduzir. O pior tipo de bug.

(c) Como corrigir: tornar a checagem-operação-gravação atômica, numa única transação, apoiada numa constraint de unicidade na coluna da chave. Há duas formas equivalentes:

  • Inserir primeiro, deixar a constraint decidir: tente inserir a chave (com a constraint de unicidade) antes ou junto de processar, dentro da transação. Se a inserção falha (a chave já existe), você sabe que é um duplicado e retorna a resposta gravada. Só uma das tentativas concorrentes consegue inserir; a outra falha na constraint. O banco serializa o acesso à chave.
  • Transação com lock: toda a sequência (checar, processar, gravar) numa transação que segura o registro da chave, de modo que a segunda tentativa espera a primeira terminar e então vê a chave já gravada.

O insight: a idempotência correta não é "checar e depois processar" — é checar-processar-gravar atomicamente, com a unicidade da chave garantida pelo banco. A atomicidade é o que fecha a janela de corrida entre retries concorrentes. Uma implementação que separa a checagem da gravação tem um furo que só a concorrência revela — e a concorrência é exatamente o que a idempotência enfrenta.

Médio
Exercício 3 · Mapear as camadas de idempotência

Uma operação flui assim: o app móvel chama uma API de pedido → a API grava no banco e publica um evento "pedido criado" → um consumidor lê o evento e chama uma API externa de pagamento. Identifique, em cada fronteira, que tipo de duplicata pode ocorrer e como deduplicar nela.

Fronteira 1 · App → API de pedido (retry de rede do cliente): o app pode retentar se não recebe resposta (timeout). Duplicata possível: dois pedidos criados. Dedup: o app gera uma idempotency-key por tentativa de pedido e a envia; a API deduplica por essa chave (tabela de dedup transacional). Camada HTTP/cliente.

Fronteira 2 · Dentro da API: gravar no banco + publicar evento (retry concorrente / atomicidade): se a API processa retries concorrentes, ou se grava no banco e publica o evento sem atomicidade, pode duplicar a gravação ou publicar o evento duas vezes (ou gravar sem publicar — o problema do "dual write"). Dedup: a transação idempotente da fronteira 1 cuida da gravação; para a publicação confiável do evento, usar o outbox pattern (Cap 11) — gravar o evento numa tabela outbox na mesma transação do pedido, e um processo separado o publica, garantindo exatamente um evento por pedido. Camada de banco/publicação.

Fronteira 3 · Fila → consumidor (reentrega at-least-once): a fila pode reentregar o evento "pedido criado" (o consumidor caiu após processar mas antes do ack, ou a fila é at-least-once por design). Duplicata possível: processar o mesmo pedido duas vezes. Dedup: o consumidor deduplica pelo ID do evento/mensagem (checa um dedup store antes de processar). Camada de consumidor de eventos (Cap 11).

Fronteira 4 · Consumidor → API externa de pagamento (retry após o commit): o consumidor pode chamar a API externa, receber timeout, e retentar — ou o próprio consumidor ser reexecutado (fronteira 3). Duplicata possível: cobrar duas vezes na API externa. Dedup: usar a idempotency-key da API externa (ex.: a chave da Stripe), derivada de forma estável do pedido (ex.: o ID do pedido), para que retries da chamada externa sejam deduplicados pela própria API de pagamento. Camada de API externa.

O insight:quatro fronteiras, cada uma com seu tipo de retry (rede do cliente, atomicidade interna, reentrega da fila, retry da chamada externa), e cada uma precisa da sua deduplicação (chave do cliente, outbox, dedup por message ID, chave da API externa). Remover qualquer uma reabre uma janela: deduplica na API mas não no consumidor → o evento ainda processa duas vezes; deduplica no consumidor mas chama a API externa sem a chave dela → ainda cobra duas vezes. Idempotência robusta é mapear todas as fronteiras e fechar cada uma.

Difícil
Exercício 4 · Projetar a idempotência de uma API — entrevista

Você projeta uma API de pagamentos que precisa ser segura para retries (clientes vão retentar sob falhas de rede). O entrevistador pede: "como você garante que um cliente nunca seja cobrado duas vezes pela mesma compra, mesmo com retries e falhas?". Cubra a chave (quem gera, escopo, ciclo de vida), a implementação no servidor (atomicidade), as camadas, e por que você não persegue exactly-once na entrega.

Enquadramento: a premissa é que retries são inevitáveis e corretos (a rede não-confiável os garante), então não tento evitá-los — torno-os inofensivos com idempotência. Vou cobrir a chave, o servidor, as camadas, e a filosofia.

1. A chave de idempotência — quem, escopo, ciclo de vida:

  • Quem gera: o cliente, porque só ele sabe a intenção — quando retenta, sabe que é a mesma compra, não uma nova. Gera a chave uma vez, ao iniciar a tentativa de pagamento, e a reusa em todos os retries (determinismo — gerar nova a cada retry quebraria tudo).
  • Escopo: única por tentativa de pagamento / por cliente — um UUID (idealmente UUIDv7 ou ULID, com timestamp para ordenação e expiração). Enviada no header Idempotency-Key (modelo Stripe).
  • Ciclo de vida: as chaves expiram após uma janela razoável (ex.: 24h) em que retries ainda podem chegar — senão a tabela de dedup cresce para sempre. O timestamp da chave ajuda a expirar.

2. A implementação no servidor — atomicidade: ao receber a chave, numa única transação: verifico se a chave já existe na tabela de dedup; se sim, retorno a resposta gravada (sem cobrar); se não, cobro e gravo (chave → resultado). A constraint de unicidade na chave + a transação garantem que retries concorrentes não escorreguem (só um insere e cobra; o outro recebe a resposta gravada). Sem a atomicidade, dois retries simultâneos cobrariam os dois — a janela de corrida.

3. As camadas: idempotência em cada fronteira de retry — a API (chave do cliente), o processamento interno (transação atômica, outbox para eventos confiáveis), o consumidor de fila se houver (dedup por message ID), e a chamada à processadora externa (a idempotency-key dela, derivada estável do pagamento). Cada camada deduplica seu tipo de retry; um furo em qualquer uma reabre a duplicata.

4. Por que não persigo exactly-once na entrega: porque é matematicamente impossível sobre rede não-confiável (problema dos dois generais — qualquer confirmação pode se perder). O que eu garanto é exactly-once no efeito: aceito que a entrega é at-least-once (a requisição pode chegar várias vezes) e torno o processamento idempotente (cobra uma vez não importa quantas chegue). O cliente não se importa quantas vezes a mensagem trafegou — ele se importa que foi cobrado uma vez. A idempotência entrega exatamente isso.

O que um bom candidato demonstra: que entende retries como inevitáveis e corretos (não tenta evitá-los); que põe a geração da chave no cliente com determinismo; que faz a dedup atômica no servidor (a janela de corrida); que pensa em camadas; e que distingue exactly-once na entrega (impossível) do efeito (alcançável via idempotência). Frase de fechamento: "eu não tento impedir que o cliente cobre duas vezes — a rede vai fazer a requisição chegar múltiplas vezes, e o cliente está certo em retentar. Eu torno isso inofensivo: o cliente gera uma chave por tentativa de pagamento e a reusa nos retries; o servidor, numa única transação atômica, processa a chave uma vez e retorna a mesma resposta para as repetições; e cada camada — API, fila, processadora externa — deduplica seu tipo de retry. Não persigo o exactly-once na entrega, que é impossível; garanto o exactly-once no efeito, que é uma cobrança, e é o que o cliente realmente quer. A idempotência é o que transforma uma rede não-confiável num sistema de pagamento confiável."

Fim do capítulo 16
Próximo capítulo: transações distribuídas e Saga. Você sabe tornar uma operação idempotente — segura para repetir. Mas e quando uma operação de negócio precisa ser atômica entre vários serviços (cobrar no serviço de pagamento e reservar no de estoque, tudo-ou-nada), e o ACID de um único banco não alcança (Cap 12)? As transações distribuídas tradicionais (2PC) são lentas e frágeis; a Saga é a alternativa pragmática — uma sequência de passos locais com compensações, construída sobre a idempotência que você acabou de dominar.
Parte IV · Capítulo 17 · Os padrões

Transações
distribuídas e
Saga.

O ACID de um banco (Cap 12) é uma maravilha: ele garante que uma operação aconteça inteira ou não aconteça, mesmo sob falha. Mas essa garantia vale dentro de um banco. Quando uma operação de negócio precisa ser atômica entre vários serviços — cobrar no serviço de pagamento e reservar no de estoque, tudo-ou-nada, cada um com seu próprio banco —, o ACID não alcança, e você cai num dos problemas mais difíceis dos sistemas distribuídos. As transações distribuídas tradicionais (2PC) tentam estender o ACID pela rede, e pagam caro por isso. A Saga é a resposta pragmática: abre mão da atomicidade estrita em troca de algo que funciona em escala.

Este capítulo trata de manter a consistência quando uma operação cruza serviços. Vamos ver por que a atomicidade distribuída é tão difícil, o protocolo de duas fases (2PC) que tenta resolvê-la com ACID e por que ele dói (bloqueio, ponto único, baixa disponibilidade), a Saga como alternativa (uma sequência de transações locais com compensações), por que uma compensação não é um rollback perfeito (e o que isso implica), os dois estilos de coordenar uma Saga (coreografia e orquestração, o tema do Cap 15 reaplicado), o problema da falta de isolamento que a Saga introduz, e quando usar cada abordagem. Quando você sair daqui, "como faço isto atômico entre serviços?" deixa de ser uma armadilha e vira uma escolha consciente entre o ACID distribuído caro e a Saga eventualmente consistente — construída, lembre-se, sobre os passos idempotentes do Cap 16.

17.1 A história — quando o ACID encontrou a rede

Contexto histórico

Quando os sistemas eram um banco monolítico, a atomicidade era resolvida: o ACID (Cap 12) garantia que uma transação acontecesse inteira ou nada, e ponto. O problema nasceu quando as operações começaram a cruzar múltiplos bancos ou sistemas — uma transferência entre dois bancos diferentes, uma reserva que tocava vários departamentos. Nos anos 1980, a resposta foi tentar estender o ACID pela rede: o two-phase commit (2PC), um protocolo onde um coordenador garante que todos os participantes ou commitam juntos, ou abortam juntos. Por décadas, foi o padrão para transações distribuídas, embutido em sistemas corporativos e padrões como o XA.

O 2PC funcionava, mas a um custo que ficou cada vez mais doloroso conforme os sistemas escalavam. Ele é bloqueante (segura recursos travados durante todo o protocolo), depende de um coordenador central (um ponto único de falha), e reduz a disponibilidade (se qualquer participante está indisponível, a transação inteira para). Essas propriedades batem de frente com tudo que a era da nuvem passou a valorizar: disponibilidade, escala, serviços independentes. O 2PC era a tentativa de ter ACID distribuído, e o CAP (Cap 5) já avisava que isso teria um preço em disponibilidade.

A alternativa veio de um lugar inesperado: um paper de 1987 de Hector Garcia-Molina e Kenneth Salem chamado "Sagas", originalmente sobre transações longas num único banco. A ideia: em vez de uma transação atômica gigante, uma sequência de transações pequenas, cada uma com uma compensação que a desfaz se algo der errado depois. Décadas depois, com a explosão dos microsserviços (anos 2010), essa ideia foi redescoberta como a solução para transações entre serviços: a Saga virou o padrão para manter a consistência distribuída sem o peso do 2PC.

Em 2026, o consenso é claro: para a maioria dos sistemas distribuídos modernos, o 2PC é evitado (caro, frágil, não escala), e a Saga é o padrão para operações que cruzam serviços. A Saga abraça a realidade do CAP — abre mão da consistência forte imediata (atomicidade estrita) em troca de disponibilidade e escala, aceitando a consistência eventual (Cap 6). Não é uma solução mágica: ela tem suas próprias complexidades (compensações, falta de isolamento, idempotência), e implementá-la mal cria seus próprios desastres. Mas é a abordagem que reconhece que, sobre uma rede não-confiável, estender o ACID é caro demais, e que a consistência distribuída é melhor alcançada por passos locais reversíveis do que por um commit global. Este capítulo é sobre fazer essa escolha — e implementá-la — bem.

17.2 O problema da atomicidade distribuída

O problema central, em uma frase: você precisa que uma operação de negócio que toca vários serviços seja tudo-ou-nada, mas cada serviço só sabe commitar a sua parte. Não há um "commit" que abranja todos.

a operação que cruza serviços
Pedido = (cobrar pagamento) E (reservar estoque) E (agendar envio)
         serviço A           serviço B            serviço C
         banco A             banco B              banco C

Quero: ou TODAS acontecem, ou NENHUMA (atomicidade).

Mas:
  - o ACID do banco A só garante a transação DENTRO de A
  - o ACID do banco B só garante a transação DENTRO de B
  - não há transação que abranja A, B e C juntos

E se A cobra, B reserva, mas C falha em agendar o envio?
Agora o cliente foi cobrado e o estoque reservado, mas não
há envio — um estado INCONSISTENTE. Como desfazer A e B?
Esse é o problema da atomicidade distribuída.
O ACID não atravessa a fronteira do serviço
A raiz do problema é que a garantia mais forte do banco — a transação ACID atômica — é local por natureza: ela vale dentro de um banco, onde o gerenciador de transações controla tudo. Assim que a operação cruza para outro serviço, com outro banco, o primeiro banco não tem como coordenar o commit do segundo — eles são sistemas independentes. Você fica com várias transações locais (uma por serviço), cada uma atômica em si, mas sem atomicidade entre elas: cada uma pode commitar ou falhar independentemente, criando estados parciais (cobrou mas não reservou). É a manifestação direta do que vimos no Cap 12 (por que SQL não escala horizontalmente) e no Cap 13 (transações entre shards): a atomicidade pressupõe coordenação, e a coordenação distribuída é cara (CAP, Cap 5). Há duas famílias de resposta: ou você força a coordenação distribuída (o 2PC, que estende o commit atômico pela rede, pagando em disponibilidade), ou você abandona a atomicidade estrita e a substitui por algo mais fraco mas viável (a Saga, que aceita estados intermediários e os corrige com compensações). Não há uma terceira opção mágica — é uma escolha entre o ACID distribuído caro e a consistência eventual gerenciada. O resto do capítulo é sobre essas duas respostas e seus trade-offs.

17.3 O 2PC e por que ele dói

O two-phase commit tenta estender o commit atômico pela rede com um coordenador e duas fases. É correto — garante atomicidade — mas suas propriedades o tornam inadequado para a maioria dos sistemas modernos.

as duas fases do 2PC
FASE 1 — PREPARAÇÃO (votação):
  Coordenador → todos: "preparem-se para commitar; conseguem?"
  Cada participante: trava os recursos, prepara, e vota
    "SIM, consigo" ou "NÃO, não consigo"
  (e fica SEGURANDO OS LOCKS, esperando a decisão)

FASE 2 — DECISÃO (commit/abort):
  Se TODOS votaram SIM → coordenador → todos: "commitem"
  Se ALGUÉM votou NÃO  → coordenador → todos: "abortem"
  Cada participante commita ou aborta, e SÓ ENTÃO solta os locks

Garante atomicidade (todos commitam ou todos abortam). MAS:
o tempo todo entre fase 1 e 2, os recursos ficam TRAVADOS,
e se o coordenador cai no meio, os participantes ficam presos
("in doubt") segurando locks, sem saber se commitam ou abortam.
2PC: bloqueante, coordenador único, disponibilidade baixa
O 2PC garante a atomicidade que você quer, mas suas três propriedades o tornam doloroso em escala. É bloqueante: durante todo o protocolo (entre as fases), os participantes seguram locks nos recursos, esperando a decisão do coordenador. Isso mata a concorrência e a vazão — recursos travados não podem ser usados por outras operações (contenção, Cap 7), e quanto mais participantes e mais latência de rede, mais tempo os locks ficam segurados. Tem um coordenador central: que é um ponto único de falha — se o coordenador cai depois da fase 1 mas antes da fase 2, os participantes ficam "em dúvida", presos segurando locks indefinidamente, sem saber se devem commitar ou abortar. Reduz a disponibilidade: se qualquer participante está indisponível, a transação inteira não pode prosseguir — a disponibilidade do todo é o produto das disponibilidades de cada parte (a multiplicação do Cap 4), então mais serviços = menos disponível. Essas propriedades batem de frente com os objetivos da nuvem (alta disponibilidade, escala, serviços independentes) e são a razão de o 2PC ser evitado em sistemas modernos: ele compra atomicidade estrita ao preço de disponibilidade e escala, e — pelo CAP (Cap 5) — esse é exatamente o trade-off que a maioria dos sistemas distribuídos prefere fazer ao contrário (abrir mão da consistência forte imediata pela disponibilidade). O 2PC não está "errado"; ele está otimizando para a propriedade (consistência forte) que a maioria dos sistemas distribuídos decide sacrificar.

17.4 A Saga — transações locais com compensação

A Saga resolve o problema por outro caminho: em vez de uma transação atômica global, uma sequência de transações locais (cada uma atômica no seu serviço), onde, se um passo falha, os passos anteriores são desfeitos por transações compensatórias — em ordem reversa.

a Saga: avançar, ou compensar de volta
CAMINHO FELIZ (cada passo é uma transação local, commita já):
  1. Pagamento: cobrar          ✓ (commitado)
  2. Estoque: reservar          ✓ (commitado)
  3. Envio: agendar             ✓ (commitado)
  → Saga completa.

CAMINHO DE FALHA (passo 3 falha):
  1. Pagamento: cobrar          ✓
  2. Estoque: reservar          ✓
  3. Envio: agendar             ✗ FALHOU
  → executa COMPENSAÇÕES em ordem REVERSA:
  2'. Estoque: liberar a reserva    (desfaz o passo 2)
  1'. Pagamento: estornar a cobrança (desfaz o passo 1)
  → Saga abortada, sistema volta a um estado consistente.

Cada passo commita IMEDIATAMENTE (não segura locks como o 2PC).
A "atomicidade" é simulada: ou todos os passos completam, ou
os que completaram são compensados. Eventual, não instantânea.
A Saga troca atomicidade instantânea por disponibilidade e escala
A genialidade da Saga é trocar a atomicidade instantânea do 2PC por uma atomicidade eventual e não-bloqueante. Cada passo da Saga é uma transação local que commita imediatamente e solta seus recursos — nada de segurar locks esperando um coordenador (a dor do 2PC). Isso a torna não-bloqueante, escalável e alinhada com microsserviços independentes: cada serviço gerencia sua própria transação, sem coordenação travante. O preço é que, durante a execução da Saga, o sistema passa por estados intermediários inconsistentes (o pagamento foi cobrado mas o envio ainda não agendado) — consistência eventual (Cap 6), não imediata. Se tudo der certo, a Saga completa e o estado fica consistente; se algo falha, as compensações desfazem os passos anteriores e o estado volta a ser consistente. A Saga não elimina a inconsistência; ela a torna temporária e gerenciada, com um caminho claro de volta à consistência (as compensações). É a escolha do CAP em ação: a Saga é AP (disponível, eventualmente consistente) onde o 2PC é CP (consistente, menos disponível). Para a maioria das operações de negócio — onde uma janela de segundos de inconsistência, resolvida por compensação, é aceitável —, a Saga é a escolha certa. Para as raras onde nenhuma inconsistência, nem temporária, é tolerável, o 2PC (ou manter tudo num banco só) pode ser necessário, ao seu custo.

17.5 A compensação não é um rollback

Um ponto sutil e crucial: a transação compensatória da Saga não é um rollback do banco (que apaga a transação como se nunca tivesse acontecido). É uma nova transação que semanticamente desfaz a anterior — e essa diferença tem consequências reais.

Compensar é uma nova ação que desfaz o efeito, não apaga o histórico
Um rollback de banco reverte uma transação não commitada — é como se ela nunca tivesse existido, sem rastro. A compensação de uma Saga é diferente: o passo original já commitou (já é um fato no mundo), então você não pode apagá-lo; você executa uma nova transação que contrabalança seu efeito. "Estornar a cobrança" não apaga a cobrança — ela permanece no histórico, e adiciona-se um estorno. "Liberar a reserva de estoque" não desfaz a reserva original — registra-se uma liberação. Isso tem três consequências importantes. Primeiro, a compensação precisa ser semanticamente projetada — você tem que definir, para cada passo, qual ação o desfaz (e nem sempre é óbvio: como se "compensa" um email já enviado? você não pode "des-enviar"; tem que mandar outro corrigindo). Segundo, alguns efeitos são difíceis ou impossíveis de compensar — efeitos no mundo real (um email enviado, um produto físico despachado) não voltam atrás, e a Saga precisa ser desenhada para que esses passos irreversíveis venham por último (depois que tudo que poderia falhar já passou) ou tenham compensações de "correção" em vez de "desfazimento". Terceiro, há uma janela em que o efeito original existiu e foi compensado — o cliente viu a cobrança e depois o estorno, o que é visível e às vezes confuso. A lição: projetar uma Saga não é só listar os passos; é projetar, para cada passo, a sua compensação semântica, ordenar os passos para que os irreversíveis venham por último, e aceitar que "desfazer" numa Saga é contrabalançar, não apagar.

17.6 Coreografia vs orquestração — coordenar a Saga

Como os passos da Saga são coordenados? Aqui reaparece, aplicada à Saga, a distinção do Cap 15 — e o trade-off é o mesmo, agora com mais peso por causa das compensações.

Saga por coreografia

Sem coordenador central. Cada serviço reage a eventos e emite os seus: "pagamento feito" → o estoque reage e reserva → emite "estoque reservado" → o envio reage... E cada serviço conhece suas próprias compensações. Descentralizado, desacoplado. Mas o fluxo (e a lógica de compensação) fica espalhado — difícil de ver e debugar a Saga inteira.

Saga por orquestração

Um orquestrador central conhece a Saga inteira: comanda cada passo, recebe o resultado, decide o próximo — e, em caso de falha, dispara as compensações na ordem certa. O fluxo e a lógica de compensação ficam num lugar, explícitos. Mais fácil de ver, monitorar e tratar erros; ao custo do orquestrador central.

Para Sagas complexas, a orquestração costuma vencer — pela compensação visível
O trade-off coreografia-vs-orquestração da Saga é o mesmo do Cap 15 (desacoplamento vs visibilidade), mas o peso pende mais para a orquestração aqui, e a razão é a compensação. Numa Saga por coreografia, quando algo falha no meio, a lógica de "quais passos compensar e em que ordem" fica espalhada pelas reações dos vários serviços — e raciocinar sobre "o que precisa ser desfeito agora?" num fluxo distribuído e implícito é genuinamente difícil, propenso a erros, e quase impossível de debugar quando dá errado. Numa Saga por orquestração, o orquestrador conhece o fluxo inteiro e a sequência de compensações: quando um passo falha, ele sabe exatamente quais passos anteriores compensar e os dispara em ordem. Essa visibilidade e controle centralizados são especialmente valiosos para a parte mais delicada da Saga (a compensação), e por isso a orquestração é frequentemente preferida para Sagas de negócio complexas, apesar do orquestrador central (que, note, é menos crítico que o coordenador do 2PC — ele não segura locks, e a Saga pode até sobreviver à sua queda temporária com persistência de estado). A coreografia brilha para Sagas simples e altamente desacopladas; a orquestração, para as complexas onde ver e controlar o fluxo de compensação importa — que são a maioria das Sagas transacionais sérias. Como no Cap 15: quanto mais complexo o fluxo, mais a orquestração compensa seu ponto central.

17.7 A falta de isolamento — o I que a Saga perde

A Saga recupera, com esforço, a atomicidade (via compensações) e a durabilidade (cada passo commita). Mas há uma garantia do ACID que ela não recupera, e ignorar isso causa bugs: o isolamento (o "I" de ACID).

Durante a Saga, outros enxergam estados intermediários — e isso pode causar anomalias
Numa transação ACID, o isolamento garante que ninguém vê os estados intermediários — a transação é invisível até commitar, então outras operações nunca observam "metade" dela. A Saga não tem isso: como cada passo commita imediatamente e é visível, outras operações podem observar e agir sobre os estados intermediários da Saga em andamento. Exemplos de anomalias que isso causa: enquanto a Saga de um pedido está no meio (pagamento feito, estoque ainda não reservado), outra operação pode ler o estoque "ainda disponível" e vendê-lo — e quando a Saga tentar reservar, falha. Ou: a Saga cobrou, mas vai compensar (estornar) porque um passo posterior falhou; nesse meio-tempo, um relatório financeiro leu a cobrança e a contou — depois ela é estornada, e o relatório fica errado. A falta de isolamento é o calcanhar de Aquiles da Saga, e exige contramedidas explícitas: locks semânticos (marcar o estoque como "reservado pendente" em vez de disponível durante a Saga), reler-e-revalidar antes de agir sobre dados que podem estar no meio de uma Saga, ou desenhar a Saga para minimizar a janela de exposição. A lição: a Saga te dá de volta a atomicidade (compensações) e a durabilidade (commits locais), mas não o isolamento — e você precisa lidar com a visibilidade dos estados intermediários conscientemente, ou anomalias de concorrência vão aparecer. É um custo real da Saga que o 2PC (que segura locks, mantendo isolamento) não tem — parte do que você troca pela disponibilidade.

17.8 Quando usar o quê

Juntando tudo, a decisão entre as abordagens segue do que a operação exige e do que você pode tolerar:

SituaçãoAbordagem
A operação cabe num único banco/serviçoTransação ACID local (a melhor opção — evite o problema)
Cruza serviços; tolera inconsistência temporáriaSaga (a escolha padrão para microsserviços)
Saga simples, poucos passos, desacoplamento prioritárioSaga por coreografia
Saga complexa, precisa de visibilidade e controleSaga por orquestração
Nenhuma inconsistência, nem temporária, é tolerável2PC (raro) — ou redesenhe para caber num banco
A melhor transação distribuída é a que você não precisa fazer
Antes de escolher entre Saga e 2PC, vale a pergunta que evita o problema inteiro: essa operação precisa mesmo cruzar serviços?. Frequentemente, a necessidade de uma transação distribuída é um sintoma de fronteiras de serviço mal desenhadas — se duas coisas precisam ser sempre atômicas juntas, talvez elas devessem estar no mesmo serviço, no mesmo banco, onde uma transação ACID local resolve tudo, simples e forte. A transação distribuída mais robusta é a que você eliminou redesenhando as fronteiras para que a atomicidade fique dentro de um serviço. Quando isso não é possível (as coisas genuinamente pertencem a serviços diferentes — pagamento e estoque são domínios distintos), aí a Saga é a resposta padrão, aceitando a consistência eventual. E o 2PC fica reservado para os casos raros onde nem uma inconsistência temporária é tolerável e a operação não pode caber num banco — pagando seu preço em disponibilidade e escala. A hierarquia de preferência: primeiro, mantenha atômico dentro de um serviço (evite o problema); segundo, use Saga (aceite consistência eventual); só em último caso, 2PC (aceite o custo da consistência forte distribuída). Muitos times pulam direto para a transação distribuída sem perguntar se poderiam ter desenhado as fronteiras para não precisar dela — e essa pergunta, feita cedo, economiza enorme complexidade.

17.9 Estudo de caso — a reserva de viagem que precisava ser atômica

Voo, hotel e carro: tudo ou nada, entre três serviços
O cenário

Uma plataforma de viagens permite reservar um pacote — voo, hotel e carro — numa única operação, mas cada um é um serviço separado, com seu próprio banco, integrando provedores externos diferentes. O requisito de negócio: ou o cliente consegue os três (a viagem completa), ou nenhum (não faz sentido ter o voo sem o hotel). Como tornar isso "atômico" entre três serviços independentes?

Passo 1 · Por que 2PC não serve aqui
descartando o 2PC
2PC exigiria que voo, hotel e carro (e seus provedores
externos!) participassem de um protocolo de commit coordenado,
segurando recursos travados durante todo ele.

Problemas fatais aqui:
  - os provedores externos (companhias aéreas, redes de hotel)
    NÃO oferecem participação em 2PC — você não controla eles
  - segurar uma reserva de voo "travada" por um protocolo
    distribuído enquanto espera hotel e carro = inviável
  - se qualquer um (ou a rede até ele) cai, tudo trava

2PC é impraticável quando os participantes são serviços
externos que você não controla. Precisamos de outra coisa.
Passo 2 · A Saga: reservar em sequência, compensar se falhar
a Saga de viagem
CAMINHO FELIZ:
  1. Reservar VOO     ✓ (commitado no serviço de voo)
  2. Reservar HOTEL   ✓ (commitado no serviço de hotel)
  3. Reservar CARRO   ✓ (commitado no serviço de carro)
  → pacote confirmado!

FALHA no passo 3 (sem carros disponíveis):
  3. Reservar CARRO   ✗ FALHOU
  → COMPENSAÇÕES em ordem reversa:
  2'. CANCELAR a reserva de hotel  (compensação do passo 2)
  1'. CANCELAR a reserva de voo    (compensação do passo 1)
  → cliente informado: "não foi possível montar o pacote"
    e NÃO fica com voo e hotel órfãos.

Cada reserva commita já (não trava recursos). A atomicidade
do pacote é simulada pela compensação. Eventual, não instantânea.
Passo 3 · As sutilezas: compensação semântica, idempotência, isolamento
os detalhes que fazem a Saga funcionar
COMPENSAÇÃO SEMÂNTICA: "cancelar a reserva de voo" não APAGA
  a reserva — cria um cancelamento (que pode ter taxa!). A
  compensação é uma nova ação, não um rollback. (E reservas
  não-canceláveis precisam vir POR ÚLTIMO ou ter outro plano.)

IDEMPOTÊNCIA (Cap 16): cada passo e cada compensação leva uma
  chave — se a rede faz o "cancelar voo" chegar duas vezes,
  cancela uma vez só. Sagas EXIGEM passos idempotentes, porque
  retries são inevitáveis (Cap 8).

ISOLAMENTO: durante a Saga, a reserva de voo já está "feita" e
  visível. Usa-se lock semântico: o voo fica "reservado
  pendente de pacote" até a Saga completar, evitando que outra
  operação aja sobre um estado intermediário.

ORQUESTRAÇÃO: um orquestrador de pacote conhece os 3 passos e
  suas compensações — visibilidade do fluxo todo e controle da
  compensação (melhor que coreografia para esse fluxo complexo).
Passo 4 · O resultado

A Saga orquestrada entregou a "atomicidade" do pacote sem 2PC: cada reserva commitava imediatamente (sem travar recursos nem exigir 2PC dos provedores externos), e se qualquer passo falhava, as compensações desfaziam as reservas anteriores, evitando voos e hotéis órfãos. Os passos e compensações eram idempotentes (seguros para os retries inevitáveis), e locks semânticos lidavam com a falta de isolamento. O cliente ou recebia o pacote completo, ou nenhuma reserva pendurada — o requisito de negócio, atendido pela consistência eventual gerenciada da Saga.

Resultado: a "atomicidade" entre três serviços independentes (e provedores externos que não se pode coordenar) foi alcançada não por estender o ACID (2PC, impraticável aqui), mas pela Saga — uma sequência de transações locais com compensações que desfazem o que já foi feito se um passo falha. O caso reúne o capítulo inteiro: o 2PC era inviável (participantes externos não controláveis, bloqueio impraticável); a Saga deu a atomicidade eventual sem travar recursos; as compensações eram semânticas (cancelar, não apagar — e com a ordem certa para os passos menos reversíveis); a idempotência (Cap 16) tornou os passos seguros para os retries inevitáveis; e os locks semânticos lidaram com a falta de isolamento. A lição central: para operações que cruzam serviços, a Saga troca a atomicidade instantânea (e cara, e frágil) do 2PC pela atomicidade eventual e gerenciada — e essa troca é quase sempre a certa em sistemas distribuídos modernos, desde que você projete as compensações, a idempotência e o isolamento com cuidado. Próximo capítulo: CQRS e Event Sourcing — dois padrões que levam adiante a ideia do log (Cap 14) e dos eventos (Cap 15), separando como você escreve de como você lê, e guardando as mudanças como a verdade. Os padrões que reorganizam o fluxo de dados de um sistema inteiro.

17.10 Erros comuns

Erro 1 · Usar 2PC em sistemas modernos sem necessidade

Estender o ACID com 2PC para operações que cruzam serviços — herdando o bloqueio (locks segurados durante o protocolo), o coordenador como ponto único, e a baixa disponibilidade (qualquer participante fora trava tudo). O 2PC raramente vale em sistemas distribuídos modernos; a Saga é a escolha padrão, aceitando consistência eventual.

Erro 2 · Não desenhar as compensações

Implementar os passos felizes da Saga e esquecer as compensações — e ficar com estados inconsistentes quando um passo falha (cobrou e não entregou, sem como desfazer). Para cada passo, projete sua compensação semântica desde o início. A Saga sem compensações não é uma Saga; é uma sequência que quebra na primeira falha.

Erro 3 · Tratar compensação como rollback de banco

Esperar que "desfazer" um passo da Saga seja como um rollback (apagar sem rastro) — quando o passo já commitou e só pode ser contrabalançado por uma nova ação (estornar, não apagar). Compensação é semântica, deixa rastro, e alguns efeitos (email enviado, produto despachado) não voltam atrás — ordene os irreversíveis por último.

Erro 4 · Passos da Saga não idempotentes

Construir uma Saga com passos e compensações não idempotentes — e, sob os retries inevitáveis (Cap 8), executar um passo ou uma compensação duas vezes (cobrar duas vezes, estornar duas vezes). Sagas exigem idempotência em cada passo e compensação, pelas mesmas razões do Cap 16. Idempotência não é opcional numa Saga.

Erro 5 · Ignorar a falta de isolamento

Esquecer que, durante a Saga, outros enxergam os estados intermediários (cada passo commita e fica visível) — e sofrer anomalias (vender um estoque que uma Saga em andamento vai reservar). A Saga não tem o isolamento do ACID; use locks semânticos ("reservado pendente"), revalidação, ou minimize a janela. Trate a visibilidade dos estados intermediários conscientemente.

Erro 6 · Coreografia para Sagas complexas

Usar coreografia (eventos descentralizados) numa Saga complexa — espalhando a lógica de compensação pelos serviços, e tornando impossível ver e controlar "o que desfazer" quando algo falha no meio. Para Sagas complexas, a orquestração (fluxo e compensação explícitos e centralizados) costuma vencer, apesar do orquestrador central.

Erro 7 · Não perguntar se a transação distribuída é necessária

Pular direto para Saga/2PC sem questionar se a operação precisa mesmo cruzar serviços — quando frequentemente a necessidade é sintoma de fronteiras mal desenhadas, e pôr as coisas que devem ser atômicas no mesmo serviço resolveria com uma transação ACID local, simples e forte. A melhor transação distribuída é a que você elimina redesenhando fronteiras.

Verifique seu entendimento
Uma operação de "reservar pacote de viagem" precisa reservar voo, hotel e carro — três serviços separados, integrando provedores externos — de forma que o cliente consiga os três ou nenhum. Qual abordagem é a mais sólida, e por quê?

17.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Local, Saga ou redesenhar?

Para cada operação, decida se é uma transação local (cabe num banco), pede uma Saga (cruza serviços), ou é sinal de fronteiras mal desenhadas (deveria caber num serviço): (a) transferir saldo entre duas contas no mesmo banco de dados; (b) um pedido que cobra pagamento, reserva estoque e agenda envio, cada um num serviço; (c) atualizar o nome e o email de um usuário, ambos na tabela de usuários; (d) duas entidades que mudam sempre juntas, mas estão em dois microsserviços diferentes.

(a) Transferir saldo no mesmo banco → transação ACID local. Tudo está num banco, então uma transação ACID resolve perfeitamente — atômica, isolada, durável. Não há problema distribuído aqui; nem pense em Saga. Esta é a situação ideal: a atomicidade cabe dentro de um banco. Use o ACID e seja feliz.

(b) Pedido cruzando pagamento/estoque/envio → Saga. Três serviços distintos, três domínios genuinamente diferentes (pagamento, estoque, logística não são a mesma coisa), cada um com seu banco. Uma Saga é a resposta padrão: passos locais com compensações, aceitando consistência eventual. Faz sentido que sejam serviços separados, então a Saga é apropriada (não é fronteira mal desenhada).

(c) Nome e email na mesma tabela → transação ACID local. Ambos os campos estão na mesma tabela, no mesmo banco — uma única transação ACID atualiza os dois atomicamente. Trivial. Nem é um "problema de transação distribuída"; é uma transação comum.

(d) Duas entidades que mudam SEMPRE juntas, em serviços diferentes → fronteiras mal desenhadas. Este é o sinal de alerta. Se duas coisas precisam ser sempre atômicas juntas, separá-las em serviços diferentes foi provavelmente um erro de design — elas têm alta coesão e deveriam estar no mesmo serviço, onde uma transação ACID local resolve. A solução não é uma Saga (que adicionaria complexidade para compensar um erro de fronteira); é redesenhar as fronteiras para que a atomicidade caiba num serviço. A necessidade de transação distribuída aqui é sintoma, não requisito.

O insight: a hierarquia de preferência — primeiro, mantenha atômico num banco (a, c); se genuinamente cruza domínios distintos, Saga (b); se a necessidade de atomicidade distribuída revela coisas que deveriam estar juntas, redesenhe as fronteiras em vez de adicionar uma Saga (d). A pergunta-chave para (d): por que duas coisas que mudam sempre juntas estão separadas?

Médio
Exercício 2 · Projetar as compensações

Para uma Saga de e-commerce com os passos (1) cobrar cartão, (2) reservar estoque, (3) enviar email de confirmação, (4) despachar o produto físico, projete a compensação de cada passo. Identifique quais passos são difíceis ou impossíveis de compensar e como ordenar a Saga para lidar com isso.

As compensações de cada passo:

  • (1) Cobrar cartão → compensação: estornar a cobrança. Relativamente limpa (uma nova transação de estorno), mas deixa rastro (o cliente vê a cobrança e o estorno no extrato) e pode ter custo/taxa de processamento. Não apaga a cobrança — contrabalança.
  • (2) Reservar estoque → compensação: liberar a reserva. Limpa e reversível — devolver as unidades reservadas ao estoque disponível. Fácil de compensar.
  • (3) Enviar email de confirmação → compensação: difícil/impossível. Você não pode des-enviar um email. A "compensação" possível é enviar outro email corrigindo ("seu pedido foi cancelado"), o que é uma correção, não um desfazimento. Efeito externo no mundo, irreversível.
  • (4) Despachar produto físico → compensação: muito difícil/cara. Um produto já despachado precisa ser interceptado (se ainda possível) ou devolvido (logística reversa, cara e demorada). Praticamente irreversível uma vez que saiu.

Quais são difíceis/impossíveis: (3) e (4) — efeitos no mundo real (email enviado, produto despachado) que não voltam atrás como uma transação de banco. (1) é compensável mas com rastro/custo; (2) é limpa.

Como ordenar a Saga para lidar com isso: a regra é colocar os passos irreversíveis ou difíceis de compensar por último, depois que tudo que poderia falhar e exigir compensação já passou. Ordem correta:

  1. Reservar estoque (compensável, e você quer saber cedo se há estoque).
  2. Cobrar cartão (compensável com estorno; saber se o pagamento funciona antes de despachar).
  3. Despachar o produto (quase irreversível — só depois que estoque e pagamento estão garantidos).
  4. Enviar email de confirmação (irreversível, mas inócuo — por último, quando tudo já deu certo; se mandado e algo desse errado, manda-se um corretivo).

O insight: projetar uma Saga é projetar as compensações e a ordem. Passos compensáveis (estoque, pagamento) vêm primeiro, para que falhas exijam apenas compensações limpas. Passos irreversíveis (despachar, emailar) vêm por último, executados só quando tudo que poderia falhar já passou — minimizando a chance de precisar "desfazer" o que não se desfaz. Se um passo é verdadeiramente irreversível e não pode vir por último, a Saga talvez não seja a abordagem certa para ele.

Médio
Exercício 3 · 2PC vs Saga — o trade-off

Explique para um colega por que o 2PC garante consistência mais forte que a Saga, mas é evitado em sistemas distribuídos modernos. Cubra o bloqueio, o coordenador, a disponibilidade, e relacione com o CAP (Cap 5).

Por que o 2PC tem consistência mais forte: o 2PC garante atomicidade real e imediata — todos os participantes commitam juntos ou abortam juntos, e durante o protocolo eles seguram locks, então ninguém vê estados intermediários (mantém o isolamento). Não há janela de inconsistência: do ponto de vista externo, a transação distribuída inteira acontece atomicamente, como uma transação ACID local. A Saga, em contraste, tem consistência eventual: passa por estados intermediários visíveis (um passo commitado, o próximo não), e só fica consistente quando completa ou compensa. O 2PC é mais forte: consistência imediata e isolamento.

Por que o 2PC é evitado mesmo assim:

  • Bloqueio: os participantes seguram locks durante todo o protocolo (entre as duas fases). Isso mata a concorrência (recursos travados não servem outras operações) e a vazão, e piora com mais participantes e mais latência de rede. A Saga não bloqueia — cada passo commita e solta na hora.
  • Coordenador central: ponto único de falha. Se o coordenador cai entre as fases, os participantes ficam "em dúvida", presos segurando locks, sem saber se commitam ou abortam. Frágil.
  • Disponibilidade reduzida: se qualquer participante está indisponível, a transação inteira não prossegue. A disponibilidade do todo é o produto das partes (Cap 4) — mais serviços, menos disponível.

A relação com o CAP (Cap 5): o 2PC é uma escolha CP — ele prioriza consistência (atomicidade forte) sobre disponibilidade, e sob uma partição ou falha de participante, ele para (fica indisponível) para não violar a consistência. A Saga é uma escolha AP — ela prioriza disponibilidade (cada serviço opera independente, sem coordenação travante) e aceita consistência eventual (os estados intermediários, resolvidos por compensação). A maioria dos sistemas distribuídos modernos prefere o lado AP (disponibilidade e escala) para a maioria das operações — exatamente a escolha que o 2PC faz ao contrário. Por isso o 2PC, que insiste na consistência forte distribuída (e paga o preço em disponibilidade que o CAP garante), é evitado: ele otimiza para a propriedade que a maioria dos sistemas decide sacrificar. A Saga não é "melhor" em absoluto — ela faz a troca (consistência eventual por disponibilidade) que a maioria dos sistemas prefere.

Difícil
Exercício 4 · Projetar a consistência distribuída de um sistema — entrevista

Você projeta o fluxo de checkout de um e-commerce, que precisa coordenar pagamento, estoque, e fulfillment (serviços separados) de forma consistente. O entrevistador pede: "como você garante a consistência dessa operação que cruza serviços?". Demonstre que você considera evitar o problema, escolhe a abordagem certa, e trata as sutilezas (compensação, idempotência, isolamento, coordenação).

Enquadramento: a resposta forte começa questionando se o problema pode ser evitado, depois escolhe a abordagem (Saga, não 2PC), e trata as quatro sutilezas que fazem uma Saga funcionar. Vou por etapas.

1. Primeiro, questionar as fronteiras: as coisas que precisam de atomicidade poderiam caber num serviço? Pagamento, estoque e fulfillment são domínios genuinamente distintos (com bancos, times e ciclos diferentes), então separá-los faz sentido — não é fronteira mal desenhada. A transação distribuída aqui é real, não sintoma. (Mas eu faria essa pergunta antes de assumir.)

2. Escolher Saga, não 2PC: o 2PC seria frágil e lento aqui (bloqueio de recursos, coordenador único, disponibilidade reduzida — e provavelmente provedores de pagamento externos que não fazem 2PC). Escolho uma Saga, aceitando consistência eventual: passos locais que commitam imediatamente, com compensações se algo falhar. É a escolha AP, alinhada com disponibilidade e escala.

3. Desenhar os passos e compensações, na ordem certa:

  • Passo 1: reservar estoque (compensação: liberar — limpa; e quero saber cedo se há estoque).
  • Passo 2: cobrar pagamento (compensação: estornar — deixa rastro, mas ok).
  • Passo 3: fulfillment/despacho (quase irreversível — por último, só quando estoque e pagamento estão garantidos).

Ordeno os irreversíveis (despacho) por último, para que falhas exijam só compensações limpas.

4. Idempotência (Cap 16): cada passo e cada compensação leva uma chave de idempotência. Retries são inevitáveis (a rede), então cobrar/reservar/estornar precisam ser seguros para repetir — senão a Saga, sob retry, cobra ou estorna duas vezes. Idempotência é pré-requisito, não opcional.

5. Isolamento (locks semânticos): durante a Saga, o estoque reservado fica visível. Uso um lock semântico — marcar como "reservado pendente" em vez de disponível — para que outra operação não venda o mesmo estoque que esta Saga está usando. Trato a falta de isolamento conscientemente.

6. Coordenação — orquestração: uso uma Saga orquestrada (um orquestrador de checkout que conhece os passos e as compensações), não coreografia — porque este é um fluxo de negócio crítico onde preciso de visibilidade (em que passo travou?) e controle da compensação (o que desfazer?). A orquestração me dá isso, e o orquestrador, diferente do coordenador 2PC, não segura locks e pode sobreviver à própria queda com estado persistido.

O que um bom candidato demonstra: que questiona as fronteiras antes de assumir a transação distribuída; que escolhe Saga sobre 2PC pelas razões certas; que desenha compensações e as ordena (irreversíveis por último); que sabe que idempotência é pré-requisito; que trata a falta de isolamento; e que escolhe orquestração para o controle da compensação. Frase de fechamento: "primeiro confirmo que pagamento, estoque e fulfillment são mesmo domínios separados — são, então a transação distribuída é real. Não uso 2PC: é bloqueante, frágil, e os provedores externos não participam. Uso uma Saga orquestrada — passos locais que commitam na hora, com compensações se algo falha, ordenando o despacho irreversível por último. Cada passo e compensação é idempotente, porque retries são certos; uso locks semânticos para a falta de isolamento; e orquestro para ter visibilidade e controle da compensação. A Saga me dá a atomicidade que o negócio precisa — o cliente consegue tudo ou nada — trocando a atomicidade instantânea do 2PC pela eventual e gerenciada, que é a troca certa em escala."

Fim do capítulo 17
Próximo capítulo: CQRS e Event Sourcing. A Saga já usou a ideia de modelar operações como passos e eventos; agora vamos a dois padrões que levam isso ao limite. O CQRS separa o modelo de escrita do modelo de leitura (porque eles têm necessidades opostas, Cap 2); o Event Sourcing guarda as mudanças (eventos) como a verdade, em vez do estado atual (o log como fonte da verdade, Cap 14). Juntos, eles reorganizam o fluxo de dados de um sistema — com ganhos poderosos e uma complexidade que você precisa saber quando vale.
Parte IV · Capítulo 18 · Os padrões

CQRS e Event
Sourcing.

Dois padrões frequentemente mencionados juntos, frequentemente confundidos, e quase sempre adotados em excesso. O CQRS separa o modelo de escrita do modelo de leitura, reconhecendo que eles têm necessidades opostas (Cap 2). O Event Sourcing guarda as mudanças (eventos) como a verdade, em vez do estado atual — o log como fonte da verdade (Cap 14) levado à aplicação. Cada um é poderoso para os problemas certos e um desastre de complexidade para os errados. Este capítulo separa os dois, mostra o que cada um ganha e cobra, e — acima de tudo — deixa claro quando não usá-los, porque o erro mais comum com ambos é aplicá-los onde não se precisa.

Este capítulo trata de dois padrões que reorganizam o fluxo de dados. Vamos ver o CQRS (separar comando de query) e por que separar (read e write têm perfis opostos), a consistência eventual que a separação introduz e como lidar com ela, o Event Sourcing (eventos imutáveis como a verdade, estado derivado por replay) e o que ele ganha (auditoria, replay, history) e cobra (ler o estado é caro, eventos são mão única), como os dois se combinam (e por que são separados, apesar de andarem juntos), e — o mais importante — quando não usá-los, já que a maioria dos sistemas não precisa de nenhum. Quando você sair daqui, CQRS e Event Sourcing deixam de ser palavras impressionantes que você se sente pressionado a usar e viram ferramentas precisas que você aplica seletivamente, onde o benefício justifica a complexidade real.

18.1 A história — de um princípio a uma arquitetura

Contexto histórico

O CQRS tem uma raiz humilde e elegante. Nos anos 1980, Bertrand Meyer formulou o princípio da separação comando-consulta (Command-Query Separation): um método deve ou mudar o estado (comando) ou retornar um valor (consulta), nunca os dois. Era um princípio de design no nível do método — limpo, simples. Décadas depois, por volta de 2010, Greg Young pegou essa ideia e a elevou ao nível da arquitetura: e se você usasse um modelo inteiramente diferente para escrever dados e para ler dados? Isso é o CQRS (Command Query Responsibility Segregation) — a separação comando-consulta aplicada não a um método, mas à arquitetura inteira.

O Event Sourcing tem raízes ainda mais antigas, embora redescobertas. A ideia de guardar as mudanças (eventos) em vez do estado atual sempre esteve na contabilidade (um livro-razão registra transações, não só o saldo) e nos bancos de dados (o write-ahead log, Cap 14). Como padrão de aplicação, ela emergiu junto com o CQRS na comunidade de Domain-Driven Design — guardar cada mudança de estado como um evento imutável, e derivar o estado atual reproduzindo os eventos. É o "log como fonte da verdade" do Cap 14, aplicado ao domínio.

Os dois ganharam fama juntos, e por boas razões: combinam-se naturalmente (os eventos do Event Sourcing alimentam os modelos de leitura do CQRS). Mas ganharam também uma reputação ambígua. Por um lado, resolveram problemas reais e difíceis em domínios complexos (finanças, sistemas com auditoria pesada). Por outro, foram massivamente adotados em excesso — times aplicando CQRS e Event Sourcing a CRUDs simples que não precisavam de nada disso, herdando enorme complexidade por nenhum benefício. Martin Fowler, um dos divulgadores do CQRS, passou a alertar repetidamente: é um padrão difícil de usar bem, que adiciona risco, e que para a maioria dos sistemas só atrapalha.

Em 2026, o consenso amadureceu para uma regra de ouro: nunca aplique CQRS ou Event Sourcing ao sistema inteiro; aplique-os seletivamente aos poucos bounded contexts que genuinamente se beneficiam, e mantenha o resto como CRUD simples. A maioria dos sistemas (ou das partes de um sistema) é melhor servida pela abordagem tradicional — um modelo só, estado atual, CRUD. CQRS e Event Sourcing são ferramentas afiadas para problemas específicos (assimetria extrema leitura/escrita, domínios complexos, auditoria crítica), não defaults. Este capítulo ensina a reconhecer esses problemas específicos — e, igualmente importante, a reconhecer quando você não os tem, e a complexidade não se justifica.

18.2 CQRS — separar o modelo de leitura do de escrita

O CQRS é, na essência, uma ideia simples: use dois modelos diferentes — um para escrever (comandos, que mudam o estado) e um para ler (queries, que retornam dados) —, em vez de um único modelo que faz as duas coisas.

Tradicional (CRUD) — um modelo só

O mesmo modelo de dados serve para escrever e ler. Você cria, lê, atualiza e deleta sobre a mesma estrutura. Simples e familiar — e suficiente para a maioria. Mas quando ler e escrever têm necessidades muito diferentes, um modelo único faz mal as duas: otimizá-lo para leitura (índices, desnormalização) piora a escrita, e vice-versa.

CQRS — modelos separados

O modelo de escrita (command) foca em validar e aplicar mudanças, com a lógica de negócio e a consistência. O modelo de leitura (query) é desnormalizado, pré-computado, otimizado para servir queries rápidas. Cada um é otimizado para seu trabalho, podendo até usar bancos diferentes. As mudanças no write propagam para o read (tipicamente via eventos).

CQRS é a separação comando-consulta na arquitetura
O CQRS é o princípio de Meyer (um método ou muda ou lê, não os dois) aplicado à escala da arquitetura: um caminho inteiro para comandos (escrita) e outro para queries (leitura), cada um com seu modelo, possivelmente seu banco, sua escala. O caminho de escrita recebe comandos, executa a lógica de negócio e a validação, e aplica as mudanças à fonte da verdade. O caminho de leitura serve queries de views desnormalizadas e pré-computadas, otimizadas para serem lidas rápido — sem lógica de negócio, só recuperação eficiente. As mudanças no lado de escrita são propagadas para atualizar as views de leitura (frequentemente via eventos, conectando ao Cap 15). O resultado é que cada lado pode ser otimizado, escalado e até desenvolvido independentemente — o time de leitura cuida das views rápidas, o de escrita cuida da lógica de negócio. É importante reconhecer que CQRS é um espectro: na forma mais leve, é só usar uma query/view separada para leituras pesadas (um "reporting database") mantendo o resto unificado; na forma completa, são modelos e bancos totalmente separados. Você não precisa do extremo — pode aplicar a dose de separação que o problema pede. E, crucialmente, CQRS não exige Event Sourcing (são padrões separados, seção 18.7) — você pode ter CQRS com bancos tradicionais, propagando mudanças por eventos simples.

18.3 Por que separar — read e write são opostos

A justificativa do CQRS vem direto do Cap 2: leitura e escrita têm perfis frequentemente opostos, e um modelo único força um compromisso que serve mal aos dois. Separá-los deixa cada um ótimo no seu.

Otimizar para leitura piora a escrita; separar resolve o conflito
O conflito que o CQRS resolve: num modelo único, as otimizações de leitura e de escrita brigam. Para ler rápido, você quer dados desnormalizados (pré-juntados, redundantes) e muitos índices — mas a desnormalização complica a escrita (atualizar dado duplicado em vários lugares), e cada índice desacelera a escrita (todo índice precisa ser atualizado a cada gravação, Cap 12). Para escrever rápido e consistente, você quer dados normalizados (cada fato num lugar) e poucos índices — mas isso torna a leitura mais lenta (joins, sem pré-computação). Um modelo único força um meio-termo que não é ótimo para nenhum dos dois. O CQRS desfaz o conflito: o modelo de escrita pode ser normalizado, com poucos índices, focado em consistência e lógica de negócio; o modelo de leitura pode ser desnormalizado, pré-computado, cheio de índices, focado em velocidade — porque são modelos diferentes, e a otimização de um não prejudica o outro. Isso é especialmente valioso quando há forte assimetria (Cap 2): num sistema read-heavy (leituras muito mais que escritas, a maioria), você quer escalar e otimizar as leituras agressivamente sem que isso pese sobre as escritas — o CQRS permite hospedar os read models em bancos altamente escaláveis e replicados (até NoSQL globalmente distribuído), enquanto o write model fica num banco ACID seguro. Cada lado escala independentemente, conforme sua própria carga. A separação rende quando os perfis de leitura e escrita divergem o suficiente para que o compromisso do modelo único doa.

18.4 A consistência eventual do read model

A separação tem um custo inescapável: quando o modelo de leitura é separado do de escrita, e as mudanças propagam de forma assíncrona, o read model fica atrás do write model por um intervalo — o sistema é eventualmente consistente (Cap 6). Ignorar isso causa bugs sutis.

"Escrevi e não vejo": o read-your-writes no CQRS
O bug mais comum do CQRS é a violação do read-your-writes (Cap 6): o usuário executa um comando (cria um pedido, edita um perfil), o write model o aceita, mas quando a UI recarrega lendo do read model, a mudança ainda não está lá — porque a propagação write→read leva um instante. O usuário vê seus próprios dados "sem a mudança que acabou de fazer", o que é confuso e parece um bug. Esse é o preço da separação, e há estratégias conhecidas para lidar com ele (sem eliminá-lo, porque a consistência eventual é inerente): UI otimista — o cliente atualiza a tela imediatamente com a mudança que ele mesmo fez (ele sabe o que fez), e reconcilia quando o read model alcança; polling por versão — após o comando, o cliente recebe a versão resultante e consulta o read model até ele alcançar essa versão; read-your-writes direcionado — por um curto intervalo após um comando, rotear as leituras daquele usuário para o write model (que está atualizado), ganhando a garantia onde ela mais importa (o próprio usuário vendo a própria mudança); ou um token de consistência causal que faz o read model esperar o evento relevante. A lição: a consistência eventual entre read e write models é inerente ao CQRS, e você precisa projetar a UX e o roteamento para acomodá-la — tipicamente garantindo o read-your-writes (o usuário vê a própria ação) enquanto aceita o pequeno lag para as visões dos outros. Adotar CQRS sem planejar para esse lag é garantir reclamações de "salvei e sumiu".

18.5 Event Sourcing — eventos como a verdade

O Event Sourcing é o "log como fonte da verdade" do Cap 14, aplicado ao estado da aplicação: em vez de guardar o estado atual (e sobrescrevê-lo a cada mudança), você guarda a sequência de eventos (mudanças imutáveis) que produziram esse estado, e deriva o estado atual reproduzindo os eventos.

estado vs eventos
TRADICIONAL (state-based): guarda o estado atual, sobrescreve
  conta_123: { saldo: 120 }
  → ao mudar, SOBRESCREVE. O histórico se perde.
  → você sabe que o saldo é 120, mas não COMO chegou lá.

EVENT SOURCING: guarda os eventos, deriva o estado
  conta_123 eventos (append-only, imutáveis):
    1. ContaCriada
    2. Depositado(100)
    3. Sacado(30)
    4. Depositado(50)
  → estado = aplicar os eventos = saldo 120 (derivado)
  → você tem o HISTÓRICO COMPLETO. Sabe cada mudança, quando
    e por quê. O estado é uma projeção; os EVENTOS são a verdade.
A escrita é trivial (append); a leitura do estado é que custa
O Event Sourcing inverte a economia da escrita e da leitura, e entender essa inversão é entender seus trade-offs. Escrever é trivial e rápido: você só adiciona um evento ao log (append-only, a operação mais rápida que existe, Cap 14) — não há "update no lugar", não há contenção de escrita sobre o estado. Mas ler o estado atual é que fica caro: para saber o estado de uma entidade, você precisa reproduzir (replay) todos os seus eventos desde o início e "dobrá-los" no estado (a dualidade do Cap 14). Para uma entidade com centenas de eventos, isso é lento. As curas são as do Cap 14: snapshots (guardar periodicamente o estado calculado, e reproduzir só os eventos desde o último snapshot, em vez de desde o começo dos tempos) e — aqui o casamento com o CQRS — manter projeções (read models) que aplicam os eventos conforme eles chegam, mantendo o estado atual sempre pronto para leitura. É por isso que Event Sourcing e CQRS andam juntos: o Event Sourcing torna a escrita um append simples mas a leitura do estado cara, e o CQRS resolve a leitura com projeções derivadas dos eventos. O log de eventos é o lado de escrita (a verdade); as projeções são o lado de leitura (as views). Mas note: você pode ter Event Sourcing pensando nessa inversão mesmo sem CQRS completo — eles são complementares, não inseparáveis.

18.6 O que o Event Sourcing dá e cobra

O Event Sourcing entrega capacidades que o modelo state-based simplesmente não tem — e cobra uma complexidade real por elas. Vale ver os dois lados com honestidade.

O que o Event Sourcing dá

Auditoria completa: cada mudança é um evento imutável — você sabe tudo que aconteceu, quando e por quê (crucial para finanças, compliance). Temporal queries: reconstrua o estado em qualquer ponto do passado. Replay/rebuild: corrija um bug e reprocesse os eventos; reconstrua um read model corrompido relendo o log. Novas views: derive visões novas a qualquer momento dos eventos passados. Debugging: a história exata de como o sistema chegou a um estado.

O que o Event Sourcing cobra

Complexidade: pensar em eventos, projeções, replay — mais peças, mais conceitos. Ler o estado é caro: exige replay ou projeções (snapshots ajudam). Schema de eventos é mão única: eventos são imutáveis e o passado não muda — evoluir o esquema dos eventos é delicado (versionar, nunca alterar). Maturidade operacional: exige observabilidade, DLQ para projeções, lidar com eventos fora de ordem e duplicados.

Os eventos são contratos imutáveis — o esquema é uma decisão de mão única
Um custo do Event Sourcing merece destaque porque pega os times de surpresa: os eventos são imutáveis e permanentes, então o esquema deles é uma decisão de mão única (Cap 1), e mais rígida que a maioria. Num sistema state-based, se você quer mudar a forma dos dados, você migra a tabela e segue. No Event Sourcing, os eventos já gravados não podem ser alterados (o passado é imutável — é a base da auditoria e do replay), então você não pode simplesmente "mudar o esquema dos eventos". Você precisa versionar: criar novos tipos de evento ou novas versões, e fazer o código de replay lidar com todas as versões históricas para sempre (porque eventos antigos, naquela forma antiga, vão ser reproduzidos eternamente). Um evento mal modelado no início é uma dívida que você carrega pela vida do sistema. Por isso, modelar os eventos exige cuidado especial: eles devem capturar a intenção de negócio ("PedidoEnviado", "PagamentoRecebido") e não detalhes de implementação ("TabelaPedidoAtualizada" — que não significa nada e envelhece mal), e devem ser pensados como o contrato durável que são. A regra: nunca modifique eventos publicados; só adicione novos tipos ou versione. Essa imutabilidade é a fonte do poder do Event Sourcing (auditoria, replay) e da sua rigidez (o esquema é para sempre) — as duas faces da mesma moeda.

18.7 Os dois juntos — e por que são separados

CQRS e Event Sourcing são mencionados quase sempre juntos, e combinam-se lindamente — mas são padrões distintos, e confundi-los (achar que um exige o outro) leva a adotar complexidade desnecessária.

Complementares, não inseparáveis — você pode ter um sem o outro
A relação correta: CQRS e Event Sourcing são padrões separados que se complementam. Você pode usar cada um sozinho. CQRS sem Event Sourcing: modelos de leitura e escrita separados, ambos com bancos tradicionais state-based, propagando mudanças por eventos simples ou replicação — você ganha a separação read/write sem a complexidade do event sourcing. Esta é uma combinação comum e mais leve. Event Sourcing sem CQRS completo: guardar eventos como verdade, derivando o estado quando preciso, mesmo sem modelos de leitura totalmente separados (embora, como vimos em 18.6, ler o estado direto dos eventos seja caro, o que naturalmente empurra para projeções — daí a afinidade). Mas juntos eles formam uma combinação natural e poderosa: o log de eventos do Event Sourcing é o lado de escrita (a verdade), e as projeções derivadas dele são os read models do CQRS. O Event Sourcing produz os eventos; o CQRS os consome em views otimizadas. É um casamento elegante para domínios complexos com auditoria crítica. A lição: não os confunda como uma coisa só ("CQRS/ES"), porque isso leva a achar que adotar um obriga o outro, importando o dobro da complexidade. Avalie cada um pelo que ele resolve: precisa separar read/write por assimetria/complexidade? CQRS (talvez sem ES). Precisa de auditoria/replay/history? Event Sourcing. Precisa dos dois? Combine-os. Mas decida cada um pelo seu mérito, não por associação.

18.8 Quando (não) usar — a parte mais importante

Esta seção é o coração do capítulo, porque o erro dominante com CQRS e Event Sourcing não é implementá-los mal — é usá-los onde não se precisa. A complexidade que eles adicionam é real e raramente justificada.

A maioria dos sistemas é melhor como CRUD simples — não use por reflexo
A regra que mais economiza sofrimento: a maioria dos sistemas (e das partes de um sistema) é melhor servida por CRUD tradicional — um modelo só, estado atual — e não deveria usar CQRS nem Event Sourcing. Esses padrões adicionam complexidade real (modelos separados, consistência eventual, projeções, replay, eventos imutáveis, maturidade operacional), e essa complexidade só se justifica em problemas específicos. Não use quando: o sistema é um CRUD direto (read e write models seriam quase idênticos — a separação só adiciona custo sem benefício); o domínio é simples o bastante para um modelo único ser fácil de raciocinar; o time não tem experiência operando sistemas eventualmente consistentes; ou você não tem a maturidade operacional (monitoramento, tracing, DLQ) que esses padrões exigem — porque os bugs de consistência eventual são invisíveis sem observabilidade. Considere usar (seletivamente!) quando: há assimetria forte entre leitura e escrita que um modelo único serve mal (CQRS); o domínio é complexo com muitas transições de estado (CQRS, e o Event Sourcing ajuda a modelá-lo); você precisa de auditoria completa, temporal queries, ou replay (Event Sourcing — finanças, compliance); ou há múltiplos consumidores com padrões de query muito diferentes (CQRS com várias projeções). E mesmo então, a regra de ouro: nunca aplique ao sistema inteiro — só aos bounded contexts específicos que se beneficiam, mantendo o resto como CRUD simples. O erro mais comum é tratar CQRS/ES como uma arquitetura top-level para tudo; o correto é aplicá-los cirurgicamente à parte (o módulo de pagamentos, o ledger contábil) onde o benefício é real, e deixar o cadastro de usuários, as configurações, e o resto como o CRUD simples que eles deveriam ser. Comece simples; adicione esses padrões só quando uma dor medida os justifique, e só onde ela existe.

18.9 Estudo de caso — o ledger que pedia Event Sourcing (e o cadastro que não)

Aplicar o padrão onde ele cabe — e não onde não cabe
O cenário

Uma fintech construía sua plataforma e debatia a arquitetura. Um arquiteto, empolgado, propôs Event Sourcing + CQRS para o sistema inteiro — "é moderno, dá auditoria, escala". A decisão certa foi mais sutil: aplicar os padrões onde eles resolviam um problema real (o ledger financeiro) e não onde só adicionariam complexidade (o cadastro de usuários, as configurações). O caso mostra a aplicação seletiva.

Passo 1 · O ledger financeiro — pede Event Sourcing
onde o padrão cabe perfeitamente
O LEDGER (registro de transações financeiras):
  - AUDITORIA é requisito legal: cada centavo movimentado
    precisa de um rastro imutável de o quê, quando, por quê
  - TEMPORAL QUERIES: "qual era o saldo em 31/dez?" (fiscal)
  - é INERENTEMENTE um log de eventos (depósitos, saques,
    transferências) — a contabilidade SEMPRE foi event sourcing!
  - REPLAY: poder reprocessar para corrigir/reconciliar

→ Event Sourcing é a escolha NATURAL aqui. Os eventos
  (transações) JÁ SÃO a verdade; o saldo é derivado. O
  padrão se encaixa no domínio como uma luva.
  E CQRS: o saldo atual (read model) é uma projeção dos
  eventos, separada do append de transações (write model).
Passo 2 · O cadastro de usuários — NÃO pede
onde o padrão só atrapalharia
O CADASTRO DE USUÁRIOS (nome, email, foto, preferências):
  - é um CRUD direto: criar, ler, atualizar o perfil
  - read e write models seriam quase IDÊNTICOS
  - não há assimetria forte nem domínio complexo
  - auditoria pesada do histórico de mudanças de foto? não
  - "qual era o nome do usuário em março"? ninguém precisa

→ Event Sourcing aqui seria COMPLEXIDADE PURA: guardar
  "EmailAlterado", "FotoAlterada" como eventos imutáveis,
  derivar o perfil por replay, lidar com schema de eventos...
  tudo para um CRUD que um UPDATE resolve. NENHUM benefício.

→ Decisão: CRUD tradicional simples. Um modelo, estado atual.
Passo 3 · A aplicação seletiva

A arquitetura final foi mista, por bounded context: o ledger financeiro usou Event Sourcing + CQRS (onde auditoria, temporal queries e a natureza event-driven do domínio justificavam plenamente); o cadastro de usuários, as configurações e a maioria dos módulos usaram CRUD tradicional simples (onde a complexidade não se pagaria). Cada parte recebeu a abordagem que seu problema pedia — não uma arquitetura única imposta sobre tudo.

Passo 4 · O resultado

O ledger ganhou a auditoria imutável que a regulação exigia, as temporal queries para relatórios fiscais, e o replay para reconciliação — tudo natural ao Event Sourcing, porque o domínio financeiro é um log de eventos. E o resto do sistema permaneceu simples, rápido de desenvolver e fácil de raciocinar, sem a sobrecarga de eventos imutáveis e consistência eventual onde elas não traziam valor. Aplicar o padrão certo a cada parte — em vez de o mesmo padrão a tudo — deu o melhor dos dois: o poder do Event Sourcing onde ele importa, a simplicidade do CRUD onde ela basta.

Resultado: a decisão sábia não foi "usar Event Sourcing" nem "não usar" — foi aplicá-lo seletivamente, ao bounded context (o ledger) onde ele resolvia problemas reais (auditoria legal, temporal queries, e o fato de o domínio financeiro ser intrinsecamente um log de eventos), e não aplicá-lo onde só adicionaria complexidade (o cadastro CRUD, onde read e write são quase idênticos e ninguém precisa do histórico). A lição central do capítulo: CQRS e Event Sourcing são ferramentas afiadas para problemas específicos, não arquiteturas top-level para tudo. O erro dominante é aplicá-los ao sistema inteiro por serem "modernos"; o acerto é reconhecer os poucos bounded contexts que genuinamente se beneficiam (assimetria forte, domínio complexo, auditoria crítica) e aplicá-los só ali, mantendo o resto como o CRUD simples que a maioria dos sistemas deveria ser. Comece simples, e adicione esses padrões cirurgicamente, onde a dor real os justifica. Próximo capítulo: rate limiting e backpressure — como um sistema se protege de ser sobrecarregado, controlando quanto trabalho aceita, para degradar com graça em vez de colapsar sob excesso. A disciplina de dizer "não" no momento certo.

18.10 Erros comuns

Erro 1 · Aplicar CQRS/ES ao sistema inteiro

Usar CQRS e Event Sourcing como arquitetura top-level para tudo — herdando complexidade enorme em partes (cadastros, configs) que seriam CRUD simples. A regra de ouro: nunca ao sistema inteiro; só aos bounded contexts específicos que se beneficiam (auditoria, assimetria, domínio complexo), mantendo o resto simples.

Erro 2 · Adotar por ser "moderno", sem dor real

Escolher CQRS/ES porque são impressionantes ou estão em alta, sem um problema que os justifique — pagando a complexidade por nenhum benefício. A maioria dos sistemas é melhor como CRUD. Adote-os só quando uma dor medida (assimetria forte, auditoria crítica, domínio complexo) os exige.

Erro 3 · Confundir CQRS com Event Sourcing

Achar que um exige o outro ("CQRS/ES" como uma coisa só) — e importar o dobro da complexidade. São padrões separados que se complementam: dá para ter CQRS sem ES (modelos separados, bancos tradicionais) e ES sem CQRS completo. Avalie cada um pelo seu mérito.

Erro 4 · Ignorar a consistência eventual do read model

Esquecer que o read model fica atrás do write model — e o usuário não ver a mudança que acabou de fazer (read-your-writes quebrado). Projete a UX para o lag: UI otimista, polling por versão, ou rotear as leituras do próprio usuário ao write model por um intervalo. A consistência eventual é inerente ao CQRS; acomode-a.

Erro 5 · Modelar eventos mal (e tê-los para sempre)

Criar eventos focados em implementação ("TabelaAtualizada") ou mal pensados — e carregá-los para sempre, já que eventos são imutáveis e o esquema é mão única. Modele eventos pela intenção de negócio ("PedidoEnviado"), com cuidado especial, porque você nunca poderá alterá-los, só versionar.

Erro 6 · Event Sourcing sem snapshots nem projeções

Guardar eventos como verdade mas ler o estado reproduzindo todos os eventos desde o início a cada vez — lento e insustentável conforme o histórico cresce. Use snapshots (estado periódico, replay só desde o último) e projeções/read models (estado sempre pronto). Ler o estado no ES exige essas curas.

Erro 7 · CQRS/ES sem maturidade operacional

Adotar esses padrões sem observabilidade (tracing, monitoramento), DLQ para projeções que falham, e tratamento de eventos fora de ordem/duplicados — e ficar com bugs de consistência eventual invisíveis e projeções travadas. Esses padrões exigem maturidade operacional; sem ela, os problemas são invisíveis até explodirem.

Verifique seu entendimento
Um arquiteto propõe usar Event Sourcing + CQRS para toda a plataforma de uma fintech, incluindo o ledger financeiro e o cadastro de usuários (nome, email, foto). Qual é a avaliação mais sólida?

18.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · CRUD, CQRS ou Event Sourcing?

Para cada caso, decida se CRUD simples basta, se CQRS ajudaria, ou se Event Sourcing se justifica, e por quê: (a) um cadastro de produtos com nome, preço e descrição; (b) um ledger de transações financeiras com auditoria legal; (c) um feed social com escrita moderada mas leitura massiva e queries complexas; (d) uma tabela de configurações que muda raramente.

(a) Cadastro de produtos → CRUD simples. Read e write models seriam quase idênticos, domínio simples, sem assimetria forte nem necessidade de auditoria/history. Um modelo único, estado atual, CRUD. CQRS ou ES aqui seriam complexidade pura. A escolha mais simples é a certa.

(b) Ledger financeiro com auditoria legal → Event Sourcing (+ CQRS). Caso ideal: auditoria é requisito legal (eventos imutáveis), temporal queries (saldo em datas passadas para fiscal), e o domínio é intrinsecamente um log de eventos (transações). O ES se encaixa naturalmente; CQRS separa o saldo (projeção) das transações (append). Aqui a complexidade se paga plenamente.

(c) Feed social, leitura massiva + queries complexas → CQRS (talvez sem ES). Assimetria forte (leitura ≫ escrita) e queries complexas que um modelo único serviria mal. CQRS ajuda: um write model para postar, read models desnormalizados otimizados para servir o feed rápido e em escala. Event Sourcing não é obrigatório aqui (a menos que você queira o histórico/replay), mas CQRS resolve a assimetria. (Veremos o feed a fundo no Cap 21.)

(d) Configurações que mudam raramente → CRUD simples (o mais simples possível). Baixíssima escrita, baixa leitura, domínio trivial. Um CRUD simples, talvez com cache. CQRS ou ES seriam absurdamente desproporcionais. Quanto mais simples o caso, mais claramente o CRUD vence.

O insight: a maioria (a, d) é CRUD simples — o default certo. CQRS entra com assimetria forte ou queries complexas (c). Event Sourcing entra com auditoria/temporal/replay e domínios que são logs de eventos (b). A pergunta: há uma dor específica (assimetria, complexidade, auditoria) que justifique a complexidade extra? Se não, CRUD.

Médio
Exercício 2 · O bug do read-your-writes

Você implementou CQRS: comandos vão ao write model, e as leituras vêm de um read model atualizado assincronamente. Usuários reclamam que, após editar o perfil, a mudança "some" por alguns segundos ao recarregar a página. (a) Por que isso acontece? (b) Por que não é um bug a "consertar" no sentido tradicional? (c) Quais estratégias resolvem a experiência?

(a) Por que acontece: no CQRS, o write model e o read model são separados, e a mudança propaga do write para o read assincronamente — leva um intervalo. Quando o usuário edita (comando vai ao write model, aceito) e imediatamente recarrega (leitura vem do read model), o read model ainda não recebeu a atualização. Então ele serve a versão antiga, e o usuário vê seus próprios dados sem a mudança que acabou de fazer. É a violação do read-your-writes (Cap 6), inerente à consistência eventual entre os dois modelos.

(b) Por que não é um bug tradicional: não há um erro de código a corrigir — o sistema está funcionando como projetado. A separação read/write e a propagação assíncrona são o CQRS, e a consistência eventual é uma consequência inerente, não um defeito. Você não pode "consertar" eliminando o lag sem eliminar a própria separação (o que descaracterizaria o CQRS). O que você pode fazer é gerenciar a experiência para que o lag não incomode. É uma propriedade a acomodar, não um bug a eliminar.

(c) Estratégias que resolvem a experiência:

  • UI otimista: o cliente atualiza a tela imediatamente com a mudança que o próprio usuário fez (ele sabe o que enviou), sem esperar o read model. Reconcilia quando o read model alcança. O usuário vê a mudança na hora; o lag fica invisível.
  • Read-your-writes direcionado: por um curto intervalo após um comando, rotear as leituras daquele usuário para o write model (que está atualizado). O usuário vê a própria mudança; os outros toleram o lag. Garante a consistência onde mais importa (você vendo o que você fez).
  • Polling por versão: após o comando, o cliente recebe a versão resultante e consulta o read model até ele alcançar essa versão antes de exibir. Garante que a leitura reflete a escrita, ao custo de uma pequena espera.

O insight: a consistência eventual do CQRS não se "conserta" — se acomoda. O alvo prático é garantir o read-your-writes (o usuário vê a própria ação, via UI otimista ou roteamento ao write model) enquanto se aceita o pequeno lag para as visões dos outros. Projetar a UX para o lag é parte de adotar CQRS, não um afterthought.

Médio
Exercício 3 · O custo da imutabilidade dos eventos

Num sistema com Event Sourcing, seis meses após o lançamento, você percebe que um tipo de evento foi mal modelado (faltou um campo importante, e o nome não reflete a intenção real). (a) Por que você não pode simplesmente "corrigir" os eventos já gravados? (b) Como lidar com a necessidade de mudança? (c) Que lição isso ensina sobre modelar eventos?

(a) Por que não pode corrigir os eventos gravados: no Event Sourcing, os eventos são imutáveis por princípio — essa imutabilidade é a base da auditoria (o histórico é confiável porque nunca muda) e do replay (você reproduz exatamente o que aconteceu). Se você pudesse alterar eventos passados, perderia ambos: a auditoria deixaria de ser confiável, e o replay produziria um histórico falsificado. Além disso, os eventos antigos já foram processados por projeções e consumidores; alterá-los retroativamente quebraria a consistência de tudo que derivou deles. Então a regra é absoluta: nunca modifique nem delete eventos já gravados em produção.

(b) Como lidar com a mudança:

  • Versionar o evento: criar uma nova versão do tipo de evento (ex.: PedidoEnviadoV2 com o campo que faltava), e usá-la para os eventos novos dali em diante. Os eventos antigos (V1) permanecem como estão.
  • Lidar com ambas as versões no replay: o código que reproduz os eventos precisa entender tanto a V1 quanto a V2 — para a V1, talvez inferir ou usar um default para o campo que faltava; para a V2, usar o campo. Esse código de compatibilidade vive para sempre, porque os eventos V1 antigos serão reproduzidos eternamente.
  • Eventos corretivos (se necessário): para "corrigir" um efeito de um evento mal gravado, emite-se um novo evento que compensa (como na Saga, Cap 17) — não se apaga o original.

(c) A lição sobre modelar eventos: os eventos são contratos imutáveis e permanentes — uma decisão de mão única (Cap 1) mais rígida que a maioria, porque você os carrega para sempre e nunca pode alterá-los. Isso significa que modelar eventos exige cuidado especial desde o início: capturar a intenção de negócio (não detalhes de implementação), incluir os campos que importam, e nomeá-los pensando que serão lidos por anos. Um evento mal modelado é uma dívida vitalícia (código de compatibilidade para sempre). A lição mais ampla: o poder do Event Sourcing (auditoria, replay) vem da imutabilidade, e a imutabilidade tem como contrapartida a rigidez — então o esquema dos eventos merece mais investimento de design antecipado do que um esquema de banco mutável, porque você não terá a saída fácil da migração.

Difícil
Exercício 4 · Decidir a arquitetura de dados de um sistema — entrevista

Você projeta uma plataforma de saúde com vários módulos: prontuários médicos (com auditoria legal rigorosa), agendamento de consultas (leitura pesada, queries complexas de disponibilidade), e um cadastro de pacientes (CRUD direto). O entrevistador pede: "como você decidiria a arquitetura de dados de cada módulo?". Demonstre que você aplica CQRS e Event Sourcing seletivamente, pelo mérito de cada contexto.

Enquadramento: a resposta forte rejeita uma arquitetura única para tudo e decide cada módulo pelo seu perfil — aplicando CQRS e Event Sourcing cirurgicamente onde se justificam, e CRUD simples onde basta. Vou por módulo.

1. Prontuários médicos → Event Sourcing (+ CQRS). Auditoria legal rigorosa é o requisito que define este módulo — em saúde, quem acessou/alterou o quê e quando é crítico (legal e ético). Isso é exatamente o que o Event Sourcing dá nativamente: cada mudança no prontuário é um evento imutável, formando um histórico completo e inviolável. Também há valor em temporal queries ("qual era o diagnóstico em tal data?") e em nunca perder informação clínica. CQRS complementa: o histórico de eventos é a verdade (write), e projeções servem as visões que os médicos leem (read). Aqui a complexidade se paga plenamente — o domínio pede auditoria imutável, e o ES a entrega.

2. Agendamento de consultas → CQRS (provavelmente sem Event Sourcing completo). O perfil é assimetria forte (muita leitura de disponibilidade, escrita moderada de agendamentos) e queries complexas ("horários livres do Dr. X na próxima semana", "consultas disponíveis perto de mim"). CQRS ajuda: um write model para agendar (com a lógica de não dar o mesmo horário a dois — recurso escasso, consistência forte) e read models desnormalizados, otimizados para as queries de disponibilidade, escalados para a leitura pesada. Event Sourcing não é obrigatório (a menos que se queira auditar o histórico de agendamentos, o que pode ter algum valor) — CQRS resolve a assimetria e as queries. Avalio ES como opcional aqui, CQRS como útil.

3. Cadastro de pacientes → CRUD simples. Nome, contato, dados demográficos — um CRUD direto. Read e write models seriam quase idênticos, sem assimetria forte nem queries complexas. (Atenção: dados de saúde têm requisitos de privacidade/segurança fortes — mas isso é ortogonal à escolha CQRS/ES; resolve-se com criptografia e controle de acesso, não com event sourcing.) A não ser que haja requisito de auditar mudanças cadastrais (possível em saúde — então um log de auditoria mais leve pode bastar, sem ES completo), um CRUD simples com um audit log é a escolha. Não imponho a complexidade do ES onde um CRUD serve.

4. O princípio transversal: arquitetura mista, por bounded context. Cada módulo recebe a abordagem que seu perfil pede — ES onde a auditoria imutável é requisito (prontuários), CQRS onde a assimetria/complexidade de query justifica (agendamento), CRUD onde basta (cadastro). Nunca uma arquitetura única imposta sobre tudo. E começo do mais simples, adicionando complexidade só onde a dor (auditoria legal, assimetria medida) a exige.

O que um bom candidato demonstra: que decide por contexto, não impõe uma arquitetura única; que reconhece o prontuário como caso ideal de ES (auditoria legal); que vê o agendamento como caso de CQRS (assimetria, queries); que mantém o cadastro como CRUD; que separa preocupações ortogonais (privacidade ≠ event sourcing); e que aplica os padrões seletivamente. Frase de fechamento: "eu não escolho uma arquitetura para a plataforma — escolho por módulo. Os prontuários pedem Event Sourcing, porque auditoria imutável é requisito legal e o histórico clínico não pode se perder — o ES dá isso nativamente. O agendamento pede CQRS pela assimetria de leitura e as queries complexas de disponibilidade, com o write model garantindo que dois pacientes não peguem o mesmo horário. O cadastro é CRUD simples. A regra que me guia: CQRS e Event Sourcing são afiados para problemas específicos — auditoria, assimetria, domínios complexos — e eu os aplico só aos contextos que genuinamente se beneficiam, mantendo o resto simples. A complexidade desses padrões só se paga onde o problema a exige, e impô-la ao sistema inteiro é o erro mais comum."

Fim do capítulo 18
Próximo capítulo: rate limiting e backpressure. Você já viu como estruturar a comunicação, garantir entrega, coordenar transações e organizar o fluxo de dados. Agora, a disciplina de autoproteção: como um sistema controla quanto trabalho aceita, para não ser sobrecarregado até o colapso. Rate limiting (limitar quantas requisições um cliente pode fazer) e backpressure (empurrar de volta quando o sistema não dá conta) são as formas de um sistema dizer "não" no momento certo — degradando com graça em vez de cair sob excesso. A arte de proteger o sistema de si mesmo e dos outros.
Parte IV · Capítulo 19 · Os padrões

Rate limiting e
backpressure.

Todo sistema tem um limite de quanto trabalho consegue fazer (Cap 7). A questão não é se esse limite será atingido, mas o que acontece quando for. Sem proteção, o excesso de carga não é só rejeitado — ele derruba o sistema, levando junto até as requisições que poderiam ser servidas. Rate limiting e backpressure são as disciplinas de autoproteção: a arte de um sistema dizer "não" no momento certo, recusando o excesso de forma controlada para continuar servindo o resto — degradar com graça em vez de colapsar. É a diferença entre um sistema que fica lento sob pico e um que cai inteiro.

Este capítulo trata de como um sistema se protege da sobrecarga. Vamos ver por que limitar é necessário (a sobrecarga não rejeitada colapsa), os algoritmos de rate limiting (token bucket, leaky bucket, fixed e sliding window) e qual usar quando, o desafio do rate limiting distribuído (vários nós que precisam compartilhar a contagem), a resposta correta a um cliente limitado (429 com Retry-After, não um erro qualquer), o backpressure (empurrar a pressão de volta quando o processamento não acompanha), e o load shedding com prioridade (descartar o trabalho menos importante primeiro). Quando você sair daqui, a sobrecarga deixa de ser uma catástrofe inevitável e vira algo que você controla — porque um sistema bem projetado não tenta fazer mais do que pode; ele recusa o excesso de forma deliberada, protegendo sua capacidade de servir.

19.1 A história — das redes às APIs

Contexto histórico

O controle de fluxo nasceu nas redes, muito antes das APIs. Nos anos 1980-90, os protocolos de rede já enfrentavam o problema de um emissor rápido afogando um receptor lento, ou de congestionamento coletivo derrubando a rede inteira. O TCP desenvolveu controle de congestionamento e de fluxo sofisticados; e algoritmos como o token bucket e o leaky bucket foram formalizados para "moldar" o tráfego (traffic shaping) — garantir que os dados fluíssem a uma taxa controlada, suavizando rajadas. Esses algoritmos, criados para pacotes de rede, são os mesmos que hoje limitam requisições de API.

Com a web e, depois, a economia de APIs (anos 2000-2010), o rate limiting migrou da rede para a aplicação. APIs públicas precisavam se proteger de abuso (um cliente fazendo milhões de requisições), de ataques (DDoS, força bruta), e de simplesmente racionar uma capacidade finita entre muitos clientes. Surgiram os limites que todo desenvolvedor conhece: "X requisições por minuto", o status 429 (Too Many Requests), os headers de rate limit. As grandes plataformas — Stripe, GitHub, Cloudflare — construíram sistemas de rate limiting sofisticados e publicaram como, tornando o tema parte do repertório de qualquer engenheiro de sistemas.

Em paralelo, a ideia irmã do backpressure amadureceu, especialmente com o processamento de streams e os sistemas reativos. Se o rate limiting recusa o excesso na entrada, o backpressure é o mecanismo de comunicar, ao longo de um pipeline, "vá mais devagar" quando um estágio não acompanha — uma pressão que se propaga de volta da parte lenta para a fonte rápida (vimos isso no Cap 11, com as filas). Os dois resolvem faces do mesmo problema: o que fazer quando chega mais trabalho do que se consegue processar.

Em 2026, rate limiting e backpressure são práticas maduras e essenciais, com algoritmos bem estabelecidos e implementações de referência públicas. O sliding window counter emergiu como o default equilibrado (usado pela Cloudflare em escala global, com taxa de erro ínfima), o token bucket como a escolha quando rajadas são desejáveis, e arquiteturas de load shedding em camadas (como a da Stripe) como o padrão para proteger a disponibilidade sob estresse. A lição que atravessa décadas: um sistema que aceita todo trabalho que chega vai cair quando chegar demais; um sistema que recusa o excesso de forma controlada sobrevive. Este capítulo é sobre construir a segunda coisa.

19.2 Por que limitar — a sobrecarga não rejeitada colapsa

A justificativa do rate limiting é mais profunda que "evitar abuso". Sem um limite, o que acontece quando chega mais trabalho do que a capacidade não é uma degradação suave — é um colapso, e entender por quê é entender por que limitar não é opcional.

Sem limite, o excesso não é só recusado — ele derruba o que poderia ser servido
A intuição ingênua é que um sistema sem rate limiting, sob excesso de carga, simplesmente fica mais lento ou rejeita o que excede — e o resto continua. A realidade é pior: sem proteção, o excesso de carga derruba o sistema inteiro, incluindo as requisições que poderiam ser atendidas. Por quê? Porque cada requisição aceita consome recursos (memória, threads, conexões, CPU), e quando chegam mais requisições do que a capacidade, elas não esperam educadamente — elas se acumulam, consumindo recursos enquanto esperam, até esgotá-los. Threads ficam todas presas, a memória enche, as filas internas estouram, a latência explode (o joelho da USL, Cap 7), e o sistema entra em colapso — frequentemente parando de servir tudo, inclusive a fração da carga que ele tinha capacidade de atender. Pior ainda, isso desencadeia retries (clientes tentando de novo, Cap 8), que aumentam a carga no exato momento em que o sistema menos aguenta — a "tempestade de retries" que transforma uma sobrecarga temporária num colapso prolongado. O rate limiting quebra esse ciclo: ao recusar o excesso antes que ele consuma recursos (devolvendo um 429 barato em vez de tentar processar), o sistema protege sua capacidade de servir o que pode. A lição: a escolha não é "limitar ou servir todo mundo" — é "limitar e servir alguns, ou não limitar e cair, servindo ninguém". Recusar o excesso de forma controlada é o que preserva a capacidade, não o que a desperdiça.

19.3 Os algoritmos — quatro formas de contar

Há um conjunto canônico de algoritmos de rate limiting, cada um com um perfil diferente de como trata rajadas, quanta memória usa, e quão preciso é. Os quatro principais (mais uma variante):

AlgoritmoComo funcionaPerfil
Fixed windowConta requisições em janelas fixas (ex.: por minuto), zera a cada janelaSimples, mas permite 2x burst na borda da janela
Sliding window logGuarda o timestamp de cada requisição, conta as do último intervalo móvelExato, mas custa memória O(n) por cliente
Sliding window counterAproxima o log com dois contadores ponderados pelo tempoO(1), quase exato, suaviza a borda — o default
Token bucketUm balde de tokens, reabastecido a uma taxa; cada requisição consome um tokenPermite rajadas controladas (acumula tokens)
Leaky bucketUma fila que drena a uma taxa constante; o que transborda é descartadoSaída constante e suave; ideal para proteger downstream
Token bucket modela rajada; leaky bucket força fluxo suave
Os dois algoritmos de "balde" capturam filosofias opostas sobre rajadas, e entender a diferença orienta a escolha. O token bucket modela a capacidade de rajada separada da taxa sustentada: o balde acumula tokens (até um máximo) quando você não usa, então um cliente que ficou quieto pode fazer uma rajada (gastar os tokens acumulados de uma vez) e depois é limitado à taxa de reabastecimento. Isso é desejável quando rajadas são uma feature — APIs de desenvolvedor, SDKs, clientes que naturalmente fazem várias chamadas de uma vez e depois pausam. O leaky bucket, ao contrário, força uma saída constante e suave independentemente da entrada: as requisições entram numa fila que drena a uma taxa fixa, e o que não cabe transborda. Não há acúmulo de rajada — o fluxo de saída é sempre suave. Isso é desejável quando rajadas são uma ameaça — quando você precisa proteger um serviço downstream frágil que não aguenta picos, garantindo que ele receba um fluxo constante. A regra mnemônica: token bucket quando a rajada é uma feature; leaky bucket quando a rajada é uma ameaça. O token bucket pergunta "você tem crédito acumulado para esta rajada?"; o leaky bucket diz "eu só deixo passar a esta taxa, ponto".
O fixed window tem um furo na borda; o sliding window counter o cura
Um detalhe que importa na prática: o fixed window (contar por janelas fixas, ex.: "100 por minuto", zerando a cada minuto) é o mais simples, mas tem um furo explorável na borda. Como o contador zera no início de cada janela, um cliente pode fazer 100 requisições no último segundo de uma janela e mais 100 no primeiro segundo da próxima — 200 requisições em ~2 segundos, o dobro do limite pretendido, porque caíram em janelas diferentes. Para endpoints públicos, isso é uma vulnerabilidade. O sliding window counter cura isso aproximando uma janela móvel: ele combina a contagem da janela atual e da anterior, ponderadas pelo tempo decorrido, suavizando a borda — sem o furo do fixed window, e com memória O(1) (só dois contadores por cliente, contra o O(n) do sliding window log exato, que guarda todos os timestamps). É por isso que o sliding window counter é o default de produção (a Cloudflare o usa em escala global, com taxa de decisão errada medida em 0,003%): ele pareia memória baixa com precisão quase exata e sem o furo de borda. A menos que você tenha uma razão específica para outro (rajada como feature → token bucket; proteger downstream → leaky bucket; exatidão absoluta com tráfego limitado → sliding window log), o sliding window counter é o ponto de partida certo.

19.4 Qual algoritmo, quando

Com os perfis no lugar, a escolha se resume a poucas perguntas sobre o que você prioriza:

Você quer…Use
Um default equilibrado para uma APISliding window counter (O(1), preciso, sem furo de borda)
Permitir rajadas controladas (devs, SDKs)Token bucket (acumula crédito de rajada)
Proteger um downstream frágil com fluxo suaveLeaky bucket (saída constante)
Exatidão absoluta, tráfego por cliente limitadoSliding window log (exato, mas O(n) memória)
O mais simples possível, e o furo de borda não importaFixed window (cuidado com o 2x na borda)
A decomposição: algoritmo, onde mora o estado, e o que responder
Uma forma clara de pensar o rate limiting é decompô-lo em três escolhas independentes. (1) O algoritmo (acima) — como você conta e decide. (2) Onde o estado mora — a contagem por cliente fica em cada instância (rápido, mas N instâncias cada uma permitindo o limite L deixam passar N×L no total — um furo), num armazenamento compartilhado (Redis — correto, mas com latência de coordenação), ou num esquema híbrido (aproximação local + reconciliação global periódica). (3) O que você responde — quando recusa, devolver um 429 com informação útil (próxima seção). Separar essas três decisões evita confundi-las: você pode trocar o algoritmo sem mudar onde o estado mora, e ajustar a resposta independentemente. E a identidade que você limita é outra escolha: por IP, por chave de API, por usuário, por tenant — cada uma serve a um propósito (IP contra abuso anônimo, chave de API para racionar entre clientes, tenant para isolar inquilinos de um SaaS). A maioria dos sistemas de produção combina sliding window counter ou token bucket, com estado em Redis, limitando por chave de API ou usuário — mas a decomposição te deixa escolher cada peça conscientemente.

19.5 Rate limiting distribuído — o problema do estado compartilhado

Aqui está o desafio que torna o rate limiting difícil em escala: se você tem vários servidores atrás de um balanceador (Cap 9), e cada um conta as requisições independentemente, o limite global é violado — e há uma corrida sutil mesmo com estado compartilhado.

N nós, cada um permitindo L, deixam passar N×L — e o TOCTOU à espreita
O primeiro problema do rate limiting distribuído é direto: se você tem N servidores, e cada um aplica o limite L localmente (contando só as requisições que ele mesmo viu), então no total o sistema deixa passar N×L requisições — N vezes o limite pretendido, porque nenhum nó vê o tráfego dos outros. Para um limite global correto, os nós precisam compartilhar a contagem, tipicamente num armazenamento central rápido (Redis). Mas isso introduz o segundo problema, mais sutil: a verificação do rate limit é um read-modify-write (ler a contagem atual, decidir se permite, escrever a nova contagem), e se duas requisições se intercalam entre o ler e o escrever, ambas podem passar por um limite que deveria parar uma — o clássico race condition TOCTOU (time-of-check-to-time-of-use). A cura é a atomicidade: fazer o ler-decidir-escrever atomicamente, o que no Redis se consegue com um script Lua (executado atomicamente no servidor, numa única ida) ou operações atômicas — fechando a janela onde duas requisições escorregam. É o mesmo padrão de atomicidade que vimos na idempotência (Cap 16): a verificação e a atualização precisam ser uma operação indivisível, ou a corrida reabre o furo. E há um trade-off de consistência: estado fortemente consistente (toda decisão consulta o Redis) é preciso mas adiciona latência; estado eventualmente consistente (aproximação local, reconciliação periódica) é mais rápido e resiliente, mas permite pequenas ultrapassagens temporárias. Como sempre (CAP, Cap 5), você escolhe entre precisão e latência/disponibilidade conforme o quão crítico é o limite exato.

19.6 A resposta certa — 429 e Retry-After

Quando você recusa uma requisição, como você recusa importa. Um erro genérico deixa o cliente perdido (e provavelmente fazendo retries que pioram tudo); uma recusa informativa permite que o cliente se comporte bem.

Um 429 sozinho não basta — diga ao cliente quando voltar
A resposta correta a uma requisição limitada é o status HTTP 429 (Too Many Requests) — mas o 429 sozinho não basta. Um cliente que recebe só "429" sabe que foi recusado, mas não sabe quando pode tentar de novo — então ele provavelmente retenta imediatamente, sendo recusado de novo, num loop que desperdiça recursos dos dois lados (e pode parecer um ataque). A resposta deve incluir quando voltar: o header Retry-After (quantos segundos esperar) é o essencial, e idealmente os headers de rate limit padronizados (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) que dizem ao cliente seu limite, quanto resta, e quando reseta — para que ele possa se autorregular antes de bater no muro. Um cliente bem-comportado, vendo "restam 5 requisições, reseta em 30s", desacelera sozinho. A diferença prática é enorme: rate limiting com respostas informativas cria um ecossistema onde os clientes cooperam (recuam quando devem, evitam a tempestade de retries); rate limiting com 429s mudos cria clientes que martelam o muro repetidamente, amplificando a carga que o limite deveria reduzir. A lição: rate limiting não é só recusar — é comunicar a política ao cliente, para que ele coopere. Recusar bem é metade técnica (o algoritmo), metade comunicação (a resposta informativa que permite ao cliente se comportar). E do lado do cliente, isso conecta ao Cap 20: ao receber um 429 com Retry-After, o cliente deve respeitar o tempo (backoff), não retentar imediatamente.

19.7 Backpressure — a pressão de volta

O rate limiting recusa o excesso na entrada. O backpressure é o mecanismo complementar, que atua ao longo do processamento: quando um estágio interno não acompanha, ele comunica "vá mais devagar" de volta para a fonte, em vez de aceitar trabalho que não consegue fazer (vimos isso no Cap 11, com as filas que crescem).

Backpressure: o consumidor lento freia o produtor rápido, em vez de afogar
Enquanto o rate limiting é uma política aplicada na borda (quantas requisições um cliente externo pode fazer), o backpressure é um mecanismo interno, ao longo de um pipeline de processamento. A situação: um produtor gera trabalho mais rápido do que um consumidor consegue processar (Cap 11). Sem backpressure, o trabalho se acumula numa fila que cresce sem limite até estourar (memória, ou latência inútil). O backpressure resolve fazendo o consumidor lento comunicar de volta ao produtor "estou sobrecarregado, desacelere" — e o produtor responde reduzindo o ritmo, ou parando, ou (se for uma cadeia) propagando a pressão ainda mais para trás, até a fonte. É a diferença entre uma mangueira que você fecha na ponta (a pressão sobe e você sente, e fecha a torneira) e uma que despeja num balde que transborda. Em sistemas, o backpressure se manifesta de várias formas: limites de buffer/fila que, ao encher, bloqueiam ou rejeitam o produtor; mecanismos de "pull" onde o consumidor puxa trabalho no seu ritmo (em vez de o produtor empurrar); ou sinais explícitos de "devagar" que se propagam pela cadeia. O princípio unificador: o sistema só deve aceitar o trabalho que consegue processar, e quando não consegue, a informação disso precisa fluir de volta para quem produz, para que ele desacelere — em vez de o trabalho não-processável se acumular até o colapso. Rate limiting protege a borda; backpressure protege o interior; juntos, garantem que nenhuma parte aceite mais do que aguenta.

19.8 Load shedding e prioridade — descartar o certo

Quando o sistema está genuinamente sobrecarregado e precisa recusar trabalho, a pergunta final é: qual trabalho descartar? Descartar aleatoriamente é melhor que cair, mas descartar com prioridade — sacrificando o menos importante primeiro — é muito melhor.

Sob estresse, sacrifique o não-essencial para proteger o crítico
O load shedding (descarte de carga) é a prática de, sob sobrecarga, deliberadamente recusar parte do trabalho para preservar a capacidade de fazer o resto — e a sofisticação está em descartar o trabalho certo. Nem toda requisição tem o mesmo valor: numa plataforma de pagamentos, processar uma cobrança é crítico; servir uma requisição de teste ou um GET de dados não-essenciais é descartável sob estresse. Uma arquitetura de load shedding em camadas (a Stripe publicou a sua) reconhece isso: ela reserva uma fração da capacidade da frota para as operações críticas (ex.: criar cobranças) e, quando o sistema se aproxima da saturação, começa a recusar o tráfego não-crítico (com um 503) para garantir que o crítico ainda seja servido. O princípio: sob estresse, um sistema bem projetado degrada por prioridade — ele sacrifica o não-essencial (relatórios, features secundárias, tráfego de teste, clientes de baixa prioridade) para proteger o essencial (pagamentos, o caminho crítico do negócio), em vez de tratar tudo igual e arriscar perder o crítico junto com o supérfluo. Isso conecta o capítulo ao Cap 15 (separar o crítico do não-crítico) e ao Cap 20 (degradação graciosa): a forma mais madura de lidar com a sobrecarga não é só recusar — é recusar inteligentemente, preservando o que mais importa. Um sistema que, sob pico, continua processando pagamentos enquanto pausa os relatórios está degradando com graça; um que cai inteiro porque tratou um relatório e um pagamento como igualmente importantes, não. A prioridade transforma o "recusar" de uma perda num ato de proteção do que importa.

19.9 Estudo de caso — a API que caía sob pico (e o cliente que martelava)

De colapso sob carga a degradação graciosa
O cenário

Uma API pública começou a cair completamente durante picos de tráfego — não ficava lenta, caía, indisponível por minutos, derrubando inclusive os clientes bem-comportados. A investigação revelou dois problemas que se reforçavam: a ausência de rate limiting (o sistema aceitava todo trabalho até colapsar) e clientes que, ao receber erros, martelavam a API com retries imediatos, amplificando a sobrecarga.

Passo 1 · O colapso e a tempestade de retries
o ciclo de morte
SEM rate limiting, no pico:
  1. Chegam mais requisições do que a capacidade
  2. Sem recusar, o sistema ACEITA todas → elas se acumulam,
     consumindo threads, memória, conexões
  3. Recursos esgotam → latência explode → timeouts
  4. Clientes que recebem timeout RETENTAM IMEDIATAMENTE
  5. Os retries AUMENTAM a carga... no pior momento
  6. → colapso total: o sistema para de servir TUDO,
     inclusive os clientes que se comportavam bem

O excesso não foi só recusado — derrubou tudo. E a tempestade
de retries dos clientes transformou um pico em colapso longo.
Passo 2 · Rate limiting na borda
recusar o excesso ANTES que ele consuma recursos
Adicionado rate limiting com SLIDING WINDOW COUNTER por chave
de API (o default equilibrado), com estado compartilhado no
Redis (atômico via Lua, sem o furo TOCTOU):

  - cada cliente: limite de X req/min
  - excesso -> recusado com um 429 BARATO (não consome os
    recursos que processar consumiria)

Agora o pico de UM cliente abusivo não derruba o sistema --
ele bate no próprio limite, e os recursos ficam livres para
servir os outros. A capacidade é PROTEGIDA, não desperdiçada.
Passo 3 · A resposta informativa que parou o martelo
429 + Retry-After para o cliente cooperar
O 429 passou a vir COM informação:
  HTTP 429 Too Many Requests
  Retry-After: 30
  RateLimit-Remaining: 0
  RateLimit-Reset: 30

Agora o cliente bem-comportado VÊ "espere 30s" e recua, em vez
de martelar. A tempestade de retries (que amplificava a carga)
parou: os clientes passaram a se autorregular.

(E no lado do cliente, retry com backoff -- Cap 20 -- em vez
de retentar imediatamente.)
Passo 4 · Load shedding com prioridade

E uma última camada, para os picos genuínos (não de um cliente, mas de carga real agregada): load shedding com prioridade. Quando a frota se aproximava da saturação, o sistema passou a reservar capacidade para as operações críticas (as que movem o negócio) e a recusar (com 503) o tráfego não-essencial — relatórios pesados, requisições de teste. Assim, mesmo sob pico extremo, o caminho crítico continuava servido, degradando o supérfluo em vez de arriscar tudo.

Resultado: a API parou de cair sob pico — passou a degradar com graça. O rate limiting com sliding window counter (estado atômico no Redis) recusava o excesso barato, antes que ele consumisse recursos, protegendo a capacidade de servir o resto; o 429 informativo (com Retry-After) fez os clientes cooperarem em vez de martelar, dissolvendo a tempestade de retries; e o load shedding com prioridade garantiu que, sob pico real, o crítico sobrevivesse ao custo do supérfluo. A lição central reúne o capítulo: a sobrecarga não rejeitada colapsa (e arrasta junto o que poderia ser servido), então a autoproteção não é opcional — é o que separa um sistema que fica indisponível de um que recusa o excesso de forma controlada e continua de pé. E recusar bem é técnica (o algoritmo certo, atômico, distribuído) e comunicação (a resposta informativa que faz o cliente cooperar) e prioridade (sacrificar o não-essencial pelo crítico). Um sistema maduro não tenta fazer mais do que pode; ele diz "não" no momento certo, da forma certa. Próximo capítulo: resiliência — timeouts, retries e circuit breakers. Você viu como o sistema se protege da sobrecarga; agora, como ele se protege das falhas das suas dependências, para que a queda de uma parte não derrube o todo. As defesas que, somadas a tudo que veio antes, fazem um sistema sobreviver ao mundo real — e que fecham a Parte IV.

19.10 Erros comuns

Erro 1 · Não ter rate limiting

Aceitar todo trabalho que chega, achando que o excesso será só recusado ou ficará lento — quando, sem proteção, a sobrecarga esgota recursos e derruba o sistema inteiro, inclusive o que poderia ser servido. Rate limiting recusa o excesso barato, antes que ele consuma recursos, protegendo a capacidade. Não é opcional para sistemas sob carga real.

Erro 2 · Fixed window em endpoint público

Usar fixed window onde o furo de borda importa — permitindo 2x o limite em ~2 segundos na virada da janela (100 no fim de uma, 100 no início da próxima). Para endpoints públicos, use sliding window counter, que suaviza a borda com O(1) de memória. O fixed window só serve onde o furo de borda é tolerável.

Erro 3 · Rate limiting local em múltiplos nós

Cada um dos N servidores aplicando o limite L localmente — deixando passar N×L no total, N vezes o pretendido. Para um limite global, compartilhe o estado (Redis), atomicamente (Lua, contra o TOCTOU). Sem estado compartilhado, o limite distribuído é furado.

Erro 4 · A corrida TOCTOU na checagem

Fazer o ler-decidir-escrever da contagem em passos separados — deixando duas requisições concorrentes passarem por um limite que deveria parar uma. A verificação e a atualização devem ser atômicas (script Lua no Redis, ou operação atômica). Mesma lição da idempotência (Cap 16): a corrida reabre o furo.

Erro 5 · Recusar com um 429 mudo

Devolver só "429" sem dizer quando voltar — e o cliente retenta imediatamente, é recusado de novo, num loop que amplifica a carga. Inclua Retry-After e os headers de rate limit, para o cliente se autorregular e cooperar. Recusar é também comunicar a política, para o cliente recuar em vez de martelar.

Erro 6 · Tratar a fila como infinita (sem backpressure)

Deixar o produtor despejar numa fila que cresce sem limite quando o consumidor não acompanha — até estourar memória ou tornar as mensagens inúteis pelo atraso. O backpressure comunica "desacelere" de volta ao produtor. O sistema só deve aceitar o trabalho que consegue processar; quando não consegue, a pressão precisa fluir de volta.

Erro 7 · Descartar carga sem prioridade

Sob sobrecarga, recusar trabalho aleatoriamente — arriscando descartar o crítico (um pagamento) junto com o supérfluo (um relatório). Faça load shedding com prioridade: reserve capacidade para o crítico e sacrifique o não-essencial primeiro. Degradar por prioridade protege o que importa; tratar tudo igual arrisca perder o essencial.

Verifique seu entendimento
Sua API pública cai completamente durante picos de tráfego, derrubando inclusive clientes bem-comportados, e você nota que clientes que recebem erros retentam imediatamente, piorando tudo. Qual combinação de medidas resolve melhor?

19.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Qual algoritmo?

Para cada caso, escolha o algoritmo de rate limiting mais apropriado e justifique: (a) uma API de desenvolvedor onde clientes naturalmente fazem rajadas de chamadas e depois pausam; (b) proteger um serviço de banco de dados frágil que não aguenta picos, garantindo um fluxo constante; (c) um endpoint público que precisa de um limite preciso sem o furo de borda; (d) um endpoint interno simples onde o furo de borda não importa e você quer o mínimo de complexidade.

(a) API de devs com rajadas naturais → token bucket. A rajada é uma feature aqui (clientes fazem várias chamadas de uma vez e pausam). O token bucket acumula tokens quando o cliente está quieto, permitindo a rajada (gastar os acumulados) e depois limitando à taxa de reabastecimento. Perfeito para tráfego naturalmente em rajadas.

(b) Proteger downstream frágil com fluxo constante → leaky bucket. Aqui a rajada é uma ameaça (o downstream não aguenta picos). O leaky bucket força uma saída constante e suave independentemente da entrada — as requisições entram numa fila que drena a taxa fixa, protegendo o serviço frágil de qualquer rajada. É o caso clássico do leaky bucket.

(c) Endpoint público, limite preciso sem furo de borda → sliding window counter. O fixed window teria o furo de 2x na borda (explorável num endpoint público); o sliding window log seria exato mas caro (O(n)). O sliding window counter equilibra: O(1) de memória, quase exato, e sem o furo de borda — o default certo para endpoints públicos.

(d) Endpoint interno simples, furo de borda tolerável → fixed window. Se é interno (não explorável por adversários) e o furo de borda não importa, o fixed window é o mais simples de implementar e o mais barato. Quando a simplicidade vale mais que a precisão de borda, e não há adversário para explorar o furo, ele serve.

O insight: a escolha segue o que você prioriza — rajada como feature (token bucket), proteção de downstream com fluxo suave (leaky bucket), precisão sem furo de borda (sliding window counter, o default), ou simplicidade máxima onde o furo não importa (fixed window). Token bucket quando a rajada é amiga, leaky bucket quando é inimiga, sliding window counter quando você quer o equilíbrio.

Médio
Exercício 2 · O rate limiting distribuído quebrado

Uma equipe implementou rate limiting de "100 req/min por usuário", mas os usuários conseguem fazer muito mais que isso. A API roda em 5 instâncias atrás de um balanceador, e cada instância conta as requisições que ela mesma vê, em memória local. (a) Por que o limite é violado? (b) Como corrigir? (c) Que problema sutil surge mesmo com a correção, e como resolvê-lo?

(a) Por que o limite é violado: cada uma das 5 instâncias conta localmente, vendo só as requisições que o balanceador roteou para ela. Como nenhuma instância vê o tráfego das outras, cada uma permite até 100 req/min — então no total, o usuário pode fazer até 5 × 100 = 500 req/min (se suas requisições se espalham pelas 5 instâncias), 5 vezes o limite pretendido. É o problema N×L: N instâncias, cada uma aplicando o limite L, deixam passar N×L globalmente.

(b) Como corrigir: o estado da contagem precisa ser compartilhado entre as instâncias, num armazenamento central rápido — tipicamente o Redis. Todas as 5 instâncias consultam e atualizam a mesma contagem por usuário no Redis, então o limite de 100 é aplicado globalmente, não por instância. Agora as requisições do usuário, não importa em qual instância caiam, decrementam o mesmo contador compartilhado.

(c) O problema sutil mesmo com a correção — TOCTOU: a verificação do rate limit é um read-modify-write (ler a contagem, decidir se permite, escrever a nova contagem). Se duas requisições do mesmo usuário chegam concorrentemente (a instâncias diferentes), ambas podem ler a contagem (digamos, 99) antes de qualquer uma escrever, ambas decidirem "99 < 100, permitido", e ambas escreverem 100 — deixando passar duas requisições quando só uma deveria. É o race condition TOCTOU (time-of-check-to-time-of-use). Como resolver: tornar o ler-decidir-escrever atômico. No Redis, isso se faz com um script Lua (executado atomicamente no servidor, numa única ida, sem janela para outra requisição se intrometer) ou operações atômicas (INCR com checagem). A atomicidade fecha a janela da corrida — só uma das requisições concorrentes consegue a decisão, e a contagem fica correta.

O insight: rate limiting distribuído tem dois problemas em camadas — o estado precisa ser compartilhado (senão N×L), e a atualização do estado compartilhado precisa ser atômica (senão TOCTOU). É a mesma lição da idempotência (Cap 16): em sistemas distribuídos, a verificação-e-atualização de um estado compartilhado tem que ser uma operação indivisível, ou a concorrência reabre o furo.

Médio
Exercício 3 · Rate limiting vs backpressure vs load shedding

Explique a diferença entre rate limiting, backpressure e load shedding, e dê um exemplo de onde cada um atua. Mostre como os três se complementam para proteger um sistema da sobrecarga.

Rate limiting — recusar o excesso na borda de entrada. É uma política sobre quantas requisições um cliente (externo) pode fazer num intervalo. Atua na entrada do sistema, recusando (com 429) o que excede o limite do cliente, antes que consuma recursos. Exemplo: "cada chave de API pode fazer 1000 req/min; o excesso recebe 429". Protege contra abuso e racionamento injusto da capacidade entre clientes.

Backpressure — empurrar a pressão de volta no processamento interno. É um mecanismo ao longo de um pipeline interno: quando um estágio (consumidor) não acompanha o anterior (produtor), ele comunica "desacelere" de volta, para o trabalho não se acumular. Atua dentro do sistema, entre estágios. Exemplo: uma fila que, ao encher, bloqueia ou faz o produtor desacelerar (Cap 11), em vez de crescer até estourar. Protege contra o acúmulo interno quando o consumo não acompanha a produção.

Load shedding — descartar o trabalho menos importante sob sobrecarga. É uma estratégia de, quando o sistema está genuinamente saturado, recusar deliberadamente parte do trabalho — idealmente o menos importante — para preservar a capacidade de fazer o crítico. Atua sob estresse real, com prioridade. Exemplo: sob pico, reservar capacidade para pagamentos e recusar (503) relatórios e tráfego de teste. Protege o caminho crítico quando nem tudo pode ser servido.

Como se complementam: os três cobrem faces diferentes da sobrecarga, em camadas:

  • Rate limiting protege a entrada — impede que qualquer cliente externo, sozinho, despeje mais do que sua cota, recusando o abuso antes que entre.
  • Backpressure protege o interior — garante que, mesmo com a carga admitida, nenhum estágio interno aceite mais do que consegue processar, propagando a pressão de volta.
  • Load shedding protege o essencial — quando, apesar dos dois anteriores, o sistema ainda satura (um pico de carga real, legítima e agregada), ele sacrifica o não-crítico para manter o crítico de pé.

O insight: nenhum dos três sozinho basta. Rate limiting sem backpressure deixa o interior acumular; backpressure sem rate limiting deixa o abuso externo entrar; e ambos sem load shedding não decidem o que sacrificar quando a carga legítima excede a capacidade. Juntos, eles garantem que o sistema só aceite o que consegue processar (rate limiting + backpressure) e, quando ainda assim for demais, sacrifique o certo (load shedding) — degradando com graça em todas as camadas em vez de colapsar.

Difícil
Exercício 4 · Projetar a autoproteção de um sistema — entrevista

Você projeta um gateway de API para uma plataforma multi-tenant (muitos clientes empresariais compartilhando a infraestrutura), que precisa se proteger de sobrecarga, abuso, e de um tenant afetar os outros. O entrevistador pede: "como você protege esse sistema de ser sobrecarregado?". Cubra rate limiting (algoritmo, identidade, estado distribuído, resposta), backpressure, e load shedding com prioridade.

Enquadramento: num sistema multi-tenant, a autoproteção tem uma dimensão extra — isolar os tenants uns dos outros (um não pode derrubar os outros, o "noisy neighbor"). Vou cobrir as três camadas, com atenção ao multi-tenancy.

1. Rate limiting — identidade primeiro (o coração do multi-tenant): limito por tenant (cada cliente empresarial tem sua cota), e possivelmente por usuário/chave dentro do tenant. Isso é o que isola os inquilinos: um tenant abusivo bate no próprio limite e não consome a capacidade dos outros — o "vizinho barulhento" fica contido na própria cota. Sem rate limiting por tenant, um cliente sozinho poderia monopolizar a infraestrutura compartilhada e derrubar todos.

2. Rate limiting — algoritmo, estado, resposta:

  • Algoritmo: sliding window counter como default (preciso, O(1), sem furo de borda) — ou token bucket se os tenants legitimamente fazem rajadas e isso é desejável.
  • Estado distribuído: compartilhado no Redis (o gateway roda em muitas instâncias; estado local deixaria passar N×L), com checagem atômica (Lua) contra o TOCTOU.
  • Resposta: 429 com Retry-After e headers de rate limit, para os tenants se autorregularem e cooperarem em vez de martelar.

3. Backpressure interno: mesmo com o tráfego de entrada limitado por tenant, os estágios internos do processamento precisam de backpressure — filas com limites que, ao encher, sinalizam de volta para desacelerar, em vez de acumular até estourar. Garante que a carga admitida não afogue os estágios internos.

4. Load shedding com prioridade: para picos de carga agregada legítima (todos os tenants ativos ao mesmo tempo), reservo capacidade para o crítico e faço shedding do não-essencial. Aqui o multi-tenancy adiciona uma dimensão: posso priorizar por tier — tenants enterprise premium têm prioridade sobre os de tier gratuito sob estresse —, sacrificando primeiro o tráfego de menor prioridade (free tier, requisições não-críticas) para proteger os clientes pagantes e o caminho crítico.

5. Cotas e isolamento: além do rate limiting de curto prazo, cotas de mais longo prazo por tenant (ex.: X requisições/mês conforme o plano), e idealmente algum isolamento de recursos para que um tenant não exaura recursos compartilhados (pools de conexão, etc.). O objetivo geral do multi-tenant: a carga de cada tenant é contida no que ele paga/tem direito, protegendo os outros.

O que um bom candidato demonstra: que coloca a identidade (por tenant) no centro, pelo isolamento; que escolhe o algoritmo e o estado distribuído atômico corretamente; que responde de forma informativa; que adiciona backpressure interno e load shedding com prioridade por tier. Frase de fechamento: "num gateway multi-tenant, a chave é limitar por tenant, com estado compartilhado e atômico no Redis — assim um tenant abusivo bate no próprio limite e não rouba a capacidade dos outros, contendo o vizinho barulhento. Respondo com 429 + Retry-After para os clientes cooperarem, ponho backpressure nos estágios internos para a carga admitida não afogar o processamento, e sob pico real faço load shedding por prioridade, protegendo os tenants premium e o caminho crítico ao sacrificar o tráfego de menor prioridade. As três camadas — entrada limitada por tenant, interior protegido por backpressure, essencial preservado por shedding — garantem que o sistema degrade com graça e que nenhum tenant derrube os outros."

Fim do capítulo 19
Próximo capítulo: resiliência — timeouts, retries e circuit breakers. Você viu como o sistema se protege da sobrecarga (o excesso de trabalho); agora, como ele se protege das falhas das suas dependências — para que a lentidão ou queda de um serviço do qual você depende não trave nem derrube o seu. Timeouts (não esperar para sempre), retries (tentar de novo, com cuidado) e circuit breakers (parar de tentar o que está claramente quebrado) são as defesas clássicas, e juntas com tudo que veio antes fecham a Parte IV — as formas de fazer um sistema sobreviver ao mundo real.
Parte IV · Capítulo 20 · Os padrões

Resiliência:
timeouts, retries,
circuit breakers.

Tudo neste livro aponta para uma verdade que a Parte II tornou inescapável: num sistema distribuído, as falhas não são exceções — são uma característica permanente do ambiente. As dependências vão ficar lentas, cair, responder erro, sumir no meio de uma resposta. A pergunta nunca foi se, mas quando — e o que o seu sistema faz quando acontece. A resiliência é o conjunto de defesas que faz a falha de uma parte não virar a falha do todo: timeouts que não esperam para sempre, retries que tentam de novo com cuidado, circuit breakers que param de bater num serviço morto, bulkheads que isolam o dano, e a degradação graciosa que serve o que dá em vez de cair. Este capítulo fecha a Parte IV reunindo essas defesas — as que, somadas a tudo que veio antes, fazem um sistema sobreviver ao mundo real.

Este é o capítulo que arma o sistema contra a falha das dependências. Vamos firmar a mentalidade (a falha é um dado, projete para ela desde o começo), e então as defesas, uma a uma: timeouts (toda chamada externa precisa de um, ou ela pendura para sempre), retries (com backoff exponencial e jitter, idempotentes, limitados — senão pioram a falha), circuit breakers (os três estados que param de tentar o que está claramente quebrado, dando tempo de recuperar), bulkheads (isolar recursos para que um serviço lento não afogue os outros), e a degradação graciosa com fallbacks (servir resposta reduzida ou cacheada em vez de erro). Terminamos com a ordem certa de compô-las. Quando você sair daqui, a falha de uma dependência deixa de ser uma catástrofe e vira algo que o seu sistema absorve — porque você o projetou esperando que a falha viesse, como a Parte II garantiu que viria.

20.1 A história — de "lidar com erros" a "projetar para a falha"

Contexto histórico

Por muito tempo, "lidar com falhas" em software significava tratar exceções — um try/catch aqui e ali, para o programa não quebrar quando algo desse errado. Era uma mentalidade de exceção: a falha era o caso raro, anormal, a ser capturado e tratado pontualmente. Para um programa rodando numa máquina só, isso bastava — falhas eram de fato raras.

A computação distribuída destruiu essa mentalidade. Quando o seu sistema depende de dezenas de outros serviços, pela rede não-confiável (Cap 8), as falhas deixam de ser raras — elas são constantes. A qualquer momento, alguma dependência está lenta, ou caindo, ou se recuperando. Michael Nygard cristalizou a nova mentalidade no livro Release It! (2007), catalogando os padrões de estabilidade (e os anti-padrões que derrubam sistemas) — timeouts, circuit breakers, bulkheads. A falha passou a ser tratada não como exceção, mas como condição normal de operação.

A Netflix levou isso ao extremo e ao mainstream. Ao migrar para a nuvem e os microsserviços, ela criou o Hystrix (2011), uma biblioteca que implementava circuit breakers, bulkheads e fallbacks de forma sistemática — e o Chaos Monkey, que deliberadamente derrubava partes do sistema em produção para forçar as equipes a projetarem para a falha. A mensagem era radical e clara: se a falha é inevitável, não a evite — provoque-a, para garantir que o sistema a sobrevive. A resiliência virou uma disciplina de engenharia, não um remendo.

Em 2026, os padrões de resiliência são maduros, bem compreendidos e amplamente implementados em bibliotecas (Resilience4j, Polly, Cockatiel e outras) que tornam triviais o que antes era código manual. As práticas se consolidaram: backoff exponencial com jitter como padrão para retries, circuit breakers de três estados, bulkheads para isolamento, degradação graciosa com fallbacks. E a mentalidade venceu: "projete para a falha" é hoje um princípio básico, não uma sofisticação. A falha, como diz a frase, é uma feature da arquitetura distribuída — não um bug a eliminar, mas uma condição a absorver. Este capítulo é sobre as defesas que fazem essa absorção acontecer, fechando a Parte IV com o que mantém um sistema de pé quando suas partes falham.

20.2 A falha é um dado, não uma exceção

Antes das técnicas, a mentalidade — porque ela muda tudo. Em um sistema distribuído, você não espera que as dependências funcionem e trata a falha como surpresa; você assume que elas vão falhar e projeta para isso desde o início.

Projete para a falha desde o começo — ela vai vir
A mudança de mentalidade que define a resiliência: parar de tratar a falha como uma exceção rara a ser capturada, e começar a tratá-la como uma condição normal e esperada do ambiente. Num sistema com muitas dependências sobre uma rede não-confiável (Cap 8), em qualquer instante alguma coisa está falhando — é estatisticamente certo. Logo, a pergunta de design não é "e se esta dependência falhar?" (como se fosse improvável), mas "quando esta dependência falhar — e ela vai —, o que o meu sistema faz?". Essa inversão tem consequências práticas profundas. Você projeta cada chamada externa assumindo que ela pode demorar para sempre (daí timeouts), falhar transitoriamente (daí retries), ou ficar persistentemente quebrada (daí circuit breakers). Você isola as dependências para que a falha de uma não contamine as outras (bulkheads). E você decide, de antemão, o que servir quando algo essencial não responde (degradação graciosa). A resiliência não é um conjunto de remendos adicionados quando os problemas aparecem — é uma propriedade projetada desde o começo, partindo da premissa de que a falha é certa. A diferença prática, como se diz, é entre "o serviço X teve um deploy ruim e serviu resultados levemente degradados por 20 minutos" e "o serviço X teve um deploy ruim e derrubou a plataforma inteira". A primeira é um sistema que projetou para a falha; a segunda, um que a tratou como exceção. Tudo neste capítulo flui dessa única mudança de mentalidade.

20.3 Timeouts — não esperar para sempre

A defesa mais básica, e a mais frequentemente esquecida: toda chamada a uma dependência externa precisa de um timeout. Sem ele, uma dependência lenta ou pendurada não retorna um erro — ela simplesmente nunca responde, e a sua requisição espera para sempre, segurando recursos.

Sem timeout, uma dependência lenta pendura tudo — a raiz da cascata
A chamada sem timeout é a fonte silenciosa da maioria das cascatas de falha. Quando você chama uma dependência e ela fica lenta (não caída — lenta, ou pendurada), sem um timeout a sua requisição espera indefinidamente por uma resposta que talvez nunca venha. E enquanto espera, ela segura recursos: uma thread, uma conexão de banco, memória, um slot no event loop. Sob carga, isso é fatal: requisições se acumulam todas esperando a dependência lenta, todas segurando recursos, até esgotá-los — e então o seu serviço cai, derrubado pela lentidão de outro (a cascata do Cap 15). O timeout quebra isso: ele define quanto tempo você está disposto a esperar, e ao estourar, libera o recurso e falha rápido (fail-fast), em vez de pendurar. Como definir o valor? Não pelo que você espera (o caso feliz), mas pelo que você pode se dar ao luxo de esperar — tipicamente um pouco acima do p95 ou p99 da latência normal da dependência (para não cortar requisições legítimas lentas), e ajustado pelas métricas reais. Um timeout bom demais (muito curto) corta requisições válidas; um ruim demais (muito longo, ou ausente) não protege. A regra absoluta, porém, é mais simples que o tuning: nunca faça uma chamada externa sem timeout. Uma única chamada sem timeout, num caminho crítico, é uma cascata esperando uma dependência lenta para acontecer. O timeout é a fundação sobre a qual todas as outras defesas se apoiam — sem ele, retries esperam para sempre, circuit breakers nunca detectam a lentidão, e o sistema não tem como falhar rápido.

20.4 Retries — backoff exponencial e jitter

Muitas falhas são transitórias: um blip de rede, uma sobrecarga momentânea. Para essas, retentar resolve — a segunda tentativa funciona. Mas retries feitos errado pioram a falha em vez de curá-la, e os detalhes (backoff, jitter, limite, idempotência) são o que separa um do outro.

Retry ingênuo amplifica a falha; backoff + jitter a cura
O retry parece simples — "deu erro, tente de novo" — mas feito ingenuamente, ele transforma uma falha pequena numa catastrófica. Considere o pior caso: 1000 requisições falham ao mesmo tempo (a dependência teve um soluço), e todas retentam imediatamente. Agora você mandou 2000 requisições para um serviço que já estava com dificuldade — você dobrou a carga no exato momento em que ele menos aguenta, empurrando-o de um soluço para um colapso. Pior ainda com a sincronização: sem cuidado, todos os clientes que falharam em t=0 retentam em t=1, falham juntos, retentam em t=2 — ondas sincronizadas de tráfego que martelam o serviço justamente quando ele tenta se recuperar (o "thundering herd"). As duas curas, ambas essenciais:

Backoff exponencial: cada retry espera mais que o anterior (100ms, 200ms, 400ms, 800ms...), dando ao serviço tempo de se recuperar entre as tentativas, em vez de martelá-lo num loop apertado.

Jitter (aleatoriedade): adicionar uma variação aleatória ao tempo de espera, para que os clientes não retentem todos no mesmo instante. Sem jitter, o backoff exponencial ainda sincroniza (todos esperam 1s, depois 2s, juntos); com jitter, um espera 1,2s, outro 1,5s, e as tentativas se espalham no tempo, eliminando as ondas. O jitter é obrigatório, não opcional — backoff sem jitter ainda cria thundering herds.

E mais três regras: limite o número de tentativas (ex.: 3) e o backoff máximo, para não retentar infinitamente nem esperar tempo demais; só retente erros retryable (transitórios — um timeout, um 503; não um "400 bad request", que vai falhar igual sempre); e — crucialmente — só retente operações idempotentes (Cap 16), porque retentar uma operação não-idempotente cria duplicatas (cobrar duas vezes). Retry sem idempotência não é resiliência; é uma máquina de efeitos duplicados.

20.5 Circuit breakers — parar de tentar o que está morto

Retries com backoff funcionam para falhas transitórias. Mas e quando uma dependência está persistentemente quebrada (caiu de vez, vai demorar a voltar)? Continuar tentando — mesmo com backoff — só desperdiça recursos e prolonga o sofrimento. O circuit breaker (disjuntor) detecta esse estado e para de tentar, dando tempo ao serviço se recuperar.

Os três estados do disjuntor: fechado, aberto, meio-aberto
O circuit breaker funciona como um disjuntor elétrico — quando detecta um problema persistente, ele "abre" e corta o circuito, em vez de deixar a corrente (as requisições) continuar fluindo para um curto. Ele tem três estados:

Fechado (Closed) — normal: as requisições passam para a dependência normalmente. O breaker conta as falhas; se elas ultrapassam um limiar (ex.: 50% de erro nas últimas N chamadas), ele abre.

Aberto (Open) — fail-fast: o breaker não chama a dependência — ele falha imediatamente (ou serve um fallback), sem nem tentar. Isso faz duas coisas vitais: protege o seu sistema (você não desperdiça recursos esperando timeouts de um serviço que você sabe estar quebrado) e protege a dependência (você para de bombardeá-la com requisições, dando-lhe espaço para se recuperar). Após um tempo, o breaker passa a meio-aberto.

Meio-aberto (Half-Open) — testando: o breaker deixa passar algumas poucas requisições de teste, para ver se a dependência se recuperou. Se elas funcionam, ele fecha (volta ao normal); se ainda falham, ele reabre (volta a fail-fast e espera mais). É um sondar cauteloso, sem reabrir as comportas de uma vez.

O insight central: o circuit breaker reconhece que, diante de uma falha persistente, continuar tentando é contraproducente — desperdiça os seus recursos e impede a recuperação da dependência. Ao falhar rápido quando o serviço está claramente morto, ele quebra a cascata (você não pendura esperando o morto) e dá ao serviço o respiro para voltar. É a diferença entre um soluço de uma dependência virar uma falha contida (o breaker abre, você degrada, o serviço se recupera, o breaker fecha) ou uma cascata (você martela o serviço caído, esgota seus próprios recursos, e cai junto). Retries lidam com o transitório; o circuit breaker lida com o persistente.

20.6 Bulkheads — isolar o dano

O bulkhead (anteparo) empresta seu nome do design naval: navios são divididos em compartimentos estanques, para que um furo num compartimento não inunde o navio inteiro. Aplicado a sistemas, o padrão isola recursos para que a falha (ou lentidão) de uma dependência não consuma os recursos de que as outras precisam.

Compartimentos estanques: um serviço lento não pode afogar todos os outros
O problema que o bulkhead resolve: sem isolamento, todas as chamadas do seu sistema às várias dependências compartilham o mesmo pool de recursos (as mesmas threads, o mesmo pool de conexões). Aí uma única dependência fica lenta, e as requisições que a chamam se acumulam, consumindo todo o pool compartilhado — e de repente as chamadas a dependências perfeitamente saudáveis também não têm recursos, porque o serviço lento monopolizou tudo. Uma dependência ruim derrubou o acesso a todas as outras. O bulkhead previne isso isolando os recursos por dependência: cada uma recebe seu próprio pool limitado (ex.: a dependência A pode usar no máximo 10 conexões concorrentes; a B, outras 10). Agora, se a dependência A fica lenta e esgota suas 10 conexões, isso afeta as chamadas à A — as chamadas à B continuam, com o pool delas intacto. O dano fica contido no compartimento da dependência problemática, como o furo fica contido no compartimento estanque do navio. O bulkhead limita o "raio de explosão" (blast radius) de uma falha: em vez de a lentidão de um serviço derrubar o sistema inteiro (consumindo todos os recursos compartilhados), ela degrada apenas a funcionalidade que depende daquele serviço. É uma das defesas mais subestimadas, porque o problema que ela previne (uma dependência lenta esgotando o pool compartilhado e derrubando o acesso a todas) é exatamente o tipo de cascata não-óbvia que causa as maiores quedas.

20.7 Degradação graciosa e fallback

Quando uma defesa "dispara" (o timeout estoura, o circuit breaker abre, o retry esgota), o que você serve ao usuário? A pior resposta é um erro duro. A melhor é uma degradação graciosa: servir o que você consegue — uma resposta reduzida, cacheada, ou aproximada — em vez de falhar completamente.

Sirva o que dá, degrade o que não dá, erre só no crítico
A degradação graciosa é o princípio de que um sistema, diante da falha de uma parte, deve continuar oferecendo o máximo de funcionalidade possível, em vez de cair por inteiro. A chave é distinguir o essencial do secundário (o tema do Cap 15 e do load shedding do Cap 19, reaparecendo): quando uma dependência não-crítica falha, você desabilita aquela feature e serve o resto, em vez de retornar um erro para tudo. Exemplos concretos de fallback: se o serviço de recomendações cai numa loja, mostre a página do produto sem as recomendações (uma feature secundária degradada) em vez de uma página de erro (o core — comprar — preservado); se o serviço de preços ao vivo falha, sirva o último preço conhecido do cache, com uma marca de "atualizado há X minutos" (dado velho explícito é melhor que nenhum dado, Cap 10); se o feed em tempo real falha, mostre os posts mais recentes que você tinha até a falha, em vez de uma tela vazia com erro; se uma operação crítica não pode completar agora, aceite-a numa fila para processar quando a dependência voltar (Cap 11). O princípio unificador: retorne um erro duro apenas quando o dado é genuinamente crítico e não há fallback; para tudo o mais, sirva uma versão degradada. Isso conecta tudo: a degradação graciosa é onde as outras defesas "aterrissam" — quando o timeout estoura, o breaker abre, ou o retry falha, o fallback é o que o usuário vê, e um bom fallback transforma uma falha de dependência numa experiência levemente reduzida em vez de uma quebra total. Uma nota prática importante: teste os fallbacks. Um fallback que nunca foi exercitado em produção pode não funcionar quando você finalmente precisar dele — e descobrir que o seu plano B está quebrado no meio de um incidente é o pior momento possível.

20.8 A ordem e a composição

Essas defesas não atuam isoladas — elas se compõem, e a ordem em que se aninham importa. Compô-las erradas faz uma anular a outra.

Retries dentro do breaker; rate limiting antes dos retries; timeout em tudo
A composição correta dessas defesas segue uma lógica: os retries devem ficar dentro do circuit breaker, não fora. Por quê? Se o breaker está aberto (a dependência está claramente morta), não faz sentido retentar — você sabe que vai falhar. Com os retries dentro do breaker, quando o breaker abre, ele impede os retries inúteis (fail-fast), em vez de você retentar contra um serviço que o breaker já sabe estar quebrado. Aninhar ao contrário (breaker dentro do retry) faria você retentar mesmo com o circuito aberto — desperdício. Segundo, o rate limiting (Cap 19) vem antes dos retries — você não quer que os retries consumam a cota de rate limit, nem que o rate limiting trate um retry legítimo como abuso; a admissão acontece primeiro, depois a lógica de resiliência. Terceiro, todo nível tem timeout — a chamada individual tem timeout, e o conjunto (com retries) tem um timeout total (um "budget"), para que a soma dos retries com backoff não exceda o que você pode esperar. E quarto, a degradação graciosa é o fallback final — quando tudo (timeout, retries, breaker) falha, o fallback é o que aterrissa. Uma nota sobre retries em sistemas multicamada: prefira retentar num único ponto da pilha de chamadas, não em cada camada — porque retries em múltiplas camadas se multiplicam (3 retries na camada A × 3 na B × 3 na C = 27 tentativas para o serviço no fundo), amplificando massivamente a carga. Escolha uma camada para os retries e deixe as outras propagarem a falha. A lição: a resiliência não é empilhar defesas ao acaso; é compô-las na ordem que faz cada uma reforçar (não anular) a outra — timeout em tudo, rate limiting na entrada, retries (idempotentes, com backoff e jitter, num único nível) dentro do breaker, bulkheads isolando, e a degradação graciosa aterrissando o que falhar.

20.9 Estudo de caso — a dependência que tentou derrubar tudo

Como um serviço de recomendações lento quase levou a loja junto
O cenário

Uma loja online tinha um serviço de recomendações ("quem comprou isto também comprou") que a página de produto chamava para enriquecer a exibição. Um dia, o serviço de recomendações ficou lento (não caiu — lento, respondendo em 30 segundos). Em minutos, a loja inteira começou a cair: páginas de produto, busca, até o checkout. Como a lentidão de uma feature secundária (recomendações) quase derrubou o core (vender)? E como a resiliência o conserta?

Passo 1 · A cascata a partir da feature secundária
como recomendações afundou a loja
A página de produto chamava recomendações SEM TIMEOUT,
síncronamente, com o pool de recursos COMPARTILHADO.

Recomendações ficou lento (30s/resposta). Então:
  1. cada página de produto esperava 30s por recomendações
     (sem timeout → espera "para sempre")
  2. cada requisição presa segurava uma thread/conexão do
     POOL COMPARTILHADO
  3. as threads esgotaram, presas esperando recomendações
  4. sem threads livres, NADA mais era servido — nem busca,
     nem checkout, que nem usam recomendações
  5. → a loja inteira caiu, por causa de uma feature
     SECUNDÁRIA lenta

Uma feature que deveria ser "enfeite" derrubou o negócio.
Passo 2 · Timeout + bulkhead: conter o dano
as duas primeiras defesas
TIMEOUT: a chamada a recomendações ganhou um timeout curto
  (ex.: 200ms — recomendações deveriam ser rápidas). Agora,
  se demora mais, a chamada falha rápido em vez de pendurar.
  Os recursos são liberados; nada espera 30s.

BULKHEAD: recomendações ganhou seu PRÓPRIO pool limitado de
  conexões (isolado do resto). Mesmo que recomendações fique
  lento e esgote o pool DELE, o pool do checkout e da busca
  fica intacto. O dano fica contido no compartimento de
  recomendações — não inunda o navio inteiro.
Passo 3 · Circuit breaker + degradação graciosa
parar de tentar, e servir sem o enfeite
CIRCUIT BREAKER: como recomendações estava PERSISTENTEMENTE
  lento, o breaker abriu após N timeouts — passou a falhar
  rápido (sem nem chamar recomendações), poupando recursos e
  dando ao serviço espaço para se recuperar. Meio-aberto
  testa de vez em quando; fecha quando recomendações volta.

DEGRADAÇÃO GRACIOSA: e o fallback — quando o breaker está
  aberto (ou o timeout estoura), a página de produto é
  servida SEM as recomendações (uma seção a menos), em vez
  de dar erro. O cliente vê o produto e PODE COMPRAR; só não
  vê o "quem comprou também comprou". A feature secundária
  degrada; o core (vender) é preservado.
Passo 4 · O resultado

Com as quatro defesas, a próxima vez que recomendações ficou lento foi um não-evento: o timeout cortou as chamadas lentas em 200ms, o bulkhead conteve o consumo de recursos ao compartimento de recomendações, o circuit breaker abriu e parou de bater no serviço lento (dando-lhe espaço para se recuperar), e a degradação graciosa serviu as páginas de produto sem a seção de recomendações. Os clientes continuaram navegando, buscando e comprando normalmente — a maioria nem percebeu que faltava uma seção. Uma falha que antes derrubava a loja inteira virou uma feature temporariamente ausente. A diferença entre "a plataforma caiu" e "faltou uma seção por 20 minutos".

Resultado: a lentidão de uma feature secundária (recomendações) que antes derrubava o negócio inteiro virou uma degradação imperceptível — porque o sistema passou a tratar a falha como esperada e a se defender em camadas. Cada defesa fez sua parte: o timeout impediu a espera infinita que pendurava recursos; o bulkhead conteve o dano ao compartimento de recomendações, protegendo o checkout e a busca; o circuit breaker parou de bater no serviço persistentemente lento, poupando recursos e dando-lhe espaço para voltar; e a degradação graciosa serviu o essencial (o produto, a compra) sem o secundário (as recomendações). A lição central que fecha o capítulo e a Parte IV: a falha de uma dependência é certa, e a resiliência é o que faz essa falha ficar contida em vez de virar uma cascata. Nenhuma defesa sozinha bastaria — o timeout sem o bulkhead ainda deixaria o pool compartilhado afogar; o bulkhead sem a degradação graciosa daria erro em vez de servir o produto. Juntas, e na ordem certa, elas transformam "uma dependência falhou, logo o sistema caiu" em "uma dependência falhou, logo uma feature degradou". É essa transformação — a falha contida em vez de propagada — que faz um sistema sobreviver ao mundo real. Isto fecha a Parte IV — os padrões. Você tem agora o repertório completo: como as partes conversam, como garantem entrega, como coordenam transações, como organizam dados, como se protegem da sobrecarga, e como sobrevivem à falha. A Parte V junta tudo: quatro sistemas inteiros, projetados do zero, onde cada conceito das quatro partes anteriores reaparece, combinado, num projeto completo.

20.10 Erros comuns

Erro 1 · Chamada externa sem timeout

Chamar uma dependência sem timeout — e, quando ela fica lenta ou pendura, esperar para sempre, segurando threads/conexões até esgotá-las e cair (a raiz da cascata). Toda chamada externa precisa de um timeout, definido pelo que você pode esperar (perto do p95/p99), não pelo caso feliz. Sem timeout, não há fail-fast.

Erro 2 · Retry sem backoff e sem jitter

Retentar imediatamente, ou com backoff mas sem jitter — amplificando a falha (dobra a carga no pior momento) e criando ondas sincronizadas (thundering herd) que martelam o serviço quando ele tenta se recuperar. Use backoff exponencial (dá tempo de recuperar) com jitter (espalha as tentativas). Jitter é obrigatório, não opcional.

Erro 3 · Retry de operação não idempotente

Retentar uma operação não-idempotente (cobrar, criar) — e criar duplicatas (cobrança dupla) a cada retry. Retry exige idempotência (Cap 16). Sem ela, retries não são resiliência; são uma máquina de efeitos duplicados. Torne idempotente antes de retentar.

Erro 4 · Retentar erros não-retryable, ou sem limite

Retentar um erro que vai falhar igual sempre (um "400 bad request" não melhora com retry), ou retentar infinitamente. Só retente erros transitórios (timeout, 503), distinga-os dos permanentes, e limite as tentativas (ex.: 3) e o backoff máximo. Retry indiscriminado e ilimitado desperdiça recursos e prolonga a falha.

Erro 5 · Não ter circuit breaker para falha persistente

Continuar tentando (mesmo com backoff) uma dependência que está persistentemente quebrada — desperdiçando recursos e impedindo a recuperação dela. Use um circuit breaker: ele abre após falhas repetidas, falha rápido (poupando seus recursos e dando espaço ao serviço), e testa a recuperação no meio-aberto. Retries são para o transitório; o breaker, para o persistente.

Erro 6 · Pool de recursos compartilhado entre dependências

Deixar todas as chamadas compartilharem o mesmo pool de threads/conexões — e uma dependência lenta esgotar o pool inteiro, derrubando o acesso a todas as outras (saudáveis). Use bulkheads: pools isolados por dependência, para o dano de uma ficar contido no seu compartimento. Sem isolamento, uma dependência ruim afoga todas.

Erro 7 · Erro duro onde caberia degradação

Retornar um erro para tudo quando uma feature secundária falha — em vez de servir o resto sem ela. Degrade com graça: desabilite a feature que falhou (recomendações, preços ao vivo) e sirva o essencial (o produto, a compra), reservando o erro duro para o dado genuinamente crítico sem fallback. E teste os fallbacks — um plano B não-testado pode estar quebrado quando você precisar.

Verifique seu entendimento
A página de produto da sua loja chama um serviço de recomendações (uma feature secundária). Um dia, recomendações fica lento (30s por resposta), e a loja inteira cai — inclusive busca e checkout, que nem usam recomendações. Qual combinação de defesas resolve melhor?

20.11 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Qual defesa para qual falha?

Para cada modo de falha, indique a defesa de resiliência mais apropriada: (a) uma dependência fica pendurada e nunca responde; (b) um blip de rede transitório faz uma chamada falhar, mas a próxima funcionaria; (c) uma dependência está caída de vez e vai demorar a voltar, e você fica tentando em vão; (d) uma dependência lenta está consumindo todas as suas conexões e afetando o acesso às outras.

(a) Dependência pendurada que nunca responde → timeout. O timeout define quanto você está disposto a esperar e, ao estourar, libera o recurso e falha rápido — em vez de esperar para sempre, segurando threads/conexões. É a defesa base contra a espera infinita.

(b) Blip transitório, a próxima funcionaria → retry (com backoff + jitter). Falhas transitórias se curam tentando de novo. Use backoff exponencial (dá tempo de recuperar) e jitter (espalha as tentativas), idempotente e limitado. É a defesa para o transitório.

(c) Dependência caída de vez, você tenta em vão → circuit breaker. Para falha persistente, retentar é desperdício. O breaker abre após falhas repetidas, para de tentar (fail-fast, poupando seus recursos) e dá espaço ao serviço se recuperar, testando no meio-aberto. É a defesa para o persistente.

(d) Dependência lenta consumindo todas as conexões → bulkhead. O bulkhead isola os recursos por dependência (pool próprio para cada), para que a lentidão de uma fique contida no seu compartimento e não esgote os recursos das outras. É a defesa contra uma dependência ruim afogar todas.

O insight: cada defesa ataca um modo de falha — timeout (espera infinita), retry (falha transitória), circuit breaker (falha persistente), bulkhead (esgotamento de recursos compartilhados). E todas aterrissam na degradação graciosa (servir o que dá quando elas disparam). Resiliência é ter a defesa certa para cada modo de falha, compostas juntas.

Médio
Exercício 2 · Por que o retry ingênuo piora tudo

Um serviço tem um soluço momentâneo e 5000 requisições falham ao mesmo tempo. Os clientes retentam imediatamente, sem backoff nem jitter. (a) Por que isso piora a situação? (b) Como o backoff exponencial ajuda? (c) Por que o jitter é necessário além do backoff? (d) Que outras salvaguardas o retry precisa?

(a) Por que piora: as 5000 requisições que falharam retentam imediatamente, então o serviço que já estava com dificuldade (o soluço) recebe agora 5000 requisições novas de uma vez, somadas a qualquer tráfego novo — você dobrou (ou mais) a carga no exato momento em que o serviço menos aguenta. Um soluço momentâneo, que se resolveria sozinho em um instante, vira uma sobrecarga sustentada: o retry ingênuo transforma uma falha pequena e transitória numa cascata. O retry, que deveria ajudar, vira o agente da catástrofe.

(b) Como o backoff exponencial ajuda: em vez de retentar imediatamente, cada tentativa espera progressivamente mais (100ms, 200ms, 400ms...). Isso dá tempo ao serviço de se recuperar entre as tentativas — em vez de ser martelado num loop apertado, ele tem espaço para drenar o soluço e voltar. A carga dos retries se distribui no tempo, em vez de chegar toda de uma vez, reduzindo o pico que derruba.

(c) Por que o jitter é necessário além do backoff: mesmo com backoff exponencial, se todos os 5000 clientes falharam no mesmo instante (t=0), eles vão todos esperar o mesmo tempo (1s) e retentar juntos em t=1; falham juntos, esperam 2s, retentam juntos em t=3... O backoff sozinho ainda produz ondas sincronizadas — 5000 requisições de uma vez, periodicamente. O jitter adiciona aleatoriedade ao tempo de espera (um cliente espera 1,2s, outro 1,7s, outro 0,9s), espalhando as tentativas no tempo em vez de concentrá-las em ondas. Sem jitter, o backoff só muda quando a onda bate, não elimina a onda. Por isso o jitter é obrigatório, não um extra.

(d) Outras salvaguardas:

  • Limite de tentativas (ex.: 3) e backoff máximo — para não retentar infinitamente nem esperar tempo demais.
  • Só erros retryable — retentar um 503 (transitório) faz sentido; retentar um 400 (bad request, vai falhar igual) é desperdício.
  • Idempotência (Cap 16) — só retentar operações idempotentes, senão cada retry duplica o efeito (cobrança dupla).
  • Circuit breaker — se as falhas persistem, parar de retentar (o breaker abre), em vez de seguir tentando em vão.
  • Retentar num único nível da pilha, não em cada camada (senão os retries se multiplicam: 3×3×3 = 27).

O insight: retry é uma faca de dois gumes — feito certo (backoff + jitter + idempotência + limite + só transitórios + dentro de um breaker), cura falhas transitórias; feito ingenuamente (imediato, sem jitter, ilimitado), amplifica uma falha pequena numa cascata. Os detalhes não são opcionais; são o que separa resiliência de auto-sabotagem.

Médio
Exercício 3 · Os três estados do circuit breaker

Explique o ciclo de vida de um circuit breaker pelos seus três estados, descrevendo o que acontece em cada um e as transições. Depois, explique por que o circuit breaker resolve um problema que retries com backoff não resolvem.

Os três estados e as transições:

  • Fechado (Closed) — normal: as requisições passam para a dependência normalmente. O breaker monitora as falhas. Transição: se a taxa de falha ultrapassa um limiar (ex.: 50% de erro nas últimas N chamadas, ou X falhas seguidas), o breaker abre.
  • Aberto (Open) — fail-fast: o breaker não chama a dependência; falha imediatamente (ou serve um fallback) sem nem tentar. Isso poupa os recursos do seu sistema (você não espera timeouts de um serviço que sabe estar quebrado) e protege a dependência (você para de bombardeá-la, dando-lhe espaço para se recuperar). Transição: após um tempo de espera (ex.: 30s), o breaker passa a meio-aberto.
  • Meio-aberto (Half-Open) — testando: o breaker deixa passar algumas poucas requisições de teste, para sondar se a dependência se recuperou. Transições: se as requisições de teste funcionam, o breaker fecha (volta ao normal); se ainda falham, ele reabre (volta a fail-fast e espera mais antes de testar de novo).

Por que o breaker resolve o que retries com backoff não resolvem: retries com backoff são para falhas transitórias — eles assumem que tentar de novo (depois de esperar) vai funcionar, porque o problema foi passageiro. Mas quando a dependência está persistentemente quebrada (caiu de vez, vai demorar minutos ou horas a voltar), retentar — mesmo com backoff — é inútil e contraproducente: cada tentativa (a) desperdiça os seus recursos (espera o timeout de um serviço morto, segura threads), (b) prolonga a sua própria degradação (você fica lento esperando timeouts em cada requisição), e (c) impede a recuperação da dependência (você continua mandando carga para um serviço que precisa de espaço para voltar). O backoff espaça as tentativas, mas ainda tenta — e contra uma falha persistente, qualquer tentativa é desperdício. O circuit breaker reconhece a persistência da falha (muitas falhas seguidas → o problema não é transitório) e para de tentar completamente (abre), falhando rápido sem nem chamar. Ele só volta a tentar quando, cautelosamente (meio-aberto), confirma que vale a pena. Em suma: retries lidam com "falhou uma vez, tente de novo" (transitório); o circuit breaker lida com "está falhando há um tempo, pare de tentar e dê espaço" (persistente). São complementares — você usa os dois, com os retries dentro do breaker (quando o breaker abre, ele impede os retries inúteis).

Difícil
Exercício 4 · Projetar a resiliência de um sistema — entrevista

Você projeta um serviço de checkout que depende de vários outros serviços: pagamento (crítico), estoque (crítico), recomendações (secundário), e um serviço de cálculo de frete de um parceiro externo (importante, mas às vezes lento). O entrevistador pede: "como você torna esse checkout resiliente às falhas dessas dependências?". Cubra timeouts, retries, circuit breakers, bulkheads, degradação graciosa, e a composição.

Enquadramento: a chave é tratar cada dependência conforme sua criticidade — o que pode degradar (recomendações) vs o que é essencial (pagamento, estoque) — e aplicar as defesas compostas na ordem certa. Vou por camadas.

1. Timeouts em tudo (a base): toda chamada às quatro dependências tem timeout, dimensionado pela latência normal de cada uma (perto do p95/p99) e pelo que o checkout pode esperar. Especialmente o frete do parceiro externo (que "às vezes é lento") precisa de um timeout firme — uma dependência externa lenta sem timeout é a cascata clássica. Sem timeout, nada mais funciona.

2. Bulkheads (isolar por dependência): cada dependência recebe seu pool de recursos isolado, para que a lentidão de uma (digamos, o frete externo) não esgote o pool compartilhado e derrube as chamadas a pagamento e estoque. Especialmente importante para o parceiro externo, que eu não controlo — seu compartimento isolado garante que a falha dele não inunde o resto.

3. Retries + circuit breakers (por dependência, conforme a natureza):

  • Pagamento e estoque (críticos): retries com backoff + jitter para falhas transitórias (mas idempotentes — chave de idempotência, Cap 16, para não cobrar duas vezes!), dentro de um circuit breaker que abre se eles ficam persistentemente fora.
  • Frete externo (lento, não controlado): circuit breaker mais sensível (abre rápido quando o parceiro fica lento), timeout firme, poucos retries. Não posso deixar um parceiro externo lento travar checkouts.
  • Recomendações (secundário): timeout curto, circuit breaker; falha rápido sem drama.

4. Degradação graciosa (por criticidade — a decisão central):

  • Recomendações falha → serve o checkout sem as recomendações. Feature secundária degrada, o core (comprar) é preservado. Nunca dar erro de checkout por causa de recomendações.
  • Frete externo falha → degradação possível: usar uma estimativa de frete cacheada/aproximada, ou oferecer "calcularemos o frete e confirmamos por email", ou um frete padrão — em vez de bloquear a compra. Importante, mas com fallback.
  • Pagamento ou estoque falha → aqui não há degradação possível — são o core do checkout. Se o pagamento não pode ser processado ou o estoque não pode ser confirmado, a compra genuinamente não pode acontecer. Aqui um erro (claro e honesto, com retry sugerido) é correto — ou aceitar o pedido numa fila para processar quando a dependência voltar, se o negócio permitir. O erro duro é reservado para o crítico sem fallback.

5. A composição: retries dentro dos circuit breakers (não retentar com o breaker aberto); rate limiting na entrada (Cap 19) antes dos retries; timeouts em cada nível e um budget total; retries num único nível (não multiplicados pelas camadas); e a degradação graciosa aterrissando o que falhar. E eu testaria os fallbacks (o checkout sem recomendações, o frete aproximado) — um fallback não-testado pode estar quebrado quando eu precisar.

O que um bom candidato demonstra: que trata cada dependência pela criticidade (degradar o secundário, preservar o crítico); que põe timeout em tudo e bulkhead por dependência; que combina retries idempotentes e circuit breakers conforme a natureza de cada falha; que decide a degradação graciosa por criticidade (o ponto central); e que compõe as defesas na ordem certa. Frase de fechamento: "eu projeto o checkout assumindo que cada dependência vai falhar, e trato cada uma pela sua criticidade. Timeout em todas, bulkhead para isolar (sobretudo o parceiro externo lento), retries idempotentes dentro de circuit breakers. A decisão central é a degradação: se recomendações cai, vendo sem ela; se o frete externo cai, uso uma estimativa ou prometo confirmar depois; mas se pagamento ou estoque cai, aí não há fingir — ou aceito numa fila para depois, ou erro honesto, porque é o core. O objetivo é que a falha de qualquer dependência degrade a experiência no máximo até onde aquela dependência importa — nunca que a falha de uma feature secundária derrube a capacidade de vender."

Fim do capítulo 20 · Fim da Parte IV
Você concluiu os padrões. Tem o repertório completo de como combinar os componentes em sistemas que funcionam sob falha: como as partes conversam (síncrono/assíncrono, eventos), como garantem entrega (idempotência), como coordenam transações (Saga), como organizam dados (CQRS, Event Sourcing), como se protegem da sobrecarga (rate limiting, backpressure) e como sobrevivem à falha das dependências (timeouts, retries, circuit breakers, bulkheads, degradação). As quatro partes do livro construíram o raciocínio, as leis, os componentes e os padrões. A Parte V os reúne: quatro sistemas inteiros, projetados do zero — um feed, um sistema de pagamentos, notificações em escala, e um data pipeline —, onde cada conceito reaparece, combinado, num projeto completo. É onde a teoria vira prática. Peça "continua" para a Parte V.
Parte V
Os sistemas inteiros

Onde a teoria vira projeto. Quatro sistemas reais, desenhados do zero — um feed social, um sistema de pagamentos, notificações em escala, e um data pipeline —, em que cada conceito das quatro partes anteriores reaparece, combinado, sob restrições concretas. Não mais uma ideia por vez, mas todas juntas, costuradas num projeto que precisa funcionar. É aqui que se aprende a projetar de verdade: escolhendo, sob trade-offs reais, o que cada sistema exige.

Feed Pagamentos Notificações Data pipeline
Parte V · Capítulo 21 · Os sistemas inteiros

Projetar um
feed.

Você abre o app, e em milissegundos aparece um feed personalizado: os posts de centenas de pessoas que você segue, ordenados, prontos. Parece trivial. É um dos problemas de design mais instrutivos que existem — porque a solução "óbvia" (consultar os posts de quem você segue quando você abre o app) não escala, e a jornada de descobrir por que e o que fazer no lugar percorre quase todos os conceitos deste livro: a assimetria leitura/escrita (Cap 2), o cache (Cap 10), as filas (Cap 11), o sharding (Cap 13), o fan-out (Cap 15), a degradação graciosa (Cap 20). Este primeiro caso da Parte V mostra como esses conceitos, isolados nos capítulos anteriores, se combinam num sistema real.

Este capítulo projeta um feed social do zero. Vamos começar pelo problema e pelos requisitos (com estimativas, Cap 3), e então percorrer a jornada de design: a abordagem ingênua (fan-out on read) e por que ela não escala para leituras; a inversão (fan-out on write) que torna as leituras rápidas mas cria o problema da escrita; o problema da celebridade (a hot key do Cap 13 em sua forma mais famosa) que quebra o fan-out on write; e a solução híbrida que o padrão de produção usa. Depois, do feed cronológico ao ranqueado, e a arquitetura completa costurando tudo. Quando você sair daqui, terá visto como uma dúzia de conceitos isolados se combinam num projeto real — e como a resposta certa não é um padrão, mas a combinação de padrões que as restrições do problema exigem. É o primeiro exercício de juntar tudo.

21.1 O problema do feed

O problema parece simples de enunciar: "mostre-me os posts mais recentes de todos que eu sigo, ordenados". A dificuldade está na escala e na assimetria. Um usuário segue centenas de contas; cada conta posta; e milhões de usuários abrem o feed dezenas de vezes por dia. A consulta ingênua — buscar, ordenar e juntar os posts de centenas de contas a cada abertura — é enganosamente cara.

O feed é o caso clássico de assimetria leitura/escrita extrema
O feed é, antes de tudo, um problema read-heavy extremo (Cap 2): as leituras superam as escritas tipicamente em 100 para 1 ou mais. As pessoas postam ocasionalmente, mas abrem o feed constantemente — dezenas de vezes por dia, cada abertura exigindo montar um feed personalizado de centenas de fontes. Essa assimetria é a chave de todo o design: ela significa que otimizar a leitura (tornar abrir o feed rápido) vale quase qualquer custo na escrita (tornar postar mais caro), porque há 100 leituras para cada escrita. É a aplicação direta da lição do Cap 2 — "o que esse sistema faz mais?" — e a resposta (lê, massivamente mais do que escreve) orienta a decisão central do capítulo: pré-computar o feed na escrita (cara, rara) para que a leitura (barata, constante) seja instantânea. Toda a jornada de design a seguir é, no fundo, sobre onde pagar o custo de montar o feed — na escrita ou na leitura — e a assimetria empurra fortemente para a escrita. Mas, como veremos, levar isso ao extremo cria o problema da celebridade, e a resposta final equilibra os dois. Reconhecer a assimetria primeiro é o que torna o resto do design compreensível: não estamos otimizando um sistema genérico, estamos otimizando um sistema que lê muito mais do que escreve.

21.2 Requisitos e estimativas

Seguindo a disciplina dos Caps 2 e 3, antes de projetar, definimos o escopo e dimensionamos. Requisitos funcionais mínimos: postar; ver o feed de quem se segue; seguir/deixar de seguir. Não-funcionais: feed rápido (leitura de baixa latência), alta disponibilidade, escala para muitos usuários. Uma estimativa de ordem de grandeza (Cap 3) ancora as decisões:

estimativa de ordem de grandeza
Premissas (ordem de grandeza, não exatidão):
  - 300M usuários ativos por dia
  - cada um segue ~200 contas
  - cada um abre o feed ~10x/dia
  - 500M posts/dia

ESCRITAS (posts):  500M/dia  ≈ 5.800/s  (pico ~20.000/s)
LEITURAS (feed):   3B/dia    ≈ 35.000/s (pico ~100.000/s)
RAZÃO leitura:escrita ≈ 100:1   ← a assimetria que define tudo

Se cada post fizer fan-out para ~200 followers (push):
  500M posts × 200 = 100 BILHÕES de writes de timeline/dia
  → caro, mas torna a LEITURA O(1). É a troca central.

Conclusão das estimativas: o sistema é massivamente read-heavy.
Otimizar a leitura vale pagar caro na escrita. E o fan-out na
escrita gera um volume enorme — que precisa ser assíncrono
(Cap 11) e, para celebridades, repensado (o problema adiante).

21.3 Fan-out on read (pull) — a abordagem ingênua

A primeira solução, a "óbvia": guardar cada post uma vez (na timeline do autor), e montar o feed na hora da leitura — quando você abre o app, o sistema busca quem você segue, pega os posts recentes de cada um, e os mescla e ordena. Isto é o fan-out on read (ou modelo pull).

O que o pull ganha

Escrita barata: postar é só uma gravação (na timeline do autor) — sem fan-out. Sem write amplification: um post de uma celebridade com milhões de seguidores é um write, não milhões. Sem desperdício para inativos: não se computa feed de quem não abre o app. Sem hot key na escrita.

O que o pull custa

Leitura cara: cada abertura do feed busca e mescla os posts de centenas de contas — um K-way merge custoso. Não escala para quem segue muitos: seguir milhares de contas torna cada leitura lenta. Péssimo para o caso comum: e o caso comum é justamente ler (100:1). Otimiza o lado errado.

O pull otimiza a escrita — o lado que acontece 100x menos
O fan-out on read é sedutor por ser simples e barato na escrita, mas tem um defeito fatal para um feed: ele torna cara justamente a operação que acontece 100 vezes mais — a leitura. Toda vez que você abre o app (dezenas de vezes ao dia), o sistema precisa buscar os posts recentes de cada uma das centenas de contas que você segue, mesclá-los numa lista ordenada (um K-way merge de listas ordenadas, custoso), aplicar ranking, e paginar. Para um usuário que segue centenas ou milhares de contas, isso é lento demais para a latência que um feed exige — e multiplicado por 35.000+ leituras por segundo, é um custo computacional insustentável. O pull otimiza o lado errado da assimetria: ele economiza na escrita (rara) e gasta na leitura (constante). Há um lugar onde o pull faz sentido — veremos que para celebridades ele é parte da solução, e para usuários inativos ele evita desperdício —, mas como abordagem geral para um feed read-heavy, ele falha porque pune o caso comum. A lição do Cap 2 ressoa: você otimiza para o que o sistema faz mais, e o feed muito mais do que escreve. Isso nos empurra para a abordagem oposta.

21.4 Fan-out on write (push) — inverter o custo

A inversão: em vez de montar o feed na leitura, pré-computá-lo na escrita. Quando alguém posta, o sistema imediatamente empurra (push) o post para a timeline pré-computada de cada um dos seus seguidores. Aí, ler o feed é só pegar a timeline já pronta. Isto é o fan-out on write (modelo push).

push: pré-computar na escrita, ler O(1)
AO POSTAR (escrita — cara, mas rara):
  1. grava o post (na timeline do autor)
  2. busca a lista de seguidores do autor (ex.: 200)
  3. para CADA seguidor, adiciona o post à timeline
     pré-computada dele (ex.: um sorted set no Redis,
     ordenado por tempo)  → 200 writes

AO LER O FEED (leitura — barata, constante):
  1. pega a timeline JÁ PRONTA do usuário (1 lookup no Redis,
     os 20 posts mais recentes)            → O(1), rápido!
  2. busca os detalhes dos posts (batch)
  3. retorna

A leitura virou um único lookup numa lista pré-computada.
Rápida o suficiente para os 35.000+ reads/s. Pagamos caro na
escrita (o fan-out) para a leitura ser instantânea — a troca
que a assimetria 100:1 justifica.
Push torna a leitura O(1) — pagando o custo na escrita, que acontece menos
O fan-out on write inverte exatamente o trade-off do pull, e na direção que a assimetria favorece. Ao pré-computar o feed de cada usuário no momento da escrita (empurrando cada post para as timelines de todos os seguidores), ele torna a leitura um simples lookup de uma lista já pronta — O(1), rápido o suficiente para o volume massivo de leituras. O custo se desloca para a escrita: postar agora exige escrever em todas as timelines dos seguidores (write amplification). Mas — e este é o ponto — a escrita acontece 100 vezes menos que a leitura, então pagar caro nela para a leitura ser barata é, na maioria dos casos, um ótimo negócio. É a aplicação direta do princípio do Cap 10 e do Cap 2: pré-compute o que é lido muitas vezes, pague o custo uma vez na escrita. O fan-out, claro, deve ser assíncrono (Cap 11): postar não pode bloquear esperando 200 (ou mais) writes de timeline — o post é gravado, um evento é publicado numa fila, e workers fazem o fan-out em segundo plano (o usuário vê "postado!" na hora). Para o usuário típico (dezenas a milhares de seguidores), o fan-out on write é excelente: leituras instantâneas, escrita assíncrona gerenciável. O problema surge num caso específico e extremo — e é o coração deste capítulo.

21.5 O problema da celebridade — a hot key famosa

O fan-out on write funciona lindamente até uma celebridade postar. Um usuário com 50 milhões de seguidores, ao postar uma vez, dispara 50 milhões de writes de timeline. É o problema da hot key (Cap 13) na sua forma mais famosa — e ele quebra o fan-out on write.

Um post de celebridade = milhões de writes — o fan-out on write não aguenta
O problema da celebridade (também chamado hot key problem, Cap 13) é o que impede o fan-out on write puro de ser a solução. A matemática é brutal: um usuário com 50 milhões de seguidores, ao postar um único post, exige que o sistema escreva esse post em 50 milhões de timelines — 50 milhões de writes para um post. Se essa celebridade posta 10 vezes ao dia, são 500 milhões de writes diários só dela. Isso satura o pipeline de fan-out: os workers ficam atolados processando os milhões de writes de um único post, atrasando o fan-out dos posts de todos os outros usuários (a falta de isolamento, Cap 20 — um post viral degrada a atualização do feed da plataforma inteira). E há o custo de armazenamento e a latência: o post da celebridade leva minutos para chegar a todos os 50 milhões de timelines, e enquanto isso, alguns seguidores o veem e outros não. O fan-out on write, que era ótimo para o usuário típico, colapsa para a celebridade — porque o write amplification, que era de centenas para um usuário normal, vira de milhões para a celebridade. É o mesmo padrão de hot key do Cap 13 (a partição da Taylor Swift), agora no fan-out: um pequeno número de contas com um número desproporcional de seguidores quebra a abordagem que funciona para a maioria. E a lição de design é a mesma: você não trata o caso extremo com a mesma estratégia do caso comum — você os separa.

21.6 A solução híbrida — push para a maioria, pull para as celebridades

A solução de produção (a que Twitter/X, Instagram e Facebook usam) é híbrida: usar fan-out on write (push) para a vasta maioria dos usuários, e fan-out on read (pull) para as celebridades — e mesclar os dois na leitura. Cada estratégia é aplicada onde ela funciona.

o híbrido: cada estratégia onde ela brilha
AO POSTAR — roteia conforme o número de seguidores:
  se autor.seguidores < ~100.000 (usuário normal):
     → FAN-OUT ON WRITE (push para as timelines dos seguidores)
  senão (celebridade):
     → NÃO faz fan-out. Só grava o post no cache da celebridade.
       (evita os milhões de writes — o problema da celebridade)

AO LER O FEED — mescla as duas fontes:
  1. pega a timeline pré-computada (posts dos que você segue
     que usam PUSH — a maioria)                    [push]
  2. busca os posts recentes das CELEBRIDADES que você segue,
     do cache delas (poucas fontes, barato)        [pull]
  3. MESCLA + ordena/ranqueia as duas fontes
  4. retorna

→ O usuário típico: leitura quase O(1) (push) + um punhado de
  pulls de celebridades (barato, poucas). O melhor dos dois:
  leitura rápida para a maioria, sem o colapso de write das
  celebridades.
O híbrido aplica cada estratégia onde ela funciona — não há solução única
A solução híbrida é a lição mais importante deste capítulo, e ela transcende o feed: não há uma estratégia única que sirva a todos os casos; a resposta é aplicar cada estratégia onde ela funciona. O fan-out on write é ótimo para o usuário típico (leitura rápida, write amplification gerenciável) e péssimo para a celebridade (milhões de writes). O fan-out on read é ótimo para a celebridade (um write, os seguidores puxam) e péssimo para o usuário típico (leitura cara). Então a solução não escolhe um — ela usa ambos, roteando por um limiar de seguidores: abaixo do limiar (a vasta maioria), push; acima (as poucas celebridades), pull. Na leitura, mescla a timeline pré-computada (dos que usam push) com os posts puxados das poucas celebridades que você segue — e esse merge é barato, porque você segue poucas celebridades (um punhado de pulls, não centenas). O resultado captura o melhor dos dois mundos: a leitura rápida do push para 99% das fontes, sem o colapso de write do push para as celebridades. Isto requer infraestrutura extra (um serviço que classifica usuários por número de seguidores, um roteador que decide push ou pull por post, uma lógica de merge que combina e deduplica as duas fontes na leitura), mas é o preço de lidar corretamente com a distribuição desigual de seguidores. A generalização, que você levará para os outros casos da Parte V: quando uma população tem uma distribuição muito desigual (a maioria normal, alguns extremos), frequentemente a melhor arquitetura trata os dois grupos com estratégias diferentes, em vez de forçar uma só. O híbrido não é um meio-termo morno; é a aplicação precisa de cada ferramenta onde ela é a certa.

21.7 Do cronológico ao ranqueado

Até aqui assumimos um feed cronológico (mais recente primeiro). Os feeds modernos são ranqueados: ordenados por relevância prevista, não só por tempo. Isso adiciona uma camada, mas se encaixa na arquitetura.

Ranqueamento: trocar a ordenação por tempo por uma pontuação de relevância
Um feed cronológico ordena por tempo; um feed ranqueado ordena por uma pontuação de relevância prevista — quão provável é que você se interesse por cada post. A pontuação combina sinais como o engajamento esperado (likes, comentários prováveis), a recência (posts novos pesam mais), a afinidade (quão próximo você é do autor), e a diversidade (evitar inundar com um só autor). Na arquitetura, isso se encaixa como uma camada sobre o que já temos: em vez de a timeline ser ordenada por timestamp puro, a entrada de cada post recebe uma pontuação, e um serviço de ranking (frequentemente um modelo de machine learning, mais heurísticas) reordena. Há uma escolha de quando ranquear: ranquear na escrita (computar a pontuação ao fazer o fan-out) é rápido na leitura mas a pontuação envelhece; ranquear na leitura (pontuar ao montar o feed) é mais fresco mas mais caro; um meio-termo comum é re-pontuar periodicamente as entradas da timeline. O ranqueamento adiciona complexidade real (o modelo, os sinais, a infraestrutura de features), mas é ortogonal à decisão central de fan-out — você primeiro resolve como entregar os posts aos feeds (push/pull/híbrido) e depois como ordená-los (cronológico/ranqueado). Para o nosso projeto, o importante é reconhecer que o ranking é uma camada adicional sobre a arquitetura de entrega, não uma mudança nela — a estrutura híbrida permanece; muda o critério de ordenação dentro dela.

21.8 A arquitetura completa

Juntando tudo, a arquitetura do feed costura quase todos os conceitos do livro. Vamos vê-la de ponta a ponta, do post à exibição.

A arquitetura do feed, de ponta a ponta
O caminho da escrita (postar)
postar um post
1. Cliente → API Gateway (autentica, rate limit — Cap 19)
2. → Serviço de Post: grava o post (banco sharded por id —
   Cap 13) e no cache de posts (Cap 10)
3. → publica evento "post criado" no Kafka (Cap 11) — a API
   responde "postado!" JÁ (não espera o fan-out — assíncrono)
4. Workers de Fan-out (consumidores do Kafka) processam:
   - se autor normal: push do post para as timelines dos
     seguidores (sorted sets no Redis)
   - se celebridade: só atualiza o cache da celebridade
     (sem fan-out — o problema da celebridade)
   - fan-out é IDEMPOTENTE (Cap 16) — reentregas não duplicam
O caminho da leitura (abrir o feed)
carregar o feed
1. Cliente → API Gateway → Serviço de Feed
2. pega a timeline pré-computada do usuário (Redis sorted
   set — os que ele segue que usam push)        [push, O(1)]
3. busca os posts recentes das celebridades que ele segue,
   do cache delas (poucas, barato)              [pull]
4. MESCLA + ranqueia (Cap 21.7) as duas fontes, deduplica
5. busca os detalhes dos posts em lote (batch do cache de
   posts — Cap 10, com TTL curto contra thundering herd de
   virais; request collapse)
6. retorna a página + cursor (paginação por cursor)
As otimizações e a resiliência
o que torna robusto e eficiente
CACHE (Cap 10): timelines em Redis; posts em cache com TTL
  curto. Um post viral = 1 leitura do DB + N cache hits
  (request collapse contra thundering herd).

INATIVOS: não manter timeline pré-computada de quem não abre
  o app (desperdício). No primeiro acesso após inatividade,
  reconstruir via pull, depois retomar o push.

SHARDING (Cap 13): timelines particionadas por user_id
  (consistent hashing). Posts por post_id.

DEGRADAÇÃO (Cap 20): se o ranking falha → cai para
  cronológico. Se o cache cai → page menor, sem ranking.
  Servir um feed degradado é melhor que erro.

FAN-OUT ASSÍNCRONO (Cap 11): nunca bloquear a API no fan-out.
  Workers, filas, retries idempotentes.
Os conceitos do livro, todos aqui

Note quantos capítulos se encontram nesta única arquitetura: a assimetria leitura/escrita (Cap 2) que define a estratégia; as estimativas (Cap 3) que a dimensionam; a consistência eventual (Cap 6 — seu feed não precisa ser perfeitamente atualizado no milissegundo); o cache (Cap 10); as filas e o fan-out assíncrono (Cap 11); o sharding e a hot key da celebridade (Cap 13); o fan-out síncrono vs assíncrono (Cap 15); a idempotência do fan-out (Cap 16); a degradação graciosa (Cap 20). Um feed não é um conceito — é a combinação de uma dúzia deles.

Resultado: o feed, que parecia um simples "mostre os posts de quem eu sigo", revelou-se um exercício de combinar quase todo o livro. A jornada de design — do pull ingênuo (otimiza o lado errado) ao push (inverte o custo para o lado favorável da assimetria) ao problema da celebridade (a hot key que quebra o push) à solução híbrida (cada estratégia onde funciona) — é o próprio método de projetar sistemas: reconhecer o que o sistema faz mais (ler), otimizar para isso (pré-computar na escrita), encontrar onde a otimização quebra (a celebridade), e tratar o caso extremo separadamente (híbrido). A lição que este primeiro caso da Parte V estabelece, e que os próximos confirmarão: projetar um sistema real não é aplicar um padrão, mas combinar os padrões certos sob as restrições do problema — e a arte está em saber qual combinação cada problema exige. O próximo caso muda completamente as restrições: pagamentos, onde a correção e a auditoria importam mais que a velocidade, e onde perder ou duplicar uma operação é inaceitável. Da escala read-heavy do feed à exatidão inegociável do dinheiro.

21.9 Erros comuns

Erro 1 · Fan-out on read como solução geral

Montar o feed na leitura (pull) para todos — otimizando a escrita (rara) e punindo a leitura (100x mais frequente). Para um feed read-heavy, o pull geral falha porque torna lento o caso comum. Use push (pré-computar na escrita) para a maioria; o pull tem seu lugar (celebridades, inativos), não como estratégia geral.

Erro 2 · Fan-out on write puro (sem tratar celebridades)

Usar push para todos, inclusive celebridades — e ver um post de 50M seguidores disparar 50M writes, saturando o pipeline e atrasando o fan-out de todos. Trate as celebridades separadamente (pull): o push puro colapsa na hot key. A distribuição desigual de seguidores exige estratégias diferentes para os dois grupos.

Erro 3 · Fan-out síncrono na API de postar

Bloquear a resposta de "postar" até o fan-out terminar — fazendo o usuário esperar centenas (ou milhões) de writes. O fan-out deve ser assíncrono (Cap 11): grave o post, responda já, e faça o fan-out em workers em segundo plano. Nunca bloqueie a API no fan-out.

Erro 4 · Manter timeline de usuários inativos

Fazer push para os seguidores que nunca abrem o app — desperdiçando writes e armazenamento em timelines que ninguém lê. Não pré-compute o feed de inativos; reconstrua via pull no primeiro acesso após a inatividade, e retome o push depois. Não gaste computação em quem não lê.

Erro 5 · Ignorar o thundering herd de um post viral

Deixar um post viral causar N leituras ao banco quando N seguidores o carregam ao mesmo tempo — martelando o DB. Cache o post individual com TTL curto e use request collapse (Cap 10): um post viral vira 1 leitura do DB + N cache hits. O viral é uma hot key na leitura, além da escrita.

Erro 6 · Não degradar quando o ranking ou o cache falha

Retornar erro quando o serviço de ranking ou o cache de timeline falha — em vez de servir um feed degradado. Degrade (Cap 20): sem ranking, caia para cronológico; sem cache, sirva uma página menor. Um feed um pouco pior é muito melhor que nenhum feed.

Erro 7 · Tratar o feed como um padrão único

Procurar "a" solução do feed (só push, ou só pull, ou só uma técnica) — quando a resposta certa é a combinação (híbrido push/pull + cache + filas + sharding + degradação). Projetar um sistema real é combinar os padrões certos sob as restrições, não aplicar um padrão isolado. A arte está na combinação.

Verifique seu entendimento
Você projeta um feed social. O fan-out on write (push) torna as leituras rápidas, mas uma celebridade com 50 milhões de seguidores, ao postar, dispara 50 milhões de writes, saturando o pipeline e atrasando o feed de todos. Qual é a melhor solução?

21.10 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Push, pull ou híbrido?

Para cada cenário, decida se push (fan-out on write), pull (fan-out on read) ou híbrido é mais apropriado, e por quê: (a) um usuário comum com 300 seguidores ativos; (b) uma celebridade com 40 milhões de seguidores; (c) um usuário que segue 5.000 contas e abre o app o dia todo; (d) uma plataforma inteira com usuários comuns e celebridades misturados.

(a) Usuário comum, 300 seguidores → push. O fan-out na escrita é gerenciável (300 writes por post), e torna a leitura dos seguidores instantânea. Para o usuário típico, push é ideal: o write amplification é baixo e a leitura fica O(1).

(b) Celebridade, 40M seguidores → pull. Push aqui dispararia 40 milhões de writes por post — o problema da celebridade, que satura o pipeline. Com pull, o post é gravado uma vez, e os seguidores o puxam na leitura. Para contas com seguidores massivos, pull evita o colapso de write.

(c) Usuário que segue 5.000 contas, lê o dia todo → push (ele como leitor). Cuidado com a perspectiva: a estratégia push/pull é decidida pelo autor (quantos seguidores ele tem), não pelo leitor. Este usuário, como leitor de 5.000 contas, se beneficia enormemente do push: sua timeline já vem pré-computada (das fontes que usam push), então ele lê rápido apesar de seguir muitos. Se fosse pull, ele teria que mesclar 5.000 timelines na leitura — inviável. (Ele puxa só as poucas celebridades entre as 5.000.) O push protege justamente o leitor que segue muitos.

(d) Plataforma inteira, comuns e celebridades misturados → híbrido. Como há os dois tipos, nenhuma estratégia única serve. O híbrido roteia por número de seguidores: push para os comuns (a maioria), pull para as celebridades (os poucos extremos), mesclando na leitura. É o padrão de produção (Twitter/X, Instagram, Facebook).

O insight: a estratégia se decide pelo perfil do autor (push se tem poucos seguidores, pull se tem milhões), e a plataforma inteira usa híbrido porque tem os dois. Note a sutileza de (c): o leitor que segue muitos é bem servido pelo push (das fontes), porque a estratégia é do autor, não do leitor — o push protege tanto quem posta para poucos quanto quem lê de muitos.

Médio
Exercício 2 · As estimativas que justificam o design

Usando ordens de grandeza, mostre por que a assimetria leitura/escrita de um feed justifica pagar caro na escrita (fan-out on write) para otimizar a leitura. Suponha 200M usuários ativos/dia, cada um seguindo ~200 contas, abrindo o feed ~15x/dia, e postando ~2x/dia. Calcule e interprete.

Os cálculos (ordem de grandeza):

estimativas do feed
ESCRITAS (posts):
  200M usuários × 2 posts/dia = 400M posts/dia
  400M / 86.400s ≈ 4.600 posts/s  (a "escrita")

LEITURAS (aberturas de feed):
  200M usuários × 15 aberturas/dia = 3B aberturas/dia
  3B / 86.400s ≈ 35.000 leituras/s  (a "leitura")

RAZÃO leitura:escrita = 35.000 / 4.600 ≈ 7,5... por usuário,
  mas o que importa é: 3B leituras vs 400M escritas ≈ 7,5:1
  (e em plataformas reais, com lurkers que só leem, chega
   a 100:1)

CUSTO DO FAN-OUT ON WRITE:
  400M posts × 200 seguidores = 80 BILHÕES de writes de
  timeline/dia ≈ 925.000 writes/s
  → muito, mas assíncrono e distribuível

A interpretação: há ~7,5 leituras para cada escrita (e em plataformas com muitos "lurkers" que só leem, a razão real chega a 100:1). Isso significa que cada otimização da leitura é "amortizada" sobre 7,5+ leituras, enquanto cada custo adicionado à escrita é pago apenas uma vez. Logo, vale muito a pena pagar caro na escrita (o fan-out, 80 bilhões de writes/dia) se isso torna a leitura O(1): você gasta uma vez na escrita para economizar em 7,5+ leituras. Se fizéssemos o contrário (pull — barato na escrita, caro na leitura), pagaríamos o custo alto da montagem do feed 7,5+ vezes mais frequentemente. A assimetria é a justificativa matemática do fan-out on write: otimize a operação frequente, mesmo às custas da rara.

A ressalva (que leva ao híbrido): os 80 bilhões de writes/dia são gerenciáveis se distribuídos entre usuários com contagens normais de seguidores. Mas se incluíssem celebridades (um post = dezenas de milhões de writes), o cálculo explodiria — daí o híbrido (pull para celebridades). As estimativas justificam o push para a maioria e revelam por que as celebridades precisam de tratamento à parte.

O insight: as estimativas (Cap 3) não são um ritual — elas justificam a decisão de design. O número "7,5:1 (ou 100:1)" é o que torna o fan-out on write a escolha certa, e o número "dezenas de milhões de writes por post de celebridade" é o que torna o híbrido necessário. Projetar bem é deixar os números guiarem a arquitetura.

Médio
Exercício 3 · Onde cada conceito do livro aparece

A arquitetura do feed combina muitos conceitos das Partes I-IV. Para cada um, explique onde e por que ele aparece no design do feed: (a) consistência eventual (Cap 6); (b) cache (Cap 10); (c) filas/mensageria (Cap 11); (d) idempotência (Cap 16); (e) degradação graciosa (Cap 20).

(a) Consistência eventual (Cap 6): o feed não precisa (nem pode, em escala) ser perfeitamente atualizado no milissegundo. Quando alguém posta, o fan-out leva um tempo para chegar a todas as timelines (especialmente as de celebridades, via pull, ou sob carga). Aceitar que seu feed reflita os posts com um pequeno atraso — consistência eventual — é o que torna a escala possível. Exigir consistência forte (todo post instantaneamente em todos os feeds) seria inviável. O feed é um caso onde a consistência eventual é perfeitamente aceitável (ninguém morre se um post aparece 2 segundos depois).

(b) Cache (Cap 10): onipresente. As timelines pré-computadas vivem num cache (Redis sorted sets) para leitura O(1). Os detalhes dos posts são cacheados (com TTL curto) para que um post viral seja 1 leitura do DB + N cache hits, não N leituras (request collapse contra o thundering herd). O cache é o que torna a leitura rápida o suficiente para o volume massivo.

(c) Filas/mensageria (Cap 11): o fan-out é assíncrono, via fila. Ao postar, publica-se um evento "post criado" no Kafka, a API responde imediatamente, e workers consumem o evento e fazem o fan-out em segundo plano. A fila desacopla o postar (rápido, síncrono) do fan-out (pesado, assíncrono) e absorve picos — sem ela, postar bloquearia esperando centenas/milhões de writes.

(d) Idempotência (Cap 16): o fan-out roda em workers consumindo uma fila at-least-once (Cap 11), que pode reentregar mensagens. Sem idempotência, um post poderia ser adicionado duas vezes à timeline de um seguidor (post duplicado no feed). O fan-out precisa ser idempotente — adicionar o mesmo post à timeline duas vezes tem o efeito de uma — para que reentregas não dupliquem.

(e) Degradação graciosa (Cap 20): se o serviço de ranking falha, o feed cai para ordenação cronológica (pior, mas funcional). Se o cache de timeline cai, serve-se uma página menor ou reconstrói-se via pull. Servir um feed degradado (sem ranking, ou menor) é muito melhor que uma tela de erro. O feed é cheio de oportunidades de degradação, porque tem muitas partes não-críticas (ranking, recomendações) sobre um core (mostrar posts).

O insight: o feed é um microcosmo do livro inteiro — quase todo conceito das Partes I-IV reaparece, combinado. É por isso que "projetar um feed" é um exercício tão rico: não testa um conceito, testa a capacidade de combinar uma dúzia deles sob as restrições de um problema real.

Difícil
Exercício 4 · Projetar o feed do zero — entrevista

O entrevistador pede: "projete o feed de uma rede social com centenas de milhões de usuários". Conduza o design do começo ao fim — requisitos, estimativas, a jornada pull → push → celebridade → híbrido, ranking, e a arquitetura completa —, demonstrando que você combina os padrões certos e critica o próprio design.

Enquadramento: conduzo o design na ordem que um bom projetista segue — esclarecer requisitos, dimensionar, e então deixar a jornada de design emergir das restrições, criticando cada passo. (E, como diz o Cap 21, a força está em nomear as limitações do próprio design.)

1. Requisitos (Cap 2): funcionais — postar, ver o feed dos seguidos, seguir/deixar de seguir. Não-funcionais — leitura de baixa latência (o feed tem que abrir rápido), alta disponibilidade, escala para centenas de milhões. Esclareço: feed cronológico ou ranqueado? (assumo que evoluirá para ranqueado). Tolera consistência eventual? (sim — um post pode aparecer com segundos de atraso).

2. Estimativas (Cap 3): ~300M DAU, ~200 seguidos cada, ~10-15 aberturas/dia, ~500M posts/dia. Resultado: ~5.000-6.000 escritas/s, ~35.000+ leituras/s, razão ~100:1. Conclusão que guia tudo: massivamente read-heavy → otimizar a leitura vale pagar na escrita.

3. A jornada de design (deixo emergir das restrições):

  • Pull (ingênuo): montar o feed na leitura. Barato na escrita, mas caro na leitura — e a leitura é 100x mais frequente. Otimiza o lado errado. Descarto como solução geral.
  • Push: pré-computar a timeline na escrita (fan-out), leitura O(1). A troca certa para a assimetria. Mas...
  • O problema da celebridade: push puro colapsa quando uma conta com 50M seguidores posta (50M writes). A hot key (Cap 13).
  • Híbrido (a solução): push para a maioria (abaixo de ~100K seguidores), pull para as celebridades, merge na leitura. Cada estratégia onde funciona.

4. A arquitetura: postar → API (auth, rate limit) → grava post (sharded) + publica evento no Kafka → API responde já → workers de fan-out (push para normais, atualiza cache para celebridades), idempotentes. Ler → pega a timeline pré-computada (Redis) + puxa as celebridades seguidas + mescla/ranqueia + busca detalhes dos posts (cache com TTL curto) + pagina por cursor. Cache em toda parte (Cap 10), fan-out assíncrono (Cap 11), sharding por user_id (Cap 13).

5. Ranking: camada sobre a entrega — pontuação por engajamento + recência + afinidade + diversidade, re-pontuada periodicamente. Ortogonal ao fan-out.

6. Degradação e casos extremos (Cap 20): ranking falha → cronológico; cache cai → página menor; post viral → cache + request collapse; inativos → não pré-computar, rebuild via pull no retorno.

7. Critico o próprio design (o que impressiona): o híbrido adiciona complexidade (classificar usuários, rotear, merge com dedup); o threshold de seguidores é um parâmetro a tunar; a consistência eventual significa que feeds não são idênticos no instante; o ranking introduz uma dependência de ML com seus próprios modos de falha. Nenhum design é perfeito — nomear isso mostra maturidade.

Frase de fechamento: "eu projeto a partir da assimetria: o feed lê ~100x mais do que escreve, então pré-computo na escrita (fan-out on write) para a leitura ser O(1). Isso funciona para a maioria, mas colapsa nas celebridades — 50 milhões de writes por post —, então uso híbrido: push para os comuns, pull para as celebridades, merge na leitura. Em cima, cache para a velocidade, filas para o fan-out assíncrono, sharding para a escala, e degradação graciosa para a resiliência. O ranking é uma camada sobre a entrega. E eu reconheço os custos: o híbrido é complexo, o threshold precisa de tuning, e a consistência é eventual. A lição que eu carrego é que projetar um feed não é escolher um padrão — é combinar os certos sob as restrições, tratando o caso comum e o extremo com estratégias diferentes."

Fim do capítulo 21
Próximo caso: projetar um sistema de pagamentos. O feed era sobre escala read-heavy e tolerava consistência eventual (um post atrasado não é grave). Pagamentos invertem as prioridades: a correção e a auditoria são inegociáveis, perder ou duplicar uma operação é inaceitável, e a consistência forte importa mais que a velocidade. Veremos a idempotência (Cap 16), a Saga (Cap 17), o Event Sourcing (Cap 18) e o ledger se combinarem num sistema onde cada centavo precisa estar certo. Da escala do feed à exatidão do dinheiro.
Parte V · Capítulo 22 · Os sistemas inteiros

Projetar
pagamentos.

Se o feed era sobre escala e tolerava imperfeição (um post atrasado não é grave), os pagamentos invertem tudo: aqui, a correção é inegociável. Cobrar um cliente duas vezes, perder uma transação, ou ter um saldo errado por um centavo não são bugs toleráveis — são falhas inaceitáveis, com consequências legais e de confiança. Um sistema de pagamentos é onde a consistência forte (Cap 6), a idempotência (Cap 16), a Saga (Cap 17) e o ledger imutável (Cap 18) deixam de ser opções e viram requisitos. Este segundo caso da Parte V mostra como projetar para a exatidão — onde a pergunta não é "quão rápido?", mas "quão certo?".

Este capítulo projeta um sistema de pagamentos. Vamos ver o que torna o dinheiro diferente (a correção acima de tudo), o ledger de partida dobrada (a estrutura imutável e auto-verificável que registra cada movimento), como representar dinheiro sem erros de arredondamento, a idempotência que impede cobranças duplas (Cap 16, aprofundada para o ledger), o fluxo de um pagamento como uma Saga (Cap 17), o problema do mundo externo (processadores que você não controla) e a reconciliação, os estornos que corrigem sem apagar, e a arquitetura completa. Quando você sair daqui, terá visto como as restrições mudam tudo: um sistema com os mesmos componentes do feed, mas com prioridades opostas (correção sobre velocidade), produz uma arquitetura completamente diferente. É a lição central da Parte V — o problema dita o projeto.

22.1 O problema dos pagamentos — correção acima de tudo

O que define um sistema de pagamentos não é a escala (embora ela importe), mas a correção absoluta. No feed, um erro ocasional é invisível; em pagamentos, um erro é dinheiro perdido, um cliente cobrado a mais, uma auditoria que não fecha. Essa inversão de prioridades muda cada decisão de design.

Em pagamentos, perder ou duplicar uma operação é inaceitável — não é um trade-off
O feed nos ensinou a aceitar imperfeições em troca de escala — consistência eventual, um post atrasado, um feed levemente diferente para cada um. Pagamentos invertem essa lógica: aqui, a correção não é negociável. Cobrar um cliente duas vezes pela mesma compra, perder o registro de uma transação, ou ter um saldo que não bate por um centavo são falhas inaceitáveis — não "bugs toleráveis sob escala", mas erros com consequências legais, regulatórias e de confiança que podem destruir um negócio. Isso reorienta cada decisão de design em torno de três exigências que, no feed, eram opcionais e aqui são absolutas: consistência forte (o saldo precisa estar certo agora, não "eventualmente" — daí bancos relacionais ACID, Cap 12, para o dinheiro); exactly-once no efeito (cada intenção de pagamento resulta em exatamente uma cobrança, nunca zero, nunca duas — daí a idempotência, Cap 16, como requisito, não otimização); e auditabilidade total (cada centavo movimentado precisa de um rastro imutável de o quê, quando e por quê — daí o ledger, Cap 18). Note como as prioridades se invertem: no feed, otimizamos velocidade e toleramos erro; em pagamentos, otimizamos correção e toleramos (dentro do razoável) latência. O mesmo engenheiro, os mesmos componentes do livro — mas porque o problema mudou (de "escalar leituras" para "nunca errar com dinheiro"), a arquitetura resultante é radicalmente diferente. Esta é a lição que a Parte V martela: não existe "a arquitetura certa" em abstrato; existe a arquitetura certa para as restrições deste problema. E a restrição dominante dos pagamentos é a correção.

22.2 O ledger de partida dobrada — a verdade imutável

No coração de todo sistema de pagamentos sério está o ledger (livro-razão): um registro append-only e imutável de cada movimento de dinheiro. E não um ledger qualquer — um de partida dobrada (double-entry), a técnica contábil de séculos que torna o ledger auto-verificável.

Partida dobrada: cada movimento tem dois lados que se equilibram
O ledger de partida dobrada é uma das ideias mais robustas da história — a contabilidade a usa há mais de 500 anos, e ela é perfeita para sistemas de pagamento. O princípio: todo movimento de dinheiro é registrado em dois lados que se equilibram — um débito de uma conta e um crédito correspondente em outra, de valor igual. Quando você paga R$100 a um comerciante, não se registra "saldo do comerciante += 100" (uma mudança solta); registra-se um par: débito de R$100 da sua conta e crédito de R$100 na conta do comerciante. A soma de todos os débitos sempre iguala a soma de todos os créditos — o ledger se equilibra, sempre. Essa propriedade é poderosíssima por três razões. Primeiro, auto-verificação: se a soma dos débitos não bate com a dos créditos, há um erro — o desequilíbrio denuncia a inconsistência, que de outra forma passaria despercebida. Segundo, nenhum dinheiro aparece ou some do nada: como todo crédito vem de um débito, o dinheiro só se move entre contas, nunca é criado nem destruído por engano (o que num sistema de "atualize o saldo" pode acontecer com um bug). Terceiro, rastreabilidade: cada movimento é um par explícito, ligando origem e destino. E o ledger é append-only e imutável (Cap 14, Cap 18): você nunca edita uma entrada (não muda "R$100" para "R$80"); você adiciona uma entrada corretiva. Isso preserva o histórico completo — a base da auditabilidade. O ledger de partida dobrada não é "uma opção de design" para pagamentos; é a fundação, porque ele torna a correção verificável (o equilíbrio) e o histórico imutável (append-only) — exatamente as duas propriedades que o dinheiro exige.

22.3 Como representar dinheiro — nunca com double

Um detalhe que parece trivial e causa desastres: como você armazena um valor monetário. A resposta tem uma regra absoluta — nunca use ponto flutuante (double/float) para dinheiro.

Ponto flutuante não representa dinheiro exatamente — use inteiros ou decimais
Parece detalhe de implementação, mas é uma fonte clássica de bugs caros: números de ponto flutuante (float, double) não representam valores decimais exatamente. O clássico: 0.1 + 0.2 em ponto flutuante não dá exatamente 0.3 — dá 0.30000000000000004, por causa de como os computadores representam frações em binário. Para a maioria das aplicações, esse erro minúsculo é irrelevante. Para dinheiro, é inaceitável: erros de arredondamento se acumulam, centavos aparecem e somem, e o ledger não fecha (a soma dos débitos não bate com a dos créditos por causa de imprecisões de float). A regra absoluta: nunca represente dinheiro com ponto flutuante. Em vez disso, use inteiros na menor unidade (ex.: armazene R$10,00 como 1000 centavos — um inteiro exato, sem fração) ou tipos decimais de precisão arbitrária (decimal/BigDecimal, que representam decimais exatamente). Muitos sistemas até transmitem valores monetários como string nas APIs, para evitar que diferentes linguagens/protocolos os interpretem com precisões diferentes (causando erros de arredondamento na serialização). É um detalhe pequeno com consequências enormes: um sistema de pagamentos construído sobre ponto flutuante vai, mais cedo ou mais tarde, ter um ledger que não fecha por causa de um centavo fantasma — e num sistema onde a correção é tudo, isso é um defeito de fundação. Inteiros (centavos) ou decimais exatos, nunca float. A correção começa na representação do número.

22.4 A idempotência do pagamento — nunca cobrar duas vezes

O Capítulo 16 estabeleceu a idempotência como fundação; em pagamentos, ela é literalmente o que impede cobranças duplas. Vale ver como ela se aplica especificamente ao ledger, porque há uma sutileza: a idempotência precisa proteger não só a API, mas a criação das entradas do ledger.

A idempotência protege a API e o ledger — uma intenção, um efeito financeiro
A idempotência em pagamentos é o Cap 16 aplicado com rigor máximo, porque o efeito a proteger é o mais sensível: o dinheiro. O padrão (o da Stripe): o cliente gera uma chave de idempotência por intenção de pagamento e a envia; o servidor guarda (merchant_id, idempotency_key) → (hash do request, resposta, status). A mesma chave com o mesmo payload retorna a mesma resposta sem reprocessar (não cobra de novo); a mesma chave com payload diferente retorna um erro (409 Conflict — a chave foi reusada para uma operação diferente, o que é um erro do cliente). Mas há uma sutileza crucial para pagamentos: a idempotência precisa garantir não só que a resposta da API não seja duplicada, mas que as entradas do ledger sejam criadas exatamente uma vez. Mesmo que a resposta da API seja retentada, mesmo que um webhook chegue duas vezes, mesmo que um consumidor de fila reprocesse a mensagem (Cap 11, at-least-once) — o ledger deve registrar o movimento uma única vez. Isso se consegue tornando a criação do pagamento e o registro de idempotência atômicos (Cap 16): a verificação da chave, a criação das entradas do ledger e a gravação da chave acontecem numa única transação (com uma constraint de unicidade na chave, ou INSERT ... ON CONFLICT), de modo que retries concorrentes (duas tentativas idênticas chegando juntas) não criem entradas duplicadas — só uma vence, a outra recebe a resposta gravada. E como sempre (Cap 16): exactly-once no efeito é alcançado por at-least-once + idempotência, não por "networking mágico exactly-once". A entrega vai duplicar (a rede, os webhooks, as filas); a idempotência é o que colapsa todas as duplicatas num único efeito financeiro. Em pagamentos, essa não é uma otimização — é o que separa um sistema confiável de um que cobra clientes em dobro.

22.5 O fluxo de um pagamento — uma Saga

Um pagamento real raramente é uma única operação atômica — ele atravessa vários serviços e estados (validar, autorizar, capturar, registrar no ledger, notificar). E como cruza serviços, com a possibilidade de falha em cada passo, ele é uma Saga (Cap 17).

o pagamento como uma Saga
Um pagamento atravessa estados, em vários serviços:

  1. Validar (fundos, fraude, dados)        [serviço de risco]
  2. Autorizar com o processador externo     [PSP externo]
  3. Capturar o valor                        [PSP externo]
  4. Registrar no ledger (débito/crédito)    [serviço de ledger]
  5. Atualizar o saldo da carteira           [serviço de wallet]
  6. Notificar (recibo, webhook)             [assíncrono]

Cada passo é uma transação local; juntos, não há ACID global
(Cap 17). Se um passo falha (ex.: a captura falha após a
autorização), COMPENSAÇÕES desfazem os anteriores (ex.: liberar
a autorização). É uma SAGA — preferencialmente ORQUESTRADA
(Cap 17), porque pagamentos exigem visibilidade e controle do
fluxo (em que passo travou? o que compensar?) e auditoria.

E cada passo é IDEMPOTENTE (Cap 16) — retries não duplicam.
O estado do pagamento é rastreado (PENDING → AUTHORIZED →
CAPTURED → SETTLED, ou → FAILED com compensações).
Pagamento = Saga orquestrada de passos idempotentes, com estado rastreado
O fluxo de um pagamento reúne a Parte IV inteira. Ele cruza serviços (risco, processador externo, ledger, carteira, notificação), então não há uma transação ACID global que o cubra (Cap 17) — é uma Saga, uma sequência de transações locais com compensações. E é uma Saga orquestrada (não coreografada), porque pagamentos exigem exatamente o que a orquestração oferece (Cap 17): visibilidade (um orquestrador que sabe em que estado cada pagamento está — crucial para suporte, auditoria, e debugar pagamentos travados), controle da compensação (quando um passo falha, o orquestrador sabe quais passos anteriores desfazer e em que ordem), e um registro do estado (o pagamento progride por estados explícitos — pendente, autorizado, capturado, liquidado, ou falho — que precisam ser rastreáveis). Cada passo da Saga é idempotente (Cap 16), porque os retries são inevitáveis e não podem duplicar o efeito financeiro. E as compensações são semânticas (Cap 17): "estornar uma cobrança" não apaga a cobrança, adiciona um estorno (o que, como veremos, é exatamente como o ledger imutável funciona). Note como os conceitos se encaixam: a Saga (Cap 17) dá a estrutura do fluxo, a idempotência (Cap 16) protege cada passo, o ledger (Cap 18) registra os efeitos imutavelmente, e a orquestração dá a visibilidade que o dinheiro exige. Um pagamento não é uma operação — é um fluxo coordenado, observável e reversível, e cada uma dessas propriedades vem de um padrão da Parte IV.

22.6 O mundo externo e a reconciliação

Um sistema de pagamentos não vive sozinho — ele depende de processadores externos (PSPs, bandeiras de cartão, bancos) que você não controla. Isso introduz um problema profundo: como garantir que o seu registro do que aconteceu bate com o deles? A resposta é a reconciliação.

Não confie no externo — reconcilie o seu ledger com o deles
A fronteira com o mundo externo é onde a correção fica mais difícil. Quando você pede a um processador externo (PSP) para capturar R$100, e a rede falha antes de você receber a confirmação (Cap 8), você fica na dúvida: a captura aconteceu (e a confirmação se perdeu) ou não? A idempotência ajuda — se o PSP suporta chaves de idempotência, você retenta com a mesma chave, e ele não captura duas vezes. Mas há uma verdade incômoda: você não pode assumir que o sistema externo está sempre certo, nem que a sua visão e a dele estão sincronizadas. Webhooks se perdem ou chegam duplicados; confirmações se atrasam; estados divergem. A defesa definitiva é a reconciliação: um processo que, assincronamente e periodicamente, compara o seu registro de transações (seu ledger) com o registro do processador externo (os arquivos de liquidação, os relatórios do PSP) e verifica que batem — identificando e investigando qualquer discrepância (uma transação que você acha que capturou mas o PSP não tem, ou vice-versa). A reconciliação é a rede de segurança que pega o que a idempotência e os webhooks deixam passar: ela não confia em nenhum sistema isoladamente, ela cruza as duas fontes da verdade e exige que concordem. Em pagamentos sérios, a reconciliação é tão fundamental quanto o ledger — porque a correção não é só "o meu sistema está certo internamente", é "o meu sistema concorda com o mundo externo sobre o que realmente aconteceu com o dinheiro". E quando não concordam, a reconciliação é o que detecta, para que um humano investigue antes que vire um problema. Nunca assuma que o externo está certo; reconcilie e verifique.

22.7 Estornos — corrigir sem apagar

Erros, reembolsos e chargebacks são inevitáveis. A questão é como corrigi-los — e a resposta, herdada do ledger imutável, é a mesma da compensação da Saga (Cap 17): você nunca apaga nem edita; você adiciona uma entrada que reverte.

Reembolsos e correções são novas entradas reversas — o histórico nunca muda
Como o ledger é imutável (append-only), você nunca corrige um erro editando uma entrada existente — não muda "R$100" para "R$80", não deleta uma transação errada. Em vez disso, você adiciona uma entrada que reverte ou corrige. Um reembolso não apaga a cobrança original; é uma nova transação (com seus próprios débito e crédito, na direção oposta) que se liga à original. Um estorno de um lançamento errado é uma entrada reversa explícita (o negativo do original), deixando ambos — o erro e a correção — no histórico. Um chargeback (disputa) é igualmente uma nova entrada que reverte o fluxo. Por que essa disciplina? Porque ela preserva o histórico completo e auditável: meses depois, numa auditoria ou disputa, você pode ver exatamente o que aconteceu — a cobrança, e depois o reembolso, e quando e por quê cada um — em vez de um estado final misterioso que não explica como se chegou ali. É a dualidade log-estado do Cap 14 e a imutabilidade do Event Sourcing do Cap 18, aplicadas ao dinheiro: o ledger é o log imutável de tudo que aconteceu, e o saldo é a derivação. Corrigir editando destruiria a auditoria (o registro mentiria sobre o passado); corrigir adicionando a preserva (o registro conta a história inteira, inclusive os erros e suas correções). E isso conecta com a compensação da Saga (Cap 17): compensar é adicionar uma ação que contrabalança, não apagar — exatamente o que um estorno no ledger faz. A regra de ouro do ledger: append, nunca edite. O passado é imutável; você só adiciona ao futuro.

22.8 A arquitetura completa

Juntando tudo, a arquitetura de pagamentos é construída inteira em torno da correção — e cada componente reflete uma exigência do dinheiro.

A arquitetura de pagamentos, de ponta a ponta
O caminho de um pagamento
processar um pagamento
1. Cliente → API (com Idempotency-Key — Cap 16)
2. Serviço de Pagamento: verifica a chave de idempotência
   (atômico). Já vista? → retorna a resposta gravada (não
   reprocessa). Nova? → continua.
3. Orquestrador da Saga (Cap 17) conduz:
   - Risco/fraude valida          (transação local)
   - Autoriza no PSP externo      (idempotente, com a chave)
   - Captura no PSP externo       (idempotente)
   - LEDGER: grava débito + crédito (partida dobrada,
     imutável, exatamente uma vez, na mesma transação da
     chave de idempotência — Cap 12, 16, 18)
   - Atualiza o saldo da carteira
   - se algum passo falha → COMPENSAÇÕES (estornos/liberações)
4. Notifica (recibo, webhook) — assíncrono (Cap 11)
5. Estado rastreado: PENDING → AUTHORIZED → CAPTURED → SETTLED
   (ou → FAILED, compensado)
As fundações de correção
o que garante a exatidão
LEDGER de partida dobrada (Cap 18): append-only, imutável,
  auto-verificável (débitos = créditos). A fonte da verdade
  do dinheiro. Banco ACID relacional (Cap 12) — consistência
  forte, não eventual.

DINHEIRO como inteiro (centavos) ou decimal — NUNCA float.

IDEMPOTÊNCIA (Cap 16): chave por intenção, dedup atômica,
  uma intenção = um efeito no ledger.

RECONCILIAÇÃO: processo assíncrono que cruza o ledger interno
  com os relatórios dos PSPs externos, detectando divergências
  para investigação humana. Não confia no externo.

ESTORNOS: novas entradas reversas, nunca edição. Histórico
  completo preservado para auditoria.
Por que esta arquitetura difere tanto do feed

Compare com o Cap 21. O feed usou consistência eventual, cache agressivo, fan-out assíncrono, e tolerou imperfeição pela escala. Pagamentos usam consistência forte (ACID), um ledger imutável e auto-verificável, idempotência rigorosa, reconciliação com o externo, e não toleram nenhum erro. Os componentes do livro são os mesmos (filas, idempotência, Saga, ACID), mas combinados de forma oposta, porque a restrição dominante é oposta: o feed otimiza velocidade e tolera erro; pagamentos otimizam correção e toleram latência. O problema ditou o projeto.

Resultado: a arquitetura de pagamentos é construída inteira em torno de uma única exigência — a correção absoluta — e cada componente a serve: o ledger de partida dobrada (Cap 18) que registra cada centavo de forma imutável e auto-verificável; a consistência forte (Cap 12, ACID) porque o saldo precisa estar certo agora; a idempotência (Cap 16) que garante uma intenção, um efeito; a Saga orquestrada (Cap 17) que coordena o fluxo com visibilidade e compensações; a reconciliação que cruza o interno com o externo; os estornos que corrigem sem apagar; e até a representação do dinheiro (inteiros, não float) que evita o centavo fantasma. A lição que este caso cristaliza, e que define a Parte V: o mesmo conjunto de componentes do livro, combinado sob restrições opostas às do feed, produz uma arquitetura oposta. Não há "a arquitetura certa" — há a arquitetura que as restrições do problema exigem, e a arte de projetar é deixar o problema ditar a combinação. O feed disse "rápido, tolere erro"; pagamentos dizem "certo, tolere latência" — e cada um produziu o sistema que sua restrição pedia. Próximo caso: notificações em escala, onde o desafio volta a ser o fan-out massivo (como o feed), mas agora com entrega a múltiplos canais, preferências, e a garantia de não notificar demais. Da exatidão do dinheiro à entrega em escala.

22.9 Erros comuns

Erro 1 · Representar dinheiro com ponto flutuante

Usar float/double para valores monetários — e acumular erros de arredondamento que fazem o ledger não fechar (centavos fantasmas). Use inteiros na menor unidade (centavos) ou decimais de precisão arbitrária, nunca float. A correção começa na representação do número.

Erro 2 · Atualizar saldos em vez de usar um ledger

Guardar só o saldo atual e atualizá-lo no lugar ("saldo += 100") — perdendo o histórico, a auditabilidade, e a auto-verificação. Use um ledger de partida dobrada (append-only, imutável, débitos = créditos): o saldo é derivado, o ledger é a verdade. Sem ledger, não há auditoria nem como provar como se chegou a um saldo.

Erro 3 · Editar ou deletar entradas para corrigir

Corrigir um erro editando uma entrada do ledger (mudar R$100 para R$80) ou deletando uma transação — destruindo a auditoria e o histórico. O ledger é imutável: corrija adicionando uma entrada reversa (estorno, reembolso). Append, nunca edite. O passado não muda; você só adiciona ao futuro.

Erro 4 · Idempotência só na API, não no ledger

Deduplicar a resposta da API mas não garantir que as entradas do ledger sejam criadas exatamente uma vez — e duplicar movimentos no ledger quando um webhook ou consumidor de fila reprocessa. A criação do pagamento e o registro de idempotência devem ser atômicos, garantindo uma intenção = um efeito no ledger. Proteja o ledger, não só a API.

Erro 5 · Consistência eventual para o saldo

Usar consistência eventual para o dinheiro (como no feed) — e ter saldos temporariamente errados, permitindo gastar o que não se tem. Pagamentos exigem consistência forte (ACID, Cap 12) para o ledger e o saldo: o dinheiro precisa estar certo agora, não "eventualmente". O que serve ao feed não serve ao saldo.

Erro 6 · Confiar cegamente no processador externo

Assumir que o seu registro e o do PSP externo estão sempre sincronizados — e não detectar divergências (uma captura que você acha que aconteceu mas o PSP não tem). Reconcilie: cruze periodicamente o seu ledger com os relatórios do externo, detectando discrepâncias para investigação. Não confie no externo; verifique.

Erro 7 · Pagamento como uma operação atômica única

Tratar o pagamento como uma única operação, ignorando que ele cruza serviços (risco, PSP, ledger, carteira) e pode falhar em cada passo. Modele-o como uma Saga orquestrada (Cap 17): passos locais idempotentes, com compensações, estado rastreado, e visibilidade. Um pagamento é um fluxo coordenado e reversível, não uma operação só.

Verifique seu entendimento
Você projeta o núcleo de um sistema de pagamentos que registra movimentos de dinheiro. Qual conjunto de decisões melhor garante a correção?

22.10 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Por que partida dobrada?

Compare duas formas de registrar uma transferência de R$50 de Alice para Bob: (a) atualizar "saldo de Alice -= 50" e "saldo de Bob += 50" como duas operações soltas; (b) registrar uma entrada de partida dobrada (débito de R$50 em Alice, crédito de R$50 em Bob). Que vantagens a partida dobrada oferece? O que pode dar errado em (a)?

O que pode dar errado em (a) — atualizações soltas: as duas operações ("Alice -= 50" e "Bob += 50") são separadas. Se um bug, uma falha, ou uma corrida fizer uma acontecer sem a outra (Alice é debitada mas Bob não é creditado, ou vice-versa), o dinheiro some ou aparece do nada — e nada no sistema detecta isso. O total do sistema muda sem motivo, mas não há um mecanismo que denuncie o desequilíbrio. Pior: se você guarda só os saldos atualizados, não há histórico de que a transferência aconteceu, nem como. Você fica com saldos que podem estar errados sem que ninguém saiba.

As vantagens da partida dobrada (b):

  • Auto-verificação: como cada movimento é um par débito/crédito de valor igual, a soma de todos os débitos sempre iguala a soma de todos os créditos. Se em algum momento não bate, há um erro — o desequilíbrio denuncia a inconsistência que (a) deixaria passar silenciosamente.
  • Conservação do dinheiro: como todo crédito vem de um débito correspondente, o dinheiro só se move entre contas, nunca é criado nem destruído por engano. Em (a), um bug pode criar ou sumir dinheiro; na partida dobrada, isso quebraria o equilíbrio e seria detectado.
  • Histórico e auditoria: cada transferência é uma entrada explícita (append-only, imutável) ligando origem e destino, com data e motivo. Meses depois, você sabe exatamente o que aconteceu. Em (a) (só saldos), o histórico se perde.
  • Atomicidade do par: o débito e o crédito são registrados juntos, numa transação — ou ambos acontecem, ou nenhum. Não há o estado inconsistente de (a) onde um lado aconteceu e o outro não.

O insight: a partida dobrada não é "burocracia contábil" — é um mecanismo de correção verificável. Ela torna impossível (ou imediatamente detectável) que o dinheiro apareça, suma, ou que os registros fiquem inconsistentes, justamente porque força todo movimento a ter dois lados que se equilibram. Para um sistema onde a correção é tudo, essa auto-verificação embutida é inestimável — é por isso que a contabilidade a usa há 500 anos e os sistemas de pagamento a adotam.

Médio
Exercício 2 · A cobrança dupla que o ledger deve impedir

Um cliente clica "pagar", mas a conexão cai e o app retenta. Internamente, a captura é processada por um consumidor de fila at-least-once que pode reprocessar a mensagem. Descreva todas as fontes de duplicação possíveis e como garantir que o ledger registre a cobrança exatamente uma vez.

As fontes de duplicação (todas as fronteiras de retry, Cap 16):

  • Retry do app: a conexão caiu, o app retentou — duas requisições de pagamento idênticas chegam.
  • Reprocessamento da fila: o consumidor de captura é at-least-once (Cap 11); se ele processa mas cai antes de confirmar (ack), a fila reentrega — a captura é processada de novo.
  • Webhook duplicado: o PSP externo pode enviar o webhook de confirmação mais de uma vez.
  • Retry concorrente: a requisição original (lenta) e o retry podem chegar quase juntos.

Como garantir exatamente uma entrada no ledger:

  • Chave de idempotência por intenção (Cap 16): o app gera uma chave única por tentativa de pagamento e a reusa em todos os retries. Mesma chave = mesma intenção; o servidor reconhece o retry.
  • Dedup atômica na criação do ledger: a verificação da chave, a criação das entradas do ledger (débito/crédito) e a gravação da chave acontecem numa única transação, com uma constraint de unicidade na chave (ou INSERT ... ON CONFLICT). Assim, mesmo retries concorrentes não criam entradas duplicadas — só um vence (insere a chave e cria as entradas); o outro falha na constraint e recebe a resposta gravada.
  • Idempotência no consumidor de fila: o consumidor de captura deduplica pelo ID da mensagem/transação antes de processar (Cap 11), para que reentregas não capturem de novo.
  • Idempotência com o PSP externo: usar a chave de idempotência do PSP ao capturar, para que retries da chamada externa não capturem duas vezes lá também.
  • Webhooks idempotentes: processar cada webhook deduplicando pelo seu ID, para que entregas duplicadas não criem efeitos duplicados.

O insight: a duplicação pode entrar por várias fronteiras (app, fila, webhook, PSP), e a idempotência precisa estar em cada uma (Cap 16, idempotência em camadas) — mas a fronteira crítica é a criação das entradas do ledger, que deve ser atômica com a verificação da chave. Garantir "uma intenção = uma entrada no ledger", mesmo com toda a duplicação possível, é o que impede a cobrança dupla. Exactly-once no efeito (uma cobrança no ledger) via at-least-once + idempotência atômica — exatamente o Cap 16, com o dinheiro como o efeito a proteger.

Médio
Exercício 3 · Reconciliação com o mundo externo

Seu sistema pediu a um processador externo para capturar R$200, mas a confirmação se perdeu na rede. (a) Qual é a incerteza? (b) Como a idempotência ajuda no retry? (c) Por que, mesmo com idempotência, a reconciliação ainda é necessária? (d) Como a reconciliação funciona?

(a) A incerteza: você pediu a captura de R$200, mas não recebeu a confirmação (a rede falhou). Você não sabe (Cap 8): a captura aconteceu no PSP e a confirmação se perdeu no caminho de volta? Ou a captura não aconteceu (o pedido nem chegou ao PSP, ou ele falhou)? Os dois cenários parecem idênticos do seu lado (silêncio). É a incerteza fundamental da rede não-confiável.

(b) Como a idempotência ajuda no retry: você retenta a captura com a mesma chave de idempotência. Se o PSP suporta idempotência (a maioria suporta): se a captura original tinha acontecido, o PSP reconhece a chave e retorna o resultado da captura original sem capturar de novo (não cobra R$200 duas vezes); se não tinha acontecido, ele captura agora. De qualquer forma, você acaba com exatamente uma captura e uma confirmação. A idempotência resolve a incerteza com segurança.

(c) Por que a reconciliação ainda é necessária: porque você não pode assumir que o sistema externo está sempre certo, nem que a sua visão e a dele permanecem sincronizadas ao longo do tempo. Mesmo com idempotência: o PSP pode ter um bug; um webhook de confirmação pode se perder (você acha que falhou, mas capturou); estados podem divergir por razões que a idempotência de uma chamada não cobre; pode haver transações que aconteceram de um lado e não foram refletidas no outro. A idempotência protege uma chamada individual; a reconciliação protege a consistência geral entre os dois sistemas ao longo do tempo. Confiar só na idempotência seria assumir que nada mais pode divergir — uma suposição perigosa com dinheiro.

(d) Como a reconciliação funciona: é um processo assíncrono e periódico (ex.: diário) que pega o seu registro de transações (o seu ledger) e o registro do PSP (os arquivos de liquidação/settlement, os relatórios que o PSP fornece), e os compara linha a linha, verificando que cada transação que você acha que aconteceu está no registro do PSP com o mesmo valor e status, e vice-versa. Qualquer discrepância — uma transação no seu ledger que o PSP não tem, ou uma no PSP que você não registrou, ou valores diferentes — é sinalizada para investigação humana (ops/finanças), antes que vire um problema (dinheiro perdido, conta que não fecha). A reconciliação não corrige automaticamente; ela detecta as divergências para que sejam investigadas e resolvidas.

O insight: a correção em pagamentos tem duas camadas — a idempotência garante que cada operação individual seja segura para repetir (não duplica), e a reconciliação garante que a sua visão geral concorde com a realidade externa ao longo do tempo (não diverge silenciosamente). A idempotência é prevenção por chamada; a reconciliação é detecção por verificação cruzada. Pagamentos sérios precisam das duas, porque a correção não é só "estou certo internamente", é "concordo com o mundo sobre o que aconteceu com o dinheiro".

Difícil
Exercício 4 · Projetar pagamentos do zero — entrevista

O entrevistador pede: "projete o sistema de pagamentos de uma plataforma — processar cobranças, manter saldos, lidar com reembolsos". Conduza o design demonstrando que você prioriza a correção, usa o ledger, a idempotência, a Saga e a reconciliação, e que entende por que as restrições aqui são opostas às de um feed.

Enquadramento: a primeira coisa que eu estabeleço é a restrição dominante — em pagamentos, a correção é inegociável, e isso (não a escala) dita cada decisão. Conduzo o design a partir daí.

1. A restrição que define tudo: diferente de um feed (escala read-heavy, tolera erro e consistência eventual), pagamentos exigem correção absoluta — nunca cobrar duas vezes, nunca perder uma transação, nunca um saldo errado. Isso me leva a consistência forte, idempotência rigorosa, e auditabilidade total. Eu otimizo correção, e aceito mais latência por ela.

2. O ledger de partida dobrada (o coração): a fonte da verdade do dinheiro é um ledger append-only, imutável, de partida dobrada — cada movimento é um par débito/crédito que se equilibra (auto-verificável: débitos = créditos). O saldo é derivado do ledger, não guardado solto. Num banco relacional ACID (Cap 12), consistência forte. Valores em inteiros (centavos) ou decimais — nunca float.

3. Idempotência (Cap 16): toda operação de pagamento carrega uma chave de idempotência por intenção. A verificação da chave + criação das entradas do ledger + gravação da chave são atômicas (uma transação, constraint de unicidade), garantindo uma intenção = exatamente uma entrada no ledger, mesmo com retries, webhooks duplicados, e reprocessamento de fila. Exactly-once no efeito via at-least-once + idempotência.

4. O fluxo como Saga orquestrada (Cap 17): um pagamento cruza serviços (risco, PSP externo, ledger, carteira) — é uma Saga orquestrada (visibilidade do estado, controle das compensações, auditoria). Estados rastreados (pendente → autorizado → capturado → liquidado, ou falho com compensações). Cada passo idempotente; compensações semânticas (estornos, não apagamentos).

5. O mundo externo e a reconciliação: com os PSPs externos, uso suas chaves de idempotência nos retries. Mas não confio cegamente: um processo de reconciliação periódica cruza o meu ledger com os relatórios de liquidação dos PSPs, detectando divergências para investigação. A correção é interna e concordância com o externo.

6. Reembolsos e correções: nunca edito o ledger. Reembolsos, estornos e chargebacks são novas entradas reversas ligadas à original, preservando o histórico completo para auditoria.

7. Critico o design: a consistência forte limita a escala de escrita (o ledger é um ponto de serialização — mitigo com particionamento por conta/região onde possível, mas a correção vem primeiro); a reconciliação tem latência (divergências são detectadas com atraso); a Saga adiciona complexidade. Mas todas essas são trocas que a correção justifica.

Frase de fechamento: "eu projeto pagamentos a partir de uma restrição oposta à de um feed: aqui a correção é inegociável, não a escala. O coração é um ledger de partida dobrada — append-only, imutável, auto-verificável (débitos igualam créditos) —, com dinheiro em inteiros, nunca float, e consistência forte ACID. A idempotência garante uma intenção, uma entrada no ledger, mesmo com toda a duplicação que a rede causa. O fluxo é uma Saga orquestrada de passos idempotentes com compensações. E como dependo de PSPs externos, reconcilio periodicamente o meu ledger com o deles, sem confiar cegamente. Correções são sempre entradas reversas, nunca edições, preservando a auditoria. O contraste com o feed é a lição: os mesmos componentes do livro, combinados sob a restrição oposta (correção em vez de velocidade), produzem uma arquitetura oposta. O problema dita o projeto."

Fim do capítulo 22
Próximo caso: projetar notificações em escala. Voltamos ao desafio do fan-out massivo (como no feed), mas com novas dimensões: entregar a múltiplos canais (push, email, SMS) com características diferentes, respeitar as preferências de cada usuário, garantir a entrega sem notificar demais, e lidar com canais externos não-confiáveis. Veremos as filas (Cap 11), a idempotência (Cap 16), o fan-out (Cap 21) e a resiliência (Cap 20) se recombinarem para um problema de entrega em larga escala. Da exatidão do dinheiro à entrega confiável em massa.
Parte V · Capítulo 23 · Os sistemas inteiros

Projetar
notificações em
escala.

"Mande um email quando alguém comentar." Simples — até você ter dezenas de milhões de usuários, cinquenta tipos de notificação, três ou quatro canais (push, email, SMS, in-app), preferências individuais, quiet hours, e um CEO perguntando por que o email de boas-vindas chegou quatro horas atrasado. Um sistema de notificações é uma das peças de infraestrutura mais subestimadas: parece um sendEmail(), e vira um dos subsistemas mais complexos da arquitetura. Este terceiro caso da Parte V combina o fan-out do feed (Cap 21) com novas dimensões — múltiplos canais, preferências, garantias de entrega, e o cuidado de não notificar demais — recombinando filas, idempotência, rate limiting e resiliência para um problema de entrega em massa.

Este capítulo projeta um sistema de notificações multi-canal em escala. Vamos ver o que o torna difícil (um evento, muitos usuários, canais, preferências), o pipeline desacoplado (separar a criação da entrega, via filas), as preferências como camada de filtro (antes de gastar com entrega), os canais e seus provedores externos (push/email/SMS, cada um com suas restrições), o combate à fadiga (dedup, rate limiting, batching — porque notificar demais faz o usuário desligar tudo), as garantias de entrega por prioridade (at-least-once para o crítico), e a resiliência com fallback de canal. Quando você sair daqui, terá visto o fan-out do Cap 21 reaparecer num contexto novo, combinado com a idempotência (Cap 16), o rate limiting (Cap 19) e a resiliência (Cap 20) — mais uma demonstração de como os mesmos padrões se recombinam conforme o problema. E uma lição própria: às vezes o requisito mais importante não é técnico (entregar) mas humano (não irritar).

23.1 O problema das notificações

O problema parece trivial e esconde uma complexidade enorme. Um único evento ("seu pedido foi enviado") pode precisar virar notificações para um usuário em vários canais, respeitando suas preferências, com a prioridade certa, sem duplicar nem exagerar, e lidando com provedores externos não-confiáveis — multiplicado por bilhões de notificações por dia.

Um evento, muitos destinos: canais, preferências, prioridades, e o limite humano
O que torna as notificações difíceis é a multiplicação de dimensões sobre o fan-out. Já vimos no feed (Cap 21) que entregar um evento a muitos destinatários (fan-out) é um problema em si. As notificações adicionam camadas: cada destinatário pode querer receber por canais diferentes (push, email, SMS, in-app), cada um com características próprias (push é instantâneo mas precisa de conexão; SMS tem alta taxa de abertura mas custa caro; email é rico mas tem problemas de entregabilidade); cada usuário tem preferências (quais tipos quer, em quais canais, com que frequência, e quando não incomodar — quiet hours); as notificações têm prioridades diferentes (um alerta de fraude é urgente; um digest semanal pode esperar); e — esta é a dimensão mais subestimada — há um limite humano: notificar demais faz o usuário desligar todas as notificações (ou desinstalar o app), então o sistema precisa ativamente evitar a fadiga (não duplicar, não exceder, agrupar). Some a isso a dependência de provedores externos não-confiáveis (APNs da Apple, FCM do Google, Twilio para SMS, SendGrid para email — cada um com suas APIs, limites e falhas) e a escala (bilhões de notificações/dia), e o que parecia um sendEmail() revela-se um sistema com tantas dimensões quanto o feed e os pagamentos. A lição de enquadramento: o desafio não é só técnico (fan-out, entrega), é também de produto (respeitar preferências, evitar fadiga) — e, como veremos, o requisito mais importante às vezes não é "entregar mais", mas "não irritar". Um sistema de notificações é tanto infraestrutura quanto cuidado com a experiência.

23.2 O pipeline desacoplado

A decisão estrutural central, herdada do feed e dos pagamentos: separar a criação da notificação da sua entrega, através de filas (Cap 11). Quem gera o evento não espera a entrega; ele publica, e um pipeline assíncrono cuida do resto.

Separe criação de entrega: o evento entra, o pipeline assíncrono entrega
A espinha dorsal de um sistema de notificações é um pipeline desacoplado: a criação da notificação (um serviço diz "aconteceu X, notifique o usuário Y") é separada da entrega (o trabalho de efetivamente mandar push/email/SMS). Por que separar? Porque a entrega é lenta, custosa e não-confiável (depende de provedores externos), e quem gera o evento não pode esperar por ela (Cap 15 — não bloquear no trabalho pesado). Então o fluxo é: o serviço que origina o evento publica-o num ponto de ingestão (que normaliza eventos de muitas fontes — APIs, webhooks, crons), e a partir daí um pipeline assíncrono via filas (Cap 11) processa: filtra por preferências, faz fan-out, roteia por canal, e entrega — tudo em segundo plano, sem bloquear a origem. Há uma sutileza de durabilidade importante (Cap 11): se o serviço de notificação escrevesse direto na fila (Kafka) e a fila tivesse uma queda após confirmar, a notificação se perderia. A solução é o outbox pattern (Cap 11): o serviço grava a notificação atomicamente no seu banco e numa tabela outbox (mesma transação), e um processo separado (CDC) lê o outbox e publica na fila — garantindo ingestão durável (nenhuma notificação perdida) e semântica próxima de exactly-once. O pipeline desacoplado dá as propriedades que já conhecemos: a origem responde rápido (não espera a entrega), os picos são absorvidos pela fila (Cap 11), e cada estágio (filtro, fan-out, entrega) escala independentemente. É a mesma arquitetura assíncrona do feed, aplicada à entrega de notificações — separar o rápido (criar) do lento (entregar) é o primeiro passo de qualquer sistema de entrega em escala.

23.3 As preferências como filtro — antes de gastar com a entrega

Antes de entregar qualquer notificação, o sistema precisa checar o que o usuário quer receber. As preferências não são um detalhe — são uma camada de filtro que intercepta as notificações antes da entrega, e errar nelas tem custos legais e de confiança.

O motor de preferências filtra antes de gastar — e alguns avisos não se desligam
O motor de preferências é uma camada de filtro que intercepta cada notificação antes dela ser entregue (e antes de gastar dinheiro com a entrega — um SMS custa). Ele avalia, para cada notificação e usuário: o usuário quer este tipo de notificação? Em qual canal? Está em quiet hours (e, se sim, adia para depter)? Já atingiu o limite de notificações (e, se sim, agrupa ou descarta)? As preferências são granulares: por canal (email para cobrança, push para mensagens, nada de SMS promocional), por categoria (marketing, social, transacional, segurança), e por horário (não incomodar das 22h às 8h, no fuso do usuário). Há duas razões para o filtro vir cedo, antes da entrega: custo (filtrar antes evita gastar com entregas que o usuário não quer — especialmente SMS, que é caro) e legalidade (respeitar preferências e quiet hours é exigência regulatória — GDPR, CAN-SPAM, TCPA —, e email precisa de descadastro de um clique por lei). Mas há uma exceção crucial: algumas notificações são mandatórias e ignoram as preferências — um código de verificação (2FA), um alerta de segurança, certas comunicações legais não podem ser desabilitadas, porque são parte essencial e às vezes obrigatória do serviço. O motor precisa distinguir o que pode ser filtrado (marketing, social) do que deve ser entregue independentemente (segurança, transacional crítico). Uma nota de design (Cap 18): trate as preferências como entidades de primeira classe — com esquema versionado, migração ao adicionar canais novos, e um endpoint dedicado — não como uma coluna JSON solta que quebra a cada novo tipo de notificação. As preferências são o filtro que respeita o usuário e a lei; colocá-las cedo no pipeline é o que torna o sistema eficiente (não gasta com o indesejado) e correto (não viola a lei nem a confiança).

23.4 Os canais e seus provedores externos

Cada canal (push, email, SMS, in-app) tem características, custos e provedores externos diferentes — e o design precisa abstrair essas diferenças atrás de uma camada comum, enquanto respeita as peculiaridades de cada um.

CanalCaracterísticaProvedor / desafio
PushInstantâneo, mas exige app instalado e conexãoAPNs (Apple), FCM (Google); tokens expiram
EmailRico, barato, mas entregabilidade variávelSendGrid etc.; spam, bounces, descadastro
SMSAlta taxa de abertura, mas caroTwilio etc.; custo por mensagem, limites
In-appControlado por você, sem provedor externoWebSocket/pub-sub; só visto se o app está aberto
Abstraia os canais atrás de dispatchers — e isole cada provedor (bulkhead)
O erro clássico é começar com um sendEmail(), depois adicionar sendSMS(), depois sendPush() — e acabar com integrações duplicadas e um emaranhado de ifs específicos de cada provedor. O design correto abstrai os canais atrás de uma interface comum: o pipeline produz uma "notificação a entregar", e dispatchers específicos por canal (um para push, um para email, um para SMS) traduzem para o provedor externo correspondente. Isso desacopla a lógica de notificação (preferências, fan-out, dedup) da integração com cada vendor. E cada provedor externo tem peculiaridades a respeitar: o push (APNs/FCM) tem limites de payload (~4KB — mande o essencial, deixe o app buscar os detalhes), tokens de dispositivo que expiram e rotacionam (você precisa de um registro de tokens e remover os inválidos quando o provedor avisa), e conexões persistentes a manter; o email tem entregabilidade (reputação, spam, bounces — pare de mandar para emails que retornam); o SMS tem custo real por mensagem (cap por usuário é essencial) e limites do provedor. Crucialmente (Cap 20), isole os provedores entre si com bulkheads: se o provedor de SMS (Twilio) fica lento ou cai, isso não pode travar a entrega de push ou email — cada dispatcher/provedor tem seu próprio pool de recursos, para que a falha de um canal não derrube os outros. A camada de abstração de canais com dispatchers isolados é o que mantém o sistema gerenciável (sem o emaranhado de ifs) e resiliente (a falha de um canal contida) — é a aplicação direta do isolamento do Cap 20 e do desacoplamento do Cap 15 ao problema multi-canal.

23.5 A fadiga — dedup, rate limiting e batching

Aqui está o requisito mais contraintuitivo: o maior risco de um sistema de notificações não é entregar de menos, é entregar demais. Notificar em excesso faz o usuário desligar tudo — então o sistema precisa ativamente se conter, com três mecanismos.

Notificar demais é pior que de menos — o usuário desliga tudo
O insight de produto que reorienta o design: a fadiga de notificação é o maior inimigo de um sistema de notificações. Se você notifica demais — duplicado, em excesso, na hora errada —, o usuário não fica levemente irritado; ele desliga todas as notificações, ou desinstala o app, e você perde o canal inteiro, inclusive para as notificações importantes. Então, contraintuitivamente, um bom sistema de notificações gasta tanto esforço em não enviar quanto em enviar. Três mecanismos combatem a fadiga:

Deduplicação (Cap 16): se o mesmo evento dispara a mesma notificação duas vezes (um retry, um evento duplicado, alguém editando um comentário sobre o qual você já foi notificado), o sistema deve colapsar as duplicatas numa só. Implementa-se com uma chave de idempotência derivada do conteúdo (ex.: hash de usuário + tipo + conteúdo + dia) num store com TTL — se a chave já existe, pula o envio. É a idempotência do Cap 16 aplicada para evitar notificações redundantes.

Rate limiting (Cap 19): limitar quantas notificações um usuário recebe por canal por janela de tempo (ex.: no máximo 5 push por hora, ou X SMS por dia). Isso impede que um pico de eventos (ou um bug) bombardeie o usuário. É o rate limiting do Cap 19, aplicado por usuário para proteger a experiência (não só a infraestrutura) — e também por canal, para respeitar os limites e custos dos provedores.

Batching (agrupamento): em vez de mandar 12 notificações separadas, agrupá-las numa só ("você tem 12 novos comentários"). Para notificações de baixa prioridade, acumular sobre uma janela de tempo e enviar um digest (o email diário/semanal). Isso reduz tanto a fadiga do usuário quanto a carga do sistema (e o custo — 1 email em vez de 12). O batching reconhece que muitas notificações de baixa urgência são melhor servidas agregadas.

Juntos, dedup + rate limiting + batching transformam um "firehose" de eventos numa experiência respeitosa. E note: dois desses (dedup, rate limiting) são padrões da Parte IV (Caps 16 e 19) reaplicados — agora não para correção ou proteção de infraestrutura, mas para proteger o usuário do excesso. O mesmo padrão, um propósito novo.

23.6 Garantias de entrega e prioridade

Nem toda notificação tem a mesma urgência nem exige a mesma garantia. Um alerta de fraude precisa chegar agora e com certeza; um digest semanal pode esperar e tolerar uma falha ocasional. O sistema trata os dois diferentemente, por prioridade.

At-least-once para o crítico, best-effort para o resto — filas por prioridade
As notificações não são iguais, e tratá-las igualmente desperdiça recursos no trivial e arrisca o importante. A dimensão central é a prioridade, que determina tanto a latência quanto a garantia de entrega. Notificações críticas (um código 2FA, um alerta de fraude, uma confirmação de pagamento) precisam de baixa latência (chegar em segundos) e at-least-once (Cap 11) — entregar com certeza, usando filas duráveis, retries, e dedup idempotente para que o at-least-once não duplique (Cap 16). Notificações não-críticas (um digest, uma sugestão, um marketing) toleram latência (entregar em horas) e podem usar best-effort (uma falha ocasional é aceitável, reduzindo a carga do sistema). Na arquitetura, isso se implementa com filas separadas por prioridade (Cap 11): uma fila de alta prioridade, processada rápido e com garantias fortes, e uma de baixa prioridade, processada quando há capacidade, com garantias relaxadas. Sob carga (Cap 19), isso permite load shedding por prioridade: se o sistema está sobrecarregado, ele garante a entrega das críticas e adia (ou descarta) as não-críticas — o digest semanal espera, o código 2FA não. E o ciclo de vida de cada notificação é rastreado (criada → enfileirada → enviada → entregue → lida, ou falha → retry → DLQ) para observabilidade — você precisa saber se as críticas estão chegando. A lição: classificar por prioridade permite gastar as garantias caras (durabilidade, retries, baixa latência) onde importam (o crítico) e economizar onde não (o trivial), em vez de tratar tudo igual — exatamente a degradação por prioridade do Cap 19 e do Cap 20, aplicada à entrega de notificações.

23.7 Resiliência e fallback de canal

Os provedores externos vão falhar (Cap 20). Um sistema de notificações maduro lida com isso com as defesas da Parte IV — e com uma específica: o fallback de canal (se um canal falha, tentar outro para o crítico).

Retry, isolamento, DLQ — e escalar de canal quando o crítico não chega
A resiliência de um sistema de notificações é a Parte IV aplicada à entrega. Retries com backoff e jitter (Cap 20): quando um provedor retorna um erro transitório (um 429 do Twilio, um timeout do APNs), retentar com backoff exponencial e jitter — idempotente (Cap 16), para não duplicar. Isolamento por provedor (bulkhead, Cap 20): a falha de um canal não derruba os outros. Dead letter queue (Cap 11): notificações que falham permanentemente (após N retries) vão para uma DLQ, para investigação manual, em vez de travar o pipeline ou se perderem. E uma defesa específica e poderosa: o fallback de canal — para uma notificação crítica, se o canal preferido falha (o push não foi entregue após N tentativas, talvez o dispositivo esteja offline), escalar para outro canal (mandar por SMS). Isso garante que o crítico chegue por algum meio, mesmo que o preferido falhe — uma degradação graciosa (Cap 20) específica: melhor entregar o alerta de fraude por SMS do que não entregar porque o push falhou. (Para o não-crítico, o fallback de canal geralmente não vale o custo — uma falha de digest é aceitável.) Some a isso o tratamento de tokens inválidos (quando o provedor avisa que um token de push expirou, removê-lo do registro — pare de tentar entregar a dispositivos que não existem mais) e emails que retornam (bounce) (pare de mandar para endereços inválidos, protegendo sua reputação de envio). A resiliência aqui reúne retries (Cap 20), bulkheads (Cap 20), DLQ (Cap 11) e degradação graciosa (Cap 20) — com o fallback de canal como a forma de a degradação garantir que o crítico chegue por algum caminho. Mais uma vez, os padrões da Parte IV se recombinam para um problema novo.

23.8 A arquitetura completa

Juntando tudo, a arquitetura de notificações é um pipeline de estágios, cada um aplicando um conceito do livro, do evento à entrega rastreada.

A arquitetura de notificações, de ponta a ponta
O pipeline, estágio a estágio
do evento à entrega
1. INGESTÃO: serviços publicam eventos ("pedido enviado") num
   ponto único que os normaliza. Outbox + CDC (Cap 11) para
   ingestão durável (não perder no outage da fila).

2. FILA por prioridade/canal (Kafka — Cap 11): desacopla a
   criação da entrega; absorve picos.

3. FAN-OUT workers: para cada evento, determinam os
   destinatários (Cap 21) e, para cada um:

4. FILTRO de PREFERÊNCIAS: o usuário quer este tipo? neste
   canal? está em quiet hours? mandatório (2FA) ignora o
   filtro. (Filtra ANTES de gastar com a entrega.)

5. ANTI-FADIGA: dedup (idempotência por conteúdo — Cap 16),
   rate limit por usuário/canal (Cap 19), batching/digest
   para baixa prioridade.

6. DISPATCHERS por canal (push/email/SMS/in-app), isolados
   (bulkhead — Cap 20), traduzem para os provedores externos
   (APNs, FCM, Twilio, SendGrid).

7. ENTREGA com retries+backoff+jitter (Cap 20), idempotentes
   (Cap 16); fallback de canal para o crítico; DLQ para
   falhas permanentes (Cap 11).

8. TRACKING: lifecycle de cada notificação (criada → enviada
   → entregue → lida / falha) para observabilidade.
Onde o feed reaparece, e onde diverge

O fan-out (Cap 21) reaparece: um evento vira muitas notificações, distribuídas a muitos usuários — o mesmo desafio do feed. Mas diverge em dimensões novas: enquanto o feed entregava a um destino (a timeline), as notificações entregam a múltiplos canais externos, com preferências, prioridades, e o cuidado anti-fadiga. O feed otimizava a leitura; notificações otimizam a entrega confiável e respeitosa. Mesmo fan-out, restrições diferentes.

Os padrões do livro, recombinados

Note a recombinação: filas e outbox (Cap 11) para o pipeline desacoplado e durável; fan-out (Cap 21) para um evento virar muitas notificações; idempotência (Cap 16) para dedup e entrega at-least-once sem duplicar; rate limiting (Cap 19) por usuário, agora para proteger a experiência (anti-fadiga), não só a infraestrutura; resiliência (Cap 20) — retries, bulkheads por provedor, DLQ, e fallback de canal; degradação por prioridade (Caps 19, 20) — garantir o crítico, adiar o trivial. Quase nenhum conceito é novo; o novo é a combinação para entrega multi-canal respeitosa.

Resultado: o sistema de notificações revelou-se, como o feed e os pagamentos, um exercício de combinar os padrões do livro sob restrições específicas. O fan-out do Cap 21 reaparece (um evento, muitos destinos), mas combinado com novas dimensões — múltiplos canais com provedores externos isolados (bulkhead, Cap 20), preferências como filtro antes da entrega, garantias por prioridade (at-least-once para o crítico, Cap 11), e o combate à fadiga (dedup do Cap 16, rate limiting do Cap 19, batching) que reconhece que notificar demais é pior que de menos. A lição própria deste caso: às vezes o requisito mais difícil não é técnico (entregar em escala) mas humano (não irritar) — e o sistema gasta tanto esforço em não enviar (filtrar, deduplicar, limitar, agrupar) quanto em enviar. E a lição da Parte V se confirma de novo: os mesmos padrões (filas, idempotência, rate limiting, resiliência, fan-out), recombinados conforme a restrição dominante do problema, produzem sistemas diferentes — aqui, um pipeline de entrega multi-canal, respeitoso e resiliente. Próximo e último caso: projetar um data pipeline, onde o desafio muda de novo — não entregar a usuários, mas processar volumes massivos de dados, em lote ou em fluxo, com o trade-off entre latência e completude, fechando a Parte V e o livro. Da entrega de notificações ao processamento de dados em escala.

23.9 Erros comuns

Erro 1 · Notificar demais (ignorar a fadiga)

Focar só em entregar, e bombardear o usuário — que então desliga todas as notificações ou desinstala o app, perdendo o canal inteiro. Combata a fadiga ativamente: dedup (não repetir), rate limiting por usuário (não exceder), batching (agrupar). Notificar demais é pior que de menos; gaste esforço em não enviar.

Erro 2 · Entrega síncrona acoplada à criação

Bloquear quem gera o evento até a notificação ser entregue — fazendo a origem esperar por provedores externos lentos. Separe criação de entrega com filas (Cap 11): a origem publica e responde; um pipeline assíncrono entrega. Use outbox (Cap 11) para ingestão durável (não perder no outage da fila).

Erro 3 · Não filtrar por preferências (ou filtrar tarde)

Entregar sem checar o que o usuário quer — violando preferências (e a lei: GDPR, CAN-SPAM) e gastando com entregas indesejadas. Filtre por preferências cedo, antes de gastar com a entrega. E lembre: 2FA, segurança e alguns avisos legais são mandatórios e ignoram o filtro. Preferências são entidades de primeira classe, não uma coluna JSON solta.

Erro 4 · Emaranhado de integrações por provedor

Começar com sendEmail(), adicionar sendSMS(), sendPush()... e acabar com integrações duplicadas e um monte de ifs específicos de cada vendor. Abstraia os canais atrás de dispatchers comuns, e isole cada provedor com bulkheads (Cap 20) — para que a falha de um canal não derrube os outros.

Erro 5 · Tratar todas as notificações com a mesma prioridade

Dar a um digest semanal a mesma latência e garantia de um código 2FA — desperdiçando recursos no trivial e arriscando o crítico. Classifique por prioridade: at-least-once e baixa latência (filas duráveis, retries) para o crítico; best-effort e batching para o trivial. Sob carga, garanta o crítico e adie o resto.

Erro 6 · Não deduplicar

Deixar o mesmo evento gerar notificações duplicadas (um retry, um evento repetido) — irritando o usuário com avisos redundantes. Deduplique com uma chave de idempotência por conteúdo (Cap 16) num store com TTL: se já enviou, pula. A dedup é idempotência aplicada para evitar a fadiga.

Erro 7 · Ignorar tokens inválidos e bounces

Continuar tentando entregar a tokens de push expirados ou emails que retornam (bounce) — desperdiçando recursos e prejudicando a reputação de envio. Processe o feedback dos provedores: remova tokens inválidos do registro, pare de mandar para emails que bounce. Os provedores avisam; aja sobre o feedback.

Verifique seu entendimento
Você projeta um sistema de notificações multi-canal. Os usuários começam a reclamar que recebem notificações demais (duplicadas, em excesso, em horários ruins) e estão desligando todas as notificações. Qual conjunto de medidas melhor resolve, sem deixar de entregar o que importa?

23.10 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Prioridade e garantia por tipo

Para cada notificação, classifique a prioridade (crítica ou não-crítica) e indique a garantia de entrega e a latência apropriadas: (a) um código de verificação 2FA; (b) um digest semanal de atividades; (c) um alerta de login suspeito; (d) uma sugestão de "pessoas que você talvez conheça".

(a) Código 2FA → crítica; at-least-once, baixa latência (segundos). Sem ele, o usuário não consegue entrar — precisa chegar agora e com certeza. Fila de alta prioridade, retries, dedup idempotente, e fallback de canal (se o SMS falha, talvez email/push). E é mandatória: ignora preferências e quiet hours.

(b) Digest semanal → não-crítica; best-effort, latência alta (horas ok). Pode esperar e tolerar uma falha ocasional. Fila de baixa prioridade, e é o caso ideal de batching (é literalmente um agrupamento de muitas atividades num email só). Respeita preferências e quiet hours.

(c) Alerta de login suspeito → crítica; at-least-once, baixa latência. Segurança — o usuário precisa saber imediatamente para agir (trocar senha). Como o 2FA: alta prioridade, garantia forte, possivelmente fallback de canal, e mandatória (você notifica sobre segurança mesmo que o usuário tenha desabilitado outras coisas).

(d) "Pessoas que você talvez conheça" → não-crítica; best-effort, latência alta. Pura sugestão/engajamento. Baixa prioridade, best-effort, sujeita a rate limiting e batching, e totalmente opcional (respeita preferências — se o usuário desabilitou social, não envia). Uma falha aqui é irrelevante.

O insight: a prioridade determina a garantia e a latência. Críticas (2FA, segurança) — at-least-once, baixa latência, mandatórias, com fallback. Não-críticas (digest, sugestões) — best-effort, latência tolerável, batcháveis, opcionais. Classificar por prioridade permite gastar as garantias caras onde importam e economizar onde não, em vez de tratar tudo igual.

Médio
Exercício 2 · Onde os padrões da Parte IV reaparecem

O sistema de notificações reusa muitos padrões dos capítulos anteriores, às vezes com um propósito diferente do original. Para cada um, explique como ele é usado nas notificações e se o propósito muda: (a) idempotência (Cap 16); (b) rate limiting (Cap 19); (c) bulkhead (Cap 20); (d) outbox (Cap 11).

(a) Idempotência (Cap 16) — dois usos: primeiro, o uso original — entrega at-least-once sem duplicar: como as filas reentregam (Cap 11), a entrega de uma notificação crítica precisa ser idempotente para não enviar duas vezes. Segundo, um uso novo — a deduplicação anti-fadiga: uma chave de idempotência derivada do conteúdo (hash de usuário+tipo+conteúdo+dia) colapsa notificações logicamente repetidas (o mesmo evento disparado duas vezes) numa só. O propósito se estende: de "não duplicar o efeito" (correção) para "não irritar o usuário com repetição" (experiência).

(b) Rate limiting (Cap 19) — propósito novo: no Cap 19, o rate limiting protegia a infraestrutura (não deixar um cliente sobrecarregar o servidor). Nas notificações, ele ganha um propósito adicional: proteger a experiência do usuário — limitar quantas notificações ele recebe (max N push/hora) para não fadigá-lo. E também protege os provedores externos (respeitar os limites do Twilio/APNs) e o custo (cap de SMS). O mesmo mecanismo (limitar uma taxa), três propósitos: experiência, provedor, custo.

(c) Bulkhead (Cap 20) — mesmo propósito, novo contexto: isolar recursos para que a falha de um não derrube os outros. Nas notificações, isola os provedores de canal: se o Twilio (SMS) fica lento ou cai, isso não pode travar a entrega de push ou email — cada dispatcher/provedor tem seu pool isolado. Mesmo propósito do Cap 20 (conter o dano de uma dependência), aplicado aos provedores externos de cada canal.

(d) Outbox (Cap 11) — mesmo propósito: garantir que a notificação não se perca entre ser criada e ser publicada na fila. O serviço grava a notificação no banco e numa tabela outbox atomicamente, e um processo (CDC) publica na fila a partir do outbox — então mesmo que a fila caia momentaneamente, a notificação está durável no outbox e será publicada. Mesmo propósito do Cap 11 (publicação confiável de eventos), aplicado à ingestão durável de notificações.

O insight: as notificações reusam os padrões da Parte IV quase inteiros, alguns com propósito idêntico (bulkhead, outbox) e alguns com propósito estendido (idempotência também para dedup anti-fadiga; rate limiting também para a experiência e o custo). Isso ilustra a lição da Parte V: projetar sistemas é recombinar um repertório de padrões, e os mesmos padrões servem a propósitos diferentes conforme o problema. Você não inventa o novo; você recombina o conhecido.

Médio
Exercício 3 · O fallback de canal para o crítico

Um alerta de fraude (crítico) é enviado por push, mas o push falha (o dispositivo está offline). (a) Por que simplesmente retentar o push pode não bastar? (b) O que é o fallback de canal e como ele ajuda aqui? (c) Por que o fallback de canal geralmente não se aplica a notificações não-críticas? (d) Que outras defesas de resiliência o sistema precisa?

(a) Por que retentar o push pode não bastar: se o dispositivo está genuinamente offline (sem internet, desligado, app desinstalado) ou o token expirou, nenhum número de retries de push vai entregar — o problema não é transitório, é que aquele canal está indisponível para aquele usuário naquele momento. Retentar o push para sempre é inútil; o alerta de fraude (crítico, urgente) precisa chegar por algum meio, e insistir só no push pode significar não entregar a tempo (ou nunca).

(b) O que é o fallback de canal: é escalar para outro canal quando o preferido falha. Para o alerta de fraude, se o push falha após N tentativas, o sistema tenta outro canal — manda por SMS (ou email). Assim, mesmo que o push esteja indisponível, o alerta crítico chega por um meio alternativo. É uma forma específica de degradação graciosa (Cap 20): melhor entregar o alerta por SMS do que não entregar porque o push falhou. O fallback de canal garante que o crítico chegue por algum caminho, não dependendo de um único canal.

(c) Por que geralmente não se aplica ao não-crítico: o fallback de canal tem custo (mandar por um segundo canal, especialmente SMS que é caro) e complexidade. Para notificações não-críticas (um digest, uma sugestão), uma falha de entrega é aceitável — não vale o custo de escalar para outro canal. Se o push de "pessoas que você talvez conheça" falha, tudo bem, não se manda um SMS por isso. O fallback de canal é reservado para o crítico, onde a entrega importa o suficiente para justificar o custo extra de tentar outro meio.

(d) Outras defesas de resiliência:

  • Retries com backoff + jitter (Cap 20) para falhas transitórias dos provedores, idempotentes (Cap 16) para não duplicar.
  • Isolamento por provedor (bulkhead, Cap 20): a falha de um canal não derruba os outros.
  • Dead letter queue (Cap 11): notificações que falham permanentemente vão para a DLQ, para investigação, em vez de travar o pipeline ou sumir.
  • Tratamento de tokens inválidos e bounces: remover tokens de push expirados e parar de mandar para emails que retornam — não desperdiçar em destinos mortos.
  • Tracking do lifecycle: saber se as notificações críticas estão sendo entregues (observabilidade), para detectar problemas antes que afetem usuários.

O insight: a resiliência de notificações combina as defesas do Cap 20 (retries, bulkheads, degradação) e do Cap 11 (DLQ), com o fallback de canal como a degradação específica que garante que o crítico chegue por algum meio. E a decisão de onde aplicar as defesas caras (fallback de canal só para o crítico) segue a priorização: gaste a resiliência cara onde a entrega importa.

Difícil
Exercício 4 · Projetar notificações do zero — entrevista

O entrevistador pede: "projete o sistema de notificações de uma plataforma com dezenas de milhões de usuários, múltiplos canais (push, email, SMS), preferências, e o requisito de não fadigar os usuários". Conduza o design demonstrando o pipeline, as preferências, o anti-fadiga, a prioridade, a resiliência, e a recombinação de padrões.

Enquadramento: estabeleço cedo que este sistema tem duas faces — a técnica (fan-out e entrega em escala) e a humana (preferências e anti-fadiga) — e que a segunda é tão importante quanto a primeira. Conduzo pelo pipeline.

1. Requisitos: funcionais — multi-canal (push/email/SMS/in-app), preferências granulares (canal, tipo, quiet hours), priorização, dedup, batching. Não-funcionais — escala (bilhões/dia), entrega confiável para o crítico (at-least-once), baixa latência para o urgente, e não fadigar (requisito de produto tão importante quanto os técnicos).

2. Pipeline desacoplado (Cap 11): serviços publicam eventos num ponto de ingestão (normaliza), com outbox + CDC para ingestão durável (não perder no outage da fila). Filas por prioridade/canal (Kafka) separam a criação da entrega — a origem responde rápido, o pipeline entrega em segundo plano.

3. Fan-out + preferências (Caps 21, 18): workers de fan-out determinam os destinatários, e um motor de preferências filtra cedo (antes de gastar com entrega): o usuário quer este tipo, neste canal, fora das quiet hours? Mandatórias (2FA, segurança) ignoram o filtro. Preferências como entidades de primeira classe (versionadas).

4. Anti-fadiga (Caps 16, 19) — o diferencial: dedup (chave idempotente por conteúdo, store com TTL — colapsa repetições); rate limiting por usuário e canal (max N/hora — protege a experiência, o custo, e os provedores); batching (agrupar baixa prioridade num digest). Gasto tanto esforço em não enviar quanto em enviar.

5. Prioridade e entrega (Caps 11, 19): filas separadas — crítico (at-least-once, baixa latência, retries) vs não-crítico (best-effort, batching). Sob carga, load shedding por prioridade — garante o 2FA, adia o digest.

6. Canais e resiliência (Cap 20): dispatchers por canal, abstraindo os provedores (APNs, FCM, Twilio, SendGrid), isolados por bulkhead (a falha de um canal não derruba os outros). Retries com backoff+jitter (idempotentes), DLQ para falhas permanentes, fallback de canal para o crítico (push falhou → SMS), tratamento de tokens inválidos e bounces.

7. Observabilidade: tracking do lifecycle de cada notificação (criada → enviada → entregue → lida / falha → DLQ) — preciso saber se as críticas chegam.

8. Critico o design: o batching adiciona latência (trade-off com a urgência — só para o não-crítico); o anti-fadiga pode ocultar algo que o usuário queria (calibrar os limites); a dependência de provedores externos é um risco (mitigado por isolamento e fallback); preferências erradas têm custo legal.

Frase de fechamento: "eu projeto notificações como um pipeline desacoplado — evento → ingestão durável (outbox) → fila por prioridade → fan-out → filtro de preferências → anti-fadiga → dispatchers por canal isolados → entrega resiliente → tracking. A face técnica é o fan-out e a entrega em escala, reusando filas, idempotência e resiliência. Mas a face que define o produto é a humana: o motor de preferências respeita o que o usuário quer (e a lei), e o anti-fadiga — dedup, rate limiting por usuário, batching — reconhece que notificar demais é pior que de menos, porque o usuário desliga tudo. Gasto tanto esforço em não enviar quanto em enviar, e priorizo para garantir o crítico (2FA, fraude, com fallback de canal) enquanto adio o trivial. É o fan-out do feed recombinado com a idempotência, o rate limiting e a resiliência da Parte IV — os mesmos padrões, um problema novo, e uma lição própria: às vezes o requisito mais difícil não é entregar, é não irritar."

Fim do capítulo 23
Último caso: projetar um data pipeline. Os três casos anteriores entregavam algo a usuários — um feed, um pagamento, uma notificação. O data pipeline muda o alvo: ele processa volumes massivos de dados, transformando-os de uma forma a outra, em lote (batch) ou em fluxo (stream), com o trade-off central entre latência (quão fresco) e completude (quão correto). Veremos o log (Cap 14), as filas (Cap 11), a idempotência (Cap 16) e a tolerância a falhas se combinarem num sistema de processamento de dados — fechando a Parte V e o livro com o quarto e último projeto completo.
Parte V · Capítulo 24 · Os sistemas inteiros

Projetar um
data pipeline.

Os três casos anteriores entregavam algo a usuários — um feed, um pagamento, uma notificação. Este último muda o alvo: um data pipeline processa dados, transformando volumes massivos de uma forma a outra (eventos brutos em métricas, logs em relatórios, transações em análises). E ele encarna, com clareza rara, um trade-off que atravessou o livro inteiro: latência versus completude — quão fresco você quer o resultado contra quão correto e completo ele é. Processar em lote (batch) é completo mas atrasado; processar em fluxo (stream) é fresco mas mais difícil de fazer correto. Este capítulo fecha a Parte V — e o livro — mostrando como esse trade-off final reúne o log (Cap 14), as filas (Cap 11), a idempotência (Cap 16) e a tolerância a falhas (Cap 20) num sistema de processamento de dados.

Este capítulo projeta um data pipeline. Vamos ver o que ele faz (transformar dados em escala) e o trade-off central (latência vs completude), as duas formas de processar — batch (lotes completos, atrasados) e stream (contínuo, fresco) — e quando cada uma; as duas arquiteturas clássicas que tentam ter os dois — Lambda (batch + stream em paralelo) e Kappa (só stream, reprocessando via replay) — e seus trade-offs; o desafio do exactly-once no processamento de stream (e como ele se alcança, via checkpointing + idempotência); e a tolerância a falhas. Quando você sair daqui, terá completado a jornada: quatro sistemas inteiros, cada um combinando os padrões do livro sob restrições diferentes — e terá visto que projetar é sempre isto, escolher trade-offs sob restrições. O livro termina onde começou: não existem soluções, só trade-offs — e a maturidade é escolhê-los bem.

24.1 O problema do data pipeline

Um data pipeline é uma série de estágios automatizados que pegam dados brutos de entrada e os transformam numa forma mais útil para o consumo a jusante — extrair, transformar, carregar (ETL/ELT). O desafio é fazer isso em escala (volumes massivos), de forma confiável (sem perder nem corromper dados), e com a latência certa para o uso.

Transformar dados em escala — e a pergunta é sempre "quão fresco vs quão completo?"
Um data pipeline transforma dados de uma forma a outra, em escala: eventos de clique em métricas de engajamento, transações em relatórios financeiros, logs em painéis, dados brutos em features para modelos. A estrutura é uma sequência de estágios (extrair os dados da fonte, transformá-los, carregá-los no destino), e o desafio técnico é processar volumes massivos de forma confiável. Mas a decisão de design que define tudo — e que reaparece de cada capítulo deste livro — é o trade-off entre latência e completude: você quer o resultado o mais fresco possível (baixa latência — saber as métricas agora) ou o mais completo e correto possível (ter todos os dados, mesmo os que chegam atrasados, antes de computar)? Esses dois objetivos puxam em direções opostas. Para ter completude, você espera todos os dados chegarem (incluindo os atrasados, fora de ordem) e processa o conjunto completo — o que adiciona latência. Para ter frescor, você processa cada dado assim que chega — mas então pode computar sobre dados incompletos (um evento atrasado chega depois de você já ter dado a resposta). Esse trade-off não é resolúvel em abstrato; ele depende do uso: um relatório financeiro mensal prioriza completude (precisa estar certo, pode esperar); um painel de monitoramento ao vivo prioriza latência (precisa ser agora, uma pequena imprecisão é tolerável). Todo o resto do capítulo — batch vs stream, Lambda vs Kappa — é, no fundo, sobre onde e como você se posiciona nesse trade-off entre o fresco e o completo. É o trade-off do CAP (Cap 5) e da consistência (Cap 6) reaparecendo numa forma nova, fechando o livro com a mesma lição com que ele começou: design é gerir trade-offs sob restrições.

24.2 Batch vs stream — duas formas de processar

Há duas formas fundamentais de processar dados, e elas se posicionam nos extremos opostos do trade-off latência/completude.

Batch — lotes completos, atrasados

Processa os dados em lotes, em intervalos regulares (de hora em hora, diariamente). Pega um conjunto completo de dados (todos os do período) e os processa de uma vez. Vantagem: completude e correção — tem todos os dados, faz operações complexas (joins, agregações) sobre o conjunto inteiro, alta confiabilidade. Custo: latência — sempre há um atraso entre os dados chegarem e o resultado sair. Nunca é "agora".

Stream — contínuo, fresco

Processa cada dado continuamente, assim que ele chega (latência de milissegundos a segundos). Vantagem: frescor — resultados quase em tempo real. Custo: complexidade — lidar com eventos fora de ordem, manter estado ao longo do fluxo, garantir exactly-once, e a dificuldade de operações que precisam do conjunto completo (joins, agregações globais). Mais fresco, mais difícil de fazer certo.

Batch é completo mas atrasado; stream é fresco mas mais difícil — o uso decide
Batch e stream são os dois extremos do trade-off latência/completude, e cada um é a escolha certa para usos diferentes. O processamento em batch espera acumular um conjunto completo de dados (um dia, uma hora) e o processa de uma vez — o que lhe dá completude (tem todos os dados, inclusive os que chegaram atrasados dentro do período) e simplicidade para operações complexas (joins e agregações são naturais quando você tem o dataset inteiro na mão). O custo é a latência: por definição, sempre há um lag entre os dados existirem e o resultado sair — o batch nunca é "tempo real". É ideal para o que prioriza correção sobre frescor: relatórios financeiros, análises históricas, treinamento de modelos. O processamento em stream processa cada evento assim que ele chega, dando frescor (resultados em segundos) — ideal para monitoramento ao vivo, detecção de fraude em tempo real, painéis. O custo é a complexidade: você processa sobre dados incompletos (um evento pode chegar atrasado ou fora de ordem depois de você já ter computado), precisa manter estado através do fluxo contínuo, garantir que não perde nem duplica dados (exactly-once, adiante), e operações que precisam do conjunto completo (certos joins e agregações) ficam mais difíceis. A escolha não é "qual é melhor" — é "o que o uso exige": completude e tolerância a latência → batch; frescor e tolerância a alguma incompletude → stream. E, como veremos, há arquiteturas que tentam ter os dois ao mesmo tempo — ao custo de complexidade própria.

24.3 Latência vs completude — o trade-off que define tudo

Vale isolar o trade-off central, porque ele é a alma do capítulo e o eco final de uma ideia que percorreu o livro: você quase nunca tem o fresco e o completo de graça; escolher entre eles (ou pagar para ter os dois) é a decisão de design.

O dado atrasado: esperar por ele (completo, lento) ou seguir sem ele (rápido, incompleto)
O trade-off latência/completude tem uma raiz concreta: o dado atrasado. Num sistema distribuído (Cap 8), os dados não chegam todos na hora nem em ordem — alguns chegam atrasados (uma rede lenta, um dispositivo offline que sincroniza depois, um evento fora de ordem). Isso cria um dilema fundamental ao computar qualquer resultado sobre os dados: você espera pelos dados atrasados (garantindo completude — o resultado inclui tudo — mas adicionando latência — você esperou) ou você segue sem eles (garantindo frescor — o resultado sai já — mas arriscando incompletude — um dado atrasado chega depois e seu resultado estava errado)? Não há resposta universal; há a resposta certa para o uso. Se você está contando o total de vendas do mês para a contabilidade, você espera — a completude é inegociável, e algumas horas de latência são irrelevantes (batch). Se você está mostrando um contador de "usuários online agora" num painel, você segue — o frescor é o ponto, e um pequeno erro (um usuário a mais ou a menos por um instante) é tolerável (stream). Esse dilema é o mesmo do CAP (Cap 5: consistência vs disponibilidade sob partição) e da consistência (Cap 6: forte vs eventual), agora na forma temporal: completude vs latência. E a beleza de fechar o livro com ele é que ele mostra como um trade-off fundamental reaparece em mil formas — CAP, consistência, push vs pull, batch vs stream — e que projetar bem é sempre reconhecer qual trade-off você está enfrentando e escolher conscientemente o lado que o problema pede. Não há como ter o resultado instantâneo e completo ao mesmo tempo quando os dados chegam atrasados; você escolhe, ou paga (com complexidade) para aproximar os dois.

24.4 A arquitetura Lambda — ter os dois, com um custo

E se você quiser os dois — o frescor do stream e a completude do batch? A arquitetura Lambda (proposta por Nathan Marz) faz isso rodando os dois em paralelo, e mesclando os resultados. Funciona — ao custo de manter dois sistemas.

arquitetura Lambda: dois caminhos em paralelo
Os dados entram e seguem por DOIS caminhos simultâneos:

  CAMADA BATCH (cold path):
    processa o conjunto COMPLETO de dados, periodicamente.
    Lenta, mas CORRETA e completa. A fonte da verdade.

  CAMADA SPEED/STREAM (hot path):
    processa os dados em tempo real, assim que chegam.
    Rápida, mas aproximada (dados recentes, ainda não no batch).

  CAMADA SERVING:
    MESCLA os dois → resultado recente (do stream) +
    resultado completo/corrigido (do batch).
    O batch eventualmente "corrige"/sobrescreve o que o
    stream computou aproximadamente.

→ Você tem o FRESCOR do stream E a COMPLETUDE do batch.
  Mas paga o "imposto do código": DUAS bases de código (a
  mesma lógica, duas vezes — stream e batch), risco de elas
  DIVERGIREM, e o dobro de infraestrutura. Complexo de manter.
Lambda dá frescor e completude — pagando o "imposto do código": dois sistemas
A arquitetura Lambda resolve o trade-off latência/completude por força bruta: rode os dois processamentos em paralelo e mescle. A camada batch (o "caminho frio") processa o conjunto completo de dados periodicamente — lenta, mas completa e correta, a fonte da verdade. A camada de velocidade (o "caminho quente") processa os dados em stream, em tempo real — rápida, mas aproximada (cobre os dados recentes que o batch ainda não processou). A camada de serviço mescla os dois: serve o resultado recente do stream e o resultado completo do batch, com o batch eventualmente "corrigindo" e sobrescrevendo as aproximações do stream. O resultado é o melhor dos dois mundos: você tem frescor (do stream) e completude (do batch). Mas o custo é real e tem nome — o "imposto do código" (coding tax): você mantém duas bases de código implementando a mesma lógica de negócio (uma para o framework de stream, outra para o de batch), com o risco constante de elas divergirem (o stream e o batch produzindo resultados sutilmente diferentes por discrepâncias de implementação), além do dobro de infraestrutura para operar e a complexidade de manter dois sistemas distribuídos distintos. A Lambda foi a resposta dominante por anos (e ainda é usada onde tanto a baixa latência quanto a alta completude são críticas e o time aguenta a complexidade), mas o imposto do código motivou a busca por uma alternativa mais simples — que veio com a maturação dos engines de stream.

24.5 A arquitetura Kappa — só stream, reprocessando via replay

A arquitetura Kappa (proposta por Jay Kreps, o mesmo do log do Cap 14) elimina a complexidade da Lambda com uma ideia elegante: e se você usasse stream, e obtivesse a completude reprocessando o histórico via replay do log?

Kappa: tudo é stream, e o replay do log substitui a camada batch
A arquitetura Kappa simplifica a Lambda eliminando a camada batch inteira — tudo é stream. A intuição vem direto do Cap 14 (não por acaso, do mesmo Jay Kreps): se você guarda todos os dados como um log imutável e ordenado (Kafka com retenção longa), então você não precisa de um sistema batch separado para obter completude ou corrigir erros — você simplesmente reprocessa (replay) o log através do mesmo processador de stream. Precisa recalcular tudo (porque a lógica mudou, ou houve um bug)? Reproduza o log inteiro pelo stream processor. Precisa de uma nova visão dos dados históricos? Reproduza o log derivando a nova visão. O log é a fonte da verdade (Cap 14), e o processamento — tanto o "tempo real" quanto o "histórico" — é o mesmo código de stream, só com entradas diferentes (eventos novos vs log reproduzido). As vantagens sobre a Lambda são claras: uma única base de código (sem o imposto do código, sem divergência de lógica), um modelo operacional mais simples, lógica de processamento consistente. O que tornou a Kappa viável foi a maturação dos engines de stream (como o Flink): com checkpointing e gerenciamento de estado robustos (que garantem correção mesmo sob falha, adiante), o stream moderno alcança a correção que antes só o batch dava — tornando a camada batch "corretiva" da Lambda redundante. Os desafios da Kappa: ela exige um armazenamento de stream robusto com retenção longa (o log precisa guardar tudo para o replay), e reprocessar terabytes de histórico exige throughput muito maior que a taxa de chegada em tempo real (daí scaling elástico durante o replay); além disso, certas operações (joins e agregações complexas que precisam do dataset completo de uma vez) são naturalmente mais fáceis em batch. A escolha Lambda vs Kappa: Lambda quando você precisa de ambos os caminhos e aguenta a complexidade dupla; Kappa quando a simplicidade de um codebase único e a natureza streaming dominam, e seu engine de stream é maduro o bastante para a correção. A tendência moderna pende para Kappa, conforme os engines de stream amadurecem — uma vitória da ideia do log (Cap 14) como a primitiva unificadora.

24.6 Exactly-once no processamento — o velho problema, de novo

O processamento de stream enfrenta o mesmo fantasma do Cap 16: como garantir que cada dado seja processado exatamente uma vez, quando falhas e reprocessamentos podem fazê-lo ser processado zero ou duas vezes? A resposta é a mesma — e vale ver como ela reaparece aqui.

Exactly-once = at-least-once + idempotência — a mesma lição do Cap 16, no stream
Num pipeline de stream, cada evento passa por estágios de processamento, e as falhas são inevitáveis (Cap 8, Cap 20): um processador pode cair depois de processar um evento mas antes de registrar que o processou, levando-o a reprocessá-lo ao reiniciar — duplicando o efeito (uma métrica contada duas vezes, um registro inserido em duplicata). O objetivo é o exactly-once: cada evento afeta o resultado final exatamente uma vez, nem zero (perdido) nem duas (duplicado). E a solução é exatamente a do Cap 16, reaparecendo no contexto de dados: o exactly-once não vem de "networking mágico", mas de at-least-once + idempotência. O pipeline garante a entrega at-least-once (cada evento é processado pelo menos uma vez, com retries — Cap 11, Cap 20) e torna o processamento idempotente (Cap 16): processar o mesmo evento duas vezes tem o mesmo efeito que uma. Na prática, isso se faz com padrões idempotentes nos sinks (o destino): upsert em vez de insert (escrever "o valor da chave X é V", que é idempotente, em vez de "adicione um registro", que duplica), ou deduplicação por ID do evento. Some a isso o checkpointing (próxima seção): o stream processor salva periodicamente seu estado e a posição no log, de modo que após uma falha ele retoma do último checkpoint consistente, sem reprocessar o que já foi commitado nem perder o que faltava. A combinação de checkpointing (retomar do ponto certo) + sinks idempotentes (reprocessar sem duplicar) é como os engines de stream modernos (Flink e outros) alcançam o exactly-once no efeito. É a mesma verdade do Cap 16, agora fechando o livro: você não evita a duplicação na entrega (a rede e as falhas a garantem); você a torna inofensiva pela idempotência. O exactly-once é sempre uma propriedade do efeito, construída sobre uma entrega que duplica — no pagamento (Cap 22), na notificação (Cap 23), e aqui no dado.

24.7 Tolerância a falhas e checkpointing

Um pipeline processa por horas ou continuamente; falhas vão acontecer no meio (Cap 20). O que distingue um pipeline robusto é como ele retoma após uma falha sem perder nem reprocessar tudo — e a técnica central é o checkpointing.

Checkpointing: salvar o progresso para retomar do último ponto bom, não do zero
Um pipeline de dados — especialmente um job de stream que roda continuamente, ou um job de batch longo — não pode se dar ao luxo de, ao falhar no meio, ter que recomeçar do zero (desperdiçando horas de processamento) ou, pior, perder ou duplicar dados. A técnica que resolve isso é o checkpointing: periodicamente, o pipeline salva seu progresso — o estado intermediário do processamento e a posição até onde já consumiu o log/stream (o offset, Cap 11). Quando uma falha acontece, em vez de recomeçar do início, o pipeline retoma do último checkpoint consistente: ele recarrega o estado salvo e continua a partir da posição salva, reprocessando apenas o que veio depois do checkpoint (não tudo). Isso dá duas propriedades vitais: eficiência (não se perde o trabalho já feito — retoma de onde parou, não do zero) e correção (combinado com idempotência, o reprocessamento do intervalo desde o checkpoint não duplica nem perde — é o mecanismo por trás do exactly-once da seção anterior). Os engines de stream modernos implementam isso de forma sofisticada — o Flink, por exemplo, usa "snapshots de barreira assíncronos" que capturam um estado globalmente consistente do pipeline inteiro sem pará-lo, permitindo retomar de um ponto coerente mesmo num sistema distribuído com muitos operadores. O checkpointing é o que torna o stream processing confiável a ponto de dispensar a camada batch corretiva da Lambda (viabilizando a Kappa): é a tolerância a falhas (Cap 20) aplicada ao processamento de dados, garantindo que um pipeline longo sobreviva às falhas inevitáveis retomando do último ponto bom, com correção. Sem checkpointing, um pipeline que falha perde horas de trabalho ou corrompe dados; com ele, uma falha é só uma pausa, da qual o pipeline se recupera continuando de onde parou.

24.8 A arquitetura completa

Juntando tudo, a arquitetura de um data pipeline reúne, pela última vez, os padrões do livro — e mostra a escolha de design (batch/stream/Lambda/Kappa) emergindo do trade-off latência/completude.

A arquitetura de um data pipeline, de ponta a ponta
A estrutura (uma Kappa moderna)
o pipeline de dados
1. INGESTÃO: os dados (eventos, mudanças) entram num LOG
   imutável e ordenado (Kafka — Cap 14), a fonte da verdade,
   com retenção longa (para permitir replay).

2. STREAM PROCESSOR (ex.: Flink): consome o log e processa
   cada evento — transforma, agrega, enriquece. Mantém ESTADO
   (contadores, janelas) através do fluxo.

3. CHECKPOINTING (Cap 20): o processor salva periodicamente
   seu estado + posição no log. Falha → retoma do último
   checkpoint, sem perder nem reprocessar tudo.

4. EXACTLY-ONCE: at-least-once (retries) + sinks IDEMPOTENTES
   (upsert, dedup por ID — Cap 16) = cada evento afeta o
   resultado exatamente uma vez.

5. SINK: o resultado processado é gravado no destino
   (data warehouse, banco de métricas, cache), idempotente.

6. REPROCESSAMENTO (a virtude da Kappa): mudou a lógica? bug?
   → REPLAY do log pelo mesmo processor (Cap 14). Sem segunda
   base de código (sem o imposto do código da Lambda).
A escolha que define a arquitetura
o trade-off decide
A pergunta que dita o design: latência ou completude?

  Precisa de FRESCOR (monitoramento, fraude em tempo real)?
    → STREAM (Kappa, se o engine é maduro).

  Precisa de COMPLETUDE/correção (relatórios, contabilidade)?
    → BATCH (ou a camada batch).

  Precisa dos DOIS, e aguenta a complexidade?
    → LAMBDA (batch + stream em paralelo) — mas pondere o
      imposto do código vs a Kappa moderna (um codebase,
      reprocessando via replay).

A arquitetura não é uma preferência — ela CAI do trade-off
que o uso impõe. O problema dita o projeto (de novo).
Os padrões do livro, uma última vez

O log (Cap 14) como a fonte da verdade e o que viabiliza o replay (e a Kappa); as filas/mensageria (Cap 11) na ingestão e no fluxo; a idempotência (Cap 16) nos sinks, garantindo o exactly-once; a tolerância a falhas (Cap 20) via checkpointing; e o trade-off latência/completude que ecoa o CAP (Cap 5) e a consistência (Cap 6). Até o ledger de pagamentos (Cap 22) era, no fundo, um log processado — os fios se cruzam até o fim.

Resultado: o data pipeline, último dos quatro sistemas, fechou a Parte V mostrando o trade-off mais fundamental do livro numa forma nova: latência versus completude — o fresco contra o completo. Batch escolhe completude (lotes corretos, atrasados); stream escolhe frescor (contínuo, mais difícil de acertar); Lambda tenta os dois ao custo de manter dois sistemas (o imposto do código); e Kappa, viabilizada pela maturação do stream e pela ideia do log (Cap 14), alcança ambos com um codebase único, reprocessando via replay. E sob tudo isso, os padrões do livro reapareceram pela última vez — o log, as filas, a idempotência (o exactly-once como at-least-once + idempotência, exatamente como no Cap 16), a tolerância a falhas via checkpointing. A lição que encerra a Parte V é a mesma dos três casos anteriores, agora cristalizada: a arquitetura não é uma preferência nem um padrão favorito — ela cai do trade-off que o problema impõe. O feed caiu da assimetria leitura/escrita; os pagamentos, da exigência de correção; as notificações, da entrega multi-canal respeitosa; e o data pipeline, do trade-off latência/completude. Quatro problemas, quatro combinações dos mesmos padrões, quatro arquiteturas diferentes — porque, como o livro inteiro vem dizendo, não existem soluções, só trade-offs, e projetar é escolhê-los sob as restrições de cada problema. Aqui termina a jornada dos sistemas inteiros — e do livro.

24.9 Erros comuns

Erro 1 · Escolher batch ou stream sem pensar no trade-off

Adotar stream porque é "moderno", ou batch por hábito, sem perguntar o que o uso exige — frescor (stream) ou completude (batch). A escolha cai do trade-off latência/completude: monitoramento ao vivo → stream; relatório financeiro → batch. Deixe o uso decidir, não a moda.

Erro 2 · Ignorar dados atrasados e fora de ordem no stream

Assumir que os dados chegam todos na hora e em ordem — e computar resultados incompletos ou errados quando um evento atrasado chega depois. No stream, trate explicitamente os dados atrasados (janelas, watermarks) e decida conscientemente entre esperar por eles (completude) ou seguir (frescor).

Erro 3 · Lambda quando a Kappa bastaria

Manter duas bases de código (batch + stream) e pagar o imposto do código (divergência de lógica, dobro de infra) quando um engine de stream maduro + replay do log (Kappa) daria a mesma completude com um codebase único. Considere a Kappa antes de assumir a complexidade da Lambda; os engines de stream modernos frequentemente a dispensam.

Erro 4 · Esperar exactly-once "mágico" na entrega

Achar que o pipeline garante exactly-once por algum truque de rede — quando exactly-once no efeito é sempre at-least-once + idempotência (Cap 16). Garanta a entrega at-least-once (retries) e torne os sinks idempotentes (upsert, dedup por ID). A duplicação na entrega é inevitável; a idempotência a torna inofensiva.

Erro 5 · Pipeline sem checkpointing

Rodar um job longo (stream contínuo ou batch demorado) sem checkpointing — e, ao falhar no meio, recomeçar do zero (perdendo horas) ou perder/duplicar dados. Use checkpointing: salve o progresso periodicamente para retomar do último ponto bom, com correção. Sem ele, uma falha é uma catástrofe; com ele, é só uma pausa.

Erro 6 · Sinks não idempotentes (insert em vez de upsert)

Gravar resultados com insert (que duplica quando o evento é reprocessado) em vez de upsert (idempotente). Sob retries e reprocessamento (inevitáveis num pipeline), inserts criam duplicatas. Use upsert ou dedup por ID nos sinks — a idempotência no destino é o que torna o reprocessamento seguro.

Erro 7 · Não guardar o log para permitir replay

Descartar os dados brutos após processá-los — e não poder reprocessar quando a lógica muda ou um bug é descoberto. Guarde o log imutável com retenção suficiente (Cap 14): ele é a fonte da verdade e o que permite o replay (e a arquitetura Kappa). Sem o log retido, corrigir o passado é impossível.

Verifique seu entendimento
Você projeta um data pipeline e precisa decidir a arquitetura. O sistema precisa de resultados frescos (quase em tempo real) para um painel de monitoramento, mas a equipe quer evitar a complexidade de manter duas bases de código. Os dados já fluem por um log durável (Kafka). Qual abordagem é a mais sólida?

24.10 Exercícios

Pratique antes de seguir adiante
Fácil
Exercício 1 · Batch ou stream?

Para cada caso, decida se batch ou stream é mais apropriado, com base no trade-off latência/completude: (a) um relatório financeiro mensal para a contabilidade; (b) um painel de "transações por segundo" ao vivo; (c) detecção de fraude que precisa bloquear uma transação suspeita em tempo real; (d) o treinamento noturno de um modelo de recomendação sobre todos os dados do dia.

(a) Relatório financeiro mensal → batch. A completude e a correção são inegociáveis (é contabilidade), e a latência é irrelevante (um relatório mensal pode levar horas para ser computado). O batch processa o conjunto completo do mês de uma vez, com todas as operações complexas (agregações, joins) que um relatório exige. Frescor não importa; completude é tudo.

(b) Painel de "transações por segundo" ao vivo → stream. O ponto é o frescor — mostrar o que está acontecendo agora. Uma pequena imprecisão (um evento atrasado que chega depois) é tolerável num painel ao vivo. O stream processa cada transação ao chegar, atualizando o contador em tempo real. Latência baixa é o requisito; completude perfeita não.

(c) Detecção de fraude em tempo real → stream. Para bloquear uma transação suspeita, você precisa decidir em milissegundos, enquanto a transação acontece — esperar um batch seria inútil (a fraude já teria passado). O stream processa cada transação ao chegar, aplicando as regras de fraude na hora. A latência é crítica; é o caso clássico de stream.

(d) Treinamento noturno de modelo sobre os dados do dia → batch. O treinamento precisa do conjunto completo dos dados do dia (completude), e roda à noite sem pressa de latência (pode levar horas). O batch é ideal: pega todos os dados do dia e os processa de uma vez, com as operações sobre o dataset inteiro que o treinamento exige. Completude e tolerância a latência → batch.

O insight: a escolha cai diretamente do trade-off. Onde a completude/correção domina e a latência é tolerável (relatórios, treinamento) → batch. Onde o frescor domina e alguma imprecisão é tolerável (painéis, fraude em tempo real) → stream. A pergunta-chave: o uso precisa do resultado agora (stream) ou precisa dele completo e correto (batch)?

Médio
Exercício 2 · Lambda vs Kappa

Explique a diferença entre as arquiteturas Lambda e Kappa, o problema que cada uma resolve, e o trade-off entre elas. Por que a Kappa se tornou mais viável com o tempo, e o que ela exige para funcionar?

A arquitetura Lambda: roda dois pipelines em paralelo — uma camada batch (processa o conjunto completo, lenta mas correta e completa) e uma camada speed/stream (processa em tempo real, rápida mas aproximada) — e uma camada de serviço que mescla os dois, com o batch eventualmente corrigindo as aproximações do stream. Problema que resolve: ter tanto frescor (stream) quanto completude (batch) ao mesmo tempo. Custo: o "imposto do código" — manter duas bases de código implementando a mesma lógica (uma para stream, uma para batch), com risco de elas divergirem, mais o dobro de infraestrutura e a complexidade operacional de dois sistemas distribuídos.

A arquitetura Kappa: elimina a camada batch — tudo é stream. Os dados ficam num log imutável (Cap 14, Kafka com retenção longa), e o mesmo stream processor lida com tudo: os eventos novos (tempo real) e, quando é preciso reprocessar (lógica mudou, bug, nova visão), o replay do log inteiro pelo mesmo código. Problema que resolve: a complexidade da Lambda — a Kappa tem um único codebase, sem o imposto do código nem o risco de divergência. Custo/desafio: exige um log durável com retenção longa (para o replay), e reprocessar grandes volumes históricos exige throughput muito maior que o tempo real (scaling elástico durante o replay); além disso, certas operações (joins/agregações que precisam do dataset completo) são naturalmente mais fáceis em batch.

O trade-off entre elas: Lambda dá frescor + completude com a robustez de uma camada batch "corretiva", mas paga em complexidade (dois sistemas). Kappa dá frescor + completude com a simplicidade de um codebase único, mas depende inteiramente da correção do stream processor e da capacidade de reprocessar via replay.

Por que a Kappa se tornou mais viável: a Lambda surgiu quando o stream processing era considerado aproximado e não-confiável — daí a necessidade da camada batch para "corrigir" os resultados do stream. Mas os engines de stream amadureceram: com checkpointing e gerenciamento de estado robustos (ex.: os snapshots de barreira assíncronos do Flink), o stream moderno mantém estado consistente e correto mesmo sob falha — alcançando a correção que antes só o batch dava. Isso torna a camada batch "corretiva" da Lambda redundante, viabilizando a Kappa. O que a Kappa exige: (1) um armazenamento de stream/log robusto com retenção longa (para o replay); (2) um engine de stream maduro com checkpointing e exactly-once (para a correção); (3) capacidade de reprocessar com throughput elevado (scaling elástico). Onde esses três estão presentes, a Kappa entrega a completude da Lambda sem o imposto do código.

O insight: a evolução Lambda → Kappa é uma vitória da ideia do log (Cap 14) como primitiva unificadora — quando o log é a fonte da verdade e o stream processor é confiável o bastante, o "tempo real" e o "histórico" viram o mesmo processamento sobre entradas diferentes, e a dualidade batch/stream colapsa num só caminho.

Médio
Exercício 3 · Exactly-once e checkpointing

Um stream processor consome eventos de um log, agrega métricas, e grava os resultados num banco. Ele pode cair a qualquer momento. (a) Que problemas uma falha pode causar? (b) Como o checkpointing ajuda? (c) Por que o checkpointing sozinho não basta para o exactly-once, e o que mais é preciso? (d) Como isso se relaciona com o Cap 16?

(a) Problemas que uma falha pode causar: se o processor cai no meio, sem proteção, há dois riscos. Perder dados: os eventos que ele estava processando (mas ainda não gravou) podem se perder ao reiniciar, se ele não souber até onde tinha chegado. Duplicar dados: se ele processou e gravou um evento, mas caiu antes de registrar que o processou, ao reiniciar ele reprocessa esse evento — gravando a métrica duas vezes (uma agregação inflada). E há a ineficiência de, sem saber onde parou, ter que recomeçar do zero, reprocessando horas de dados.

(b) Como o checkpointing ajuda: o processor salva periodicamente um checkpoint — seu estado intermediário (as agregações parciais) e a posição até onde consumiu o log (o offset). Quando cai e reinicia, ele retoma do último checkpoint: recarrega o estado salvo e continua a partir da posição salva, reprocessando os eventos depois do checkpoint (não tudo). Isso resolve a eficiência (não recomeça do zero) e estabelece um ponto consistente de retomada (não perde o que estava antes do checkpoint).

(c) Por que o checkpointing sozinho não basta, e o que mais é preciso: mesmo retomando de um checkpoint, há um intervalo — os eventos entre o último checkpoint e a falha — que serão reprocessados (porque o checkpoint é anterior a eles). Se o processamento desses eventos não for idempotente, reprocessá-los duplica seus efeitos (a métrica deles é contada duas vezes). Então o checkpointing precisa ser combinado com idempotência nos sinks (Cap 16): gravar os resultados de forma idempotente — upsert ("o valor da chave X é V", que sobrescreve, idempotente) em vez de insert ("adicione", que duplica), ou dedup por ID do evento. Assim, reprocessar o intervalo desde o checkpoint produz o mesmo resultado que processá-lo uma vez. Checkpointing (retomar do ponto certo) + sinks idempotentes (reprocessar sem duplicar) = exactly-once no efeito.

(d) Como se relaciona com o Cap 16: é exatamente a mesma lição, num contexto novo. O Cap 16 estabeleceu que exactly-once na entrega é impossível, mas exactly-once no efeito é alcançável via at-least-once + idempotência. Aqui: o checkpointing + retries garantem at-least-once (cada evento é processado pelo menos uma vez, com reprocessamento do intervalo após a falha), e a idempotência dos sinks garante que esse "pelo menos uma vez" tenha o efeito de "exatamente uma vez". A duplicação no processamento (causada pelo reprocessamento após a falha) é inevitável; a idempotência a torna inofensiva. É o Cap 16 fechando o livro: no pagamento (Cap 22), na notificação (Cap 23), e agora no dado, o exactly-once é sempre uma propriedade do efeito, construída sobre uma entrega/processamento que duplica.

Difícil
Exercício 4 · Projetar um data pipeline do zero — entrevista

O entrevistador pede: "projete um pipeline que processa eventos de clique de um site com milhões de usuários, alimentando tanto um painel de métricas ao vivo quanto relatórios analíticos diários". Conduza o design — o trade-off latência/completude, a escolha de arquitetura, exactly-once, tolerância a falhas — e critique suas escolhas.

Enquadramento: noto de cara que há dois usos com requisitos opostos no trade-off — o painel ao vivo (frescor) e os relatórios diários (completude) —, então a arquitetura precisa servir aos dois. Conduzo a partir do trade-off.

1. Os requisitos e o trade-off: o painel ao vivo precisa de frescor (métricas em segundos, pequena imprecisão tolerável) → puxa para stream. Os relatórios diários precisam de completude (todos os cliques do dia, inclusive os atrasados, com correção) → puxam para completude. Preciso de ambos.

2. A escolha de arquitetura — Kappa (preferida) ou Lambda:

  • Kappa (minha escolha default): tudo num log durável (Kafka, retenção longa) processado por um stream processor maduro (Flink). O painel ao vivo lê o resultado em tempo real; os relatórios diários saem do mesmo stream (agregando por janela diária) ou de um replay do log do dia quando preciso de completude/correção. Um único codebase — sem o imposto do código. Viável porque o stream moderno (checkpointing) dá a correção que os relatórios exigem.
  • Lambda (se necessário): se os relatórios diários precisam de operações complexas (joins pesados) que são genuinamente mais fáceis em batch, ou se a completude/correção dos relatórios for absolutamente crítica e eu não confiar plenamente no stream, eu rodaria uma camada batch para os relatórios + a camada stream para o painel. Mas pondero o imposto do código antes de assumir essa complexidade.

3. Ingestão e fluxo (Caps 11, 14): os cliques entram num log imutável e ordenado (Kafka), a fonte da verdade, com retenção longa (para replay). O log absorve os picos de tráfego e desacopla a ingestão do processamento.

4. Exactly-once (Cap 16): at-least-once (retries) + sinks idempotentes — gravo as métricas com upsert (não insert) ou dedup por ID do clique, para que reprocessamento (após falha, ou replay) não infle as contagens. O exactly-once é no efeito, não na entrega.

5. Tolerância a falhas (Cap 20): checkpointing no stream processor — salva estado + offset periodicamente, retoma do último ponto bom após uma falha, sem perder nem reprocessar tudo. Combinado com a idempotência, garante correção mesmo com falhas.

6. Dados atrasados/fora de ordem: uso janelas com watermarks no stream para lidar com cliques atrasados — para o painel ao vivo, sigo com o que tenho (frescor); para os relatórios diários, espero um pouco mais (ou reprocesso via replay) para incluir os atrasados (completude). O trade-off resolvido por uso.

7. Critico o design: a Kappa exige um engine de stream maduro e um log com retenção suficiente (custo de armazenamento); o replay de grandes volumes exige scaling elástico; certas agregações complexas dos relatórios podem ser mais difíceis em stream que em batch (talvez justificando uma camada batch só para elas). E preciso calibrar os watermarks (quanto esperar pelos atrasados) por uso.

Frase de fechamento: "eu reconheço dois usos com requisitos opostos — o painel quer frescor, os relatórios querem completude. Minha escolha default é Kappa: tudo num log durável processado por um stream maduro, servindo o painel em tempo real e os relatórios via janelas ou replay do log, com um único codebase. Garanto o exactly-once com at-least-once + sinks idempotentes (upsert), e a tolerância a falhas com checkpointing, retomando do último ponto bom. Os dados atrasados eu trato por uso — sigo para o painel, espero para os relatórios. Considero Lambda só se os relatórios exigirem operações batch que não valem o esforço em stream, ponderando o imposto do código. No fim, a arquitetura cai do trade-off latência/completude que cada uso impõe — que é, afinal, a lição do livro inteiro: não existem soluções, só trade-offs, e projetar é escolhê-los sob as restrições do problema."

Fim do capítulo 24 · Fim da Parte V
Você completou os quatro sistemas inteiros — um feed, pagamentos, notificações e um data pipeline — e, com eles, o livro. Cada caso combinou os mesmos padrões das Partes I a IV sob uma restrição dominante diferente, e cada um produziu uma arquitetura distinta: a assimetria do feed, a correção dos pagamentos, a entrega respeitosa das notificações, o trade-off latência/completude do pipeline. A lição que a Parte V inteira ensina, e que fecha a jornada: projetar um sistema real não é aplicar um padrão favorito, é reconhecer a restrição dominante do problema e combinar os padrões certos para satisfazê-la. O que vem a seguir não é mais um capítulo — é a síntese de tudo.

O livro está completo.

Vinte e quatro capítulos, cinco partes, uma única tese: não existem soluções, só trade-offs. Projetar um sistema nunca foi escolher o padrão certo de um catálogo — foi reconhecer as restrições que o problema impõe e gerir, conscientemente, as trocas entre objetivos que puxam em direções opostas.

A Parte I ensinou o raciocínio: o que é projetar um sistema, como extrair requisitos e restrições, como estimar ordens de grandeza, e o vocabulário dos trade-offs. A Parte II trouxe as leis que não se negociam: o CAP e o PACELC, os modelos de consistência, as leis de Amdahl, Little e da escalabilidade universal, e as falácias da computação distribuída. A Parte III apresentou os componentes — balanceamento, cache, filas, bancos SQL e NoSQL, sharding, e o log como primitiva. A Parte IV mostrou os padrões que os combinam — síncrono e assíncrono, idempotência, Saga, CQRS e Event Sourcing, rate limiting e backpressure, e a resiliência que faz tudo sobreviver à falha. E a Parte V reuniu tudo em quatro sistemas inteiros — um feed, pagamentos, notificações e um data pipeline —, mostrando que os mesmos padrões, recombinados sob restrições diferentes, produzem arquiteturas diferentes.

Se um único fio atravessa o livro, é este: a assimetria do feed, a correção dos pagamentos, a entrega respeitosa das notificações, o trade-off latência/completude do pipeline — todos são a mesma coisa em formas diferentes. CAP, consistência, push versus pull, batch versus stream: um punhado de tensões fundamentais, reaparecendo sem fim. Dominar system design não é memorizar arquiteturas; é treinar o olho para ver, sob cada problema novo, qual tensão você está enfrentando — e escolher o lado que aquele problema, com suas restrições, exige.

Este é o Volume II. O Volume I, "Operação ao redor do código", tratou de manter sistemas vivos depois de prontos — observabilidade, deploys, incidentes, a disciplina de operar. Os dois se completam: um sobre projetar sistemas que valem a pena construir, o outro sobre operá-los com cuidado. Entre projetar bem e operar bem mora quase tudo que importa na engenharia de software séria.

Obrigado por chegar até aqui. Agora vá projetar algo de verdade — e, quando travar numa decisão, lembre-se de que travar é o sinal de que você encontrou um trade-off real. Não procure a resposta certa. Encontre a troca certa para o seu problema.