Entrevistas de system design · parte 3 de 3
System design sênior: 15 perguntas conceituais com respostas
intermediárioPublicada em · 177 min de leitura
Enquanto perguntas complexas exigem projetar sistemas gigantescos (como Uber, Netflix ou Twitter), os entrevistadores de nível Sênior frequentemente iniciam a conversa ou fazem testes de profundidade com perguntas comuns, diretas e conceituais de arquitetura.
Aqui, o objetivo não é desenhar uma topologia inteira em 45 minutos, mas demonstrar domínio cirúrgico dos blocos fundamentais de sistemas distribuídos, explicando trade-offs, armadilhas ocultas e decisões baseadas em requisitos reais.
Índice Rápido das Perguntas
- Estratégias de Cache: Cache-Aside vs Write-Through vs Write-Behind
- Gargalos de Cache: Thundering Herd, Cache Penetration e Cache Avalanche
- Escala de Banco de Dados: Read Replicas vs Sharding vs Particionamento
- O Teorema CAP e PACELC na Prática do Mundo Real
- Comunicação Síncrona vs Assíncrona (REST/gRPC vs Filas/Eventos)
- Resiliência: Circuit Breaker, Exponential Backoff com Jitter e Bulkhead
- Idempotência e o Padrão Transactional Outbox
- Autenticação em Alta Escala: JWT Stateless vs Sessão Centralizada
- Balanceamento de Carga: Balanceador L4 (Rede) vs L7 (Aplicação)
- Políticas de Despejo de Cache: Como funciona o LRU e LFU?
- Tempo Real na Web: Polling vs Long-Polling vs WebSockets vs SSE
- Deploys sem Downtime e Migrações de Banco (Expand & Contract)
- Quando Escolher SQL vs NoSQL (Sem Mitos)
- O Problema N+1 em Consultas de Banco e APIs (DataLoader)
- Observabilidade: As Quatro Métricas Douradas (Golden Signals) e o p99
1. Estratégias de Cache: vs vs
A Pergunta do Recrutador: “Quais são as principais estratégias de escrita e leitura em cache e quando você escolheria cada uma?”
- Cache-Aside ():
- Como funciona: A aplicação consulta primeiro o cache. Se der Cache Miss, busca no banco de dados relacional, grava no cache e retorna ao usuário.
- Prós: Só guarda em cache o que é realmente acessado; se o cache cair, a aplicação continua funcionando (com latência maior).
- Contras: O primeiro acesso é lento (cache miss penalty); dados podem ficar desatualizados se houver alteração direta no banco.
- Write-Through:
- Como funciona: A aplicação escreve no cache e no banco de dados na mesma operação (o cache atualiza o banco de forma síncrona antes de responder sucesso).
- Prós: Os dados em cache nunca ficam defasados; leituras subsequentes são sempre imediatas e consistentes.
- Contras: Maior latência na escrita (escreve em dois lugares de forma síncrona).
- Write-Behind (Write-Back):
- Como funciona: A aplicação grava apenas no cache imediatamente, e o cache faz a persistência assíncrona em lote no banco de dados em segundo plano.
- Prós: Escrita com latência quase nula e altíssima vazão (ideal para contadores, logs, métricas).
- Contras: Risco de perda de dados se o nó de cache queimar antes de persistir no banco.
- Equivalentes AWS: Amazon ElastiCache (Redis / Memcached) integrado com RDS ou DynamoDB Accelerator (DAX).
2. Gargalos de Cache: Thundering Herd, Cache Penetration e
A Pergunta do Recrutador: “O que acontece quando uma chave muito acessada expira no cache e milhares de requisições chegam no mesmo milissegundo?”
- / Thundering Herd:
- O Problema: A chave quente (ex.: página inicial de um e-commerce) expira; 10.000 requisições simultâneas sofrem Cache Miss e todas disparam a mesma query pesada no banco ao mesmo tempo, derrubando o banco de dados.
- Solução Sênior:
- Mutex / Lock Distribuído: Apenas a primeira requisição adquire uma trava no Redis para calcular o valor no banco; as outras 9.999 aguardam o lock ou recebem o dado ligeiramente antigo.
- Expiração Probabilística (Algoritmo ): A aplicação recalcula e renova o cache em segundo plano antes do tempo de expiração expirar com base na probabilidade matemática de acesso.
- :
- O Problema: Requisições maliciosas ou erradas buscam IDs que não existem nem no cache nem no banco (ex.:
GET /users/-9999). Todas batem no banco de dados à toa. - Solução Sênior:
- Gravar valor nulo temporário no cache (
key: null, TTL: 60s). - Usar um Filtro de Bloom (Bloom Filter) na camada de entrada para responder instantaneamente se a chave com certeza não existe.
- Gravar valor nulo temporário no cache (
- O Problema: Requisições maliciosas ou erradas buscam IDs que não existem nem no cache nem no banco (ex.:
- Cache Avalanche:
- O Problema: Milhares de chaves foram salvas com o mesmo fixo (ex.:
TTL: 3600s) e expiram exatamente no mesmo segundo. - Solução Sênior: Adicionar um Jitter aleatório ao TTL (ex.:
TTL = 3600 + random(0, 300) segundos), espalhando a expiração no tempo.
- O Problema: Milhares de chaves foram salvas com o mesmo fixo (ex.:
3. Escala de Banco de Dados: Read Replicas vs vs Particionamento
A Pergunta do Recrutador: “Seu banco PostgreSQL está com CPU a 95% e disco quase cheio. Qual é o plano de ação gradual?”
- Passo 1: Read Replicas (Escala de Leitura):
- Se 90% do tráfego for leitura (SELECT), cria-se 1 ou mais réplicas de leitura assíncronas no RDS. O master recebe apenas INSERT/UPDATE/DELETE.
- Atenção do Sênior: Avisar sobre (se o usuário acabou de criar um post e a réplica ainda não sincronizou, ele pode recarregar a página e não ver seu próprio post).
- Passo 2: Particionamento Vertical:
- Separar tabelas pesadas e não relacionadas em bancos de dados distintos (ex.: banco de Autenticação separado do banco de Pedidos e do banco de Catálogo).
- Passo 3: Sharding Horizontal (Escala de Escrita e Armazenamento):
- Quando o volume de dados e escritas ultrapassa a capacidade física de uma máquina. Os dados de uma mesma tabela são divididos em múltiplos nós de banco.
- A Escolha da Sharding Key: Fundamental para evitar Hot Sharding. Se a chave for
data_criacao, todas as escritas de hoje cairão no mesmo servidor. Se forhash(user_id), a distribuição é uniforme. - Trade-off: Consultas com JOIN entre shards diferentes tornam-se proibitivamente caras e lentas.
4. O Teorema e na Prática do Mundo Real
A Pergunta do Recrutador: “Na prática, o que significa ter que escolher entre Consistência e Disponibilidade no Teorema CAP?”
- O Mito: O CAP não diz que você escolhe 2 entre 3 livremente. Como partições de rede (Network Partitions - P) são inevitáveis em sistemas distribuídos, a escolha real é: quando a rede falha, o que você prioriza?
- Sistema CP (Consistency + Partition Tolerance): Prefere dar erro (
500 Internal Errorou timeout) a entregar dados desatualizados.- Exemplo real: Transferência bancária, débito de cartão, reserva de assento de avião.
- Sistema AP (Availability + Partition Tolerance): Prefere responder dados ligeiramente desatualizados a deixar o sistema fora do ar.
- Exemplo real: Curtidas no Instagram, comentários no YouTube, carrinho de compras da Amazon.
- Sistema CP (Consistency + Partition Tolerance): Prefere dar erro (
- O Teorema PACELC (A Visão Completa do Sênior):
- Se há Partição ($P$), escolhe entre Disponibilidade ($A$) ou Consistência ($C$).
- Else ($E$ - em operação normal sem falhas): escolhe entre Latência ($L$) ou Consistência ($C$).
- Exemplo: O DynamoDB e Cassandra no dia a dia trocam consistência forte imediata por latência de leitura ultrabaixa ($< 5\text{ms}$).
5. Comunicação Síncrona vs Assíncrona (REST/gRPC vs Filas/Eventos)
A Pergunta do Recrutador: “Quando dois microsserviços devem falar via HTTP/gRPC síncrono e quando devem se comunicar via fila ou pub/sub?”
- Comunicação Síncrona (REST / gRPC):
- Quando usar: Quando o cliente precisa da resposta imediata para continuar o fluxo (ex.: autenticar senha, consultar saldo, validar se um cupom é válido).
- Risco: Alto acoplamento temporal. Se o serviço B estiver lento ou fora do ar, o serviço A também trava, gerando falhas em cascata.
- Comunicação Assíncrona (Amazon SQS / RabbitMQ / Apache Kafka):
- Quando usar: Processamento de tarefas demoradas (envio de e-mail, geração de PDF, transcodificação de mídia, liquidação de pedido).
- Conceito Chave do Sênior (Backpressure): Filas atuam como um amortecedor (buffer). Se chegarem 10.000 pedidos por segundo na Black Friday, a fila armazena e os workers consomem na velocidade estável suportada pelo banco de dados, sem derrubá-lo.
- Dead Letter Queue (DLQ): Fila secundária para onde mensagens que falharam repetidamente após $N$ tentativas são movidas para análise humana, evitando travar a fila principal (poison pill).
6. Resiliência: Circuit Breaker, Exponential Backoff com Jitter e Bulkhead
A Pergunta do Recrutador: “Se o serviço downstream de consulta de CEP começar a falhar e retornar timeout de 5 segundos, como você protege sua API?”
- 1. Circuit Breaker (Disjuntor):
- Closed (Fechado): Fluxo normal. Se a taxa de erros ultrapassar um limite (ex.: 50% de falhas nos últimos 10s), o disjuntor Abre.
- Open (Aberto): Não tenta mais chamar o serviço lento; falha imediatamente ou retorna resposta de contingência (fallback com cache antigo), sem gastar threads e conexões.
- Half-Open (Meio Aberto): Após um tempo de espera, deixa passar algumas requisições de teste. Se tiverem sucesso, fecha o circuito; se falharem, volta a abrir.
- 2. Exponential Backoff com Full Jitter:
- Retries imediatos após falha provocam o efeito de tempestade de retries (retry storm).
- A solução sênior: esperar um tempo exponencial com ruído aleatório (jitter): $$\text{Espera} = \text{random}(0, ; \text{base} \times 2^{\text{tentativa}})$$ Isso impede que milhares de clientes façam retry sincronizados no mesmo milissegundo.
- 3. Bulkhead (Compartimentalização):
- Inspirado nos compartimentos estanques de navios: pools de threads e conexões HTTP separadas para cada serviço externo. Se o serviço de CEP travar seu pool de 10 threads, as outras 90 threads dedicadas ao checkout continuam operando normalmente.
7. Idempotência e o Padrão Transactional Outbox
A Pergunta do Recrutador: “Como você garante que um pagamento ou pedido não seja cobrado duas vezes se a requisição sofrer timeout de rede?”
- Idempotência com Idempotency Key:
- O frontend gera um UUID único (
Idempotency-Key: 7b2a-4f...) antes de enviar oPOST /checkout. - O backend tenta adquirir uma trava no Redis com essa chave (
SET idempotency:7b2a NX EX 120). - Se a chave já existir: ou o processamento está em andamento (retorna
409 Conflict), ou já terminou (retorna a resposta salva em cache sem reprocessar). - Se for nova: executa o pagamento, grava o resultado no banco, salva a resposta no cache e libera a trava.
- O frontend gera um UUID único (
- O Problema do Dual-Write e a Solução: Transactional Outbox Pattern:
- O Problema: A aplicação grava o pedido no banco de dados e depois publica o evento no Kafka/SQS. Se a aplicação cair exatamente entre o commit do banco e a mensagem na fila, o banco tem o pedido, mas nenhum evento foi disparado.
- Solução Outbox: Na mesma transação SQL que grava o pedido, grava-se uma linha na tabela
outbox_events. Um processo em segundo plano (ou CDC via Debezium) lê a tabela e publica na fila com garantia de que nada se perde.
8. Autenticação em Alta Escala: JWT Stateless vs Sessão Centralizada
A Pergunta do Recrutador: “Quais são os prós e contras reais de usar JWT stateless em vez de sessões salvas no Redis em microsserviços?”
- Sessões Centralizadas (Redis):
- Prós: Revogação instantânea (basta deletar a sessão no Redis se o usuário for banido ou trocar a senha); controle fácil de múltiplos logins simultâneos.
- Contras: Todo request precisa bater no Redis para validar a sessão (dependência de rede e IO).
- JWT Stateless:
- Prós: Cada microsserviço valida a assinatura criptográfica localmente com a chave pública sem bater em nenhum banco de dados ou rede (latência zero).
- O Calcanhar de Aquiles: É impossível revogar um JWT antes do prazo de expiração sem criar um estado centralizado (o que anularia a vantagem do stateless).
- O Padrão Ouro do Sênior (Híbrido):
- (JWT): Validade muito curta (ex.: 5 a 15 minutos).
- : Validade longa (ex.: 7 a 30 dias), opaco e armazenado no banco/Redis com rotação a cada uso. Se o usuário for desativado, o refresh token é invalidado e ele perde o acesso assim que o access token expirar em poucos minutos.
9. Balanceamento de Carga: Balanceador L4 (Rede) vs L7 (Aplicação)
A Pergunta do Recrutador: “Qual a diferença entre um balanceador de Camada 4 e um de Camada 7 e quando você escolhe cada um?”
| Característica | Layer 4 (L4) - Network | Layer 7 (L7) - Application |
|---|---|---|
| Camada OSI | Camada de Transporte (TCP / UDP) | Camada de Aplicação (HTTP, HTTPS, gRPC, WebSockets) |
| Visibilidade | Olha apenas IP de origem/destino e portas TCP. Não enxerga URLs nem headers. | Inspeciona headers HTTP, cookies, rotas de URL, métodos e payloads JSON. |
| Roteamento Inteligente | Não faz roteamento por rota (ex.: /api vs /static). | Roteia por path (/users $\to$ Serviço A, /orders $\to$ Serviço B), headers e host. |
| Performance e Vazão | Extremamente rápido, lida com milhões de pacotes com baixíssima CPU (sem terminação SSL complexa). | Maior consumo de CPU (precisa descriptografar SSL e inspecionar o pacote HTTP). |
| Equivalente AWS | AWS Network Load Balancer (NLB) | AWS Application Load Balancer (ALB) |
- Algoritmos Comuns de Balanceamento:
- : Distribuição sequencial simples (bom quando as instâncias e requisições são homogêneas).
- Least Connections: Envia para o servidor com menos conexões ativas no momento (ideal para conexões longas, WebSockets ou requisições de tempo variável).
- IP Hash / Sticky Sessions: Mantém o mesmo usuário no mesmo servidor. Atenção do Sênior: Evitar Sticky Sessions em aplicações escaláveis porque cria desbalanceamento de carga e quebra o auto-scaling.
10. Políticas de Despejo de Cache: Como funciona o e ?
A Pergunta do Recrutador: “Como funciona a política de despejo LRU e como você implementaria uma estrutura LRU com tempo $O(1)$?”
- LRU (Least Recently Used):
- Despeja o item que está há mais tempo sem ser lido ou atualizado. É a política padrão mais eficiente para a maioria das aplicações web.
- Implementação em $O(1)$: Combinação de um Hash Map (para busca instantânea em $O(1)$) com uma Lista Duplamente Encadeada (Doubly Linked List).
- Quando um item é lido ou atualizado, ele é removido de sua posição na lista e movido para a cabeça (Head).
- Quando a memória atinge o limite máximo, o nó na cauda (Tail) é removido em $O(1)$ tanto da lista quanto do Hash Map.
- LFU (Least Frequently Used):
- Despeja o item que tem o menor número de acessos acumulados (frequência).
- Desvantagem: Dados que foram muito populares no passado (um pico de acessos) permanecem no cache mesmo que não sejam mais acessados hoje (cache pollution).
11. Tempo Real na Web: Polling vs Long-Polling vs WebSockets vs SSE
A Pergunta do Recrutador: “Qual tecnologia você escolhe para atualizar dados em tempo real no frontend e por quê?”
| Tecnologia | Direção | Como Funciona | Caso de Uso Ideal |
|---|---|---|---|
| Short Polling | Unidirecional (Client $\to$ Server) | O cliente faz GET a cada $N$ segundos (ex.: 5s). | Péssimo para escala; aceitável apenas para protótipos simples ou status com baixa frequência. |
| Long Polling | Unidirecional (Client $\to$ Server) | O cliente faz GET e o servidor segura a requisição aberta até ter novos dados ou estourar timeout. | Sistemas legados sem suporte a WebSockets/SSE. |
| Server-Sent Events (SSE) | Unidirecional (Server $\to$ Client) | Conexão HTTP persistente padrão onde o servidor envia eventos em formato de texto (text/event-stream). Suporta reconexão automática nativa do browser. | Streaming de respostas de IA (LLMs), cotações de ações, placar esportivo, notificações e feeds unidirecionais. |
| WebSockets | Bidirecional Full-Duplex | Faz upgrade da conexão HTTP para protocolo TCP persistente (ws:// ou wss://). Ambos os lados enviam mensagens a qualquer momento sem overhead de headers HTTP. | Chats multiusuário, jogos multiplayer em tempo real, ferramentas de edição colaborativa (estilo Figma/Google Docs). |
12. Deploys sem Downtime e Migrações de Banco (Expand & Contract)
A Pergunta do Recrutador: “Como você faz deploy de uma nova versão que renomeia uma coluna no banco de dados relacional sem derrubar os usuários?”
- Estratégias de Deploy:
- : Atualiza as instâncias aos poucos (ex.: 25% por vez). Risco: versões v1 e v2 rodam simultaneamente por alguns minutos.
- Deployment: Sobe um ambiente idêntico (Green) com a versão nova. Testa e comuta 100% do tráfego no balanceador de carga. Se falhar, reverte na hora.
- Release: Envia 5% do tráfego para a versão nova e monitora logs/métricas de erro. Se estável, aumenta gradualmente para 100%.
- O Padrão (Para Migrações de Banco de Dados):
- Nunca renomeie uma coluna diretamente (
ALTER TABLE RENAME COLUMN), pois a versão antiga em execução quebrará instantaneamente. - Fase 1 (Expand): Adiciona a coluna nova (
new_column) mantendo a antiga (old_column). - Fase 2 (): A aplicação grava em ambas as colunas e lê da antiga. Roda um script de migração em segundo plano copiando os registros legados.
- Fase 3: A aplicação passa a ler da nova coluna.
- Fase 4 (Contract): Quando nenhuma instância mais acessar a coluna antiga, remove-se a coluna antiga com segurança.
- Nunca renomeie uma coluna diretamente (
13. Quando Escolher SQL vs NoSQL (Sem Mitos)
A Pergunta do Recrutador: “Em que situações você escolheria um banco NoSQL em vez de um banco relacional maduro como PostgreSQL?”
- O Mito a Desmistificar: “NoSQL escala e SQL não escala”. PostgreSQL e MySQL modernos atendem com facilidade centenas de milhares de requisições por segundo com réplicas de leitura, poolers de conexão (PgBouncer) e particionamento.
- Quando o SQL Relacional é a Escolha Correta:
- Integridade referencial estrita e transações multi-tabela com garantias ACID (ex.: sistemas financeiros, e-commerce, ERPs).
- Padrões de consulta flexíveis e imprevisíveis (necessidade constante de JOINs, agregações e filtros combinados).
- Quando o NoSQL Brilha de Verdade:
- Chave-Valor / Documento (DynamoDB / MongoDB): Padrões de acesso ultra previsíveis (consultas sempre por
PartitionKeyouID), volume de petabytes e necessidade de latência previsível de milissegundos sem gerenciamento de infraestrutura. - Colunar / Time-Series (ClickHouse / TimescaleDB / Cassandra): Ingestão massiva de eventos append-only e consultas analíticas agregadas em milhões de linhas.
- Grafos (Neo4j / Amazon Neptune): Relações altamente conectadas (redes sociais, detecção de fraudes, grafos de conhecimento).
- Chave-Valor / Documento (DynamoDB / MongoDB): Padrões de acesso ultra previsíveis (consultas sempre por
14. O Problema em Consultas de Banco e APIs ()
A Pergunta do Recrutador: “O que é o problema N+1 e como você o previne em ORMs e APIs GraphQL?”
- O Problema: Você quer listar 50 posts e seus respectivos autores. O ORM faz 1 consulta para pegar os posts (
SELECT * FROM posts LIMIT 50) e depois dispara mais 50 consultas separadas para pegar o autor de cada post (SELECT * FROM users WHERE id = ?). Resultado: 51 consultas ao banco em vez de 1. - Solução no Banco / ORM:
- Eager Loading: Uso explícito de
JOINou operadores de pré-carregamento (ex.:include,select_related,preload):SELECT * FROM posts JOIN users ON posts.author_id = users.id;
- Eager Loading: Uso explícito de
- Solução em GraphQL (Padrão DataLoader):
- O GraphQL resolve campos de maneira aninhada e independente, tornando o N+1 frequente.
- O DataLoader intercepta os IDs solicitados no mesmo ciclo de eventos (tick de execução), agrupa todos em um lote (Batching) e executa uma única query:
SELECT * FROM users WHERE id IN (1, 2, 3, ..., 50); - Em seguida, distribui as respostas para cada resolvedor correspondente.
15. Observabilidade: As Quatro Métricas Douradas () e o p99
A Pergunta do Recrutador: “Por que a média de tempo de resposta é uma métrica ruim para avaliar a saúde de uma API e o que você monitora?”
- Por que a Média Aritmética Mente:
- Se 99 usuários tiverem tempo de resposta de 10ms e 1 usuário sofrer um timeout de 10.000ms, a média será de aproximadamente $109\text{ms}$ (parece razoável no painel, mas 1 em cada 100 usuários teve uma experiência terrível).
- Percentis de Latência (p50, p95, p99):
- p50 (Mediana): O tempo de resposta da metade dos seus usuários.
- p99: O tempo de resposta dos 1% mais lentos. Em e-commerces de alto volume, os clientes p99 costumam ser os usuários mais ativos, com carrinhos mais cheios e maior poder de compra.
- Os 4 Golden Signals do Google SRE:
- Latência: O tempo que leva para atender a requisição (separando latência de sucesso da de erro).
- Tráfego: A quantidade de demanda no sistema (medida em requisições HTTP/s ou transações/s).
- Erros: A taxa de requisições que falham (erros explícitos HTTP 5xx, erros de regra de negócio ou respostas com conteúdo incorreto).
- Saturação: Quão perto de 100% da capacidade seus recursos mais restritos estão operando (uso de CPU, pressão de memória RAM, pool de conexões do banco, IOPS de disco).
🔗 Veja Também
- 📄 System Design - Perguntas para Vagas Seniores: As 12 perguntas completas de arquitetura de alta escala (Uber, WhatsApp, YouTube, Netflix, Twitter, Stripe).
- 📄 AWS: Visão geral dos principais serviços da nuvem e fluxos integrados.
respostas
Para treinar, leia o roteiro em voz alta com um cronômetro, sem olhar para a explicação. Depois leia “por dentro” e confira o que você teria deixado de dizer. Os diagramas são parte da resposta: tente redesenhá-los num papel enquanto fala, porque numa entrevista de System Design o quadro faz metade do trabalho.
Os termos que o Gemini usou de forma misturada ficam separados aqui. “Cache stampede” e “thundering herd” são o mesmo fenômeno (uma chave quente expira e muita gente recalcula ao mesmo tempo). “Cache avalanche” é outro (muitas chaves expiram juntas) e “cache penetration” é um terceiro (consultas por chaves que não existem). A seção 2 trata os três lado a lado.
Para treinar, leia só o roteiro, feche a nota e fale. Depois abra “por dentro” e confira o que ficou de fora. Os diagramas têm animação, então vale abrir cada um e acompanhar o caminho dos pacotes. O mapa de cores é o mesmo em todas as figuras: laranja é o cliente ou o código de aplicação que você controla, terracota escuro é o mecanismo em estudo, azul é serviço gerenciado da AWS, verde é dado e armazenamento (e o caminho saudável), roxo é mensageria e eventos, rosa é falha ou bloqueio, e cinza é software de terceiros.
Data da pesquisa de fatos da AWS e do Redis: 2026-10-22.
Os diagramas seguem um mapa de cores fixo em todas as figuras: laranja para quem pede (cliente, usuário), terracota para o código que você escreve, verde para dados, azul para serviços de entrada, rede e deploy, roxo para observabilidade, rosa para falha, risco ou versão ruim. Nos pacotes animados, laranja é requisição, verde é dado, azul-claro é versão nova ou evento empurrado, roxo é métrica e vermelho é erro.
Hoje é 2026-10-22. Os limites da seção 11 foram conferidos na documentação oficial nesta data. Os detalhes de serviços AWS das seções 12 a 15 vêm de conhecimento geral e não foram conferidos; o que é incerto está marcado no texto e na seção “O que não foi verificado”.
1. Estratégias de cache
A pergunta e o que ela testa
“Quais são as principais estratégias de escrita e leitura em cache e quando você escolheria cada uma?” O entrevistador quer saber se você trata cache como uma decisão de consistência e de falha, e não como um botão de velocidade. Quem responde só “coloco Redis na frente” soa júnior. Quem responde “depende de quanto dado velho eu tolero e do que acontece se o cache morrer” soa sênior.
Como responder em voz alta
“Cache guarda uma cópia barata de um dado caro, e toda estratégia responde a duas perguntas: quem preenche a cópia e quando ela deixa de valer. Para leitura, o padrão mais comum é o cache-aside: a aplicação pergunta ao cache, se não achar, busca no banco, grava no cache com um TTL e devolve. Ele só guarda o que alguém realmente pediu e, se o cache cair, o sistema continua funcionando, só mais lento. O preço é que o primeiro acesso é lento e que o dado pode ficar velho até o TTL vencer.
Para escrita, tenho três opções. No write-through a escrita passa pelo cache e só responde depois de gravar no banco, então o cache nunca fica velho, mas cada escrita é mais lenta. No write-behind a escrita fica no cache, a resposta é imediata e o banco recebe depois em lote, o que dá muita vazão, mas se o nó do cache morrer antes de persistir eu perco dados. E tem o write-around, em que escrevo direto no banco e deixo o cache se preencher na leitura, bom para carga em massa que ninguém vai reler logo.
Eu começaria com cache-aside e TTL, por ser o mais simples e o que falha melhor. Usaria write-through quando ler o dado logo depois de escrever é o caso comum, e write-behind só para dado que posso perder um pouco, como contadores e métricas. Na AWS, isso é ElastiCache com cache-aside no meu código, ou DAX se o banco for DynamoDB, porque o DAX já faz write-through sozinho.”
Por dentro
Pense no cache como a mesa de estudos na entrada da biblioteca que a nota Redis usa de analogia: o acervo (o banco) é grande e confiável, a mesa é pequena e rápida. A pergunta de design é sempre a mesma: quem decide o que vai para a mesa e quando um livro da mesa deixa de valer? As respostas se dividem em estratégias de leitura e de escrita.
Na leitura, o cache-aside (também chamado lazy loading) deixa a decisão com a aplicação. Um acerto (hit) custa uma ida ao cache. Um erro (miss) custa três viagens: perguntar ao cache, consultar o banco, gravar no cache. A doc do ElastiCache descreve exatamente esse custo, e descreve também as duas virtudes: só entra no cache o que foi pedido, e a falha de um nó não derruba a aplicação, apenas aumenta a latência enquanto o cache esquenta de novo. Existe também o read-through, em que a própria camada de cache sabe buscar no banco; para quem usa a aplicação o efeito é o mesmo, mas a lógica de carga mora no cache, não no seu código. O DAX é um exemplo: o cliente DAX fala com o DAX como se fosse o DynamoDB, e o DAX busca na tabela quando há miss.
Na escrita, a TTL do vault já descreve write-through, write-behind e write-around. O ponto de fundo é o que cada uma sacrifica. O write-through sacrifica latência de escrita para ganhar frescor. A doc do ElastiCache lembra que o usuário tolera melhor lentidão ao salvar do que ao ler, o que torna a troca razoável. O write-behind sacrifica durabilidade para ganhar vazão: enquanto o dado só está na memória do cache, uma falha o apaga. O write-around sacrifica frescor, porque o cache só aprende a mudança quando o TTL vence ou alguém invalida.
Falta o outro lado da moeda, a invalidação. O jeito mais comum de manter o cache-aside correto é apagar a chave quando o dado muda (em vez de reescrever o valor), porque apagar é idempotente e não precisa recalcular nada. Mesmo assim existe uma corrida clássica: um leitor tem miss, lê o valor antigo no banco, o escritor atualiza o banco e apaga a chave, e só então o leitor grava o valor antigo no cache. O cache fica velho sem erro nenhum. A defesa prática é o TTL: ele limita o estrago no tempo, o que explica por que a doc do ElastiCache recomenda combinar TTL com qualquer estratégia. A nota Outside the API mostra o fluxo do cache-aside com Redis em Go, e a nota Reliability trata do outro requisito: o cache é uma otimização, nunca a fonte da verdade, então o código precisa de fallback para o banco quando ele falhar.
Por fim, a memória do cache é finita. Quando ela enche, uma política de despejo (como LRU, o menos recentemente usado) decide quem sai. Isso significa que um miss pode acontecer mesmo antes do TTL, e é mais um motivo para o código nunca assumir que a chave está lá.
A figura mostra as três estratégias em três faixas, com a mesma cor para o mesmo papel. Na faixa de cima, a aplicação conversa com o cache e, quando há miss, vai ao banco por conta própria (pacotes verdes mais lentos). Na do meio, a espessura 4 marca o caminho principal: a escrita atravessa o cache e só volta com o ack depois que o banco confirma. Na de baixo, o cache já está em amarelo de alerta porque guarda dados que o banco ainda não viu, e os pacotes vermelhos do lote assíncrono mostram o ponto em que a perda acontece se o nó cair.
Na AWS
O serviço de cache de uso geral é o Amazon ElastiCache. Ele roda os motores Valkey, Redis OSS e Memcached, em dois formatos: serverless (você só cria o cache e a AWS cuida de capacidade) ou cluster com nós que você escolhe. Para um banco relacional (RDS ou Aurora), o ElastiCache é o cache natural, e a estratégia é cache-aside no seu código. Se você usa Valkey ou Redis, ganha também estruturas de dados (listas, conjuntos, hashes), que servem para rate limit, filas simples e travas, como mostra Locks Distribuídos com Redis. Memcached é mais simples, só chave e valor, e escala bem para blocos de dados descartáveis.
O DAX (DynamoDB Accelerator) é o cache específico do DynamoDB. A doc oficial diz que ele reduz leituras eventualmente consistentes de milissegundos de um dígito para microssegundos, é compatível com a API do DynamoDB (poucas mudanças no código) e funciona como cache write-through: a escrita vai ao DynamoDB de forma síncrona e depois o DAX atualiza o item cache. Ele mantém dois caches independentes, um de itens (GetItem) e um de consultas (Query e Scan), e as escritas de itens não invalidam o cache de consultas. Verifiquei em 2026-10-22 que o DAX continua documentado e sem aviso de descontinuação, e a doc lista bem para quais casos ele serve: leituras que precisam de tempo mínimo de resposta (lances em tempo real, jogos, trading), poucos itens lidos com muito mais frequência que os outros (a chave quente de uma promoção de um dia), cargas de leitura pesada e sensíveis a custo (você pode comprar menos capacidade de leitura) e leituras repetidas sobre um conjunto grande. A doc também diz para quem o DAX não é ideal: quem exige leitura fortemente consistente (o DAX repassa essas leituras ao DynamoDB e não as guarda), quem não precisa de microssegundos, quem escreve muito (a replicação entre nós sobrecarrega o cluster) e quem tem poucas leituras repetidas (ela cita taxa de acerto acima de 90% como o ponto em que o DAX rende bem).
A decisão é pelo banco. À esquerda, o losango pergunta qual banco está atrás. Se for relacional, o caminho é o ElastiCache, com o cache-aside escrito por você (a descrição azul da seta diz “qualquer ordem de acesso”, porque o ElastiCache aceita qualquer chave e qualquer formato). Se for DynamoDB, o caminho é o DAX, em que você troca o cliente e o resto é transparente. Em ambos os casos, as setas verdes tracejadas e lentas são os misses indo ao banco, que é o único caminho caro.
E por que não outro? Para um banco relacional o DAX não serve, porque só conversa com a API do DynamoDB. Para o DynamoDB, o ElastiCache funciona, mas você escreve a lógica de preenchimento e invalidação, o que só compensa se você precisa de estruturas de dados do Redis, de cache de resultados compostos (uma página montada com dados de várias tabelas) ou de um cache compartilhado entre vários bancos. O write-behind não existe pronto em nenhum dos dois: se você precisar dele, o desenho é gravar no ElastiCache e deixar um worker drenar para o banco (por exemplo, via fila SQS), assumindo o risco de perda que a figura 1 mostra. Para conteúdo estático e respostas de API públicas, antes de qualquer cache de aplicação, vem a borda: veja Amazon CloudFront e CDN.
A figura 3 reúne as duas formas como o cache falha. No grupo de cima o nó do ElastiCache foi substituído e voltou vazio: a API tem um timeout curto no cache (o X vermelho marca a chamada barrada) e cai para o banco, que passa a absorver toda a leitura e fica em amarelo. É o fallback da nota Reliability em ação, e é também o ensaio do problema da seção 2. No grupo de baixo ninguém caiu, mas um job atualiza o banco sem avisar o cache, o pacote vermelho “preço 100 (velho)” segue para a API, e a seta bloqueada “ninguém invalida” mostra a lacuna: o dado errado dura até o TTL vencer.
Dicas para a entrevista
Antes de escolher, faça perguntas: a razão leitura/escrita, o quanto de dado velho o negócio tolera (segundos? minutos? nada?), o tamanho do conjunto quente, o que acontece se o cache cair e se o mesmo dado é lido logo depois de escrito. Essas perguntas já mostram o raciocínio.
O que o entrevistador espera ouvir: o trade-off de cada estratégia dito em uma frase (frescor contra latência de escrita, durabilidade contra vazão), a menção ao TTL como rede de segurança, e a invalidação como o problema difícil. Uma frase que costuma agradar: “só existem duas coisas difíceis em ciência da computação, invalidar cache e nomear coisas” é clichê; prefira mostrar a corrida de invalidação descrita acima.
Respostas que soam juniores: “uso Redis” sem dizer a estratégia, cache sem TTL, cache como fonte da verdade e esquecer de dizer o que acontece quando ele cai. Se travar, volte para as duas perguntas básicas (quem preenche, quando expira) e desenhe a faixa do cache-aside, que quase sempre é a resposta certa.
Números úteis: o ElastiCache e o DAX respondem em sub-milissegundo a microssegundos contra milissegundos de um dígito de um banco, e a doc do DAX fala em taxa de acerto acima de 90% como pré-condição de ganho. Não invente números para um sistema que você ainda não conhece, peça os dados.
Follow-ups prováveis
Como você evita dado velho? TTL curto como teto, invalidação por apagar a chave na escrita e, se o dado é crítico, write-through. Aceito que haverá uma janela, e digo qual.
Cache-aside ou write-through para um catálogo de produtos? Cache-aside com TTL, porque a leitura domina, ninguém relê logo depois de escrever e o catálogo tolera segundos de atraso.
E se o cache cair? A aplicação trata o erro como miss, com timeout curto no cache, e vai ao banco. Se o banco não aguenta a carga inteira, preciso de proteção contra a enxurrada de misses, que é o assunto da próxima seção.
Qual política de despejo? LRU é o padrão sensato para acesso com localidade temporal. LFU serve quando poucas chaves dominam por muito tempo. O Gemini tem uma pergunta própria sobre isso (a número 10), não coberta aqui.
2. Stampede, avalanche e penetration
A pergunta e o que ela testa
“O que acontece quando uma chave muito acessada expira no cache e milhares de requisições chegam no mesmo milissegundo?” A pergunta parece ter uma resposta só, mas o entrevistador está medindo se você separa três problemas que se parecem (o banco derrete depois que o cache falha) e têm causas e remédios diferentes. Quem junta tudo sob um nome só (“thundering herd”) entrega o remédio errado.
Como responder em voz alta
“O que descreve a pergunta é o cache stampede, também chamado de thundering herd: uma chave quente expira, todas as requisições dão miss ao mesmo tempo e todas vão ao banco rodar a mesma query cara. O cache deveria proteger o banco e, naquele instante, deixa de fazê-lo. A defesa é fazer só um recalcular. Dentro de cada instância eu uso single-flight, que junta as chamadas iguais numa só. Entre instâncias eu uso um lock curto no próprio Redis, com SET NX e expiração: quem ganha o lock recalcula e os outros esperam um instante ou servem o valor velho. Dá para ir além e renovar a chave um pouco antes de vencer, de forma probabilística, para ninguém sequer ver o miss.
Há dois primos que eu separo. Cache avalanche é quando muitas chaves diferentes expiram juntas, tipicamente porque foram carregadas com o mesmo TTL. A cura é jitter no TTL: três mil segundos mais um aleatório de alguns minutos. E cache penetration é quando as consultas pedem chaves que não existem nem no banco, então o cache nunca preenche e cada pedido bate no banco. Aí eu uso cache negativo com TTL curto, um filtro de Bloom na frente, e valido a entrada. Em resumo: stampede é uma chave quente, avalanche são muitas chaves sincronizadas, penetration é chave inexistente.”
Por dentro
Comece pelo stampede, porque é o que a pergunta descreve. Em condições normais, a chave quente (a home de um e-commerce, o preço de um produto em promoção) tem taxa de acerto próxima de 100%, e o banco quase não a vê. O banco foi dimensionado supondo que o cache absorve quase tudo. No instante em que o TTL vence, a chave some, e as requisições que chegarem dentro da janela de recálculo (que pode durar centenas de milissegundos se a query for cara) dão miss. Se são 10.000 por segundo, são milhares de queries idênticas e caras em paralelo. O banco satura, a query demora ainda mais, a janela de recálculo se alonga, entram mais requisições. É uma retroalimentação, e por isso o colapso é tão rápido.
A defesa é impedir que mais de um recalcule. Há quatro ferramentas, que se combinam.
A primeira é o single-flight, dentro do processo. Em Go, o pacote golang.org/x/sync/singleflight faz com que chamadas concorrentes para a mesma chave esperem o resultado de uma única execução. Resolve a multiplicação por goroutines dentro de uma instância, mas não entre instâncias: com 50 pods, ainda são 50 queries. A nota Worker Pools e Rate Limiting e a de concorrência ajudam a pensar em como limitar a concorrência na saída.
A segunda é o lock distribuído. Quem tem miss tenta SET lock:chave token NX PX 10000. O NX só grava se a chave não existe e o PX dá um prazo, de modo que um processo que morre não segura o lock para sempre. Quem ganha recalcula e grava o valor; quem perde espera um pouco (com jitter) e relê o cache, ou serve a versão antiga. Os cuidados de verdade (token único para só liberar o lock que é seu, script Lua atômico, renovação do prazo para tarefas longas) estão em Locks Distribuídos com Redis. Um lock bem curto com TTL é suficiente aqui, porque o pior caso (o lock expira e dois recalculam) é só um desperdício, não uma corrupção, ao contrário de um lock que protege escrita.
A terceira é servir velho enquanto recalcula (stale-while-revalidate). O cache guarda o valor com dois prazos: um prazo “macio” depois do qual o dado é considerado velho e um prazo “duro” em que ele realmente some. Entre os dois, quem chega recebe o valor velho imediatamente e uma única requisição dispara a atualização em segundo plano. O usuário nunca espera.
A quarta é a renovação antecipada probabilística, que a literatura chama de XFetch (do artigo “Optimal Probabilistic Cache Stampede Prevention”, de Vattani, Chierichetti e Lowenstein, 2015). A ideia é que cada leitor, antes do vencimento, joga uma moeda viciada: quanto mais perto do fim do TTL e quanto mais cara a recomputação, maior a chance de ele mesmo renovar a chave. Na prática um leitor renova cedo e o miss em massa nunca acontece. Descrevo de memória, o artigo é a fonte para os detalhes da fórmula.
Agora a avalanche. O cenário típico é um carregamento em lote (um deploy que aquece o cache, um job que preenche mil chaves) com TTL fixo de 3600 segundos: uma hora depois, as mil expiram no mesmo segundo. A diferença para o stampede é que não há uma chave quente, há uma sincronização de muitas chaves. A cura é espalhar os vencimentos com jitter, algo como TTL = 3600 + random(0, 300), e aquecer o cache aos poucos em vez de tudo de uma vez. A avalanche também acontece por outro caminho: o cache inteiro some (um nó substituído, um failover, um flush por engano). O efeito no banco é o mesmo, e a defesa é arquitetural: réplicas em outra zona de disponibilidade, limitar a taxa de leituras ao banco e, se for o caso, deixar a aplicação degradar (devolver menos dados) em vez de tentar tudo. A nota Resiliência com Circuit Breaker e Retry mostra o mesmo fenômeno do lado dos retries (sem jitter, todos os clientes tentam de novo ao mesmo tempo) e o circuit breaker como proteção do banco.
Por fim a penetração. Aqui o cache funciona perfeitamente, e é esse o problema: um atacante (ou um bug) pede GET /usuarios/-9999, o cache dá miss, o banco responde “não existe”, nada é gravado no cache, e a próxima requisição repete o caminho. Cada pedido custa uma consulta ao banco, sem que o cache jamais ajude. Os remédios são três. O cache negativo grava “não existe” com TTL curto (30 a 60 segundos é comum), de modo que a pergunta seguinte é respondida pelo cache. O filtro de Bloom é uma estrutura probabilística que responde “com certeza não existe” ou “talvez exista” para um conjunto de chaves; se ele diz que não existe, você nem consulta o cache nem o banco. Ele nunca dá falso negativo, mas dá falso positivo (a taxa depende do tamanho), e o filtro clássico não permite remover chaves, então ele precisa ser reconstruído de vez em quando. E a validação de entrada: um ID negativo ou fora do formato nem deveria chegar ao cache. Somar um Rate Limiting por cliente fecha a porta para o abuso.
A figura 4 mostra o mesmo instante nos dois mundos. No grupo de cima, a chave acabou de expirar (o rótulo “TTL venceu às 12:00:00”), três instâncias da API recebem pacotes alaranjados dos clientes em sequência escalonada (atraso=auto), todas dão miss e disparam as setas vermelhas grossas e rápidas para o banco, que está em erro. No grupo de baixo as instâncias recebem o mesmo tráfego, mas só a API 1 ganha o lock (seta verde “SET lock NX” para o cache) e faz a única query (seta verde lenta e grossa, que representa a query cara que acontece uma vez). As APIs 2 e 3 recebem o valor velho do cache (setas azuis pontilhadas), então o banco só trabalha uma vez.
A figura 5 é um roteiro de diagnóstico para o momento em que o banco derrete e você precisa decidir qual dos três problemas é. Siga os losangos de cima para baixo: muitas chaves expiraram juntas (avalanche, remédio jitter), é uma chave só e muito quente (stampede, remédio um recalcula e os outros esperam), a chave nunca existiu (penetration, cache negativo e Bloom). Se nenhuma resposta for “sim”, o problema não é o cache: olhe índices (Índices), a query e as conexões. Os pacotes vermelhos no início são o sintoma e os verdes são a cura.
Na AWS
Pensando na pilha do desenho, existem quatro camadas de defesa e cada uma tem um serviço.
Na borda, o Amazon CloudFront já junta pedidos concorrentes iguais: a documentação tem uma seção chamada “Simultaneous requests for the same object (request collapsing)” nas páginas de comportamento de requisição e resposta, e o Origin Shield consolida os pedidos de várias regionais em um só, o que, segundo a doc, pode levar a “tão poucos quanto um” pedido à origem por objeto. Se a chave quente é uma resposta pública e cacheável (a home, o catálogo), o stampede morre antes de chegar à sua aplicação, e é a defesa mais barata. Veja também CDN.
No cache, o ElastiCache com Valkey ou Redis dá o SET NX PX para o lock e o TTL com jitter vive no seu código. Desde o Valkey 8.1 o ElastiCache também tem o filtro de Bloom como tipo de dado nativo (verifiquei o anúncio da AWS em 2026-10-22; ela diz que usa até 98% menos memória que um Set para o mesmo propósito e é compatível com o módulo valkey-bloom), o que resolve a penetração sem montar a estrutura sozinho. No DynamoDB, o DAX tem cache negativo nativo: se o item não existe, ele guarda um resultado vazio até o TTL vencer, o que é exatamente o remédio da penetração.
Na computação, o Lambda permite limitar a concorrência reservada de uma função, o que impede que um pico de misses abra mil execuções simultâneas contra o banco. Na entrada, rate limiting e regras baseadas em taxa (API Gateway com throttling, AWS WAF) cortam o abuso que causa penetração. E no banco, o RDS Proxy mantém um pool de conexões: ele protege contra uma tempestade de conexões, mas não alivia uma query cara, porque a mesma query ainda roda. Esse limite precisa ser dito na entrevista, porque o RDS Proxy é frequentemente citado como solução de stampede, e não é.
A figura 6 percorre a pilha da esquerda para a direita e de cima para baixo. Os usuários (seis pacotes na seta grossa) chegam ao CloudFront, que junta pedidos iguais e manda uma só requisição de origem (seta azul lenta). O Lambda fala com o ElastiCache em ida e volta (GET e SET NX); só quem ganha o lock desce para o RDS Proxy e dali para o banco, num caminho lento e verde, porque só uma query cara acontece por chave. O CloudWatch recebe a métrica de taxa de acerto do cache (seta roxa pontilhada), que é o alarme certo para detectar uma avalanche antes de o banco cair. Observe que o RDS Proxy está em azul, e a etiqueta “pool” mais a descrição “não alivia query cara” lembram o limite.
Dicas para a entrevista
Pergunte: a chave quente é pública (então CloudFront resolve), quanto custa recalcular, quanto dado velho é tolerável, quantas instâncias leem o cache. Cite a ordem de proteção do mais barato para o mais caro: borda, single-flight, lock, servir velho, renovação antecipada.
O que o entrevistador quer ouvir: os três nomes usados corretamente, o jitter no TTL, o lock com TTL curto, e a lembrança de que o próprio retry sem jitter causa o mesmo efeito. Soa júnior: dizer “aumentar o TTL” como solução (só adia e piora o frescor), dizer “colocar mais servidores de banco” ou misturar os três problemas. Também soa júnior propor um lock sem prazo: se quem o detém morrer, a chave nunca mais é calculada.
Se travar, volte ao desenho da figura 4: “um recalcula, os outros esperam ou recebem o velho”. Isso cobre a maior parte da pergunta, e você ganha tempo para lembrar os primos.
Follow-ups prováveis
O que acontece se quem tem o lock morrer? O lock tem TTL (PX), então ele expira, outro ganha e recalcula. O custo é uma espera extra, não um bloqueio permanente.
Filtro de Bloom ou cache negativo? Cache negativo é mais simples e basta quando o conjunto de chaves inexistentes pedidas é pequeno ou se repete. O Bloom compensa quando o espaço de chaves inexistentes é enorme e variado (um ataque com IDs aleatórios), porque ele responde sem guardar cada chave inexistente. O custo do Bloom é manter o filtro em dia com as chaves novas.
Servir dado velho não é arriscado? Depende do dado. Para home, catálogo e contadores, alguns segundos de atraso são aceitáveis. Para saldo ou estoque na hora de pagar, não: nesses casos vale mais o lock e a espera do que o dado velho.
Como você sabe que está acontecendo? Taxa de acerto do cache caindo, picos de queries idênticas e CPU do banco subindo em degrau no mesmo instante do vencimento. Uma métrica de misses por chave também ajuda a achar a chave quente.
3. Read replicas, particionamento e sharding
A pergunta e o que ela testa
“Seu banco PostgreSQL está com CPU a 95% e disco quase cheio. Qual é o plano de ação gradual?” O entrevistador quer ver três coisas: que você diagnostica antes de escalar, que sabe em que ordem usar as ferramentas (da mais barata e reversível à mais cara e irreversível) e que distingue réplica, particionamento e sharding, três palavras que muita gente usa como sinônimos. A pergunta tem uma armadilha embutida: réplica de leitura resolve CPU de leitura, mas não resolve disco cheio, porque cada réplica guarda uma cópia inteira dos dados.
Como responder em voz alta
“Primeiro eu descubro o que está consumindo a CPU: leitura, escrita ou queries ruins. Com a lista das queries mais caras eu costumo achar índice faltando ou um N+1, e isso resolve muita coisa sem mudar a arquitetura. Em paralelo, o disco cheio pede outro diagnóstico: o que cresce, e quanto disso é dado antigo que ninguém lê.
Se a CPU é leitura, o primeiro degrau é cache e, depois, read replicas, em que o primário recebe as escritas e uma ou mais réplicas assíncronas atendem as leituras. O cuidado é o replication lag: quem acabou de escrever pode não ver o próprio dado na réplica, então leituras logo após uma escrita vão para o primário. Mas eu diria claramente que réplica não resolve disco, porque cada uma guarda o conjunto completo. Para disco eu ligo o crescimento automático de armazenamento como socorro e ataco a causa: particionar a tabela grande por data e arquivar ou descartar as partições antigas.
Só se as escritas ou o tamanho passarem do que uma máquina aguenta, mesmo depois de escalar na vertical, eu parto para o sharding, que divide os dados entre servidores por uma chave. É o degrau mais caro: JOINs e transações entre shards ficam difíceis, e escolher mal a chave cria um shard quente. Na AWS, o Aurora ajuda no meio do caminho, porque as réplicas compartilham o volume de armazenamento com o escritor e o volume cresce sozinho, e o Aurora Limitless Database oferece sharding gerenciado.”
Por dentro
Diagnóstico vem antes de qualquer arquitetura. CPU a 95% é um sintoma, e há pelo menos quatro causas diferentes: muitas leituras legítimas, muitas escritas, poucas queries muito caras (uma varredura de tabela inteira por falta de índice) e padrões de acesso ruins como o N+1. No PostgreSQL, a extensão pg_stat_statements ordena as queries por tempo total, e o EXPLAIN ANALYZE mostra por que uma delas é lenta. Muitas vezes um índice certo derruba a CPU pela metade e adia a decisão de arquitetura por meses. A nota Como você melhora a performance de uma API? lista as alavancas do lado da aplicação, e Query pattern ajuda a pensar na forma das consultas. Só depois de esgotar isso, e ver que a carga é legítima, vale subir a escada.
A escada tem uma lógica de custo e de reversibilidade. Escalar na vertical (instância maior) é um clique e se desfaz, mas tem teto físico e financeiro; a nota Amount of scale needed trata dessa escolha entre escala vertical e horizontal. Cache (seção 1) tira leituras repetidas do banco sem mudar o modelo de dados. Read replicas escalam leitura na horizontal e mantêm o modelo, mas introduzem o atraso de replicação. Particionamento reorganiza uma tabela grande dentro do mesmo servidor. Sharding espalha os dados entre servidores e muda como você escreve, consulta e faz transações. Cada degrau resolve um gargalo diferente, e a figura abaixo organiza a decisão.
Leia a figura de cima para baixo. O sintoma (rosa) entra no primeiro losango: a leitura domina a CPU? Se sim, o caminho é réplicas e cache, e a caixa rosa à direita lembra o custo, “réplica não libera disco”. Se não, o próximo losango pergunta se o disco cresce com dado antigo, e a resposta leva a particionar por data e arquivar, com o storage autoscaling como socorro (verde, porque ajuda, mas não é o plano de longo prazo). O terceiro losango pergunta se a escrita passa do que uma máquina aguenta; se sim, sharding (e a caixa rosa avisa que JOIN entre shards fica caro), se não, resta instância maior e revisão de queries.
Agora a diferença entre os três termos, que é onde mais se erra.
Read replica é uma cópia completa do banco, mantida por replicação, que aceita só leitura. No PostgreSQL, o primário envia o log de escrita antecipada (WAL, o registro de tudo que mudou) e a réplica o reaplica. Quando a replicação é assíncrona, o primário confirma a escrita ao cliente sem esperar a réplica, o que mantém a escrita rápida e cria o replication lag: durante alguns milissegundos (ou segundos, sob carga) a réplica está atrasada. Duas consequências práticas. A primeira é a leitura do próprio dado: um usuário publica um post e, ao recarregar a lista (lida de uma réplica), não o vê. A correção, mostrada na figura 10, é guardar o instante da última escrita da sessão e, durante alguns segundos, ler do primário. A segunda é que a réplica herda o conjunto de dados inteiro, então réplicas escalam CPU e I/O de leitura, não armazenamento nem escrita. Uma observação de MVCC para quem quiser se aprofundar: no PostgreSQL, uma consulta longa numa réplica pode entrar em conflito com a aplicação do WAL e ser cancelada, e há parâmetros para trocar um risco pelo outro.
No grupo de cima da figura 10, a usuária publica, a API grava no primário (seta verde “INSERT”) e, na leitura seguinte, consulta a réplica, que ainda não recebeu o WAL (seta vermelha pontilhada e lenta) e devolve uma lista sem o post (pacote vermelho e rápido, o dado errado chegando depressa). No grupo de baixo, a API guarda o instante da última escrita e um losango decide: escreveu há menos de 5 segundos? Se sim, lê do primário (consistente); se não, da réplica (barata). Os 5 segundos são um exemplo, o valor certo vem do lag que você mede.
Particionamento (partitioning) divide uma tabela grande em pedaços menores dentro do mesmo servidor, por uma regra (intervalo de datas, lista de valores, hash). O banco continua tratando tudo como uma tabela lógica e usa a para ler só as partições relevantes (partition pruning). O ganho principal é de manutenção: apagar ou arquivar o mês mais antigo vira um DROP ou DETACH da partição inteira, em vez de um DELETE gigante que gera lixo e trava o autovacuum. É a ferramenta certa para “disco cheio de dados antigos”. A nota Particionamento explica função e esquema de partição e as estratégias de escolha, e a nota O que é uma partição de banco de dados (database partition)? aponta para ela. Quando o Gemini fala em “particionamento vertical” como separar tabelas em bancos distintos (autenticação, pedidos, catálogo), o nome mais correto é particionamento funcional, ou divisão por domínio. Particionamento vertical, no sentido estrito, é dividir as colunas de uma tabela em duas (as colunas pesadas e raramente lidas numa tabela à parte). A separação por domínio é boa, mas é uma decisão de arquitetura de serviços, e traz o problema de transações entre bancos, que o Saga Pattern trata.
Sharding divide os dados entre servidores diferentes (shards), cada um com uma parte e capaz de receber escritas. É o único degrau que escala escrita e armazenamento além de uma máquina. O preço é alto. Cada consulta precisa saber em que shard está o dado, o que exige uma chave de roteamento. JOINs entre shards viram chamadas de rede e agregação na aplicação. Transações entre shards perdem as garantias de ACID locais e pedem coordenação (saga ou commit em duas fases). IDs precisam ser únicos globalmente sem uma sequência central, o que leva a soluções como Snowflake ID. E redistribuir dados quando se acrescenta um shard (resharding) é uma operação delicada, por isso se escolhe uma estratégia de hash que reduz a movimentação.
A escolha da chave de shard é a decisão que mais pesa. Uma chave de data (data_criacao) parece natural, mas manda todas as escritas de hoje para o shard do mês atual, enquanto os outros ficam ociosos. Uma chave de hash de user_id espalha de forma uniforme, mas cada consulta que não filtra por usuário precisa perguntar a todos os shards. A chave certa é aquela que alinha a unidade natural de acesso (o usuário, o tenant, a conta) com a unidade de distribuição.
A figura 9 compara as duas chaves lado a lado. À esquerda, com hash(user_id), os três shards recebem pacotes verdes na mesma cadência (atraso=auto), cada um com cerca de um terço das escritas. À direita, com data_criacao, as setas para os shards antigos são cinza, finas, lentas e pontilhadas, quase vazias, e a seta para o shard do mês atual é vermelha, grossa e carrega oito pacotes rápidos: o shard em erro é o gargalo que o sharding deveria ter eliminado.
Na AWS
No RDS, a réplica de leitura usa a replicação nativa do motor, é assíncrona e você a cria manualmente: a documentação diz explicitamente que o RDS não faz autoscaling de réplicas. O limite padrão é de 15 réplicas por instância primária no RDS (a página de cotas diz que é ajustável, exceto para Aurora). Cada réplica é cobrada como uma instância comum e pode ficar em outra região, o que também serve a recuperação de desastre, já que ela pode ser promovida a instância independente. Não confunda com o standby do Multi-AZ: esse é síncrono, existe para alta disponibilidade e, na implantação Multi-AZ de instância, não atende leitura. Além disso, cada réplica RDS tem o seu próprio disco, de modo que o armazenamento é multiplicado, e a documentação do PostgreSQL avisa que, sem transações no primário, o lag reportado pode chegar a cerca de cinco minutos por causa da troca de segmento do WAL, ou seja, o número do lag exige interpretação.
No Aurora a arquitetura é outra, e dizer isso na entrevista é um bom diferencial. O cluster tem um volume de armazenamento compartilhado que se estende por três zonas de disponibilidade, com cópias dos dados em cada uma (a descrição clássica fala em seis cópias, duas por zona, que não reconfirmei nesta consulta). Existe uma instância escritora e até 15 Aurora Replicas, que leem do mesmo volume em vez de manterem cópia própria. Por isso a documentação diz que o lag costuma ficar muito abaixo de 100 ms, que acrescentar uma instância é rápido (ela se conecta ao volume, sem copiar tabelas) e que o failover promove uma réplica sem recriar dados. O volume cresce automaticamente; a página de cotas indica um máximo de 128 TiB por cluster e outra página da doc cita 256 TiB para versões específicas, então confira o limite da sua versão. O endpoint de leitura distribui as conexões entre as réplicas. Mas o Aurora não é um banco de escrita distribuída: continua existindo uma só instância escritora, então ele não resolve, sozinho, o limite de escrita de uma máquina.
A figura 8 coloca os dois modelos um acima do outro. No RDS (grupo de cima), o primário e cada réplica têm o seu volume EBS, e o WAL viaja de forma assíncrona (setas lentas; a vermelha pontilhada marca a réplica cujo lag cresce com a carga). Disco cheio no primário continua sendo disco cheio, e cada réplica precisa de espaço para o conjunto inteiro. No Aurora (grupo de baixo), o escritor envia o log de redo para as três zonas (leque de três setas verdes, defasadas por atraso=auto) e as réplicas leem páginas desse mesmo volume (setas azuis pontilhadas), por isso o selo “lag típico < 100 ms” e o “até 15 por cluster”.
Para o disco, o RDS tem o armazenamento com autoscaling (ele aumenta o volume quando o espaço acaba, dentro de um limite que você define), o que evita a parada de emergência, mas só adia o problema; o plano estrutural é o particionamento nativo do PostgreSQL, que funciona no RDS e no Aurora PostgreSQL, com arquivamento das partições antigas para o S3 (veja S3). Para conexões, o RDS Proxy mantém um pool compartilhado e evita que muitas réplicas e muitos clientes esgotem max_connections, já que a doc de cotas recomenda o proxy para aplicações que abrem e fecham conexões com frequência ou mantêm muitas abertas.
Para sharding, não há um “RDS sharded” clássico: ou você faz o roteamento na aplicação sobre várias instâncias RDS, ou usa o Aurora PostgreSQL Limitless Database. A documentação o descreve como escala horizontal automatizada, com grupos de shards (a página de cotas menciona “DB shard groups”), capaz de processar milhões de transações de escrita por segundo e petabytes de dados, mantendo a operação como um único banco. Existe e está documentado; verifique as versões e regiões suportadas antes de recomendar, porque não conferi esse detalhe. Outra via é o DynamoDB, que particiona os dados automaticamente pela chave de partição, ao custo de abrir mão do modelo relacional e dos JOINs: faz sentido quando o padrão de acesso é conhecido e por chave (veja Structure of data).
Por que não Aurora sempre? Porque o RDS comum cobre muito bem cargas menores, tem previsibilidade de custo e oferece outros motores (MySQL, MariaDB, Oracle, SQL Server). Aurora compensa com muitas réplicas, failover rápido e crescimento de armazenamento sem planejamento. Por que não réplica para disco? Pelo motivo repetido acima: cada réplica RDS copia tudo. Hoje não há aula no vault sobre Aurora nem DynamoDB (a nota RDS menciona o Aurora em uma seção curta); o link [[Aurora]] nela ainda não aponta para uma nota existente.
Dicas para a entrevista
Faça perguntas: qual a razão leitura/escrita, o que cresce no disco, quanto de atraso de leitura é tolerável, se o acesso é naturalmente por usuário ou tenant, se há JOINs entre grandes tabelas. Diga que mediria antes de agir (pg_stat_statements, métricas de CPU, IOPS, lag, conexões).
O que o entrevistador espera ouvir: a ordem dos degraus e o porquê, o replication lag com a solução de ler do primário após escrever, a diferença exata entre particionamento e sharding, a escolha da chave contra o hot shard e o custo dos JOINs entre shards. Também agrada citar que o primeiro passo é quase sempre índice, query e cache, e que o sharding é o último recurso.
Respostas que soam juniores: “faço sharding” como primeira resposta, propor réplica para resolver disco cheio, escolher created_at como chave de shard, ignorar o lag, chamar de “sharding” o que é apenas particionamento de tabela. Se travar, volte à pergunta “o que está saturado: leitura, escrita ou armazenamento?” e escolha o degrau correspondente na figura 7.
Follow-ups prováveis
Como você garante que o usuário veja o que acabou de escrever? Leio do primário por alguns segundos depois de uma escrita da sessão (ou uso uma marca de versão e só uso réplicas que já a alcançaram).
Como escolher a chave de shard? Pela unidade natural de acesso (usuário, tenant), com alta cardinalidade e distribuição uniforme, evitando chaves monotônicas como data ou ID sequencial.
E se uma consulta precisa de dados de vários shards? Vira uma consulta distribuída (scatter-gather) feita pela aplicação, que é lenta. O desenho certo evita esse caso: agrupa o que se consulta junto no mesmo shard, ou mantém uma cópia desnormalizada.
Quando o Aurora basta? Quando o gargalo é leitura e armazenamento e uma única instância escritora, escalada na vertical, ainda aguenta a escrita. Quando a escrita excede isso, entra o Limitless ou outra estratégia de sharding.
4. CAP e PACELC
A pergunta e o que ela testa
“Na prática, o que significa ter que escolher entre Consistência e Disponibilidade no Teorema CAP?” Quase todo candidato sabe recitar “escolha dois entre três”. O entrevistador quer saber se você sabe que essa frase está errada, que a escolha só existe durante uma partição de rede, e se você consegue ir além com PACELC e dar exemplos reais, de preferência de produtos que você usaria.
Como responder em voz alta
“O CAP diz que, num sistema distribuído, quando a rede se parte entre os nós, eu não consigo ao mesmo tempo responder a todas as requisições e garantir que todas as respostas reflitam a escrita mais recente. Como partições de rede acontecem, a letra P não é opcional, então a escolha real é: durante a partição, o que eu prefiro? Num sistema CP, o lado que não tem quórum recusa ou espera, e eu aceito erro em troca de nunca devolver dado errado. É o que eu quero para saldo bancário, reserva de assento, estoque no momento do pagamento. Num sistema AP, todos os lados continuam respondendo, mesmo que divergam, e eu reconcilio depois. É o que eu quero para curtidas, contadores de visualização, carrinho de compras.
O CAP só fala do momento da falha, mas a maior parte do tempo não há partição, e aí existe outra troca: o PACELC completa o quadro. Se há partição (P), escolho entre disponibilidade e consistência; senão (E, de else), escolho entre latência e consistência, porque para garantir consistência eu preciso coordenar réplicas, e coordenação custa tempo. Isso não é uma propriedade do sistema inteiro: o mesmo banco costuma deixar escolher por operação. No DynamoDB, a leitura é eventualmente consistente por padrão, e eu posso pedir leitura fortemente consistente em uma requisição específica, pagando o dobro e aceitando mais latência. E nas global tables, o modo MRSC, de consistência forte entre regiões, troca latência de escrita por RPO zero.”
Por dentro
Vamos devagar, porque cada letra tem um significado mais estrito do que parece. A consistência (C) do CAP é a linearizabilidade: depois que uma escrita termina, qualquer leitura, em qualquer nó, vê aquele valor ou um mais novo, como se existisse uma única cópia. Não é o mesmo C do ACID, que fala de manter as invariantes do banco (chaves, restrições) dentro de uma Transação. A disponibilidade (A) significa que todo nó que não falhou responde a toda requisição, não que o sistema “esteja no ar” em média. E a tolerância a partição (P) é continuar funcionando quando mensagens entre nós se perdem ou atrasam. A fonte de origem é a conjectura de Eric Brewer (2000), provada por Gilbert e Lynch (2002).
Por que “escolher dois” engana? Porque você não escolhe P. Em qualquer sistema com mais de uma máquina, a rede pode falhar, e um sistema que “abre mão” de P simplesmente não sabe o que fazer quando isso acontece (ele fica incorreto ou trava). O que se escolhe é o comportamento na partição. Imagine dois nós A e B, com os clientes divididos entre eles, e o cabo entre os nós cortado. Chega uma escrita x=2 ao nó B, que não consegue avisar o nó A. Ou B recusa a escrita (sacrificou disponibilidade para manter a consistência, é CP) ou aceita e A e B passam a ter valores diferentes (sacrificou a consistência, é AP).
A figura 11 mostra os dois caminhos. No grupo de cima (CP), o cliente A escreve no nó A, que tem quórum (a maioria dos nós), e tudo funciona (pacote verde). O nó B está na minoria, fica rosa e degradado, e o cliente B recebe “erro 503”: o pacote barrado com o X mostra a disponibilidade sacrificada. No grupo de baixo (AP), os dois clientes escrevem e os dois nós aceitam (pacotes verdes dos dois lados), a replicação entre eles está partida (o X cinza), e o resultado é o quadro rosa de conflito: x=1 aqui, x=2 lá, para reconciliar depois.
Como se “reconcilia depois” é a parte de engenharia de um sistema AP, e vale citar três técnicas. A mais simples é “o último que escreve ganha” (last writer wins), com um relógio para decidir; é fácil, mas pode descartar uma escrita legítima. Outra é guardar versões concorrentes (vector clocks) e pedir à aplicação que as una, como fazia o carrinho de compras do artigo do Dynamo. A terceira são tipos de dados que se fundem sem conflito (CRDTs), como contadores e conjuntos. Em todos os casos, a aplicação precisa ser desenhada para tolerar dados temporariamente diferentes, e é aqui que o Saga Pattern e a consistência eventual entram na conversa de microsserviços.
Do lado CP, o mecanismo comum é o quórum: com N réplicas, exigir que escritas e leituras toquem maiorias garante que se enxerguem (a regra R + W > N). Sem maioria acessível, o nó recusa. Isso explica por que sistemas de consenso (Raft, Paxos) são o coração dos bancos CP.
O PACELC (Daniel Abadi, 2012) cobre o que o CAP omite. Em operação normal, não há partição, e mesmo assim uma réplica pode ter dado velho. Garantir leitura atual significa consultar o líder ou uma maioria, o que custa uma ida e volta a mais, possivelmente entre regiões. Escolher a réplica mais próxima custa consistência e ganha latência. Então o PACELC é “se P, A ou C; senão (E), L ou C”. As siglas dos sistemas vêm daí: um sistema PA/EL privilegia disponibilidade e latência, um PC/EC privilegia consistência sempre, e há mistos (PA/EC, PC/EL). Uma ressalva importante: classificar um produto numa sigla única é uma simplificação, porque muitos oferecem os dois comportamentos por configuração.
A figura 13 é um fluxo de duas perguntas. Uma requisição chega e o primeiro losango pergunta se há partição agora. Se sim, o segundo losango decide A ou C; se não (o caso comum, “o dia comum”), outro losango decide L ou C. Nos quatro destinos, a cor diz o que foi escolhido: terracota é o lado rápido e disponível (responde, talvez velho), verde é o lado consistente (recusa, espera ou consulta o líder, mais lento). Os pacotes azuis rápidos e os verdes lentos reforçam a diferença de velocidade, e cada caixa já traz o exemplo AWS correspondente.
Na AWS
O DynamoDB é o melhor exemplo, porque o modelo de consistência está exposto na API. A documentação diz que tabelas e índices secundários locais oferecem duas opções por requisição: leituras eventualmente consistentes (o padrão) e fortemente consistentes. Em GetItem, Query e Scan existe o parâmetro ConsistentRead; com ele verdadeiro, a resposta reflete todas as escritas bem-sucedidas anteriores. Leituras fortemente consistentes só existem em tabelas e LSIs: índices secundários globais () e streams são sempre eventualmente consistentes. A leitura eventual custa metade da forte, o que é um bom argumento de custo para usar a forte só onde importa. Isso responde à pergunta do brief: sim, a escolha é por requisição.
Para várias regiões, as global tables têm dois modos, definidos na criação e imutáveis depois. O padrão é MREC, consistência eventual entre regiões: a escrita é replicada de forma assíncrona, em geral em até um segundo, e conflitos em escritas simultâneas são resolvidos pelo “último que escreve ganha” por item. Uma leitura fortemente consistente numa réplica MREC devolve a versão mais recente apenas se o item foi atualizado por último naquela região; se foi atualizado em outra, pode devolver dado velho. O RPO (quanto dado se pode perder num desastre) é maior que zero.
A novidade que o brief pediu para conferir existe: o MRSC, consistência forte multi-região, está documentado hoje (consultei em 2026-10-22). Nele a escrita é replicada de forma síncrona para ao menos uma outra região antes de responder sucesso, e leituras fortemente consistentes em qualquer réplica sempre devolvem a versão mais recente. A tabela precisa ter exatamente três regiões: três réplicas, ou duas réplicas e uma testemunha (witness), um componente gerenciado pelo DynamoDB que guarda os dados escritos mas não aceita leitura nem escrita. O RPO é zero. As restrições são sérias: só em uma lista de regiões (que inclui o Canadá Central, Virgínia, Ohio, Oregon, Irlanda, Frankfurt, Tóquio e outras), a tabela precisa estar vazia ao converter, não há TTL, não há índices secundários locais, não há transações, escritas concorrentes no mesmo item em regiões diferentes falham com ReplicatedWriteConflictException (e podem ser repetidas), e a latência de escrita e de leitura forte depende da distância entre as regiões. Em termos de PACELC, o MRSC move a tabela do lado “latência” para o lado “consistência”, e a doc recomenda testar a carga nas regiões escolhidas. Não verifiquei a data de lançamento do MRSC; o importante é que hoje ele faz parte da documentação principal.
A figura 12 reúne as três escalas de consistência. No quadro “Por requisição”, a aplicação manda dois tipos de leitura para a tabela: a eventual (seta azul, rápida, fina) e a forte (seta amarela, lenta e grossa, “custa 2x”). No quadro MREC, as réplicas do Canadá e da Virgínia trocam atualizações de forma assíncrona e lenta (pontilhado roxo, “last writer wins”), e o selo “RPO > 0” mostra o custo. No quadro MRSC, a escrita na Virgínia só termina depois que a réplica de Ohio confirma (seta verde grossa e lenta, “replica antes do ack”), e a testemunha (borda tracejada) só recebe o log; o selo “RPO = 0” é o ganho.
Nos bancos relacionais gerenciados, o desenho é mais simples. O RDS tem um escritor, e as réplicas de leitura são assíncronas; ler delas é “EL” no PACELC, ler do primário é “EC”. O standby Multi-AZ é síncrono, mas existe para disponibilidade, não para leitura. O Aurora mantém um escritor e réplicas que leem o mesmo volume, com o lag típico abaixo de 100 ms (seção 3), e para vários países há o Aurora Global Database, com clusters secundários em outras regiões (a doc fala em até 10 secundários somente leitura para Aurora PostgreSQL). Nesses bancos, CAP quase nunca aparece como escolha explícita, porque há um único escritor; o que aparece é o atraso de replicação.
No S3, o assunto costuma aparecer na entrevista por causa de uma mudança antiga: desde dezembro de 2020 o S3 oferece consistência forte de leitura após escrita (não reconferi nesta consulta). Já o DAX só oferece leitura eventual e repassa as leituras fortes ao DynamoDB sem cachear, o que a doc da seção 1 confirmou. Por que não usar consistência forte para tudo? Custa o dobro no DynamoDB, adiciona latência e reduz a disponibilidade durante falhas. Por que não eventual para tudo? Porque há dados que não toleram a janela, como saldo e estoque no checkout.
Dicas para a entrevista
Pergunte: o que acontece com o negócio se o usuário ler um dado de segundos atrás? E se o sistema recusar a operação durante uma falha? A resposta a essas duas perguntas é o requisito que define CP ou AP para aquela operação específica, e dizer isso já mostra maturidade.
Espera-se ouvir: que P não é uma escolha, que a decisão só vale durante a partição, o PACELC, o fato de a escolha ser por operação (leitura de perfil eventual, cobrança forte), a distinção entre o C do CAP e o C do ACID, e exemplos concretos de produto. Soa júnior: “o MongoDB é CP e o Cassandra é AP” como fatos fixos (ambos são configuráveis), dizer “escolho CA” (não existe para sistemas distribuídos reais), tratar consistência eventual como “dado errado” e esquecer o custo de latência no dia normal.
Se travar, descreva o cenário dos dois nós e do cabo cortado, e pergunte qual lado deveria responder. A resposta sai sozinha.
Uma observação sobre números: o Gemini escreveu que DynamoDB e Cassandra trocam consistência por latência “ultrabaixa (< 5 ms)”. A documentação do DynamoDB fala em “single-digit milliseconds” (milissegundos de um dígito) para a maior parte dos casos e microssegundos só com o DAX. Evite prometer um valor fixo.
Follow-ups prováveis
Um sistema pode ser CA? Só se não for distribuído (uma única máquina) ou se aceitar ficar incorreto ou indisponível numa partição, o que na prática é uma escolha de CP ou AP feita sem querer.
O DynamoDB é CP ou AP? Depende da operação. Com leitura eventual ele funciona como um sistema PA/EL, e a leitura forte move a operação para o lado da consistência dentro de uma região. Entre regiões, MREC é eventual e MRSC é forte. Esta classificação vem da literatura de PACELC, e não de uma garantia contratual da AWS.
Como você desenharia o saldo de uma conta? Consistência forte na escrita (um escritor com transação, ou MRSC se precisar de RPO zero entre regiões), leitura forte quando o valor decide uma operação, e leitura eventual para o extrato exibido na tela.
E curtidas? Aceito eventual: escrevo em um contador sem coordenação, agrego depois, e tolero que o número demore a convergir.
5. Comunicação síncrona e assíncrona
A pergunta e o que ela testa
“Quando dois microsserviços devem falar via HTTP ou gRPC síncrono e quando devem se comunicar via fila ou pub/sub?” O entrevistador quer um critério, não uma preferência. A resposta fraca é “assíncrono é melhor porque escala”; a forte diz qual pergunta fazer (o chamador precisa do resultado para continuar?), conhece o custo de cada lado (acoplamento temporal contra consistência eventual e duplicatas) e escolhe o serviço certo da AWS entre SQS, SNS, EventBridge e os streams.
Como responder em voz alta
“Eu pergunto uma coisa: o chamador precisa da resposta para continuar? Se precisa, como autenticar uma senha, consultar um saldo ou validar um cupom, é uma chamada síncrona, via REST ou gRPC. O risco é o acoplamento temporal: se o serviço B está lento ou fora, o serviço A fica com threads presas esperando, a lentidão sobe a cadeia e vira falha em cascata. Eu mitigo com timeout curto, retry com backoff e jitter, circuit breaker e bulkhead.
Se o chamador pode continuar sem o resultado, como enviar e-mail, gerar PDF, transcodificar mídia ou liquidar um pedido, uso comunicação assíncrona com uma fila. A fila é um amortecedor: se chegam dez mil pedidos por segundo na Black Friday, ela guarda o excesso e os workers consomem no ritmo que o banco aguenta, o que se chama de backpressure. Isso desacopla no tempo: o produtor não precisa que o consumidor esteja de pé. O custo é que o sistema fica eventualmente consistente, a mensagem pode chegar duplicada (então o consumidor precisa ser idempotente) e mensagens que falham sempre precisam ir para uma depois de N tentativas, para não travar a fila.
Na AWS, a escolha é: SQS para uma fila com consumidores competindo (standard para vazão, FIFO quando ordem por chave ou deduplicação importam), SNS quando um fato precisa chegar a vários interessados (SNS mais uma SQS por assinante), EventBridge quando eu quero rotear por conteúdo, integrar serviços AWS e SaaS ou conectar contas, e Kinesis ou MSK quando preciso reler o histórico.”
Por dentro
Comece pela natureza da chamada síncrona. Quando A chama B por HTTP, A fica bloqueado até a resposta (ou um timeout), e a disponibilidade de A passa a depender da de B. Se cada serviço de uma cadeia de três tem 99,9% de disponibilidade e todos são necessários para a resposta, a disponibilidade composta é de cerca de 99,7% (0,999 elevado a 3), e isso sem contar a lentidão, que é pior que a queda: um serviço lento mantém threads, conexões e memória ocupadas em quem o chama, e a exaustão desses recursos propaga a falha para cima. Os retries pioram a situação, porque transformam cada requisição lenta em várias, a mesma dinâmica do thundering herd da seção 2. A nota Resiliência com Circuit Breaker e Retry mostra as defesas (timeout, backoff exponencial com jitter, circuit breaker, bulkhead) e a nota Microsserviços em Go contextualiza o desenho. Ainda assim, síncrono é a escolha certa quando há uma dependência de dados real: a resposta é o objetivo da chamada.
Agora o assíncrono. A mensagem é entregue a um intermediário (o broker) que a guarda, e o consumidor a processa quando puder. Três modelos aparecem, e vale distingui-los. Na fila (ponto a ponto), cada mensagem é processada por um consumidor entre vários que competem; serve para distribuir trabalho. No pub/sub (tópico), cada mensagem é copiada para todos os assinantes; serve para avisar vários interessados sobre um fato. No log (como o Kafka), as mensagens ficam retidas em ordem e cada grupo de consumidores lê no seu ritmo e pode reler o passado; serve para eventos que precisam de replay e ordenação em alta vazão. A nota Kafka, com O que é o Kafka e como ele funciona, trata do modelo de log; pub - sub e NATS cobrem o modelo de tópicos; e Mensageria Prática com RabbitMQ e Kafka mostra RabbitMQ e Kafka em código Go, com DLQ e ack manual. Todo esse estilo é o território da Arquitetura orientada a eventos. Uma boa distinção de vocabulário: comando (peço que algo seja feito, tem um destinatário) e evento (fato que já aconteceu, quem se interessar assina). Eventos acoplam menos.
O que o assíncrono compra. A primeira vantagem é o amortecimento de picos (load leveling): o produtor grava rápido, e a vazão do consumidor é controlada por você, o que protege o banco. A segunda é o desacoplamento temporal: o consumidor pode estar fora do ar por um tempo e a fila retém o trabalho. A terceira é a falha isolada: se o e-mail cai, o pedido continua sendo aceito. A quarta é o retry que não bloqueia ninguém: a mensagem volta a ficar visível e é tentada de novo, sem o usuário esperando.
O que ele custa, e é aqui que a entrevista aprofunda. Primeiro, consistência eventual: a resposta ao usuário é “recebi” (HTTP 202), não “concluí”, e a interface precisa lidar com isso (status, notificação). Segundo, entrega pelo menos uma vez: a maioria dos brokers prefere entregar duas vezes a perder uma, então o consumidor precisa ser idempotente; a nota Idempotência explica a chave de idempotência e o caso do pagamento. Terceiro, ordem: filas não garantem ordem global, e quando a ordem por entidade importa é preciso um mecanismo explícito (grupo de mensagens, partição por chave). Quarto, escrever no banco e publicar a mensagem são duas operações que podem falhar separadamente (o problema do dual write), e a solução é o Transactional Outbox, que a nota de mensageria em Go detalha. Quinto, fluxos de vários passos com compensação pedem o Saga Pattern. Sexto, depuração: sem rastreamento, ninguém sabe onde a mensagem está, então a nota de tracing se torna parte do pacote.
O mecanismo da fila merece um passo a passo, porque a pergunta sobre DLQ depende dele. No SQS, quando um consumidor recebe uma mensagem, ela não é removida: fica invisível por um período (o visibility timeout, 30 segundos por padrão). Se o consumidor terminar e chamar DeleteMessage, ela sai da fila. Se ele falhar ou demorar mais que o timeout, a mensagem fica visível de novo e é entregue outra vez, com o contador de recebimentos aumentado. Uma mensagem que sempre falha (um JSON inválido, um dado que quebra o código) é uma poison pill: ela voltaria para sempre, consumindo capacidade. A política de redrive define um maxReceiveCount: ao ultrapassar o limite, a mensagem é movida para a dead-letter queue, uma fila separada onde fica retida para análise, sem atrapalhar o resto.
A figura 14 põe o mesmo pico de tráfego contra dois desenhos. No grupo de cima (síncrono), oito pacotes rápidos entram no serviço de pedidos, que chama o serviço de e-mail por HTTP; o e-mail está em erro e a chamada é barrada (X), então o serviço de pedidos fica em amarelo, com threads presas, e o banco também sofre (pacote vermelho lento). A lentidão de um serviço periférico derrubou o fluxo principal. No grupo de baixo (assíncrono), o serviço de pedidos só enfileira (pacotes azuis rápidos) e responde 202; a fila SQS absorve o pico (selo “backpressure”), e os workers consomem poucos pacotes por vez (setas verdes lentas) até o banco, que recebe uma carga constante. O envio de e-mail está numa seta pontilhada e lenta: pode cair e voltar sem que ninguém perceba.
Na AWS
O Amazon SQS é a fila gerenciada. Ele tem dois tipos, e a escolha segue o que a doc descreve. A fila standard oferece vazão praticamente ilimitada, entrega pelo menos uma vez (pode haver duplicatas) e ordenação de melhor esforço (pode haver mensagens fora de ordem). É a escolha padrão para distribuir trabalho em volume, desde que o consumidor seja idempotente. A fila FIFO garante ordem dentro de cada grupo de mensagens (MessageGroupId, obrigatório) e evita duplicatas por meio de MessageDeduplicationId ou deduplicação por conteúdo; o paralelismo vem de usar vários grupos, já que dentro de um grupo o processamento é sequencial. A vazão da FIFO é menor: sem lotes, 300 chamadas de API por segundo por ação; com lotes de até 10 mensagens, 3.000 mensagens por segundo; e o modo de alta vazão aumenta isso bastante. Aqui há uma divergência na própria documentação: a página de tipos de fila cita até 30.000 transações por segundo no modo de alta vazão, enquanto a página de cotas de mensagens lista valores por região, como até 70.000 TPS em Virgínia, Oregon e Irlanda, e 2.400 TPS como padrão em outras regiões. Use a página de cotas, que é a mais específica, e confirme a região que você vai usar.
Outros números do SQS, conferidos na página de cotas em 2026-10-22: a retenção padrão é de 4 dias (de 60 segundos a 14 dias), o visibility timeout padrão é de 30 segundos (de 0 a 12 horas), a mensagem pode ter até 1 MiB (para mais que isso, a biblioteca estendida guarda o corpo no S3, até 2 GB), e um lote tem no máximo 10 mensagens. Com Lambda como consumidor, o serviço lê a fila e entrega lotes à função; há também um modo de resposta parcial de lote, para não reprocessar as mensagens que deram certo (confira o nome exato do recurso na documentação do Lambda, que não reverifiquei).
O DLQ no SQS é uma fila comum, na mesma conta e região da fila de origem, ligada por uma redrive policy com maxReceiveCount. Três cuidados que a doc destaca. Escolha um maxReceiveCount alto o bastante para permitir retentativas legítimas (com 1, uma única falha já manda a mensagem para a DLQ). Em filas standard, o prazo de retenção da mensagem conta a partir do momento original de envio e não é reiniciado ao ir para a DLQ, então a retenção da DLQ deve ser maior que a da fila principal; em filas FIFO o relógio reinicia. E não use DLQ com FIFO se não pode quebrar a ordem das operações: a mensagem que sai da fila rompe a sequência. Depois que a causa é corrigida, o redrive devolve as mensagens da DLQ para a fila de origem. Um alarme do CloudWatch sobre a quantidade de mensagens na DLQ é o que transforma a DLQ em um sinal: sem ele, a fila de mortos vira um cemitério silencioso.
A figura 16 segue uma mensagem veneno. O produtor envia pedidos normais (seta laranja) e a mensagem veneno entra pontilhada e vermelha. O consumidor recebe da fila: se processa, apaga e o pedido sai (seta verde “processou”); se falha, não apaga, e a mensagem volta à fila depois do visibility timeout (seta vermelha lenta “falhou, volta”, com três pacotes que simulam as tentativas). Quando o contador passa de 5, a seta grossa e vermelha leva a mensagem à DLQ. De lá, uma métrica dispara o alarme do CloudWatch, que notifica a pessoa de plantão, que lê a mensagem, corrige o problema e faz o redrive de volta para a fila de origem.
O Amazon SNS é um serviço de publicação e assinatura: uma mensagem publicada no tópico é empurrada (push) para todos os assinantes, que podem ser filas SQS, funções Lambda, endpoints HTTP, e-mail, SMS e push móvel. A combinação clássica é o fan-out SNS mais SQS: o SNS espalha, e cada interessado tem a sua própria fila, que retém a mensagem se o consumidor estiver fora e tem a sua própria DLQ. As retentativas do SNS são generosas para destinos gerenciados pela AWS (SQS e Lambda): segundo a doc, até 100.015 tentativas durante 23 dias, e, esgotada a política, a mensagem é descartada a não ser que haja uma DLQ na assinatura. Existem tópicos FIFO, que mantêm ordem e deduplicação e podem ter filas SQS FIFO ou standard como assinantes (isso mudou em setembro de 2023, segundo o histórico da documentação; com fila standard você perde a garantia de ordem e de unicidade). Um tópico standard, em contraste, entrega a filas standard.
O Amazon EventBridge é o barramento de eventos serverless. Em vez de assinantes que recebem tudo, ele filtra por conteúdo: uma regra diz “só eventos de pedido com valor acima de X vão para o antifraude”. Ele integra eventos de serviços AWS e de parceiros SaaS, atravessa contas, tem o Pipes para integrações ponto a ponto com transformação, e o Scheduler para tarefas agendadas. Para falhas, cada alvo pode ter uma DLQ (apenas SQS standard) e, para erros que não adianta repetir (sem permissão, alvo inexistente), o evento vai direto para a DLQ sem retentativas. Um ponto que mudou e vale conhecer: a documentação atual descreve dois produtos, o Custom Event Bus (novo, com retenção de eventos, entrega ordenada, replay e assinantes criados pelos consumidores) e o Custom Event Bus Classic (regras e alvos escritos por quem é dono do barramento), e recomenda começar pelo novo em aplicações novas. Como a doc diz que ambos continuam disponíveis, confira qual deles seu time usa antes de dar detalhes de replay ou ordem.
A figura 15 mostra o fato “PedidoCriado” saindo da API (seta grossa laranja) por três portas. No grupo “Fan-out”, o SNS espalha para duas filas standard (e-mail e estoque, setas roxas defasadas), e a fila de estoque tem uma DLQ à direita (seta vermelha “após N falhas”) vigiada por um alarme do CloudWatch; a fila de e-mail alimenta workers Lambda em lotes. No grupo “Roteamento por regra”, a API envia o evento ao EventBridge, que o entrega à Lambda antifraude (só pedidos acima do valor) e a um parceiro SaaS (seta pontilhada, menos crítica). Fora dos grupos, a fila FIFO de cobrança recebe mensagens direto da API, porque um tópico SNS padrão não entrega a filas FIFO: ordem por cliente e ausência de duplicata ficam por conta do MessageGroupId e da deduplicação.
Para streams, o Kinesis e o MSK (Kafka gerenciado) são a resposta quando você precisa reler o histórico, ter vários leitores independentes do mesmo dado com ordem por partição, ou lidar com vazão muito alta e contínua (telemetria, clickstream, CDC). O Amazon MQ (RabbitMQ ou ActiveMQ gerenciados) é a ponte para quem já usa esses protocolos. O Step Functions entra quando o fluxo tem vários passos com estado e compensação e você prefere orquestração central (a versão orquestrada do Saga Pattern).
A figura 17 resume a escolha como uma árvore. Parte do ponto “preciso desacoplar A de B”. Se preciso reler o histórico, vou para Kinesis ou MSK. Se não, pergunto se vários consumidores precisam do mesmo fato: se sim, e o roteamento é por conteúdo ou envolve SaaS e outras contas, EventBridge; se sim e o fan-out é simples, SNS com uma SQS por assinante. Se só há um consumidor, pergunto se a ordem por chave ou a ausência de duplicata importam: sim leva à SQS FIFO e não leva à SQS standard, com consumidor idempotente.
Por que não usar sempre SQS? Porque para vários interessados ele obriga a publicar a mesma mensagem várias vezes. Por que não sempre EventBridge? Por simplicidade e custo em fan-out simples e por ele ser um roteador de eventos, não uma fila de trabalho com visibility timeout e contagem de recebimentos; a combinação com SQS atrás é comum. Por que não Kinesis ou MSK para tudo? Porque exigem pensar em partições e posição de leitura, e para “faça este trabalho uma vez” a fila é mais simples.
Dicas para a entrevista
Pergunte: o chamador precisa do resultado? Qual o pico de volume e o que o banco aguenta? Há mais de um interessado no evento? A ordem importa por qual entidade? É aceitável processar duas vezes? O que acontece com a mensagem que falha sempre?
O que o entrevistador espera ouvir: o critério (precisa da resposta?), o risco de falha em cascata do síncrono e as defesas, o backpressure, a idempotência do consumidor, a DLQ com maxReceiveCount e alarme, o outbox para publicar com segurança, e a escolha entre SQS, SNS, EventBridge e stream com a razão. Números úteis: visibility timeout padrão de 30 s, retenção padrão de 4 dias, lote de 10, mensagem de 1 MiB e, para FIFO, 300 chamadas por segundo por ação sem lote.
Soa júnior: “assíncrono sempre” (e a UX da confirmação?), dizer que a fila garante entrega exata sem falar de duplicatas, usar uma DLQ sem alarme, esquecer a ordem, e propor Kafka para um simples envio de e-mail. Se travar, volte ao critério e ao desenho da figura 14: quem espera por quem, e o que acontece quando o de baixo cai.
Follow-ups prováveis
Como você evita processar duas vezes? Tornando o consumidor idempotente (chave de idempotência gravada junto com o efeito, ou uma operação naturalmente idempotente). Na FIFO, a deduplicação do SQS ajuda no envio, mas o consumidor ainda deve ser seguro contra reprocessamento.
Como garante que o evento é publicado quando o pedido é gravado? Transactional Outbox: gravo o pedido e o evento na mesma transação do banco, e um processo separado publica o que está na tabela de outbox.
Standard ou FIFO? Standard por padrão, pela vazão. FIFO quando a ordem por entidade ou a deduplicação são requisitos de negócio (cobrança, alterações de preço), usando muitos grupos para manter o paralelismo.
Quando um fluxo síncrono é melhor? Quando o usuário precisa do resultado na mesma tela (login, saldo, validação), com timeout, circuit breaker e fallback. Muitos sistemas bons misturam: síncrono para o caminho crítico de leitura e decisão, assíncrono para os efeitos colaterais.
6. circuit breaker, backoff com jitter e bulkhead
A pergunta e o que o entrevistador quer descobrir
“Se o serviço downstream de consulta de CEP começar a falhar e retornar timeout de 5 segundos, como você protege sua API?” O entrevistador quer ver se você entende falha em cascata: o problema não é o CEP estar lento, é o seu serviço ficar lento junto, porque cada requisição presa segura uma conexão e uma thread. Ele também quer ouvir que você separa três ferramentas com papéis diferentes (retry, disjuntor, isolamento) e que sabe que retry mal feito piora o incêndio.
Como responder em voz alta
“Primeiro eu coloco timeout curto na chamada, porque sem timeout nada do resto funciona: o timeout é o que transforma ‘lento’ em ‘falha’ que eu consigo contar. Depois uso retry só para erro transitório, com backoff exponencial e full jitter, com um número pequeno de tentativas e um teto de espera. O jitter é essencial: sem ele, mil clientes que falharam juntos voltam juntos e derrubam o serviço de novo.
Em cima disso eu coloco um circuit breaker por dependência. Fechado, ele deixa passar e conta falhas numa janela. Se a taxa de erro passa de um limite, por exemplo 50% na janela, ele abre e passa a falhar na hora, sem tocar na rede, devolvendo um fallback, como o último CEP em cache. Depois de um tempo de espera ele vira meio aberto e deixa passar poucas sondas: se dão certo, fecha; se falham, abre de novo.
Por fim, bulkhead: um pool de conexões separado por dependência, para que o CEP lento só consiga esgotar o próprio pool e não o do checkout. E eu retento em uma camada só, porque retry em cada camada se multiplica.”
Por dentro
O ponto de partida é entender por que uma dependência lenta mata quem a chama. Se o CEP demora 5 segundos e você recebe 100 requisições por segundo, em poucos segundos você tem centenas de requisições esperando, cada uma segurando uma thread (ou goroutine) e uma conexão HTTP. O pool de conexões acaba, a fila de requisições cresce, o pod estoura memória e o serviço inteiro para de responder, inclusive as rotas que nem usam o CEP. Isso é a falha em cascata, e a aula Resiliência com Circuit Breaker e Retry (Go) mostra o mesmo cenário com código, com sony/gobreaker e um bulkhead feito com canal. A versão em Kotlin, com Resilience4j, está em Resiliência e empacotamento.
O circuit breaker é uma máquina de estados de três posições. No estado fechado (closed) tudo passa e o disjuntor mantém uma janela deslizante de resultados. Quando a taxa de falhas ou de chamadas lentas passa do limite, ele abre (open): daí em diante cada chamada falha imediatamente, sem consumir rede nem thread, e o seu código decide o que fazer, normalmente um fallback. Passado o tempo de espera, ele entra em meio aberto (half-open) e libera um número pequeno de chamadas de teste. Essas sondas decidem o futuro: sucesso fecha o circuito, falha o reabre por mais um período. O porquê do meio aberto é não religar o tráfego inteiro de uma vez em cima de um serviço que acabou de se levantar.
Comece pelo “Seu serviço” à esquerda: com o disjuntor fechado, os pacotes verdes seguem até a API de CEP. Quando a taxa de erros passa de 50% (seta grossa vermelha), o circuito abre e a chamada nova é barrada com o X antes de sair do processo, indo para o fallback. O caminho roxo e lento mostra o disjuntor esperando antes de testar, e as duas setas de volta mostram as sondas fechando ou reabrindo o circuito.
Retry e disjuntor se complementam, não competem. O retry resolve falha curta (um reset de conexão, um 503 que dura meio segundo). O disjuntor resolve falha longa, em que repetir só aumenta a carga. O backoff exponencial espaça as tentativas: a espera máxima de cada tentativa é a base multiplicada por 2 elevado ao número da tentativa, com um teto. O jitter sorteia o valor real entre zero e esse máximo (full jitter). A diferença é visível na figura a seguir.
À esquerda, mil clientes falharam no mesmo instante e todos esperam exatamente 1 segundo: o servidor recebe uma barra de 1.000 requisições no mesmo bucket de tempo, o chamado thundering herd, e cai de novo. À direita, o full jitter espalha os retries pela janela, e o servidor vê uma carga baixa e constante que ele consegue absorver. Os números do gráfico são ilustrativos (1.000 clientes, buckets de 0,2 s), não uma medição.
Há uma armadilha que separa júnior de sênior: retry multiplica entre camadas. A Amazon Builders’ Library dá o exemplo de uma pilha de cinco camadas com três tentativas cada: se cada camada retenta por conta própria, o banco no fundo vê 243 vezes a carga original. A recomendação é retentar em um único ponto da pilha, e limitar retries localmente com um token bucket, de modo que, quando o serviço está mesmo doente, o cliente para de insistir. Outra condição: só retente operação idempotente (veja a pergunta 7), porque “APIs com efeito colateral não são seguras para retry a não ser que ofereçam idempotência”.
O bulkhead vem da engenharia naval: as divisórias estanques de um navio impedem que um furo alague tudo. Em software, é separar recursos por dependência.
À esquerda, um pool único de 30 conexões é inteiramente ocupado pelas chamadas ao CEP lento, e o checkout fica sem vaga (o X). À direita, cada dependência tem o seu pool: o pool do CEP satura (rosa), mas os pools do checkout e dos relatórios continuam com conexões livres e o tráfego deles segue verde. O custo é capacidade reservada que pode ficar ociosa, e é por isso que os tamanhos dos pools merecem número e revisão, não um chute.
Por fim, os casos de borda. O circuit breaker introduz comportamento “modal”, como diz o texto da Builders’ Library: o sistema se comporta de um jeito com o circuito fechado e de outro com ele aberto, e esse segundo modo é difícil de testar e pode alongar a recuperação. Por isso o fallback precisa ser exercitado em teste, e o limite e a janela precisam de métricas (taxa de abertura, chamadas rejeitadas). Em serviço com pouco tráfego, uma janela baseada em porcentagem sofre de amostra pequena: use um mínimo de chamadas antes de avaliar a taxa.
Na AWS
O circuit breaker não é um serviço da AWS. Ele vive em quatro lugares possíveis, e a AWS fornece as peças ao redor dele.
O primeiro lugar, e o mais comum, é a biblioteca dentro do seu código (gobreaker em Go, Resilience4j em Kotlin e Java), no quadro “Seu código”. Funciona em Lambda, ECS ou EKS igualmente, mas atenção: o estado do disjuntor fica na memória de cada instância, e em Lambda isso significa por ambiente de execução, então cada instância aprende a falha por conta própria. O segundo lugar é a malha de serviço (service mesh), em que um proxy Envoy ao lado do app faz detecção de outliers e retries com limite sem mudar o código. O AWS App Mesh era a opção gerenciada, mas a documentação oficial traz o aviso de fim de suporte: em 30 de setembro de 2026 a AWS descontinua o App Mesh e, depois dessa data, o console e os recursos deixam de estar acessíveis. Como hoje é 22 de outubro de 2026, a data já passou, e a página que consultei ainda exibia o aviso no futuro, então trate o App Mesh como descontinuado e não confirmei o estado exato da migração. A AWS aponta o Amazon ECS Service Connect como caminho de migração para quem roda no ECS, e quem está no EKS usa Envoy ou Istio por conta própria.
O terceiro lugar é a orquestração com Step Functions, e aqui existe um padrão oficial nas Prescriptive Guidance: uma máquina de estados Express consulta uma tabela do DynamoDB (CircuitStatus) para saber se o circuito da dependência está aberto, com um carimbo de expiração e TTL do DynamoDB para fechar sozinho; se estiver fechado, chama a Lambda, retenta com backoff e, se falhar após as tentativas, grava o registro que abre o circuito. O DynamoDB pode ser trocado por ElastiCache para ganhar latência. Esse desenho dá o estado compartilhado entre instâncias, ao custo de uma ida ao DynamoDB por chamada.
O quarto lugar são os recursos de retry e de falha que a AWS dá prontos. Os SDKs da AWS retentam sozinhos: o modo standard usa backoff exponencial com full jitter, com base de 50 ms para erros transitórios e 1.000 ms para throttling, teto de 20 segundos por espera, um padrão de 3 tentativas no total (4 no DynamoDB) e uma cota de retry em token bucket (500 tokens) que faz o cliente desistir quando muita coisa falha. Detalhe de 2026: a documentação diz que esse comportamento atualizado exige o opt-in AWS_NEW_RETRIES_2026=true até virar padrão; sem a variável, o SDK usa o comportamento anterior, que varia por linguagem. No Step Functions, Retry aceita IntervalSeconds (padrão 1), MaxAttempts (padrão 3), BackoffRate (padrão 2.0), MaxDelaySeconds e JitterStrategy (FULL ou NONE, e o padrão é NONE, então ligue o FULL de propósito), e Catch desvia para um estado de fallback depois que as tentativas acabam. No SQS, a DLQ recebe a mensagem depois de maxReceiveCount recebimentos sem sucesso; e o Lambda assíncrono tenta mais duas vezes por padrão (esperas de 1 e 2 minutos) e depois entrega a um DLQ ou a um on-failure destination. Um alarme do CloudWatch sobre a idade da mensagem mais velha da DLQ fecha o ciclo.
Por que não outro serviço? O API Gateway tem throttling, que protege o seu backend de excesso de chamadas dos clientes, mas não protege o seu serviço de um downstream lento: é a direção oposta. O ALB tira instâncias doentes do rodízio pelas health checks, mas não sabe que o CEP, que fica atrás da sua aplicação, está lento.
Dicas para a entrevista
Comece pelo timeout; é a resposta que mais gente esquece. Pergunte: a chamada ao CEP é crítica para a resposta ou há fallback aceitável (CEP em cache, campo opcional)? Pergunte também se a operação é idempotente, porque isso decide se o retry é seguro. Diga os trade-offs que o entrevistador espera: fallback tem custo de dado desatualizado; disjuntor por instância reage mais rápido, mas cada instância aprende sozinha; bulkhead custa capacidade ociosa; retry aumenta a carga em quem já está doente.
Números úteis para ter na ponta da língua: janela de 10 s e limite de 50% de erro para abrir, 30 s de espera, 3 sondas no meio aberto, 3 tentativas de retry com base de 100 ms a 1 s e teto de alguns segundos. São ordens de grandeza razoáveis, não valores universais: diga que você calibra com métricas reais (p99 da dependência, taxa de erro histórica).
O que soa júnior: “coloco retry três vezes” sem backoff nem jitter, retry em todo erro (incluindo 4xx), retry em cada camada, ou circuit breaker sem fallback e sem métrica. Se travar, volte ao mecanismo físico: “o que acaba primeiro, threads ou conexões? Como eu impeço que a falha dessa dependência consuma isso?”. A resposta quase sempre cai em timeout, isolamento e falhar rápido.
Follow-ups prováveis
Qual a diferença entre retry e circuit breaker? Retry cobre falha curta repetindo; disjuntor cobre falha longa parando de repetir. Usam-se juntos, com o retry dentro do disjuntor.
Como o jitter ajuda? Ele dessincroniza os clientes: sem jitter a carga de retry chega em ondas alinhadas no tempo, com jitter ela chega diluída.
Onde fica o estado do disjuntor com várias instâncias? Por padrão em cada instância (rápido, simples, cada uma aprende sozinha). Se precisar compartilhar, use um armazenamento externo como DynamoDB ou ElastiCache, pagando a latência, ou o Envoy como proxy de borda.
E se o fallback também falhar? Falhe rápido com erro claro (por exemplo 503 com Retry-After) e preserve os recursos para o que ainda funciona. Dependência não crítica deve ser opcional no contrato da resposta.
7. idempotência e transactional outbox
A pergunta e o que o entrevistador quer descobrir
“Como você garante que um pagamento ou pedido não seja cobrado duas vezes se a requisição sofrer timeout de rede?” A pergunta tem duas camadas. A primeira é o cliente que repete a chamada porque não sabe se ela deu certo (idempotência). A segunda é o serviço que grava no banco e precisa avisar o resto do sistema sem perder nem duplicar o aviso (outbox). O entrevistador quer ver se você sabe que “exatamente uma vez” não existe de graça na rede, e que o que se constrói na prática é entrega pelo menos uma vez somada a processamento idempotente.
Como responder em voz alta
“O timeout deixa o cliente na dúvida: o pagamento pode ter acontecido ou não, então ele vai repetir. Por isso a API de pagamento precisa ser idempotente. O cliente gera um UUID e manda no header Idempotency-Key. No servidor eu tento reservar essa chave de forma atômica, num banco com restrição de unicidade ou com um SET NX. Se a chave é nova, executo o pagamento, gravo o resultado junto da chave e respondo. Se a chave já existe e a primeira tentativa ainda roda, devolvo 409. Se já terminou, devolvo a mesma resposta guardada, sem cobrar de novo. Eu também comparo o corpo da requisição com o da primeira vez, para rejeitar a mesma chave com payloads diferentes.
O segundo problema é o dual write: gravo o pedido no banco e depois publico o evento no broker; se o processo cai entre os dois passos, o banco tem o pedido e ninguém foi avisado. A solução é o transactional outbox: na mesma transação que grava o pedido eu gravo uma linha numa tabela de outbox. Um relay lê as linhas pendentes, ou lê o log do banco com CDC, e publica no broker. Isso garante que o evento sai pelo menos uma vez; então o consumidor também precisa ser idempotente, descartando o duplicado por um id de evento.”
Por dentro
Idempotência significa que repetir a operação produz o mesmo efeito que fazê-la uma vez. A aula Idempotência mostra a ideia com o elevador (apertar o botão do 10º andar cinco vezes não leva ao 50º), os métodos HTTP idempotentes (GET, PUT, DELETE) e o único que não é, o POST, além de uma tabela de chaves de idempotência com implementação em Go. O que importa aqui é o porquê: numa rede, o cliente nunca sabe se a requisição se perdeu na ida, no processamento ou na volta. Sem idempotência, a única opção segura seria nunca repetir, e então o usuário perderia pedidos; com ela, repetir passa a ser sempre seguro.
O fluxo tem uma decisão em dois níveis, que a figura mostra.
O cliente envia o POST com a chave (seta grossa laranja) e a API reserva a chave no armazém, num passo atômico: INSERT com chave primária única, ou SET chave NX EX. A resposta do armazém alimenta o primeiro losango. Se a chave é nova, o caminho verde grosso leva ao pagamento, depois à gravação do resultado junto da chave e ao 201. Se a chave já existe, o segundo losango pergunta se a primeira execução ainda roda: sim leva ao 409 (rosa), não leva a reenviar a resposta salva (roxo), que tem o mesmo corpo e não cobra de novo.
Três detalhes de borda separam implementação correta de implementação ingênua. Primeiro, a reserva da chave e o efeito colateral precisam ser consistentes: se você reserva a chave e o processo cai antes de pagar, a chave fica em andamento para sempre, então a reserva precisa de expiração, e o ideal é gravar a chave e o pagamento na mesma transação do banco (ACID, Transação), de modo que ou os dois existem ou nenhum. Segundo, a chave guarda a resposta e não só a trava: depois que o pagamento termina, a chave não pode ser “liberada” e apagada, porque o retry tardio do cliente precisaria da resposta guardada; o prazo de retenção deve ser maior que o maior retry plausível (horas ou dias, não segundos). Terceiro, o mesmo Idempotency-Key com corpo diferente é bug do cliente e deve ser rejeitado, o que exige guardar um hash do payload.
Agora o dual write. Sem o outbox, os dois passos (banco e broker) são duas operações independentes, e não existe transação distribuída barata entre um banco e um broker. Há quatro falhas possíveis (cair antes, entre os passos, depois, ou o broker recusar), e uma delas deixa o sistema inconsistente de forma silenciosa.
À esquerda, o serviço grava no banco (verde) e tenta publicar no broker, mas o processo cai ou o broker recusa (o X): o pedido #42 existe, o evento nunca saiu e o estoque nunca soube. À direita, o serviço faz dois INSERT na mesma transação, em orders e em outbox_events, então os dois existem ou nenhum existe. Um relay lê as linhas pendentes e publica no broker, e o consumidor idempotente descarta repetições. A aula Mensageria Prática com RabbitMQ e Kafka mostra o mesmo padrão com RabbitMQ e Kafka em Go, e Saga Pattern mostra por que consistência entre serviços se resolve com eventos e compensações e não com transação distribuída.
O relay tem duas formas. Na primeira, um processo faz polling: lê as linhas com enviado = false (em Postgres, algo como SELECT ... FOR UPDATE SKIP LOCKED permite vários relays sem pegar a mesma linha), publica e marca como enviada. Se ele cai depois de publicar e antes de marcar, a linha é publicada de novo, e é daí que vem o “pelo menos uma vez”. Na segunda, o CDC (change data capture) lê o log de transações do banco e transforma cada INSERT na outbox em evento, sem polling e com menos latência, ao custo de operar uma ferramenta a mais. Em ambas, a ordem por agregado só é preservada se você usar a chave certa de partição (veja Kafka).
Na AWS
Há dois caminhos naturais, conforme o banco.
No caminho A (DynamoDB), a aplicação usa TransactWriteItems, que agrupa até 100 ações atômicas (e até 4 MB), então o item do pedido e o item da outbox entram juntos ou nenhum entra. O DynamoDB Streams registra cada modificação por até 24 horas, com cada registro aparecendo uma vez e na ordem das alterações de cada item. O EventBridge Pipes lê o stream, filtra só os itens do tipo outbox e entrega ao destino: um bus do EventBridge para fan-out entre times, ou uma fila SQS para um consumidor. A ordem da fonte é mantida de ponta a ponta no pipe. Por que Pipes e não uma Lambda solta lendo o stream? Pipes dá filtro, enriquecimento e entrega gerenciados sem código; uma Lambda dá mais controle e é a alternativa se você precisa de lógica.
No caminho B (banco relacional, RDS ou Aurora Postgres), a aplicação faz os dois INSERT na mesma transação SQL, e o AWS DMS faz CDC lendo o log do banco e entrega a um Kinesis Data Streams ou a um Kafka (MSK). A alternativa é o Debezium rodando no MSK Connect, que escreve em tópicos do MSK. Ponto de atenção: o DMS lista Kinesis e Kafka entre os alvos, mas SQS não está entre eles, e o Pipes não aceita um banco relacional como fonte (as fontes são stream do DynamoDB, Kinesis, MQ, MSK, SQS e Kafka). Se você quiser SQS com RDS, o relay vira polling feito por uma Lambda agendada ou um worker, ou você coloca um Kinesis entre o DMS e um Pipe.
O consumidor idempotente fecha o ciclo. Uma Lambda recebe o evento (a SQS padrão entrega pelo menos uma vez, e o Lambda assíncrono também pode entregar o mesmo evento mais de uma vez) e consulta uma tabela de chaves já processadas. O Powertools for AWS Lambda tem um utilitário de idempotência que guarda no DynamoDB (chave id e atributo expiration como TTL), marca o registro como INPROGRESS enquanto roda e COMPLETE com o resultado depois, trata o timeout da Lambda e expira os registros (padrão de uma hora na versão Python que consultei). Se o seu “evento” já chega com um id estável, a chave é esse id.
Para o Idempotency-Key na borda da API, as escolhas na AWS são: DynamoDB com PutItem condicional (attribute_not_exists) e TTL, que dá durabilidade e atomicidade; ElastiCache com SET NX, mais rápido, mas é cache e pode perder a chave num failover, então não deve ser a única barreira de um pagamento; ou a própria tabela do pedido com restrição de unicidade na chave, que é a mais simples quando já existe um banco relacional. O TransactWriteItems do DynamoDB aceita ainda um ClientRequestToken, que torna a própria chamada idempotente por 10 minutos. Para filas, o SQS FIFO tem deduplicação por 5 minutos, usando o MessageDeduplicationId ou um hash SHA-256 do corpo; é uma proteção na entrada da fila e não substitui a idempotência do consumidor, porque a janela é curta.
Dicas para a entrevista
Pergunte: o que acontece hoje se o cliente repetir? Quem é o cliente (app móvel com rede ruim, outro serviço com retry automático)? O banco já suporta transação para a chave e o pedido juntos? Há necessidade de ordem entre eventos do mesmo pedido? Diga os trade-offs que o entrevistador espera: outbox com polling é simples mas tem latência e carga no banco; CDC reduz a latência mas adiciona infraestrutura; Redis é rápido mas não é a fonte da verdade; guardar a resposta custa armazenamento e exige política de retenção. Cite que “exatamente uma vez” de ponta a ponta é pelo menos uma vez somado a consumidor idempotente.
O que soa júnior: “uso uma trava no Redis e pronto” (a trava some no failover ou na expiração e o pagamento duplica), “uso uma transação distribuída (2PC) entre banco e Kafka”, “publico o evento antes do commit” (agora o evento pode existir sem o pedido), ou “o broker garante exactly-once” sem explicar o escopo. Se travar, redesenhe o problema em dois quadros: “onde pode cair o processo entre esses dois passos, e o que fica gravado em cada caso?”.
Follow-ups prováveis
E se o relay publicar duas vezes? O consumidor descarta pelo id do evento, guardado numa tabela com TTL; a chave pode ser o id da linha da outbox.
Como você limpa a tabela de outbox? Apaga ou arquiva as linhas enviadas depois de um prazo, em lotes, para não inflar o banco e não competir com a carga de escrita. No DynamoDB, use TTL.
Como garantir ordem? Particione por agregado (o id do pedido) para que os eventos do mesmo pedido caiam na mesma partição ou shard, e processe cada shard em ordem. Ordem global entre pedidos diferentes você não precisa e não deve pagar.
Qual a diferença entre idempotência e deduplicação? Deduplicar é descartar mensagens repetidas na entrada; idempotência é o processamento tolerar a repetição. A deduplicação tem janela e pode falhar, a idempotência é a defesa final.
8. JWT stateless contra sessão centralizada
A pergunta e o que o entrevistador quer descobrir
“Quais são os prós e contras reais de usar JWT stateless em vez de sessões salvas no Redis em microsserviços?” O entrevistador quer ouvir o custo de cada modelo, e não o slogan de cada um. Sessão no Redis dá revogação imediata e cobra uma consulta à rede em toda requisição. JWT dá validação local, sem rede, e cobra uma revogação que não se faz de forma simples. Quem responde “JWT é stateless, logo escala melhor” e para deixa a pergunta pela metade. Quem mostra onde cada modelo dói, propõe o meio-termo e pergunta quanto tempo um usuário banido pode continuar entrando já mostra que operou autenticação em produção.
Como responder em voz alta
“A diferença está em onde fica o estado de quem está logado. Com sessão no Redis, o cookie leva só um identificador opaco, e cada serviço pergunta ao Redis quem está por trás dele. Isso torna a revogação instantânea, porque basta apagar a chave, mas cada requisição depende da rede e de o Redis estar de pé.
Com JWT, o token carrega os dados do usuário assinados pelo emissor, e cada serviço confere a assinatura com a chave pública, sem perguntar a ninguém. É rápido e escala bem, mas o token vale até o campo exp, e não existe um botão para desligá-lo antes disso.
Na prática eu uso os dois juntos. O access token é um JWT curto, de uns quinze minutos, e o refresh token é longo, opaco, guardado no servidor e trocado a cada uso. Se o usuário for banido, o servidor recusa a próxima renovação e ele perde o acesso quando o access token vencer. Se um caso exigir corte imediato, como uma conta comprometida, eu acrescentaria uma com o identificador dos tokens até o vencimento, aceitando guardar um pouco de estado de novo.”
Por dentro
A sessão centralizada existe desde que a web ganhou login, e a ideia nunca foi o problema. O problema apareceu com a escala. Num monólito, quem valida a sessão é o próprio app, com a memória dele. Com dez serviços, todos precisam do mesmo estado, e o Redis vira uma dependência compartilhada: se ele cai, a autenticação do sistema inteiro cai junto. O JWT nasceu para tirar essa consulta do caminho crítico de cada requisição.
Um JWT é uma string com três partes separadas por ponto: o cabeçalho, que diz o algoritmo e o identificador da chave (o kid); o payload, com as claims; e a assinatura. Cabeçalho e payload são apenas base64url, então quem tem o token lê o conteúdo sem nenhum segredo. Por isso o token não guarda senha nem dado sensível, e por isso o payload deve ser mínimo. As claims registradas que você precisa conhecer são iss (quem emitiu), sub (o usuário), aud (para qual serviço o token vale), exp (até quando vale, em segundos desde 1970), iat (quando foi emitido) e (um identificador único do token, que reaparece na revogação). Na assinatura RS256, o emissor usa a chave privada, e os serviços verificam com a pública. Quem não tem a privada não consegue fabricar um token que passe na verificação.
A parte de cima da figura mostra o caminho com sessão. Cada serviço faz um GET no Redis (setas verdes de ida e volta), então toda chamada atravessa a rede. A seta rosa do usuário banido apaga a chave e corta a sessão na hora. A parte de baixo mostra o JWT: os serviços usam a chave pública, que foi baixada uma vez, e a seta bloqueada indica que nada chega ao token para cortá-lo antes do exp.
A tentativa mais comum de revogar um JWT é a denylist: guardar o jti dos tokens revogados até o exp e consultar essa lista a cada verificação. Funciona, mas você volta a ter um estado centralizado, com o custo que o JWT queria evitar. A alternativa mais honesta é encurtar a validade. Com um access token de quinze minutos, a janela de dano de um token vazado é de quinze minutos, e o custo de renovar a cada quinze minutos é pequeno. Isso leva ao refresh token.
O refresh token é uma string aleatória, sem conteúdo legível, que o servidor de identidade guarda em forma de hash, porque quem lê o banco não deveria conseguir se passar pelo usuário. Quando o access token vence, o cliente troca o refresh por um novo par. Com rotação, cada refresh vale uma única vez: o servidor emite outro e marca o antigo como usado. Os refresh tokens de um mesmo login formam uma família. Se alguém apresentar um refresh já usado, isso indica que o token vazou e está sendo usado por duas partes, e o servidor revoga a família inteira, obrigando um novo login. Na entrevista, essa detecção de reúso chama atenção porque mostra que o roubo de token foi considerado, e não só a expiração.
Leia a figura de cima para baixo. Os passos 1 e 2 são o caminho feliz, com o login gerando o par e a API aceitando o Bearer. O passo 3 mostra o access vencido (X rosa na API), a renovação em roxo e a rotação no banco de refresh tokens. O passo 4 é o banido: o servidor de identidade recusa a renovação e a família é revogada, em rosa, junto com o cadastro desativado.
Há três detalhes de borda que aparecem em produção. Um cookie comporta cerca de 4 KB, então um JWT com muitas claims pode estourar esse limite. Relógios desalinhados exigem uma pequena tolerância (leeway) na checagem de exp. E o verificador deve fixar o algoritmo esperado, sem aceitar o que o próprio cabeçalho declara, porque essa confiança já abriu brechas, incluindo a aceitação do algoritmo “none”. O erro de iniciante é decodificar o payload e usá-lo sem verificar a assinatura, o que equivale a acreditar num bilhete que qualquer pessoa pode reescrever.
O formato e a validação do JWT também deveriam estar na nota JWT, mas, na data desta revisão, ela tem só o frontmatter. Esta seção cobre o que a entrevista pede, e a nota deve ser completada depois sem repetir este texto. Para a sessão no Redis, veja Redis, e para o tempo de vida das chaves da denylist, veja TTL.
Na AWS
O Amazon Cognito é o emissor. Um user pool gera ID token, access token e refresh token. Os tokens são assinados com RS256, e as chaves públicas ficam num endereço no formato https://cognito-idp.{região}.amazonaws.com/{userPoolId}/.well-known/jwks.json, que é o . Os valores padrão que conheço são de 1 hora para o access token e 30 dias para o refresh, mas confirme na configuração do seu pool. Um detalhe que pega quem copia a regra de outro emissor: o access token da Cognito traz o identificador do cliente no campo client_id, e não no aud, então a checagem de audiência precisa ser configurada com esse campo em mente.
O caminho de cima é o HTTP API com JWT authorizer. Ele valida assinatura pelo JWKS, iss, aud, exp e scope conforme a configuração, sem nenhum código seu, e entrega as claims à Lambda. Não há consulta extra nem como revogar, então é o caminho mais barato e o mais simples. O caminho de baixo é o REST API com Lambda authorizer, que valida o token e consulta a denylist no ElastiCache pelo jti antes de liberar o serviço no ECS. Esse caminho custa uma ida à Lambda e ao cache, e o resultado do authorizer pode ser guardado em cache por um tempo configurado, o que significa que a revogação demora até esse tempo para valer.
Sobre a rotação de chaves, a Cognito pode trocar as chaves de assinatura. O verificador identifica a chave pelo kid do cabeçalho, e um kid desconhecido deve disparar uma nova busca do JWKS. O authorizer gerenciado faz isso sozinho, mas eu não verifiquei hoje o comportamento exato do cache dele. Sobre o refresh token, o user pool tem uma configuração relacionada à rotação, e eu não confirmei hoje se a rotação a cada uso está disponível nem como se ativa; confirme antes de afirmar isso numa entrevista.
Dicas para a entrevista
Antes de escolher, pergunte quem consome a API (um app próprio, parceiros externos ou um navegador), quanto tempo um usuário banido pode continuar com acesso e se há operações sensíveis que exigem checagem fresca. Essas respostas decidem o desenho. Diga os trade-offs que o entrevistador espera: JWT dá validação local, mas revogação lenta e token maior; sessão no Redis dá revogação imediata, mas adiciona latência, um ponto de falha compartilhado e uma dependência de rede.
Os números que ajudam são access token de 5 a 15 minutos e refresh de 7 a 30 dias, sempre com a ressalva de que dependem do risco do produto. O que soa júnior: “o JWT guarda a senha”, “o token vale um ano porque é mais cômodo”, “o logout é apagar o token do localStorage” (sem revogar nada no servidor) e “decodifico e confio sem verificar a assinatura”. Se travar, volte ao caso concreto: “um usuário foi banido agora, o que impede o acesso dele e em quanto tempo?”
Follow-ups prováveis
Como fazer logout com JWT? Apaga-se o refresh token no servidor, e o access token continua válido até vencer. Para corte imediato, acrescenta-se a denylist de jti pelo tempo que falta até o exp.
Onde guardar o token no navegador? Um cookie HttpOnly e Secure reduz o roubo por script injetado (XSS), mas exige proteção contra CSRF. O localStorage fica acessível a qualquer script da página, então um XSS basta para levá-lo.
Por que não usar JWT para tudo? Porque o custo da revogação volta no momento em que o caso exige corte imediato. JWT compensa onde a validação local vale mais que o corte instantâneo, e sessão compensa onde o corte precisa ser instantâneo.
9. balanceador L4 contra L7
A pergunta e o que o entrevistador quer descobrir
“Qual a diferença entre um balanceador de Camada 4 e um de Camada 7 e quando você escolhe cada um?” O entrevistador quer ver se você sabe que a camada define o que o balanceador enxerga e, portanto, o que ele pode decidir. Quem decora “L4 é rápido e L7 é inteligente” não consegue explicar o que acontece com gRPC ou WebSocket, nem por que costuma dar problema. Como a entrevista quase sempre cai na AWS, a resposta também precisa citar os serviços certos.
Como responder em voz alta
“A diferença é o quanto o balanceador lê. No L4 ele trabalha com conexões TCP: enxerga IP e porta, escolhe um alvo para a conexão inteira e a repassa sem entender o conteúdo. Como não interpreta nada, é barato e aguenta volume enorme. Na AWS, esse é o NLB.
No L7 ele entende HTTP: lê método, host, path e headers, e pode mandar /users para um serviço e /orders para outro. Em troca, faz mais trabalho por requisição. Na AWS, esse é o ALB.
Para gRPC e WebSocket a diferença pesa. O gRPC multiplexa muitas requisições numa só conexão HTTP/2, então um balanceador L4 manda a conexão inteira para um alvo e o trabalho deixa de ser distribuído. O ALB distribui por requisição. Eu evitaria sticky sessions, porque prendem um usuário a um servidor, desequilibram a carga e atrapalham a retirada de instâncias. A solução melhor é a aplicação sem estado, com a sessão guardada fora dela, como no Redis da pergunta anterior.”
Por dentro
Um balanceador é um porteiro que decide, para cada cliente, qual cópia do serviço vai atendê-lo. Sem ele, um único servidor cai e o serviço inteiro cai, ou um servidor recebe tudo enquanto os outros ficam ociosos. As camadas do modelo OSI importam porque cada uma oferece uma informação diferente: a camada de transporte (L4, com TCP e UDP) enxerga conexões, e a de aplicação (L7, com HTTP) enxerga requisições dentro dessas conexões.
No L4, o cliente abre uma conexão TCP com o balanceador, com o handshake de SYN, SYN-ACK e ACK. O balanceador escolhe um alvo com base no fluxo, normalmente um hash dos endereços e portas, e encaminha os pacotes para ele. Ele não termina HTTP, não lê o path e não sabe quantas requisições passam por aquela conexão. Um detalhe que confunde: o NLB também pode terminar TLS, então criptografia não separa as duas camadas. O que separa é se o balanceador entende o conteúdo HTTP da requisição.
No L7, o cliente conversa com o balanceador como se ele fosse o servidor. O balanceador termina o TLS, lê a requisição HTTP inteira, aplica as regras e abre (ou reaproveita) uma conexão separada com o alvo. Ele é um proxy com dois lados, e por isso pode tomar decisões por host, path, header ou query string. O custo é que cada requisição passa por uma leitura e uma reescrita.
O percurso começa nas duas faixas, uma por balanceador. Na faixa de cima, o L4 recebe o TCP SYN e manda a conexão inteira para um dos três alvos, sem olhar nada além da porta. Na faixa de baixo, o L7 lê o GET /orders/42 e entrega a requisição ao serviço de pedidos (a seta mais grossa), enquanto /users e o host de assets seguem para os outros serviços.
O algoritmo de distribuição decide o que o balanceador faz com cada nova conexão ou requisição. O round robin entrega um por vez, em ordem, o que funciona bem quando os alvos são iguais e as requisições custam parecido. O hash de fluxo, usado pelo L4, mantém o mesmo fluxo no mesmo alvo. O least outstanding requests manda a próxima requisição para o alvo com menos trabalho em andamento, o que faz diferença quando alguns alvos respondem devagar.
Os dois painéis recebem as mesmas requisições. À esquerda, o round robin reparte por igual, e o alvo lento (rosa) continua recebendo a sua parte enquanto a fila dele cresce. À direita, o least outstanding requests percebe que o alvo lento tem requisições paradas e manda quase tudo para os outros dois. O custo do LOR é manter a contagem por alvo, o que o round robin não precisa.
Há um detalhe de borda que aparece em teste de carga. Um balanceador só distribui bem se conseguir tirar do rodízio um alvo doente, o que depende de health checks, e só deixa uma instância sair sem cortar requisições em andamento se houver drenagem (no ALB, o deregistration delay). Conexões longas, como WebSocket, tornam a distribuição desigual com o tempo, porque um alvo que recebeu muitas conexões longas continua com elas.
Na AWS
A AWS separa as camadas em produtos diferentes. O Network Load Balancer (NLB) opera em L4 com TCP, UDP e TLS, e oferece IP estático por zona de disponibilidade. O Application Load Balancer (ALB) opera em L7 com HTTP, HTTPS, gRPC e WebSocket, com regras por host, path, header e query, e organiza os alvos em target groups. O Gateway Load Balancer (GWLB) opera na camada 3 e envia o tráfego, encapsulado em GENEVE pela porta 6081, para appliances de segurança de terceiros, que inspecionam e devolvem o pacote. O Route 53 fica antes de todos, resolvendo o nome para o balanceador certo.
O percurso começa no Route 53 e segue para a faixa que corresponde ao protocolo. A faixa de cima é o HTTP, com o WAF filtrando antes do ALB, que roteia /users para o ECS, /orders para uma Lambda e /ws para instâncias EC2. A faixa do meio é o NLB, que encaminha um protocolo TCP próprio sem ler HTTP. A faixa de baixo é o GWLB, com a seta de inspeção que volta, porque o appliance devolve o tráfego depois de olhar.
No ALB, o gRPC exige que o use a versão de protocolo gRPC (ou HTTP/2), porque o balanceador precisa falar HTTP/2 com o alvo. O WebSocket funciona porque o ALB reconhece o upgrade da conexão e a mantém aberta. Para a distribuição, o ALB usa round robin como padrão, e o least outstanding requests é uma opção de configuração do target group. O NLB distribui por hash de fluxo e não usa cookie. Se você precisar de afinidade, o ALB faz stickiness com cookie gerado por ele, mas o efeito colateral é o mesmo que descrevi: a carga desequilibra e as instâncias novas recebem pouco tráfego até o cookie expirar. Os detalhes exatos dos nomes de cookie e da opção LOR eu não verifiquei hoje; confirme na documentação do Elastic Load Balancing.
Dicas para a entrevista
Pergunte qual é o protocolo do tráfego (HTTP, gRPC, WebSocket ou TCP puro), se há necessidade de roteamento por path e se a aplicação mantém estado por usuário. Essas três respostas quase determinam o balanceador. Diga os trade-offs: L4 é mais barato e mais rápido, mas cego; L7 é inteligente, mas faz mais trabalho e pode virar gargalo quando o volume de requisições cresce; sticky sessions ajudam a manter estado local, mas espalham a carga de forma desigual.
O que soa júnior: “L7 é sempre melhor”, “uso sticky session para o carrinho” (em vez de guardar o carrinho fora da instância), “o NLB descriptografa o TLS e por isso é lento” (ele pode fazer isso, mas a lentidão não é a razão) e “o balanceador resolve a carga sozinho” sem falar de health check. Se travar, pergunte a si mesmo: “esse balanceador consegue ler o que eu quero decidir?”. Se a resposta é não, a camada está errada.
Follow-ups prováveis
Por que o NLB é o certo para gRPC de alto volume? Ele não é, em geral. Como o gRPC multiplexa muitas requisições numa conexão, o L4 concentra o tráfego de um cliente num único alvo. O caminho usual é o ALB, com HTTP/2 até o alvo, ou clientes com várias conexões.
O ALB termina TLS? Sim. O certificado fica no ACM, e a conexão com o alvo pode seguir em HTTP ou ser recriptografada, conforme a configuração.
Quando faz sentido usar o GWLB? Quando o tráfego precisa passar por firewall, IDS ou IPS de terceiros de forma transparente, sem o balanceador mudar o endereço do pacote. É um caso de segurança de rede, não de distribuição de carga de aplicação.
10. LRU e LFU: despejo de cache
A pergunta e o que o entrevistador quer descobrir
“Como funciona a política de despejo LRU e como você implementaria uma estrutura LRU com tempo O(1)?” São duas perguntas numa. A primeira mede se você entende que o cache tem memória finita e que escolher a vítima é uma aposta sobre o futuro. A segunda mede se você transforma essa aposta em código, com as estruturas de dados certas e o custo de cada operação na ponta da língua. Citar o LFU mostra que você conhece a alternativa e onde ela erra. A notação usada para dizer “O(1)” está explicada em Big O.
Como responder em voz alta
“Um cache tem memória finita. Quando ela enche e chega um item novo, alguém precisa sair, e a política de despejo decide quem. O LRU aposta que o item usado há mais tempo é o que menos vai ser usado daqui para frente, o que costuma valer quando há localidade temporal, como uma página de produto em alta. O LFU aposta na frequência: remove o item com menos acessos acumulados. Cada aposta erra de um jeito. O LFU segura um item que foi popular numa semana passada e já não interessa, e o LRU descarta um item consultado a cada hora, mas que não apareceu nos últimos minutos.
Para implementar o LRU em O(1), eu combino duas estruturas. Um hash map dá acesso direto ao nó pela chave, e uma lista duplamente encadeada mantém a ordem de uso. Em cada leitura, tiro o nó da posição atual e o coloco na cabeça da lista, o que custa O(1) porque já tenho o ponteiro. Quando a capacidade estoura, removo o nó da cauda e apago a chave do map. No Redis, a implementação é outra: uma aproximação por amostragem, porque guardar ponteiros em cada uma das milhões de chaves custaria memória demais.”
Por dentro
Imagine uma mesa pequena num restaurante cheio. Ela comporta quatro pratos, e quando chega o quinto, alguém precisa sair. O garçom que escolhe o prato que sai está fazendo o trabalho de uma política de despejo. Em software, o cache serve para guardar o que é caro de recalcular ou buscar, como o resultado de uma consulta ao banco. A RAM é cara e finita, então o cache precisa de uma regra para decidir o que descartar. Sem regra, ou você nunca descarta e a memória estoura, ou você descarta ao acaso e perde os itens quentes.
O exemplo mais claro da diferença é uma sequência de acessos. Imagine um cache com capacidade para dois itens, e a sequência A, A, A, B, C. Depois de A, A, A e B, o cache tem A e B. Quando C chega, o LRU olha para a ordem do último acesso: A foi lido pela última vez antes de B, então A sai, e o cache fica com B e C. O LFU olha para a contagem: A foi lido três vezes e B uma vez, então B sai, e o cache fica com A e C. A mesma sequência gera dois resultados, e nenhum dos dois é errado sem o contexto de quem consome o cache.
Os casos reais mostram quando cada aposta vale. Um feed de notícias tende a ser bem servido por LRU, porque o que foi lido agora é o que vai ser lido de novo. Um catálogo com produtos que estouram em uma campanha e depois caem tende a favorecer o LFU, porque a frequência acumulada indica quem é realmente popular. E um dado que expira em minutos, como a sessão de um login, raramente deveria depender de despejo, e sim de TTL.
Para o LRU em O(1), cada nó da lista guarda a chave, o valor e os ponteiros prev e next. O hash map aponta para o nó de cada chave. Essa combinação permite três operações em tempo constante: encontrar o nó pela chave, remover um nó do meio da lista e inserir na cabeça. A lista precisa ser duplamente encadeada porque remover um nó do meio exige religar o vizinho anterior, e sem o ponteiro prev você teria que percorrer a lista inteira até achá-lo, o que custa O(n).
Comece pelo cliente, à esquerda. O get(A) consulta o HashMap, que aponta direto para o nó A, sem percorrer lista nenhuma. Em seguida, A vai para a frente, marcado como HEAD (seta verde e grossa). Quando um put de D chega com o cache cheio, o nó da cauda, B, sai da lista e o HashMap perde a entrada dele (em rosa). O custo de cada passo não depende do tamanho do cache.
O LFU exige mais cuidado. Cada item guarda um contador, e o despejo precisa achar o contador mínimo, o que com um heap custa O(log n) por operação. Existe uma versão em O(1) com listas por frequência, mas ela não é o que se espera que você escreva de cabeça numa entrevista. O problema mais sério do LFU é que o contador só cresce. Um item que foi muito acessado uma vez fica para sempre com um contador alto, e é esse o efeito que os textos chamam de cache pollution. A correção é o decaimento: o contador cai com o tempo, de modo que a popularidade antiga perde peso.
O Redis não usa a lista duplamente encadeada. Para despejar, ele limita a memória com a diretiva maxmemory e escolhe a vítima com a política configurada em . No LRU, cada chave guarda apenas um carimbo do último acesso. Quando precisa despejar, o Redis sorteia uma amostra de N chaves, em que N é maxmemory-samples (o padrão que eu conheço é 5), guarda as candidatas num pool que persiste entre as rodadas e apaga a mais ociosa. Por isso o resultado é aproximado: a chave removida pode não ser a menos recente de todo o keyspace, mas com amostras suficientes o resultado fica perto. O ganho é que a memória por chave não cresce com dois ponteiros.
A faixa de cima é o LRU aproximado. O keyspace solta uma amostra, o pool guarda as candidatas, e a chave k2, ociosa há 90 segundos, sai. A faixa de baixo é o LFU: cada acesso sobe um contador logarítmico, o decaimento reduz esse contador com o tempo (o intervalo padrão que eu conheço é de 1 minuto), e a mesma amostragem escolhe a chave menos usada.
O LFU do Redis também é aproximado. O contador tem 8 bits, cresce de forma probabilística, e a velocidade do crescimento é controlada por lfu-log-factor (o padrão que eu conheço é 10). Essa escala logarítmica é o que permite contar milhões de acessos com um único byte por chave. O preço é a imprecisão, que o decaimento e a amostragem tentam compensar.
As políticas do Redis combinam duas perguntas: quais chaves são candidatas e qual critério escolhe a vítima. Com allkeys-lru, allkeys-lfu ou allkeys-random, qualquer chave pode sair. Com volatile-lru, volatile-lfu, volatile-random ou volatile-ttl, só saem as chaves que têm TTL definido. Há também noeviction, que não despeja nada e devolve erro quando a memória enche. Essa diferença entre allkeys e volatile é o ponto que mais surpreende: uma política volatile com chaves sem TTL deixa o cache cheio sem candidatas, e as escritas falham.
Na AWS
No ElastiCache, a política e a amostragem ficam no parameter group, junto com maxmemory-policy e maxmemory-samples. A AWS também reserva uma parte da memória do nó para overhead, e o diagrama desta seção usa 25% como exemplo; confirme o valor exato na sua versão do engine.
A faixa de cima mostra o erro. A aplicação grava chaves sem TTL numa política volatile-lru, o cache enche, não há candidatas a despejo, e o SET falha com erro de falta de memória (, em rosa). As leituras seguem normalmente, o que torna o problema silencioso até que alguém olhe para as escritas. A faixa de baixo é o desenho recomendado: chaves com TTL, uma política allkeys, e o CloudWatch observando as métricas de Evictions e CacheHitRate.
Vale notar a separação de papéis. O banco continua sendo a fonte da verdade, e o cache, em cache-aside, é recalculado quando há miss, como a seta laranja da faixa do meio mostra. Por isso, uma política de despejo que descarta dados é segura, desde que a aplicação saiba recarregá-los. Um alarme em Evictions crescendo junto com CacheHitRate caindo indica que o cache está menor que o conjunto quente de dados, e a resposta é mais memória ou um desenho diferente de chave, não uma política nova.
Dicas para a entrevista
Pergunte se o cache guarda dados recalculáveis ou se é a fonte da verdade, se as chaves têm TTL e qual é o padrão de acesso: predominam os itens recentes ou os populares? Depois diga o trade-off. O LRU é simples e responde bem a mudanças de tendência, mas sofre com varreduras, em que um job que lê milhares de itens únicos expulsa os quentes. O LFU resiste a varreduras e segura bem os populares, mas precisa de decaimento e demora a reagir a um item que virou tendência.
Os números que ajudam são maxmemory-samples em torno de 5 e lfu-log-factor de 10, sempre com a ressalva de que o ajuste depende da carga. O que soa júnior: “uso um dicionário e removo com min()” (que é O(n) por despejo), “o Redis tem LRU exato” (não tem), “LFU é sempre melhor” (depende do padrão) e “política volatile e pronto” sem checar se há chaves com TTL.
Follow-ups prováveis
Por que o Redis não usa uma lista duplamente encadeada? Porque os ponteiros de cada chave custariam memória em escala de milhões de chaves. A amostragem dá uma aproximação com custo fixo e sem ponteiros.
Como o LFU evita manter itens antigos populares? Com o decaimento do contador, que faz a popularidade antiga perder peso com o tempo.
Qual a diferença entre TTL e despejo? O TTL expira um item por tempo, mesmo que a memória sobre. O despejo remove itens por pressão de memória, sem olhar para o tempo. A invalidação ativa, quando a aplicação apaga a chave ao mudar o dado, é um terceiro mecanismo, e a diferença está explicada em TTL.
Por que uma política volatile pode dar erro de escrita? Porque ela só pode remover chaves com TTL. Se o cache estiver cheio de chaves sem TTL, não há o que tirar, e o Redis recusa a escrita. Uma chave sem TTL, nesse caso, vira um contrato de memória que a aplicação precisa cumprir.
11. tempo real na web (polling, long polling, SSE e WebSocket)
11.1 a pergunta e o que o entrevistador quer descobrir
A pergunta é: “Qual tecnologia você escolhe para atualizar dados em tempo real no frontend e por quê?”. O entrevistador não quer ouvir “WebSocket, porque é o mais moderno”. Ele quer ver se você sabe que existem quatro técnicas com custos diferentes, se escolhe pelo requisito (quem fala, com que frequência, quantos clientes) e se pensa no que acontece depois que a conexão é aberta: balanceamento, reconexão, autenticação e limites da infraestrutura.
11.2 como responder em voz alta
“Eu começo perguntando duas coisas: o dado muda sem o usuário pedir, e o cliente também precisa mandar mensagens com frequência? Se o dado quase não muda e há poucos clientes, short polling resolve e é o mais simples de operar. Se preciso de empurrar dados do servidor para o cliente, como cotação, notificação ou o texto que um modelo de linguagem vai gerando, eu vou de Server-Sent Events, o SSE, porque é HTTP comum, o navegador reconecta sozinho e o servidor só precisa escrever numa resposta que não termina.
Se os dois lados falam o tempo todo, como num chat com digitação ao vivo, jogo ou edição colaborativa, aí sim WebSocket. O long polling eu uso quando alguma peça no caminho, como um proxy corporativo, não aceita conexão longa, ou como fallback.
Depois da escolha eu falo do que custa: conexão longa é estado no servidor. Preciso de um jeito de espalhar mensagens entre instâncias, por exemplo um pub/sub, de keepalive para o balanceador não derrubar a conexão por silêncio, e de reconexão com backoff e um cursor para o cliente recuperar o que perdeu. Na AWS eu começaria por um serviço gerenciado, API Gateway WebSocket ou AppSync, e só manteria as conexões eu mesmo, num ALB com ECS, se o volume ou o protocolo justificassem.”
11.3 por dentro
Comece pelo problema que todas as quatro técnicas resolvem. O HTTP clássico é uma conversa em que só o cliente pode falar primeiro: ele pergunta, o servidor responde, fim. Isso é ótimo para abrir uma página e péssimo para avisar que chegou uma mensagem. A pergunta “como o servidor avisa o cliente?” tem quatro respostas, e a figura abaixo mostra as quatro lado a lado, com o custo de cada uma na coluna da direita.
Leia a figura de cima para baixo. No short polling o cliente pergunta a cada cinco segundos, e os pacotes cinzentos de volta são as respostas vazias, que são a maioria. Doze requisições por minuto por cliente é a conta de 60 dividido por 5, e ela acontece mesmo quando nada mudou. No long polling o cliente pergunta uma vez e o servidor segura a resposta até haver dado ou até um tempo limite, e por isso o pacote da ida é lento: ele espera. No SSE a requisição sai uma vez e os eventos voltam em fila pela mesma resposta, que nunca termina. No WebSocket os pacotes andam nos dois sentidos.
O short polling é o mais simples, e por isso vale respeitá-lo. Cada requisição é independente, qualquer cache, balanceador e CDN entende, e escalar é só acrescentar instâncias. O custo é a latência (o dado só chega na próxima pergunta, então com intervalo de 5 s o atraso médio é de uns 2,5 s) e o desperdício de requisições vazias. Para um painel de status que atualiza a cada minuto com poucas centenas de usuários, é uma escolha correta e barata.
O long polling reduz o atraso e o desperdício, mas cada evento custa uma reconexão: o servidor responde, o cliente abre outra requisição. Em eventos frequentes isso volta a ser quase um polling, e em eventos raros fica ótimo. Hoje ele aparece mais como plano B, por exemplo em bibliotecas como o Socket.IO, que tentam WebSocket e caem para long polling quando o caminho de rede não deixa passar.
O SSE é HTTP puro. O servidor responde com Content-Type: text/event-stream e vai escrevendo blocos de texto no formato id:, event: e data:, separados por linha em branco. O navegador expõe isso pela API EventSource, que reconecta sozinho quando a conexão cai e reenvia o último id recebido no cabeçalho Last-Event-ID, o que permite ao servidor retomar de onde parou. As limitações são reais: só o servidor fala, só carrega texto, o EventSource só faz GET e não aceita cabeçalhos customizados (então a autenticação costuma ir em cookie ou você usa uma biblioteca baseada em fetch), e no HTTP/1.1 os navegadores limitam as conexões por origem (tipicamente seis), o que pesa se o usuário abrir várias abas. Essas últimas duas regras são do comportamento dos navegadores e não estão na documentação da AWS, então confira o seu alvo.
O WebSocket começa como uma requisição HTTP com Upgrade: websocket, o servidor responde 101 Switching Protocols, e a partir daí a mesma conexão TCP passa a carregar mensagens do protocolo WebSocket, em quadros pequenos, nos dois sentidos. A vantagem é o canal bidirecional com pouco cabeçalho por mensagem. O preço é o estado: cada conexão aberta é um objeto na memória de uma instância específica. A aula WebSocket mostra o handshake, os quadros de dados e de controle (ping, pong, close) e uma implementação completa em Go, com hub de salas, e vale ler antes da entrevista se o seu alvo for backend em Go.
O ponto que separa respostas sênior das juniores é o que vem depois da escolha. Conexão longa quebra três hábitos de quem só fez REST:
- O balanceamento deixa de ser por requisição. Uma conexão fica presa a uma instância durante horas, então um deploy ou um scale-in derruba milhares de conexões de uma vez. Isso conversa com Graceful Shutdown e Signal Handling: a instância precisa avisar os clientes, fechar com um código de status e deixar que reconectem em outro lugar, sem criar uma tempestade de reconexões simultâneas.
- Falar com todos os clientes exige um barramento. Se o usuário A está conectado na instância 1 e o evento nasce na instância 2, alguém precisa levar a mensagem. É o padrão de pub - sub, e na prática costuma ser Redis (Redis) ou um serviço gerenciado.
- O silêncio mata conexões. Balanceadores, proxies e NATs derrubam conexões sem tráfego. O remédio é um batimento (ping e pong, ou um comentário
:periódico no SSE) mais um cliente que reconecta com espera crescente e aleatória (backoff com jitter, o mesmo raciocínio de Resiliência com Circuit Breaker e Retry) e que retoma por um cursor ouLast-Event-ID.
A segunda figura mostra o terceiro ponto como uma falha concreta, com os limites reais da AWS.
A linha A é o caminho saudável: o navegador manda ping a cada 30 s, o ALB vê tráfego e não fecha nada. Na linha B nada trafega por 60 s, que é o tempo de inatividade padrão do ALB, e o próximo pacote encontra a conexão fechada (o X vermelho). Na linha C o cliente resiliente reconecta e manda o último id recebido, e o servidor reenvia o que faltou. A linha D mostra o limite que não depende do seu silêncio: no API Gateway WebSocket uma conexão vive no máximo duas horas, mesmo com tráfego, e esse limite não pode ser aumentado. Reconectar, portanto, não é tratamento de erro raro. É rotina.
Por fim, a árvore de decisão. Ela cabe numa figura e é o que eu desenharia no quadro.
Siga as setas verdes (sim) e cinzas (não). A primeira pergunta elimina quem nem precisa de tempo real. A segunda reserva o polling para o caso em que atraso de segundos é aceitável. A terceira separa WebSocket de SSE pelo requisito mais importante: o cliente manda dados com frequência ou só escuta? A quarta separa SSE de long polling pelo que a rede aguenta.
11.4 na AWS
A AWS oferece cinco caminhos, e a escolha entre eles é a mesma decisão entre gerenciar a conexão você mesmo ou pagar para outro gerenciar.
No grupo de cima está o API Gateway com WebSocket API. O API Gateway segura as conexões e chama uma função Lambda por rota ($connect, $disconnect, $default e rotas customizadas). Como a função não guarda estado, você grava o connectionId no DynamoDB e, para empurrar uma mensagem, usa a API de gerenciamento de conexões (o endpoint @connections). Os limites, conferidos na documentação em 2026-10-22: duração máxima de conexão de 2 horas e inatividade de 10 minutos, ambos fixos; mensagem de até 128 KB, em quadros de 32 KB (um quadro maior fecha a conexão com o código 1009); 500 novas conexões por segundo por conta e região, ajustável; e tempo de integração de 29 segundos no máximo. O quadro “concorrentes” não tem limite próprio: ele resulta de 500 conexões por segundo vezes as duas horas, até 3,6 milhões. A vantagem é não operar servidor de conexão. O custo é o código para manter a tabela de conexões e a limpeza de conexões mortas.
No segundo grupo está o caminho do controle: ALB mais ECS. O ALB aceita WebSocket e SSE, o idle timeout padrão é 60 s e aceita de 1 a 4000 s, então com batimento a cada 30 s a conexão sobrevive. Suas tasks mantêm as conexões e conversam entre si por um pub/sub (aqui ElastiCache). Você ganha o protocolo e a latência que quiser, e paga com a operação: deploy sem derrubar todo mundo, escala por número de conexões e não por CPU, memória por conexão.
No terceiro grupo estão os serviços de pub/sub gerenciados. O AppSync entrega subscriptions de GraphQL sobre WebSocket, e em outubro de 2024 ganhou o AppSync Events, uma API serverless de pub/sub por WebSocket que, desde março de 2025, também permite publicar pela mesma conexão. O IoT Core entrega mensagens MQTT, inclusive sobre WebSocket (essa capacidade eu não reverifiquei na documentação hoje). São a melhor escolha quando o problema é “muitos assinantes, poucos publicadores”, como placar ao vivo ou notificações.
No quarto grupo está o SSE sem servidor. O Lambda tem response streaming: a função vai escrevendo e o cliente vai recebendo, com limite de resposta de 200 MB contra 6 MB no modo normal. Pela documentação, ele é suportado nos runtimes gerenciados de Node.js, e para outras linguagens você usa um runtime customizado ou o Lambda Web Adapter. Dois cuidados: o Lambda cobra pela duração total mesmo que o cliente desconecte, e as function URLs não suportam streaming dentro de uma VPC. O API Gateway REST também passou a suportar streaming em novembro de 2025: você muda o modo de transferência da integração de BUFFERED para STREAM, e a documentação diz que o tempo de integração pode chegar a 15 minutos. A mesma página avisa que, ao alterar uma integração existente, um tempo customizado é removido e o streaming vale por até 5 minutos, então confira o valor depois de alterar.
Por que não o contrário? Para chat com milhares de salas e regras de negócio pesadas, o API Gateway WebSocket com Lambda vira uma colcha de rotas e de leituras no DynamoDB, e ALB mais ECS fica mais legível. Para um feed de notificações, ALB mais ECS é exagero, e AppSync Events ou SNS mais um assinante simples resolvem. CloudFront também repassa WebSocket, mas a documentação desta consulta não cobre esse limite, então trate-o como pergunta em aberto.
11.5 dicas para a entrevista
Antes de responder, faça perguntas: quantos clientes simultâneos (mil ou um milhão)? A frequência dos eventos (um por minuto ou dez por segundo)? O cliente também envia? Precisa de garantia de entrega e ordem? Há proxies corporativos no caminho? Os números mudam a resposta.
- Diga que escolhe pelo requisito, não pela moda. A frase “SSE para quase tudo em que o servidor empurra” costuma agradar, porque mostra que você conhece a alternativa mais simples ao WebSocket.
- Fale de reconexão, cursor, backoff com jitter e keepalive sem ser perguntado. É o que separa quem já operou conexão longa.
- Cite os limites do API Gateway (2 h, 10 min) e o idle timeout do ALB. Número exato impressiona, e mostra que você leu o limite antes de sofrer com ele.
- Fale de autenticação: o token vai na query string ou no primeiro frame? Token expira durante uma conexão de duas horas, então é preciso reautenticar ou fechar.
- Armadilhas que soam júnior: dizer “WebSocket é sempre melhor porque é tempo real”, esquecer que o servidor precisa de estado, ignorar o que acontece em deploy, e não falar de mensagens perdidas durante uma queda.
- Se travar: volte à árvore. “Quem fala? Com que frequência? O que a rede aguenta?” Três perguntas que sempre geram uma resposta defensável.
11.6 follow-ups prováveis
Como você escala WebSocket para um milhão de conexões? Cada instância mantém uma fração, o balanceamento é por conexão, e as mensagens cruzam instâncias por um barramento (Redis pub/sub, ou um serviço gerenciado). A memória por conexão e o número de descritores de arquivo viram o limite, então escale por conexões e não por CPU.
E se a conexão cair no meio de um evento? O servidor numera os eventos, o cliente reenvia o último id recebido e o servidor reenvia o que faltou a partir de um buffer curto (ou o cliente refaz uma consulta REST de recuperação). Sem isso, a única garantia é “no máximo uma vez”.
Como autenticar? No handshake, validando um token curto (por exemplo um JWT, tema da aula Autenticação e JWT) e fechando a conexão quando ele expira. No browser, como o EventSource e o WebSocket não permitem cabeçalhos customizados, o token costuma ir em cookie, na query string (com cuidado de log) ou na primeira mensagem.
Por que não usar só polling para tudo? Porque o custo cresce com clientes vezes frequência, mesmo sem novidade. Com 100 mil clientes perguntando a cada 5 segundos são 20 mil requisições por segundo só de perguntas, e a maioria volta vazia.
12. deploys sem downtime e migrações (expand and contract)
12.1 a pergunta e o que o entrevistador quer descobrir
A pergunta é: “Como você faz deploy de uma nova versão que renomeia uma coluna no banco relacional sem derrubar os usuários?”. O entrevistador quer ver se você separa duas trocas que costumam se misturar. Uma é a troca de versão da aplicação, feita por rolling update, blue/green ou canary. A outra é a troca de esquema do banco, que é compartilhado pelas duas versões ao mesmo tempo. Quem responde só com a estratégia de deploy resolve a metade fácil. Quem percebe que a versão antiga e a nova vão conviver por um tempo, e que o banco precisa servir às duas, mostra que já passou por uma migração em produção.
12.2 como responder em voz alta
“Eu separo o deploy da aplicação do deploy do esquema. Para a aplicação escolho a estratégia pelo risco: rolling update quando as versões convivem bem e manter duas delas custa pouco, blue/green quando quero um corte rápido e reversível, e canary quando quero expor a versão nova a uma fração do tráfego e olhar erro e latência antes de avançar. Para renomear a coluna nunca faço ALTER TABLE RENAME direto, porque a versão antiga ainda está no ar e passa a falhar na primeira consulta que usa o nome antigo. Uso expand and contract em quatro deploys. Primeiro adiciono a coluna nova e mantenho a antiga. Depois a aplicação grava nas duas e lê da antiga, enquanto um em lotes copia o histórico. Em seguida troco a leitura para a coluna nova, mantendo a escrita nas duas, para que voltar à versão anterior continue possível. Só removo a coluna antiga quando nenhuma instância a usa. Cada passo é um deploy que funciona com a versão que ainda está no ar.”
12.3 por dentro
Um deploy com downtime acontece quando a versão nova substitui a antiga de uma vez: a instância antiga desliga, a nova ainda não está pronta, e o usuário vê erro. Para evitar isso existem formas de trocar a aplicação sem parar o serviço, e a diferença entre elas é quanto tráfego e quanta infraestrutura ficam expostos ao risco.
O rolling update troca as instâncias aos poucos. Se você tem dez instâncias e troca duas por vez, durante alguns minutos há instâncias v1 e v2 respondendo ao mesmo tempo, atrás do mesmo balanceador. Esse é o ponto que quase todo mundo esquece: o custo do deploy não é só a troca, é a convivência. O blue/green mantém dois ambientes completos, o azul com a versão atual e o verde com a nova. O tráfego passa de um para o outro de uma vez, e a volta é tão rápida quanto a ida, porque o azul continua de pé. O canary é uma fração: 5% ou 10% do tráfego vai para a versão nova, e a fatia só cresce se o erro e a latência dela ficarem parecidos com os da versão antiga.
Nenhuma dessas três estratégias muda o banco. Todas assumem que a versão antiga e a nova conseguem usar o mesmo esquema ao mesmo tempo, e isso não acontece sozinho. Renomear uma coluna quebra essa premissa: o SQL da versão antiga referencia name, e depois do rename essa coluna não existe mais. O problema está no tempo de convivência, não no comando em si.
A técnica expand and contract resolve isso dividindo a mudança em passos em que cada versão continua compatível com o esquema que existe naquele momento. Martin Fowler descreve a ideia com o nome de Parallel Change, e a figura abaixo mostra os quatro passos sobre a tabela users.
Leia a figura da esquerda para a direita. Na fase 1 a migração só adiciona full_name como nullable, e a versão 1 segue funcionando porque não conhece a coluna nova. Na fase 2 a versão 2 grava nas duas colunas e lê de name, enquanto o backfill, em azul, copia o histórico em lotes de mil linhas. Na fase 3 a leitura passa para full_name, e a versão 3 ainda grava nas duas, o que mantém o ponto de retorno: se algo der errado, a v2 ainda encontra os dados que precisa. Só na fase 4 a coluna name sai.
O backfill em lotes existe para não segurar a tabela inteira num único UPDATE gigante. Cada lote é uma transação curta, e o job pode ser pausado se a carga do banco subir. Os lotes também são o motivo de a fase 2 durar mais que um deploy: a cópia é um processo à parte, e a aplicação não precisa esperar por ela para continuar funcionando.
O rename direto parece inofensivo até o tráfego ser misto. Durante um rolling update, o balanceador manda metade das requisições para instâncias v1 e metade para v2. A figura compara as duas abordagens com essa realidade.
À esquerda, as instâncias v1 ainda fazem SELECT name, e essa consulta é barrada com o X rosa porque a coluna já não existe. A metade que cai na v2 funciona, então a falha parece intermitente, o que a torna mais difícil de diagnosticar. À direita, a tabela tem as duas colunas e as duas versões funcionam: a v1 lê name, e a v2 grava nas duas.
12.4 na AWS
O CodeDeploy coordena a troca gradual para ECS e Lambda, com as estratégias blue/green e canary. Os pesos do tráfego vêm do balanceador: o ALB aceita dois ou mais target groups com pesos, e é isso que faz a divisão de 90/10 da figura. O Route 53 também tem roteamento ponderado, mas ele atua no DNS, e o TTL dos registros decide quanto tempo os clientes continuam indo para o destino antigo. Por isso, para uma troca que precisa ser rápida e reversível, o balanceador costuma ser a escolha mais previsível.
O ALB manda 90% para o grupo azul, com a v1, e 10% para o verde, com a v2. As duas versões gravam no mesmo Aurora, por isso o esquema precisa servir às duas, como na seção anterior. O tráfego de produção aparece em laranja, a fatia pequena da versão nova em azul-claro, e a seta roxa leva erros 5xx e p99 do verde para o CloudWatch. Quando o alarme dispara (seta vermelha), o CodeDeploy muda os pesos de volta (seta pontilhada), e a reversão é uma alteração de configuração, não um novo deploy.
No banco, o Aurora e o RDS têm blue/green deployments: a AWS cria um ambiente de staging que se mantém sincronizado com a produção por replicação, você testa nele uma nova versão do motor ou novos parâmetros, e depois troca os papéis. A figura mostra a aplicação gravando pelo RDS Proxy no blue e o green recebendo as linhas pela replicação. A seta verde diz que a replicação leva linhas, não DDL, e a seta vermelha mostra um ALTER TABLE indo direto para o blue. A documentação sobre o mecanismo exato dessa replicação não foi conferida hoje, então a afirmação sobre DDL fica como leitura da figura.
Essa é a razão de o blue/green de banco não substituir o expand and contract. Ele protege a troca de motor ou de parâmetros, mas a mudança de esquema que a aplicação percebe continua sendo uma decisão de projeto.
12.5 dicas para a entrevista
Antes de desenhar a solução, pergunte quantas versões podem estar no ar ao mesmo tempo, se o banco é compartilhado entre serviços e se a coluna antiga pode ser apagada sem perda. Essas respostas decidem quantos deploys você precisa. Diga os trade-offs: o rolling é barato, mas exige compatibilidade entre versões; o blue/green dá reversão rápida, mas dobra a infraestrutura durante a troca; o canary reduz o raio do erro, mas só funciona se você tiver métrica boa; o expand and contract é seguro, mas leva tempo e exige escrever nas duas colunas por um período.
O que soa júnior é “faço o rename num deploy só e, se der errado, reverto o código”. Reverter o código não reverte o banco. Se a versão antiga não consegue mais ler o esquema, o rollback quebra o serviço do mesmo jeito. Outra frase que denuncia pouca prática é “o blue/green resolve a migração”: ele resolve a troca da aplicação e, no máximo, a troca do motor do banco.
12.6 follow-ups prováveis
Como reverter depois da fase 4? Não dá para voltar a coluna sem reconstruí-la. Por isso a fase 4 só entra depois de um período de observação e de um backup verificado, e não logo depois da fase 3.
Como fazer o backfill numa tabela enorme? Em lotes, com pausas, monitorando a carga e o atraso das réplicas, e de um jeito que possa ser retomado do ponto em que parou. As linhas gravadas pela v2 já saem com as duas colunas preenchidas, então o backfill só precisa cuidar do histórico anterior.
Blue/green resolve a migração de banco? Não sozinho. Ele é a janela para trocar a aplicação ou o motor com segurança, mas o rename continua pedindo expand and contract, porque as duas versões ainda precisam conviver com o mesmo esquema.
13. SQL ou NoSQL (sem mitos)
13.1 a pergunta e o que o entrevistador quer descobrir
A pergunta é: “Em que situações você escolheria um banco NoSQL em vez de um banco relacional maduro como PostgreSQL?”. Quem responde “NoSQL escala melhor” já caiu na armadilha. O entrevistador quer ver se você rejeita o mito de que relacional não escala, se sabe o que cada modelo cobra em troca (JOIN, transação entre tabelas e consulta livre de um lado, previsibilidade e particionamento do outro) e se decide pelo padrão de acesso, não pela tecnologia da moda.
13.2 como responder em voz alta
“Eu começo pelas consultas, não pelo banco. Se a aplicação precisa de JOIN, de transação que atravessa tabelas ou de consultas que ainda não existem, eu fico no relacional. Ele escala mais do que o mito sugere: réplicas de leitura, um pool de conexões e particionamento levam muito longe, e na AWS o Aurora com um proxy de conexões é a resposta concreta para isso. Eu troco para NoSQL quando o acesso é conhecido e estável, sempre pela chave ou por poucos índices previstos, e preciso de latência previsível num volume que não quero administrar, como no DynamoDB. Também troco quando a forma do problema pede outra estrutura, como grafo, busca textual com relevância ou série temporal. O custo do NoSQL é que o modelo precisa estar pronto antes dos dados, e cada consulta nova vira trabalho de modelagem, não apenas uma query a mais.”
13.3 por dentro
O mito tem uma origem razoável. Os primeiros bancos relacionais cresciam sobretudo para cima, numa máquina cada vez maior, e os sistemas que nasceram na web precisaram espalhar dados em muitos nós desde o começo. Hoje o relacional também replica e particiona. A pergunta certa deixou de ser “escala ou não escala” e passou a ser “quais operações continuam baratas quando o dado está espalhado”. Num banco distribuído, um JOIN que atravessa nós custa caro, e um banco NoSQL costuma exigir que cada consulta leia de uma partição só. Essa restrição é o que dá a previsibilidade, e é também o que você paga.
Para entender o que o relacional oferece de graça, vale lembrar as propriedades de uma transação, descritas em ACID: atomicidade (ou tudo acontece, ou nada), consistência (as regras do banco continuam valendo depois da escrita), isolamento (transações simultâneas não se atrapalham de forma visível) e durabilidade (o que foi confirmado sobrevive a uma queda). Quando um pagamento e um pedido precisam mudar juntos, essas garantias são o que impede o estado pela metade. Um NoSQL pode oferecer transações em alguns casos, mas a escolha passa a ter custo de modelagem e de limite que precisa ser conferido.
A decisão começa com três perguntas que as notas do vault já desenvolvem. Query pattern pede que você liste as perguntas da aplicação, com a frequência de cada uma e a latência que ela aceita. Structure of data separa o dado estruturado, o semiestruturado e o não estruturado, e o nível de rigidez muda a ferramenta. Amount of scale needed mede volume, concorrência e latência, e diz se o problema é vertical ou horizontal. Com essas três respostas em mãos, a árvore da figura 11 quase se preenche sozinha.
A figura abaixo mostra a mesma pergunta, os pedidos do cliente 42 com seus itens, nos dois modelos.
No lado esquerdo, a pergunta vira três JOINs entre customers, orders e order_items, e o banco resolve a combinação na hora da consulta. No lado direito, a tabela Pedidos responde pela chave de partição, e a consulta por status depende de um GSI, um índice secundário global com outra chave de partição. Esse índice foi preciso porque o padrão de acesso foi desenhado antes da tabela, e cada índice a mais custa armazenamento e escrita.
Quando o volume cresce, o relacional tem um caminho conhecido, mostrado na figura seguinte.
Uma aplicação com milhares de requisições por segundo passa por três defesas antes de exigir outro banco. O cache responde às leituras repetidas sem tocar no banco (seta verde rápida). O RDS Proxy agrupa as conexões da aplicação, e a escrita vai para um único writer, que é o limite real desse desenho. As leituras se espalham pelos readers, que compartilham o mesmo volume, e a seta pontilhada mostra essa replicação do armazenamento. O gargalo costuma ser a escrita, e é ela que decide quando a conversa sobre outro banco começa.
A árvore de decisão reúne os caminhos. Ela começa pela estabilidade dos padrões de acesso, e a resposta mais comum é o relacional. Só sai dele quando há um motivo concreto, e cada folha tem um uso específico.
Leia a árvore de cima para baixo. A primeira pergunta separa padrões conhecidos de padrões que ainda vão mudar, e a segunda verifica se há JOIN, agregação livre ou transação entre tabelas. Só quando ambas as respostas apontam para longe do relacional a terceira pergunta entra, e ela escolhe entre chave, grafo, texto e tempo. O ElastiCache aparece como folha, mas com um aviso: ele serve para ler mais rápido, não para guardar a verdade.
13.4 na AWS
O Aurora e o RDS são o caminho relacional gerenciado, e o Aurora permite réplicas de leitura e um proxy de conexões, como na figura 10. O DynamoDB cobre chave-valor e documento com latência previsível e sem administração de servidor. O Neptune é o banco de grafos, o OpenSearch cobre busca textual com relevância e facetas, e o Timestream for InfluxDB aparece na figura para série temporal. O ElastiCache é cache e não fonte da verdade, o que conversa com a seção 10 sobre despejo.
Os nomes e as categorias vêm da figura 11 e não foram conferidos hoje na documentação da AWS. Em particular, qual oferta de série temporal é a recomendada para cada caso muda com o tempo, então confirme antes de citar uma delas numa entrevista.
13.5 dicas para a entrevista
Pergunte qual é a consulta mais frequente e quanta latência ela aceita. Pergunte também quais consultas devem aparecer nos próximos meses, porque o NoSQL cobra caro por consulta que não estava no desenho. Pergunte se há transação que envolve mais de uma entidade, porque isso decide muita coisa. Diga os trade-offs: o relacional dá flexibilidade de consulta e garantias fortes, com um escritor como limite; o NoSQL dá previsibilidade e escala horizontal, com modelagem antecipada e menos liberdade para consultas novas.
O que soa júnior é “NoSQL é mais rápido”, sem dizer para qual padrão de acesso. Também denuncia pouca prática “SQL não escala”, que é o mito, e “uso Mongo porque o dado é JSON”, já que o Postgres guarda JSON em colunas próprias e indexáveis. Outra frase para evitar é “sem esquema, então não preciso validar”: a validação não some, ela muda de lugar, e normalmente vai para a aplicação.
13.6 follow-ups prováveis
Dá para usar os dois? Sim, e é comum. O relacional fica como fonte da verdade e um DynamoDB recebe uma projeção para leituras por chave, sincronizada por eventos. O cuidado é o mesmo da pergunta 7: a sincronização precisa de outbox e de consumidores idempotentes, senão a projeção diverge.
Por que o DynamoDB não tem JOIN? Porque um JOIN exige ler de várias partições, e o modelo do DynamoDB existe para que cada consulta leia uma só. Quem precisa dos dados juntos os grava juntos, ou os junta na aplicação com chaves conhecidas.
O Postgres com JSONB substitui um banco de documentos? Para muitos casos, sim. O que muda é a consulta sobre o documento e o índice que você cria, e o limite depende do volume e do padrão de acesso, que você mede antes de decidir.
14. o problema N+1 e o DataLoader
14.1 a pergunta e o que o entrevistador quer descobrir
A pergunta é: “O que é o problema N+1 e como você o previne em ORMs e APIs GraphQL?”. O entrevistador quer ver se você reconhece o padrão pelo número de consultas, se sabe que ele aparece em dois lugares com causas diferentes (o ORM que carrega relacionamentos sob demanda e o resolver de GraphQL que roda uma vez por item) e se você sabe confirmar o problema num trace antes de corrigi-lo.
14.2 como responder em voz alta
“O N+1 é quando eu faço uma consulta para a lista e depois uma consulta por item para completar os dados. Listar cinquenta posts e os autores de cada um, de forma ingênua, gera cinquenta e uma consultas em vez de uma ou duas. No ORM, o caminho clássico é o carregamento lazy de um relacionamento dentro de um loop, e a correção é trazer o relacionamento junto, com JOIN ou com um IN com os ids. Em GraphQL, o problema aparece porque cada campo é resolvido por conta própria, e a correção é o DataLoader: ele junta todas as chaves pedidas no mesmo instante, faz uma consulta com IN e distribui as respostas. Antes de mudar qualquer coisa, eu confirmo no trace, porque ele mostra dezenas de consultas iguais em sequência, e depois de corrigir eu meço de novo. A conta é simples: cada viagem à rede custa alguns milissegundos, e cinquenta viagens somam um tempo que nenhuma otimização de CPU recupera.”
14.3 por dentro
Um banco de dados gasta uma parte do tempo com a ida e a volta da rede, independentemente do tamanho da consulta. Quando o código faz uma consulta por item, essa parte se repete a cada linha, e o total cresce com a lista. É por isso que o N+1 passa despercebido em teste com dez registros e aparece em produção com mil. O nome vem da contagem: uma consulta para a lista, mais uma para cada um dos N itens.
A figura compara o caminho ingênuo e o caminho em lote com a mesma requisição.
Na parte de cima, o ORM lê os posts e depois consulta o autor de cada um, em 50 viagens com a seta rosa e a fila de consultas em vermelho. Na parte de baixo, a API pede os posts uma vez e os autores com um único IN, em duas viagens. Os valores de 2 ms por viagem são um exemplo para comparar a ordem de grandeza, e não uma medição.
No ORM, o caso aparece com o carregamento lazy: o relacionamento só é buscado quando o código lê o campo. A aula JPA, Hibernate e N+1 mede isso num exemplo com três clientes e dez pedidos. O log mostrou 14 instruções: uma para os pedidos, três para os clientes, que são carregados junto por causa do @ManyToOne com EAGER (o padrão da API de persistência para esse tipo de relacionamento), e dez para os itens, uma por pedido. A mesma aula mostrou que um join fetch reduziu para 4 instruções, e um @EntityGraph com os itens e o cliente reduziu para uma só. Ela também registrou um detalhe que confunde: o join fetch devolveu 10 pedidos, e não 30 linhas, porque o Hibernate 6 filtra as repetições de entidades nesse caso.
Em GraphQL, o problema vem de outro lugar. Cada resolver de campo roda isoladamente, então o resolver de author de cada post é chamado uma vez e dispara uma consulta própria. O DataLoader muda essa ordem. A figura mostra o funcionamento.
Os resolvers de author chamam load(id) em sequência, mas o DataLoader não busca na hora: ele guarda os ids pedidos durante o mesmo tick de execução, que é o intervalo em que o GraphQL ainda está montando a resposta. No fim do tick, faz uma única consulta com IN (...) e devolve a cada resolver o resultado do seu id. O id 7 aparece duas vezes, mas entra na consulta uma só vez, porque o cache da requisição já o conhece. Duas regras importam: o DataLoader vive uma vez por requisição, para não vazar dados entre usuários, e o resultado precisa voltar na mesma ordem dos ids pedidos, senão cada resolver recebe o autor do outro.
Para confirmar que o problema existe, o trace é a ferramenta certa. Um é um intervalo de tempo com nome, e a árvore de spans de uma requisição mostra quanto cada etapa durou. A figura compara dois traces da mesma rota.
Na parte de cima, a requisição dura 104 ms, e a barra rosa aparece dezenas de vezes, uma por consulta igual, em fila. Na parte de baixo, a mesma rota tem uma consulta de posts e uma de autores com IN, e a barra verde fica curta. O formato de cascata mostra o problema que um total não mostra: o N+1 não é uma consulta lenta, é uma consulta rápida repetida demais. Os tempos da figura são ilustrativos. A aula Tracing Distribuído e OpenTelemetry mostra como gerar esses spans em Go, e a seção 15 volta a eles.
14.4 na AWS
No AppSync, a resolução de campos tem o mesmo problema e o mesmo remédio. Sem batching, cada item de uma lista chama a Lambda de resolução separadamente. Com batching, o resolver recebe uma lista e devolve uma lista na mesma ordem, e a figura mostra o exemplo da documentação com um cliente que pede a lista de posts e os relacionados de cada um.
Sem batching, a seta vermelha lenta mostra uma invocação por item, e a própria figura registra 31 invocações no exemplo, o que não reconferi hoje. Com batching, a Lambda recebe um lote e faz uma consulta com IN no RDS, ou o resolver APPSYNC_JS faz um BatchGetItem no DynamoDB com todas as chaves. O X-Ray, em roxo, recebe os traces, e é ele que torna visível a diferença entre as duas formas. O detalhe de como o AppSync agrupa as chamadas, e quais limites se aplicam, não foi conferido hoje.
14.5 dicas para a entrevista
Pergunte qual é a forma da consulta: uma lista com relacionamentos, ou um grafo de campos aninhados? Pergunte também se a API é pública e quanto a profundidade do GraphQL pode crescer, porque isso decide se o DataLoader basta ou se é preciso limitar a consulta. Diga os trade-offs: o JOIN resolve o N+1 do ORM, mas repete linhas e pode piorar a paginação; o IN com lote é mais previsível, mas exige montar a lista de ids; o DataLoader resolve no lado da API, mas não muda a consulta que o ORM faria por conta própria.
O que soa júnior é “uso eager loading em tudo”, sem olhar o custo, ou “resolvo com cache”, que esconde o problema na primeira requisição fria. Também é sinal de pouca prática dizer “o ORM resolve sozinho”, porque o ORM faz exatamente o que o código pediu, e o código é que pediu uma consulta por item. Se travar, peça o trace: “quantas consultas essa requisição fez, e quais se repetem?”.
14.6 follow-ups prováveis
Como detectar N+1 antes de produção? Num teste que conta as instruções executadas. A própria aula Kotlin usa a estatística de statements do Hibernate, com hibernate.generate_statistics ligado, e um teste que falha quando o número cresce com a entrada. Sem essa propriedade, o contador fica em zero, mesmo com as consultas acontecendo.
O JOIN fetch resolve tudo? Não. Com coleções, a paginação não pode ser aplicada no banco sem cuidado, e o Hibernate costuma avisar que faz a paginação em memória. A escolha entre JOIN, IN com lote e DataLoader depende da forma da consulta e da paginação, e isso precisa ser verificado no caso real (o aviso e o comportamento exato não foram conferidos hoje na documentação).
O DataLoader tem cache? Dentro da requisição, sim: a mesma chave não vai duas vezes ao banco. Entre requisições, não, e não deve ter, porque o cache de um usuário não pode vazar para outro.
15. observabilidade (golden signals e p99)
15.1 a pergunta e o que o entrevistador quer descobrir
A pergunta é: “Por que a média de tempo de resposta é uma métrica ruim para avaliar a saúde de uma API e o que você monitora?”. O entrevistador quer ver se você entende o que a média esconde, se conhece percentis e sabe que um alarme deve observar a cauda, se conhece os quatro sinais dourados e se tem um caminho do painel até a causa, com trace e profiling.
15.2 como responder em voz alta
“A média mistura todos os usuários num número só, e ele pode parecer saudável enquanto uma parte grande sofre. Se noventa e nove requisições levam 10 ms e uma leva 10 segundos, a média fica perto de 110 ms, e o painel parece bom, mas um em cada cem usuários esperou dez segundos. Por isso eu olho percentis: o p50 mostra a experiência típica, e o p99 mostra a cauda, os 1% mais lentos, que muitas vezes são os clientes que mais importam. Eu monitoro os quatro sinais dourados do SRE: latência, separando sucesso de erro, tráfego, erros e saturação dos recursos mais apertados. O alarme que acorda alguém eu ligo no p99 e na taxa de erro, não na média. Quando o painel acusa, o trace mostra onde o tempo foi gasto, e o profiling mostra o que a CPU fez. Na AWS, uso o CloudWatch para percentis e alarmes, o X-Ray para os traces, e o Application Signals para medir a latência de cada operação contra um objetivo.”
15.3 por dentro
A média é a soma dividida pelo número de amostras. Ela é sensível a valores extremos apenas na medida em que eles pesam na soma, e um único valor muito grande pode ser diluído por muitos valores pequenos. Quando a distribuição da latência é assimétrica, como costuma ser, a média descreve pouco da experiência real. Um responde outra pergunta: qual valor fica acima de uma fração das requisições. O p50 é a mediana, o valor que metade das requisições fica abaixo. O p95 e o p99 são as caudas. Um p99 de 3 segundos significa que 1% das requisições levou pelo menos isso.
A figura usa 100 requisições: 90 de 40 ms, 8 de 120 ms e 2 de 3 000 ms.
A média é 105,6 ms, um número que parece aceitável. O p50 é 40 ms, o que descreve bem a maioria. O p99 é 3 000 ms, e a nota rosa aponta as duas requisições que esperaram três segundos. Quem olha só a média concluiria que o serviço está bem, e quem olha só o p50 também. A cauda é o que dói, e por isso ela precisa de um número próprio.
O percentil também precisa de cuidado na hora de virar alarme. A figura seguinte compara dois alarmes sobre a mesma métrica de latência do ALB.
O alarme de média, em verde, continua em OK, porque 105,6 ms fica abaixo do limite de 500 ms. O alarme de p99, em rosa, dispara porque 3 000 ms passa do limite de 1 s e notifica o plantão pelo SNS. O painel parece calmo para quem olha a média, mas 2% dos usuários estão sofrendo, e só o segundo alarme percebe isso. A escolha do limite é outra conversa, que depende do que o negócio aceita.
Os quatro sinais dourados vêm do livro do SRE do Google. Latência é o tempo para atender uma requisição, e vale separar o tempo das requisições bem-sucedidas do tempo das que falharam, porque um erro rápido, como um 500 devolvido na hora, puxa a média para baixo e esconde o problema. Tráfego é a demanda, em requisições por segundo. Erros é a taxa de falhas, incluindo respostas com conteúdo errado que não geram código de erro. Saturação mostra quão perto de esgotar está o recurso mais apertado, que pode ser CPU, memória, pool de conexões ou IOPS de disco.
O painel mostra o sintoma e não a causa. Quando o p99 sobe, o próximo passo é um trace de uma requisição lenta, que mostra em que etapa o tempo foi gasto, como uma consulta repetida da seção 14. Se o trace aponta para um trecho de CPU, o profiling diz qual método consome o tempo, e a aula Profiling mostra as ferramentas gerais. Para montar esse caminho, as aulas Observabilidade com Micrometer e OpenTelemetry (Kotlin) e Tracing Distribuído e OpenTelemetry (Go) cobrem métricas, traces e logs com correlação. A nota SRE está no vault, mas o arquivo está vazio, então não a usei como fonte.
15.4 na AWS
O CloudWatch recebe métricas com estatísticas de percentil, como o p99 do TargetResponseTime do ALB, e usa essas estatísticas em alarmes. Para investigar logs, o Logs Insights permite consultar campos e agregar por janela de tempo. O X-Ray guarda os traces das requisições, e o Application Signals agrupa latência e disponibilidade por operação, com objetivos definidos. Para quem já usa Prometheus, o Managed Prometheus recebe métricas por remote write e aceita consultas em PromQL, e o Managed Grafana monta os painéis.
Cada sinal da esquerda tem a métrica que o representa na AWS, e a figura mostra que o nome muda de serviço para serviço. A latência do ALB é o TargetResponseTime, que no API Gateway vira Latency e na Lambda, Duration. Os erros do ALB são o HTTPCode_Target_5XX_Count, e o alarme de erro dispara a notificação pelo SNS. A saturação não tem uma métrica única: ela é CPU, memória, conexões do Aurora ou throttles do DynamoDB, dependendo do recurso. O Application Signals e o X-Ray ficam na coluna da direita, porque respondem à pergunta seguinte: qual operação está lenta e por quê.
Os nomes das métricas por serviço vêm da figura e não foram conferidos hoje contra a documentação. A lista de sinais e a relação com alarmes são do que sei, e a existência de percentis em métricas do CloudWatch também precisa de confirmação na versão atual.
15.5 dicas para a entrevista
Pergunte que objetivo de latência e de disponibilidade o negócio tem, porque isso define o percentil e o limite do alarme. Pergunte se a API tem clientes com volumes muito diferentes, porque um p99 global pode esconder um cliente grande que sofre. Diga os trade-offs: percentis custam mais para calcular e agregar do que médias, e um percentil muito alto precisa de amostras suficientes para ser estável; alarmes muito sensíveis geram ruído, e alarmes frouxos chegam tarde.
O que soa júnior é “monitoro a média e o CPU”, que mostra só o lado da infraestrutura. Também é sinal de pouca prática “o p99 é sempre o certo”, porque depende do que o usuário percebe e do SLO. Outra frase que denuncia falta de tempo com produção é “olho o painel quando o cliente reclama”, já que o alarme deveria chegar antes. Se travar, volte às duas perguntas: quem está sofrendo, e onde o tempo foi gasto?
15.6 follow-ups prováveis
Por que separar a latência de sucesso da latência de erro? Porque um erro costuma ser rápido, e misturar os dois faz o p99 parecer melhor do que a experiência real. Um serviço que falha depressa em 30% das requisições pode ter uma latência geral bonita e uma taxa de erro ruim.
Como escolher o limite do alarme de p99? Pelo objetivo do serviço e pela medição histórica, e não por um número redondo. Um limite que o serviço ultrapassa todo dia gera ruído e ensina a equipe a ignorar o alarme. Uma janela de avaliação com mais de um ponto consecutivo reduz picos isolados.
Um p99 global esconde um cliente específico? Pode esconder. Se um cliente grande concentra o tráfego lento, a média e o p99 do conjunto podem parecer aceitáveis. Quando isso acontece, o recorte por cliente ou por operação, feito com traces ou com o Application Signals, mostra o caso.
correções ao texto do Gemini
Perguntas 1 a 5
- Termos misturados (pergunta 2). O Gemini usou stampede, thundering herd, avalanche e penetration como se fossem partes de um mesmo problema. Stampede e thundering herd são o mesmo fenômeno: uma chave quente expira e muitas requisições recalculam ao mesmo tempo. Avalanche são muitas chaves expirando juntas, geralmente porque foram carregadas com o mesmo TTL. Penetration são consultas a chaves que não existem, de modo que o cache nunca preenche e cada pedido vai ao banco.
- Particionamento vertical (pergunta 3). O Gemini chamou de particionamento vertical a separação de tabelas em bancos distintos por domínio. O nome correto para isso é particionamento funcional, ou divisão por domínio. Particionamento vertical, no sentido estrito, divide as colunas de uma tabela em duas, com as colunas pesadas e raramente lidas numa tabela à parte.
- Latência do DynamoDB (pergunta 4). O Gemini escreveu que DynamoDB e Cassandra trocam consistência por latência “ultrabaixa (< 5 ms)”. A documentação do DynamoDB fala em milissegundos de um dígito na maior parte dos casos, e em microssegundos apenas com o DAX. Não convém prometer um valor fixo.
Perguntas 6 a 10
Revisei as perguntas 6 a 10 do arquivo System Design - Perguntas Fundamentais e Conceituais para Seniores em 2026-10-22, sem consultar a documentação de novo. Os pontos abaixo são o que notei ao comparar o texto com o que esta nota explica. Quando um ponto depende de um valor padrão ou de um nome exato, ele está na seção de verificação pendente.
- Pergunta 6, fórmula do full jitter: a fórmula
random(0, base × 2^tentativa)está sem o teto (cap). Na prática, o valor da base multiplicada pelo expoente é limitado a um máximo antes do sorteio, senão a espera cresce sem limite. - Pergunta 7, passo 4: o texto manda liberar a trava depois de salvar a resposta. Isso contradiz o objetivo da idempotência. A chave precisa ficar guardada junto com a resposta, para que um retry tardio receba o mesmo resultado, e só expira depois de um prazo de retenção.
- Pergunta 7, outbox: o texto diz que o CDC “lê a tabela”. O CDC lê o log de transações do banco. Quem lê a tabela é o relay por polling.
- Pergunta 8: “latência zero” na validação do JWT é exagero. A verificação custa CPU para checar a assinatura, uma operação pequena, mas não nula. A frase “impossível revogar” também precisa de ressalva: sem estado centralizado não há revogação imediata, mas uma denylist de jti reintroduz esse estado e faz a revogação funcionar.
- Pergunta 9, tabela: a linha do L4 fala em ausência de “terminação SSL complexa” e a do L7 diz que ele precisa descriptografar. O NLB também pode terminar TLS. A diferença real é que o L4 não lê o conteúdo HTTP.
- Pergunta 9, algoritmos: “Least Connections” e “IP Hash” não são as opções que o ALB oferece. O ALB usa round robin como padrão e tem least outstanding requests como opção, e a afinidade é feita por cookie. Além disso, dizer que sticky session “quebra o auto-scaling” exagera: o efeito real é desequilibrar a carga e dificultar a retirada de instâncias.
- Pergunta 10: “LRU é a política padrão mais eficiente” depende do padrão de acesso e não deveria ser apresentado como regra. O texto também omite que o Redis usa LRU aproximado por amostragem, o que muda a resposta quando o entrevistador pergunta sobre o Redis.
Perguntas 11 a 15
Revisei as perguntas 11 a 15 do arquivo System Design - Perguntas Fundamentais e Conceituais para Seniores em 2026-10-22, sem consultar a documentação de novo. Os pontos abaixo vêm da comparação com o que esta nota explica. O que depende de um valor ou de um comportamento atual está marcado como não verificado hoje.
- Pergunta 11, tabela do WebSocket: a frase “sem overhead de headers HTTP” é exagero. O handshake inicial tem cabeçalhos, e depois dele cada quadro carrega pouco cabeçalho. A diferença real é o custo por mensagem depois da conexão aberta (não verificado hoje, do que sei).
- Pergunta 12, rolling update e blue/green: a frase “se falhar, reverte na hora” vale só se o esquema do banco aceitar as duas versões. Com o banco compartilhado, reverter o código não desfaz uma mudança de esquema já aplicada.
- Pergunta 12, “a versão antiga quebrará instantaneamente”: na prática ela quebra na primeira consulta que usa o nome antigo, e durante um rolling update isso é uma janela de falhas, não um instante (do que sei, não verificado hoje).
- Pergunta 13, números e exemplos: “centenas de milhares de requisições por segundo” e “volume de petabytes” aparecem sem fonte nem condição de carga. Esses valores dependem do caso e não deveriam ser citados como regra (não verificado hoje).
- Pergunta 13, categoria colunar e série temporal: Cassandra é um banco de colunas largas, orientado a escrita e chave de partição, e não é o mesmo tipo de armazenamento colunar analítico do ClickHouse. Juntar os dois na mesma linha confunde a escolha (do que sei, não verificado hoje).
- Pergunta 14, eager loading: o texto cita apenas o JOIN explícito e omite que o padrão do
@ManyToOnena API de persistência éEAGER, o que gera consultas extras mesmo sem o código pedir o relacionamento. A aula JPA, Hibernate e N+1 mediu isso. - Pergunta 15, média do exemplo: com 99 usuários a 10 ms e 1 a 10 000 ms, a média é (99 × 10 + 10 000) / 100 = 109,9 ms. O texto diz “aproximadamente 109 ms”, e o valor mais correto é cerca de 110 ms.
- Pergunta 15, “clientes p99 costumam ser os usuários mais ativos”: a afirmação aparece como fato sem fonte. Pode acontecer em alguns produtos, e merece ser apresentada como hipótese a verificar com os dados do próprio sistema (não verificado hoje).
o que não foi verificado
Perguntas 1 a 5
Os pontos abaixo a nota marca como incertos, de memória ou não reconferidos. Confira antes de usá-los numa entrevista.
- Fórmula do XFetch (seção 2): descrita de memória. O artigo de Vattani, Chierichetti e Lowenstein (2015) é a fonte para os detalhes.
- Aurora (seção 3): a descrição clássica de seis cópias dos dados, duas por zona, não foi reconfirmada.
- Aurora (seção 3): o limite de armazenamento aparece como 128 TiB na página de cotas e como 256 TiB em outra página, para versões específicas. Confira o limite da versão que você usa.
- Aurora Limitless (seção 3): existe e está documentado, mas as versões e regiões suportadas não foram conferidas.
- S3 (seção 4): a consistência forte de leitura após escrita, em vigor desde dezembro de 2020, não foi reconferida.
- Lambda (seção 5): o nome exato do recurso de resposta parcial de lote não foi reverificado.
- SQS FIFO em alta vazão (seção 5): a documentação diverge. A página de tipos de fila cita até 30.000 transações por segundo, e a página de cotas lista até 70.000 em Virgínia, Oregon e Irlanda, com 2.400 como padrão em outras regiões. A nota usa a página de cotas, por ser mais específica, e pede que você confirme a região.
- MRSC (seção 4): a data de lançamento não foi verificada.
- PACELC (seção 4): a classificação de produtos vem da literatura, não de garantia da AWS, e muitos produtos oferecem os dois comportamentos por configuração.
- EventBridge (seção 5): a nota não sabe qual dos dois barramentos (Custom Event Bus ou Custom Event Bus Classic) o seu time usa. Confirme antes de falar de replay e ordem.
Valores de limites e preços mudam com o tempo. Os serviços citados foram conferidos na documentação em 2026-10-22, mas quem for usar um número numa decisão real deve checar a página de cotas do momento.
Perguntas 6 a 10
Esta revisão não consultou fontes novas, e os pontos abaixo ficam em aberto. Os itens são de 2026-10-22.
- Status do AWS App Mesh. A página que consultei na seção 6 anunciava o fim de suporte para 30 de setembro de 2026, data que já passou. Não confirmei o estado atual nem o caminho oficial de migração. Trate o App Mesh como descontinuado e confira o console e a documentação antes de citá-lo.
- Valores padrão da Cognito (1 hora para o access token e 30 dias para o refresh) e se a rotação do refresh token a cada uso está disponível e como se ativa.
- Algoritmo least outstanding requests no ALB, formato dos cookies de afinidade e a versão de protocolo gRPC no target group.
- Política padrão do ElastiCache (volatile-lru) e a reserva de 25% de memória mostrada no diagrama da seção 10.
- Valores padrão de maxmemory-samples (5) e de lfu-log-factor (10), além do intervalo de decaimento do LFU.
- Comportamento do cache de JWKS no JWT authorizer gerenciado.
- A nota JWT, que ainda não tem conteúdo além do frontmatter.
Perguntas 11 a 15
Esta revisão não consultou fontes novas, e os pontos abaixo ficam em aberto. Os itens são de 2026-10-22.
- Documentação atual do CodeDeploy para ECS e Lambda (estratégias blue/green e canary, nomes das configurações de tráfego).
- Pesos de target groups no ALB e o comportamento do TTL do Route 53 na troca ponderada.
- Mecanismo exato do blue/green do Aurora e a afirmação de que a replicação leva linhas e não DDL, que é leitura da figura 08.
- Número de réplicas de leitura do Aurora e o volume de seis cópias em três zonas de disponibilidade, citados nas figuras 10 e 13 e não conferidos.
- Nomes e categorias de serviços da figura 11, em especial a oferta de série temporal indicada como Timestream for InfluxDB.
- Consistência padrão das leituras do DynamoDB e a existência do GSI com a forma usada na figura 09.
- Valor de 31 invocações no exemplo do AppSync (figura 15) e a forma como o AppSync agrupa as chamadas em lote.
- Aviso do Hibernate sobre paginação em memória com coleções no fetch join.
- Nomes e existência de métricas por serviço usados na figura 18, e a estatística de percentil nas métricas do CloudWatch na versão atual.
- Nome exato e conteúdo do artigo de Martin Fowler sobre Parallel Change.
- A nota SRE está vazia (zero bytes), então a sua orientação sobre sinais dourados não foi usada. Os quatro sinais vêm do livro do SRE do Google, de conhecimento de base, e precisam de conferência na fonte.
glossário
-
cache-aside: padrão em que a aplicação consulta o cache, vai ao banco em caso de miss e grava o resultado no cache.
-
write-through: escrita que passa pelo cache e só responde depois de gravar no banco.
-
write-behind: escrita que fica no cache e chega ao banco depois, em lote. É rápida, mas perde dados se o nó cair antes da persistência.
-
TTL: tempo de vida de uma chave no cache. Vencido o prazo, a chave expira.
-
cache stampede: muitas requisições recalculam ao mesmo tempo uma chave quente que acabou de expirar. Também chamado thundering herd.
-
cache avalanche: muitas chaves expiram juntas, geralmente por terem sido carregadas com o mesmo TTL.
-
cache penetration: consultas a chaves que não existem, de modo que o cache nunca preenche e cada pedido chega ao banco.
-
XFetch: renovação antecipada probabilística de chaves, em que quem lê pode renovar a chave antes do vencimento.
-
replication lag: atraso entre a escrita no primário e a sua chegada à réplica.
-
sharding: divisão dos dados entre servidores diferentes, cada um capaz de receber escritas.
-
CAP: teorema que diz que, durante uma partição de rede, um sistema distribuído precisa escolher entre responder sempre (A) e devolver a escrita mais recente (C).
-
PACELC: extensão do CAP que cobre também o caso sem partição, em que a escolha é entre latência e consistência.
-
dead-letter queue: fila separada onde vão as mensagens que falharam repetidamente.
-
Access token: credencial curta, geralmente um JWT, que o cliente envia em cada chamada.
-
Refresh token: credencial longa, opaca e guardada no servidor, usada só para obter novos access tokens.
-
Claim: campo do payload de um JWT, como sub, exp ou aud.
-
JWKS: conjunto de chaves públicas que um emissor publica para que os serviços verifiquem assinaturas.
-
Denylist: lista de identificadores revogados que o verificador consulta antes de aceitar um token.
-
jti: identificador único de um JWT, usado para revogar um token específico.
-
L4 e L7: balanceador que decide por conexão de transporte (L4) ou por requisição HTTP (L7).
-
Target group: conjunto de alvos de um balanceador da AWS, como instâncias, IPs ou funções Lambda.
-
Sticky session: afinidade que prende um cliente ao mesmo alvo por um período.
-
Round robin: distribuição em rodízio, um alvo de cada vez.
-
LOR: least outstanding requests, que envia a requisição ao alvo com menos trabalho em andamento.
-
Eviction: remoção de itens do cache por pressão de memória, e não por tempo.
-
LRU: política que descarta o item usado há mais tempo.
-
LFU: política que descarta o item com menos acessos acumulados.
-
maxmemory-policy: diretiva do Redis que escolhe as candidatas e o critério de despejo.
-
OOM: erro de falta de memória, que o Redis devolve quando não consegue aceitar uma escrita.
-
Rolling update: troca as instâncias de uma aplicação aos poucos, com versões antigas e novas convivendo durante a troca.
-
Blue/green: dois ambientes completos, um ativo e outro em espera, com a troca de tráfego entre eles.
-
Canary: envio de uma fração do tráfego para a versão nova, com a fatia aumentada se as métricas se mantiverem saudáveis.
-
Expand and contract: migração de esquema em fases, que adiciona antes de remover, para que as versões convivam.
-
Dual write: a aplicação grava nas colunas antiga e nova durante a migração.
-
Backfill: cópia dos dados antigos para a estrutura nova, feita em lotes.
-
Chave de partição: campo que decide em qual partição um item é guardado em um banco distribuído.
-
GSI: índice secundário global, com outra chave de partição, para consultar por campos que não são a chave principal.
-
N+1: padrão em que uma consulta de lista é seguida de uma consulta por item.
-
Lazy loading: carregamento de um relacionamento só quando o código o lê pela primeira vez.
-
DataLoader: componente que agrupa as chaves pedidas no mesmo tick e faz uma consulta em lote.
-
Span: intervalo de tempo com nome dentro de um trace, que representa uma etapa da requisição.
-
Percentil: valor abaixo do qual fica uma fração das amostras. O p99 é o valor que 99% das requisições não ultrapassam.
-
Golden signals: latência, tráfego, erros e saturação, os quatro sinais básicos para monitorar um serviço.
fontes
Perguntas 1 a 5
A nota cita documentos pelo nome, sem URLs no corpo. Consultados em 2026-10-22, salvo onde a nota diz o contrário.
- Documentação do Amazon ElastiCache: estratégias de cache (cache-aside, read-through, write-through, write-behind, write-around) e recomendação de TTL.
- Documentação do DAX (DynamoDB Accelerator): comportamento write-through, cache negativo, casos de uso e limites.
- Documentação do Amazon CloudFront: request collapsing e Origin Shield.
- Anúncio da AWS sobre o filtro de Bloom no Valkey 8.1 (ElastiCache).
- Documentação do Amazon RDS (réplicas de leitura, Multi-AZ, cotas e RDS Proxy) e do Amazon Aurora (cotas, cluster e Aurora Limitless).
- Documentação do PostgreSQL sobre replicação, WAL e atraso de réplica.
- Documentação do DynamoDB sobre consistência de leitura e sobre global tables nos modos MREC e MRSC.
- Documentação do Amazon SQS (tipos de fila, cotas, DLQ e visibility timeout), do Amazon SNS (retentativas e assinantes FIFO) e do Amazon EventBridge (Custom Event Bus).
- Artigos: Vattani, Chierichetti e Lowenstein, “Optimal Probabilistic Cache Stampede Prevention” (2015), para o XFetch; Eric Brewer (2000) e Gilbert e Lynch (2002), para o CAP; Daniel Abadi (2012), para o PACELC; e o artigo do Dynamo, para vector clocks e o carrinho de compras.
Perguntas 6 a 10
Não houve consulta nova nesta revisão, em 2026-10-22. Os conceitos das seções 8 a 10 vêm do conhecimento de base desta nota, e os documentos abaixo são os que convém conferir antes de citar um valor.
- RFC 7519, JSON Web Token (JWT).
- RFC 9068, JWT Profile for OAuth 2.0 Access Tokens.
- RFC 9700, Best Current Practice for OAuth 2.0 Security, para a rotação e a detecção de reúso de refresh tokens.
- Documentação do Amazon Cognito, seções de tokens e de JWKS.
- Documentação do Amazon API Gateway, seções de JWT authorizer e Lambda authorizer.
- Documentação do Elastic Load Balancing, para ALB, NLB e Gateway Load Balancer.
- Documentação do Amazon ElastiCache, para parâmetros de eviction e métricas.
- Documentação do Redis sobre key eviction, para maxmemory-policy e maxmemory-samples.
Perguntas 11 a 15
Não houve consulta nova nesta revisão, em 2026-10-22. Os conceitos desta seção vêm do conhecimento de base e das aulas do vault citadas no texto. Os documentos abaixo são os que convém conferir antes de citar um valor ou um nome.
- Livro Site Reliability Engineering, do Google, capítulo sobre monitoramento de sistemas distribuídos, para os quatro sinais dourados.
- Martin Fowler, artigo sobre Parallel Change, para expand and contract.
- Documentação do AWS CodeDeploy, para blue/green e canary em ECS e Lambda.
- Documentação do Elastic Load Balancing, para target groups ponderados no ALB.
- Documentação do Amazon Route 53, para roteamento ponderado e TTL.
- Documentação do Amazon Aurora e do Amazon RDS, para blue/green deployments.
- Documentação do Amazon DynamoDB, para chave de partição, índices secundários globais e leituras consistentes.
- Documentação do AWS AppSync, para batching de resolvers.
- Documentação do AWS X-Ray, do Amazon CloudWatch, do Application Signals e do Managed Prometheus e Managed Grafana.
- Guia de migração do Hibernate 6 e documentação do Spring Data, citados na aula JPA, Hibernate e N+1.