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.
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.
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.
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.
É 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.
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.
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.
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 forte | Disponibilidade ou latência (Cap 5) |
| Mais throughput por cache | Frescor / risco de dado velho (Cap 10) |
| Escala horizontal | Complexidade de coordenação (Cap 13) |
| Desacoplamento por filas | Latência e complexidade de entrega (Cap 11) |
| Flexibilidade de schema (NoSQL) | Garantias transacionais (Cap 12) |
| Baixa latência por replicação | Consistência entre réplicas (Cap 6) |
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:
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.
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?".
"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ível | A pergunta | Exemplo de decisão |
|---|---|---|
| Arquitetura | Quais são as grandes peças e como se relacionam? | Monólito ou serviços? Síncrono ou orientado a eventos? |
| Componente | Como cada peça é estruturada por dentro? | Qual banco? Qual estratégia de cache? |
| Dados | Como a informação é modelada, guardada, acessada? | Normalizar ou desnormalizar? Como particionar? |
| Integração | Como as peças conversam? | REST, gRPC, fila? Qual contrato? Qual garantia de entrega? |
| Operação | Como isto roda, é observado e recuperado? | Todo o volume anterior deste livro |
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).
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.
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.
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.
"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.
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.
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.
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.
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.
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?".
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.
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 é.
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.
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.
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:
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.
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.
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.
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):
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."
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.
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".
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.
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:
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).
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.
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.
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.
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.
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 vago | A pergunta que o aterra | O 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 |
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.
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.
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.
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.
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.
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".
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.
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.
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".
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.
"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."
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).
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)
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.
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.
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.
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".
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.
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.
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.
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.
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.
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:
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.
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.
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):
2. Definir a forma funcional (o quê):
3. Extrair os não-funcionais (o como — onde mora a dificuldade):
4. Os implícitos e o escopo com não-metas:
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."
"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.
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.
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.
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.
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:
| Operação | Ordem de grandeza | O que ensina |
|---|---|---|
| Ler da memória (RAM) | ~100 nanossegundos | A 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 milissegundo | Rede local custa, mas é tolerável |
| Ida e volta entre continentes | ~100-150 milissegundos | A luz tem velocidade: distância = latência |
| Ler 1 MB sequencial de SSD | ~1 milissegundo | Throughput de disco é decente, latência é o problema |
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
Estimar qualquer coisa segue o mesmo procedimento, e tê-lo explícito evita se perder no meio das contas:
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?
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.
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 (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.
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 é 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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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:
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.
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.
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.
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:
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):
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."
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.
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.
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.
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.
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 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.
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.
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.
| Propriedade | O que mede | Você quer |
|---|---|---|
| Acoplamento | Quanto uma parte depende de/conhece outra | Baixo — partes que mudam independentemente |
| Coesão | Quanto o conteúdo de uma parte pertence junto | Alta — cada parte tem um propósito claro |
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).
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.
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.
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).
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?).
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.
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ência | Baixa — chega assim que acontece | Alta — só na próxima checagem |
| Eficiência | Alta se eventos são raros (só envia quando há) | Desperdício se pergunta e não há nada |
| Carga | Pico quando muitos eventos ao mesmo tempo | Constante e previsível (intervalo fixo) |
| Complexidade | Maior (manter conexões, saber quem quer o quê) | Menor (o cliente só pergunta) |
| Acoplamento | Fonte precisa conhecer os interessados | Interessado conhece a fonte; fonte não sabe de ninguém |
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 outro | Onde aparece |
|---|---|---|
| Latência (op individual rápida) | Throughput (volume total) | Batching, filas (Cap 11) |
| Consistência (sempre correto) | Disponibilidade / latência | CAP, PACELC (Cap 5, 6) |
| Espaço (memória/disco) | Tempo (computação) | Cache, índices, desnorm. (Cap 10, 12) |
| Acoplamento baixo | Simplicidade (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) |
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.
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.
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.
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.
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.
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.
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á.
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.
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.
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.
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.
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.
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?".
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:
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.
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."
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.
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.
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.
Primeiro, as três propriedades, com as definições precisas (não as intuitivas, que enganam):
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".
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.
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.
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).
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.
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.
| Sistema | Classificação PACELC | O que significa |
|---|---|---|
| DynamoDB, Cassandra (default) | PA / EL | Na partição, escolhe disponibilidade; no normal, escolhe latência. Prioriza estar rápido e no ar; aceita inconsistência. |
| Spanner, sistemas de consenso | PC / EC | Na partição, escolhe consistência (pode parar); no normal, escolhe consistência (paga latência). Prioriza correção sempre. |
| PNUTS (Yahoo) | PC / EL | Curioso: no normal troca consistência por latência, mas na partição vira consistente. Mostra que as duas escolhas são independentes. |
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.
Juntando tudo, o CAP/PACELC vira um procedimento concreto de design, aplicável a cada operação importante do seu sistema:
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.
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.
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.
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.
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.
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".
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.
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.
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".
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.
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.
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ê.
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.
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.
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.
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):
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."
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.
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.
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.
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).
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.
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.
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.
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.
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).
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ão | O que promete | O bug que evita |
|---|---|---|
| Read-your-writes | Você sempre vê suas próprias escritas | Postar um comentário e ele "sumir" ao recarregar |
| Monotonic reads | Você nunca vê dado mais velho do que já viu | Ver 50 likes, recarregar, e ver 45 (o tempo "andar para trás") |
| Monotonic writes | Suas escritas são aplicadas na ordem que você as fez | Editar e depois apagar, mas a edição chegar depois |
| Writes-follow-reads | Uma escrita que reage a uma leitura vem depois dela | Responder um post e a resposta aparecer antes do post |
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".
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ção | Nível mínimo suficiente | Por quê |
|---|---|---|
| Transferência bancária / saldo | Linearizável (ou forte) | Errar a ordem ou ver saldo velho = dinheiro errado |
| Reservar recurso único (ingresso) | Linearizável | Duas pessoas não podem reservar o mesmo |
| Thread de comentários / chat | Causal | Resposta não pode aparecer antes da pergunta |
| Ver seu próprio perfil após editar | Read-your-writes | Você precisa ver sua edição; os outros podem esperar |
| Contador de curtidas, visualizações | Eventual (+ monotonic reads) | Número aproximado serve; só não pode "andar para trás" |
| Feed de notícias, catálogo | Eventual | Alguns segundos de atraso é irrelevante |
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.
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".
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.
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.
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á.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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."
"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.
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.
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.
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.
A lei de Little é talvez a relação mais útil e mais subutilizada da engenharia de sistemas. Ela diz, para qualquer sistema estável:
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.
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.
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%.
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:
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.
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.
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 C | O que é | Cresce | Como reduzir |
|---|---|---|---|
| Concorrência | O trabalho paralelizável — o que permite escalar | Linear (ideal) | Maximizar: mais trabalho independente |
| Contenção (α) | Espera por recursos compartilhados (locks, banco) | Linear, impõe teto | Reduzir serialização, eliminar gargalos compartilhados |
| Coerência (β) | Custo de manter os nós consistentes entre si | Quadrático (N²), causa retrocesso | Reduzir coordenação, particionar para isolar |
Juntando as três, elas formam um conjunto de perguntas que mudam como você aborda a escala:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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."
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.
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.
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 é.
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.
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.
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.
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.
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).
Estas duas tratam da natureza dinâmica e descentralizada dos sistemas reais, que o iniciante assume estáticos e centralizados.
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.
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.
As duas últimas, frequentemente esquecidas, mas com consequências reais de custo e compatibilidade.
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ável | Retries + idempotência; timeouts; circuit breakers (Cap 16, 20) |
| Latência é zero | Reduzir round-trips; batching; evitar o N+1; async (Cap 4) |
| Banda é infinita | Comprimir, mandar deltas, CDN; paginar (vol. anterior) |
| A rede é segura | TLS sempre, zero trust, autenticar e validar tudo (vol. anterior) |
| Topologia não muda | Service discovery dinâmico; statelessness (Cap 4) |
| Há um administrador | Resiliência a terceiros; contratos explícitos |
| Custo de transporte é zero | Localidade de dados; minimizar egresso (FinOps) |
| A rede é homogênea | Padrões abertos; contratos versionados (Cap 1) |
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.
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.
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.)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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."
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.
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.
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.
Reduzir o balanceador a "distribui requisições" esconde a maioria das razões pelas quais ele é essencial. Seus papéis reais:
/api para uns servidores, /static para outros, /ws para os de WebSocket — roteamento inteligente baseado no que a requisição pede.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.
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.
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.
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:
| Algoritmo | Como decide | Quando usar |
|---|---|---|
| Round-robin | Reveza em ordem: 1, 2, 3, 1, 2, 3... | Requisições uniformes, servidores iguais |
| Weighted round-robin | Reveza, mas servidores mais fortes recebem mais | Servidores de capacidades diferentes |
| Least connections | Manda para quem tem menos conexões ativas | Requisições de duração muito variável |
| Least response time | Manda para quem responde mais rápido agora | Adaptar à performance real em tempo real |
| IP hash / consistent hash | Mesmo cliente/chave → sempre o mesmo servidor | Afinidade de sessão, cache (ver 9.6) |
| Power of two choices (P2C) | Sorteia 2, manda para o menos carregado dos 2 | Bom equilíbrio com baixo custo, em escala |
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.
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.
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.
/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.
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.
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.
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.
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.
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.
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.
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?
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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).
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.
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."
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.
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.
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.
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:
| Camada | O que cacheia | Invalidaçã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ário | TTL + purge explícito |
| Aplicação (Redis/Memcached) | Dados, resultados computados, sessões | Controle total: delete explícito + TTL |
| Banco de dados | Queries, páginas de índice (interno) | Gerenciado pelo banco |
| Memória do processo (local) | O mais rápido, mas não compartilhado entre instâncias | Por instância — cuidado com inconsistência |
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.
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égia | Como funciona | Trade-off |
|---|---|---|
| Cache-aside (lazy) | App lê o cache; no miss, busca no banco e popula o cache | Simples, comum; risco de stampede e dado velho na janela do TTL |
| Read-through | O 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-through | Toda escrita vai ao cache E ao banco, juntos | Cache sempre fresco; escrita mais lenta (escreve em dois lugares) |
| Write-back (write-behind) | Escreve no cache; persiste no banco depois, async | Escrita rápida; RISCO DE PERDA se o cache cai antes de persistir |
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:
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.
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.
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 cache é poderoso, mas não é grátis nem universal. Há casos onde cachear é o erro — e reconhecê-los é tão importante quanto saber cachear:
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?
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
/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).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.
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.
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."
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.
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.
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:
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.
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).
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ê).
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:
| Garantia | O que promete | O risco |
|---|---|---|
| At-most-once (no máximo uma) | Entrega 0 ou 1 vez; nunca duplica | Pode perder mensagens |
| At-least-once (pelo menos uma) | Entrega 1 ou mais vezes; nunca perde | Pode duplicar mensagens |
| Exactly-once (exatamente uma) | Entrega 1 vez exata | Caro e complexo; garantias limitadas na prática |
"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.
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 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.
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ípica | Por quê |
|---|---|---|
| Fila de trabalho simples, roteamento flexível, request-reply | RabbitMQ | Broker inteligente, roteamento rico; default sensato para a maioria |
| Fila gerenciada, na nuvem (AWS), zero operação | SQS | Sem broker para operar; standard (at-least-once) ou FIFO (ordenado) |
| Streaming de alto volume, replay, muitos consumidores na mesma stream, event sourcing | Kafka | É um log: retenção, replay, consumer groups, ordem por partição |
| Mensageria leve, já usa Redis | Redis Streams | Simples, leve; menos garantias de durabilidade que os outros |
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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:
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."
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.
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.
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.
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:
"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ília | Modelo | Brilha em | Exemplos |
|---|---|---|---|
| Key-value | Chave → valor (o mais simples) | Lookups por chave ultrarrápidos, cache, sessões | Redis, DynamoDB |
| Documento | Documentos JSON aninhados, esquema flexível | Dados semiestruturados, evolução rápida, agregados | MongoDB, CouchDB |
| Wide-column (colunar) | Linhas com colunas dinâmicas, particionável | Escrita massiva, séries temporais, escala enorme | Cassandra, HBase |
| Grafo | Nós e arestas (relações como cidadãs de 1ª classe) | Relacionamentos complexos, travessias (redes sociais) | Neo4j, Neptune |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.)
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.
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:
(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?
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):
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.
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."
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.
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".
Três termos são usados de forma intercambiável e não deveriam ser, porque resolvem problemas distintos. Esclarecê-los é o primeiro passo:
| Termo | O que faz | Resolve |
|---|---|---|
| Particionamento | Divide os dados em pedaços, tipicamente dentro de uma instância | Queries varrem menos dados; manutenção mais fácil |
| Sharding | Distribui os pedaços entre múltiplas instâncias/máquinas | Dados/escrita que não cabem num nó (escala além de um nó) |
| Replicação | Copia os mesmos dados em várias máquinas | Disponibilidade e escala de leitura — NÃO o tamanho |
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.
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égia | Como divide | Bom para | O risco |
|---|---|---|---|
| Por hash | hash(chave) decide o shard — distribui aleatoriamente | Distribuição uniforme, evita hotspots | Range queries ruins (dados próximos espalhados) |
| Por intervalo (range) | Faixas contíguas (A-M, N-Z; jan-mar, abr-jun) | Range scans, séries temporais | Hotspots se os valores são enviesados |
| Por geografia | Por região/data center (US, EU, APAC) | Localidade, residência de dados (leis) | Desbalanceio se uma região domina |
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.
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 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.
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.
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".
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.
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?
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.
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.
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).
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.
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".
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.
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.
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.
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.
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.
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).
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?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.
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.
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.
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):
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."
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.
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.
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:
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:
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.
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.
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.
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.
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.
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 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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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 (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:
(c) O que você perde e como mitigar:
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.
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.
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):
Onde o event sourcing (log como verdade) brilha (casos específicos):
Onde é complexidade desnecessária:
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."
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.
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.
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.
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:
| Propriedade | Síncrono | Assíncrono |
|---|---|---|
| Acoplamento temporal | Alto — ambos online ao mesmo tempo | Baixo — desacoplados no tempo |
| Resiliência a falha | Falha se propaga em cascata | Falha isolada (a mensagem espera) |
| Latência percebida | O chamador espera o trabalho todo | Resposta imediata; trabalho depois |
| Simplicidade | Simples de entender e debugar | Complexo (entrega, ordem, rastreio) |
| Absorver picos | Pico afoga (sem buffer) | Fila absorve o pico |
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.
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").
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.
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).
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.
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:
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.
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.
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.
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.
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).
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.
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.
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?
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.
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.
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.
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.
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.
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.
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á.
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.
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.
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.
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.
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.
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.
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.
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:
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:
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."
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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".
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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: há 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.
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:
Idempotency-Key (modelo Stripe).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."
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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).
Juntando tudo, a decisão entre as abordagens segue do que a operação exige e do que você pode tolerar:
| Situação | Abordagem |
|---|---|
| A operação cabe num único banco/serviço | Transação ACID local (a melhor opção — evite o problema) |
| Cruza serviços; tolera inconsistência temporária | Saga (a escolha padrão para microsserviços) |
| Saga simples, poucos passos, desacoplamento prioritário | Saga por coreografia |
| Saga complexa, precisa de visibilidade e controle | Saga por orquestração |
| Nenhuma inconsistência, nem temporária, é tolerável | 2PC (raro) — ou redesenhe para caber num banco |
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?
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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?
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:
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:
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.
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:
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.
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:
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."
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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:
PedidoEnviadoV2 com o campo que faltava), e usá-la para os eventos novos dali em diante. Os eventos antigos (V1) permanecem como estão.(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.
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."
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.
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.
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.
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):
| Algoritmo | Como funciona | Perfil |
|---|---|---|
| Fixed window | Conta requisições em janelas fixas (ex.: por minuto), zera a cada janela | Simples, mas permite 2x burst na borda da janela |
| Sliding window log | Guarda o timestamp de cada requisição, conta as do último intervalo móvel | Exato, mas custa memória O(n) por cliente |
| Sliding window counter | Aproxima o log com dois contadores ponderados pelo tempo | O(1), quase exato, suaviza a borda — o default |
| Token bucket | Um balde de tokens, reabastecido a uma taxa; cada requisição consome um token | Permite rajadas controladas (acumula tokens) |
| Leaky bucket | Uma fila que drena a uma taxa constante; o que transborda é descartado | Saída constante e suave; ideal para proteger downstream |
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 API | Sliding 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 suave | Leaky bucket (saída constante) |
| Exatidão absoluta, tráfego por cliente limitado | Sliding window log (exato, mas O(n) memória) |
| O mais simples possível, e o furo de borda não importa | Fixed window (cuidado com o 2x na borda) |
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.
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.
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.
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).
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.
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.
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.
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.
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.)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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:
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."
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.
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.
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.
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.
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.
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.
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.
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.
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.
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?
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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:
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).
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):
4. Degradação graciosa (por criticidade — a decisão central):
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."
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.
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.
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.
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:
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).
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).
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.
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.
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).
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.
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.
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.
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.
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.
Juntando tudo, a arquitetura do feed costura quase todos os conceitos do livro. Vamos vê-la de ponta a ponta, do post à exibição.
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
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)
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.
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.
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.
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.
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.
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ê.
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.
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.
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.
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.
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):
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.
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.
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):
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."
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.
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.
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.
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.
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.
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.
(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.
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).
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).
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.
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.
Juntando tudo, a arquitetura de pagamentos é construída inteira em torno da correção — e cada componente reflete uma exigência do dinheiro.
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)
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.
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.
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.
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.
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.
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.
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.
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.
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ó.
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):
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.
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):
Como garantir exatamente uma entrada no ledger:
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.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.
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".
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."
"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).
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.
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.
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.
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.
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.
| Canal | Característica | Provedor / desafio |
|---|---|---|
| Push | Instantâneo, mas exige app instalado e conexão | APNs (Apple), FCM (Google); tokens expiram |
| Rico, barato, mas entregabilidade variável | SendGrid etc.; spam, bounces, descadastro | |
| SMS | Alta taxa de abertura, mas caro | Twilio etc.; custo por mensagem, limites |
| In-app | Controlado por você, sem provedor externo | WebSocket/pub-sub; só visto se o app está aberto |
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.
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.
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.
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.
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).
Juntando tudo, a arquitetura de notificações é um pipeline de estágios, cada um aplicando um conceito do livro, do evento à entrega rastreada.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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:
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.
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."
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.
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.
Há duas formas fundamentais de processar dados, e elas se posicionam nos extremos opostos do trade-off latência/completude.
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".
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.
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.
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.
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.
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 só stream, e obtivesse a completude reprocessando o histórico via replay do log?
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.
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.
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.
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 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).
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.
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.
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).
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.
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.
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.
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.
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.
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)?
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.
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 só 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.
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:
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."
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.