Pular para o conteúdo

Neste texto

  1. O Framework de Resposta em 45 Minutos
  2. As 12 Principais Perguntas de System Design para Nível Sênior
  3. 1. Design de um Rate Limiter Distribuído
  4. 2. Design de um Chat em Tempo Real com Mensagens Offline (WhatsApp / Discord)
  5. 3. Design de um Feed de Notícias / Linha do Tempo (Twitter / Meta)
  6. 4. Design de Processamento de Pagamentos e Ledger Financeiro (Stripe / PayPal)
  7. 5. Design de uma Plataforma Global de Streaming de Vídeo (YouTube / Netflix)
  8. 6. Design de um Banco de Dados Chave-Valor Distribuído (Estilo DynamoDB / Cassandra)
  9. 7. Design de Sincronização e Armazenamento de Arquivos em Nuvem (Google Drive / Dropbox)
  10. 8. Design de Venda de Ingressos Concorrentes / Flash Sale (Ticketmaster / Amazon Prime Day)
  11. 9. Design de um Encurtador de URLs Global (TinyURL / Bitly)
  12. 10. Design de um Web Crawler Distribuído em Larga Escala (Googlebot)
  13. 11. Design de Infraestrutura e Gateway de Inferência para LLMs (OpenAI / Anthropic API)
  14. 12. Design de uma Plataforma de Métricas e Observabilidade Global (Datadog / Prometheus)
  15. O que Separa um Candidato Pleno de um Sênior?
  16. respostas
  17. 1 rate limiter distribuído
  18. 1.1 a pergunta e o que ela mede
  19. 1.2 como responder em voz alta
  20. 1.3 por dentro: como funciona
  21. 1.4 mapeamento na AWS
  22. 1.5 dicas para a entrevista
  23. 1.6 follow-ups prováveis
  24. 2 chat em tempo real com mensagens offline
  25. 2.1 a pergunta e o que ela mede
  26. 2.2 como responder em voz alta
  27. 2.3 por dentro: como funciona
  28. 2.4 mapeamento na AWS
  29. 2.5 dicas para a entrevista
  30. 2.6 follow-ups prováveis
  31. 3 feed de notícias
  32. 3.1 a pergunta e o que ela mede
  33. 3.2 como responder em voz alta
  34. 3.3 por dentro: como funciona
  35. 3.4 mapeamento na AWS
  36. 3.5 dicas para a entrevista
  37. 3.6 follow-ups prováveis
  38. 4 pagamentos e ledger financeiro
  39. 4.1 a pergunta e o que ela mede
  40. 4.2 como responder em voz alta
  41. 4.3 por dentro: como funciona
  42. 4.4 mapeamento na AWS
  43. 4.5 dicas para a entrevista
  44. 4.6 follow-ups prováveis
  45. 5 streaming de vídeo global
  46. 5.1 a pergunta e o que ela mede
  47. 5.2 roteiro para falar
  48. 5.3 por dentro: como funciona
  49. 5.4 mapeamento na AWS
  50. 5.5 dicas de entrevista
  51. 5.6 follow-ups
  52. 6 banco chave-valor distribuído
  53. 6.1 a pergunta e o que ela mede
  54. 6.2 roteiro para falar
  55. 6.3 por dentro: como funciona
  56. 6.4 mapeamento na AWS: o DynamoDB resolve cada peça
  57. 6.5 dicas de entrevista
  58. 6.6 follow-ups
  59. 7 sincronização de arquivos tipo Drive e Dropbox
  60. 7.1 a pergunta e o que ela mede
  61. 7.2 roteiro para falar
  62. 7.3 por dentro: como funciona
  63. 7.4 mapeamento na AWS
  64. 7.5 dicas de entrevista
  65. 7.6 follow-ups
  66. 8 venda de ingressos e flash sale
  67. 8.1 a pergunta e o que ela mede
  68. 8.2 roteiro para falar
  69. 8.3 por dentro: como funciona
  70. 8.4 mapeamento na AWS
  71. 8.5 dicas de entrevista
  72. 8.6 follow-ups
  73. 9 encurtador de URLs global
  74. 9.1 a pergunta e o que ela mede
  75. 9.2 roteiro para falar
  76. 9.3 por dentro: como funciona
  77. 9.4 mapeamento na AWS
  78. 9.5 dicas de entrevista
  79. 9.6 follow-ups
  80. 10 web crawler distribuído
  81. 10.1 a pergunta e o que ela mede
  82. 10.2 roteiro para falar
  83. 10.3 por dentro: como funciona
  84. 10.4 mapeamento na AWS
  85. 10.5 dicas de entrevista
  86. 10.6 follow-ups
  87. 11 gateway de inferência para LLMs
  88. 11.1 a pergunta e o que ela mede
  89. 11.2 roteiro para falar
  90. 11.3 por dentro: como funciona
  91. 11.4 mapeamento na AWS
  92. 11.5 dicas de entrevista
  93. 11.6 follow-ups
  94. 12 plataforma de métricas e observabilidade
  95. 12.1 a pergunta e o que ela mede
  96. 12.2 roteiro para falar
  97. 12.3 por dentro: como funciona
  98. 12.4 mapeamento na AWS
  99. 12.5 dicas de entrevista
  100. 12.6 follow-ups
  101. correções ao texto do Gemini
  102. Perguntas 1 a 4
  103. Perguntas 5 a 8
  104. Perguntas 9 a 12
  105. glossário
  106. fontes
  107. Perguntas 1 a 4
  108. Perguntas 5 a 8
  109. Perguntas 9 a 12

Entrevistas de system design · parte 2 de 3

System design sênior: 12 perguntas complexas com respostas

avançadoPublicada em · 162 min de leitura

Tamanho da fonte:
Largura da coluna:

Em entrevistas para posições de Engenharia de Software Sênior e Especialista, os recrutadores e avaliadores de Big Techs (FAANG) e unicórnios não estão procurando apenas um desenho genérico de caixas com um balanceador de carga e um banco de dados.

O objetivo da entrevista de System Design é avaliar:

  1. Maturidade diante de ambiguidades: Como você faz perguntas clarificadoras e delimita requisitos funcionais e não-funcionais (SLA, p99, disponibilidade de 99.99%).
  2. Justificativa técnica de Trade-offs: Escolhas conscientes baseadas no Teorema CAP, consistência eventual vs. transações ACID, push vs. pull, latência vs. custo.
  3. Identificação e resolução de gargalos reais: Efeito manada (Thundering Herd), chaves quentes (Hot Keys / Celebrity problem), réplicas defasadas e particionamento.
  4. Resiliência operacional e custos: Failure modes (o que quebra quando a rede engargala ou um nó cai), , degradação graciosa e eficiência financeira na nuvem.

💡 Nota: Para perguntas conceituais mais diretas e menos complexas (estratégias de cache, Teorema CAP/PACELC, idempotência, L4 vs L7, resiliência e deploys sem downtime), consulte o guia complementar: System Design - Perguntas Fundamentais e Conceituais para Seniores.


O Framework de Resposta em 45 Minutos

Para conduzir a entrevista no nível Sênior, utilize um roteiro estruturado (como o framework RADIO):

1. Clarificação e Escopo (5 min)  ──>  2. Estimativas de Escala (3 min)
               │
               ▼
3. Desenho de Alto Nível (10 min)  ──> 4. Deep Dives nos Gargalos (20 min)
               │
               ▼
5. Trade-offs, Falhas e Custos (7 min)
  1. Requisitos Funcionais & Não-Funcionais: QPS de pico, taxa de leitura/escrita, latência alvo (ex.: p99 < 50ms) e garantias de durabilidade.
  2. Estimativas Back-of-the-Envelope: Volume de dados gerado por dia/ano, largura de banda (Gbps) e dimensionamento de memória para cache.
  3. High-Level Design: Topologia básica de clientes, API Gateway, serviços, barramentos assíncronos e bancos de dados.
  4. Deep Dive Arquitetural: Foco nas 2 ou 3 partes mais complexas do problema (onde a maioria dos candidatos falha).
  5. Failure Modes & Cost Optimization: Single Points of Failure (SPOF), estratégias de rollback, circuit breakers e tiering de custos.

As 12 Principais Perguntas de System Design para Nível Sênior

1. Design de um Rate Limiter Distribuído

Empresas que mais perguntam: Stripe, Uber, Cloudflare, AWS.

  • O Problema: Projete um limitador de taxa capaz de controlar milhões de requisições por segundo por usuário/IP/API Key em múltiplos data centers.
  • O que os recrutadores avaliam no nível Sênior:
    • Qual algoritmo você escolhe e por quê: (permite bursts), Leaky Bucket (vazão uniforme) ou Sliding Window Counter (combate picos no limite da janela).
    • Como lidar com condições de corrida (Race Conditions) no Redis usando scripts Lua atômicos.
    • Sincronização entre múltiplas regiões sem explodir a latência (Rate Limiting local na borda com sincronização assíncrona eventual).
    • Failure Mode: O que acontece se o cluster de Redis cair? (Estratégia Fail-Open para não derrubar o produto vs. Fail-Closed para APIs críticas de billing).
  • Mapeamento AWS: Amazon API Gateway Usage Plans + AWS WAF Rate-based rules + Amazon ElastiCache (Redis) com scripts Lua.
  • Serviços AWS Relacionados: AWS, EC2, Lambda.

2. Design de um Chat em Tempo Real com Mensagens Offline (WhatsApp / Discord)

Empresas que mais perguntam: Meta, Discord, Slack.

  • O Problema: Projete uma plataforma de mensagens instantâneas 1:1 e em grupo com suporte a confirmação de entrega (Delivery/Read receipts) e mensagens enviadas enquanto o destinatário estava offline.
  • O que os recrutadores avaliam no nível Sênior:
    • Gerenciamento de conexões persistentes (WebSockets) em clusters distribuídos e o papel de um Connection Manager com tabela de sessões ativas.
    • Garantia de ordem das mensagens: Geração de IDs distribuídos estritamente monotônicos e ordenados no tempo (ex.: Twitter Snowflake).
    • Como entregar mensagens para usuários offline sem sobrecarregar memória (Filas transitórias no Amazon SQS/Kafka e persistência em banco colunar/NoSQL).
    • Escala em chats com muitos participantes (50 mil pessoas em um canal do Discord): Transição de envio P2P para arquitetura Pub/Sub .
  • Mapeamento AWS: Amazon API Gateway APIs + Lambda + Amazon DynamoDB + Amazon ElastiCache + Amazon SQS/Kinesis.

3. Design de um Feed de Notícias / Linha do Tempo (Twitter / Meta)

Empresas que mais perguntam: Meta, X (Twitter), LinkedIn, TikTok.

  • O Problema: Projete a linha do tempo onde usuários postam mensagens e seguidores visualizam os tweets mais recentes e relevantes de quem seguem.
  • O que os recrutadores avaliam no nível Sênior:
    • Fan-out-on-Write (Push): Na publicação, insere o post na timeline em cache de cada seguidor. Excelente para leitura rápida ($O(1)$), péssimo para usuários com milhões de seguidores.
    • Fan-out-on-Read (Pull): No momento da leitura, busca os posts de todos os seguidos e ordena na hora. Economiza escrita, mas explode a latência de leitura ($O(N)$).
    • A Solução Híbrida do Sênior: Push para usuários comuns (99%) e Pull sob demanda para celebridades (Hot Accounts com > 1M de seguidores), mesclando as timelines no momento da consulta.
    • Estratégias de cache multinível para timeline (Redis list com tamanho fixo, ex.: últimos 800 posts).
  • Mapeamento AWS: Amazon CloudFront + Application Load Balancer + EC2 / ECS / EKS + Amazon ElastiCache (Redis Cluster) + Amazon Aurora / DynamoDB.

4. Design de Processamento de Pagamentos e Ledger Financeiro (Stripe / PayPal)

Empresas que mais perguntam: Stripe, Uber, Nubank, PayPal, Mercado Livre.

  • O Problema: Projete uma plataforma de pagamento que interage com adquirentes e bancos externos, garantindo que nenhum usuário seja cobrado em duplicidade e que todo centavo seja rastreado.
  • O que os recrutadores avaliam no nível Sênior:
    • Idempotência absoluta: Uso de chaves de idempotência (Idempotency Keys) no cabeçalho HTTP, bloqueios distribuídos temporários (distributed locks) e registro de estado no banco para evitar duplo débito em caso de timeout de rede.
    • Contabilidade por Partidas Dobradas (Double-Entry Bookkeeping): Cada transação precisa ter débitos e créditos com saldo zero; tabelas de transação com modelo Append-Only (sem updates em valores históricos).
    • Padrão vs. Two-Phase Commit (2PC): Coordenação de transações distribuídas assíncronas com etapas compensatórias em caso de rejeição bancária.
    • Reconciliação Bancária: Jobs em lote para conciliar os logs da aplicação com os arquivos de retorno bancário (D+1).
  • Mapeamento AWS: Amazon API Gateway + AWS Step Functions (Saga Pattern) + Amazon RDS / Aurora PostgreSQL (ACID) + Amazon SQS FIFO (ordenação estrita e deduplicação).

5. Design de uma Plataforma Global de Streaming de Vídeo (YouTube / Netflix)

Empresas que mais perguntam: Netflix, Google/YouTube, Amazon Prime Video.

  • O Problema: Projete um sistema onde criadores sobem vídeos de vários gigabytes e milhões de usuários assistem com streaming fluido em diferentes dispositivos e resoluções.
  • O que os recrutadores avaliam no nível Sênior:
    • Pipeline de Transcodificação Assíncrona: Fatiamento () do vídeo em blocos de 2 a 10 segundos, convertendo paralelamente em múltiplos formatos e resoluções (1080p, 720p, 480p) via HLS/DASH ( Streaming).
    • CDN e Tiering de Armazenamento: Uso de CDNs de borda para vídeos populares (80% do tráfego vem de 20% dos vídeos mais recentes) e transição para classes frias de armazenamento para vídeos com poucas visualizações.
    • Armazenamento de metadados (títulos, comentários, curtidas) separado do armazenamento de blobs pesados de mídia.
  • Mapeamento AWS: Amazon S3 (Upload via Pre-signed URLs) + AWS Elemental MediaConvert / Lambda / EC2 para transcodificação + Amazon S3 Glacier (Cold storage) + Amazon CloudFront (CDN global).

6. Design de um Banco de Dados Chave-Valor Distribuído (Estilo DynamoDB / Cassandra)

Empresas que mais perguntam: Amazon, Apple, Google, Microsoft.

  • O Problema: Projete um storage chave-valor distribuído que escale horizontalmente para petabytes de dados mantendo latências de leitura e escrita abaixo de 10ms.
  • O que os recrutadores avaliam no nível Sênior:
    • Consistent Hashing com Nós Virtuais: Distribuição uniforme de chaves entre servidores físicos sem precisar remapear todo o cluster ao adicionar ou remover servidores.
    • Teorema CAP e Tunable Consistency: Fórmulas de Quorum ($R + W > N$ para consistência forte; onde $N$ = fator de replicação, $R$ = leituras mínimas, $W$ = escritas mínimas).
    • Detecção de partições e conflitos: Uso de Vector Clocks, Read Repair, Gossip Protocol para monitoramento de nós saudáveis e árvores de Merkle Trees para sincronização de dados divergentes.
    • Estrutura de armazenamento local no disco do nó: LSM-Trees e SSTables com Bloom Filters para escritas ultrarrápidas.

7. Design de Sincronização e Armazenamento de Arquivos em Nuvem (Google Drive / Dropbox)

Empresas que mais perguntam: Google, Dropbox, Microsoft.

  • O Problema: Projete um sistema de sincronização de arquivos para desktop e mobile com capacidade de versionamento, deduplicação e upload eficiente de grandes arquivos modificados.
  • O que os recrutadores avaliam no nível Sênior:
    • Chunking e Sincronização Delta (Delta Sync): Divisão dos arquivos em pedaços de 4MB usando algoritmos como Rabin Fingerprints; se o usuário altera apenas 1 parágrafo de um arquivo de 100MB, apenas o bloco alterado é transmitido.
    • Deduplicação de Blocos: Hashes SHA-256 de cada bloco armazenados em uma tabela global; se múltiplos usuários sobem o mesmo arquivo, o bloco é gravado fisicamente apenas uma vez no storage.
    • Resolução de Conflitos de Edição: Manipulação de edições concorrentes no mesmo arquivo offline através de versionamento ветores e geração de cópias de conflito.
  • Mapeamento AWS: Amazon S3 Multipart Upload + Amazon DynamoDB (metadados e índices de blocos) + Amazon SQS/SNS para notificações de sincronização entre dispositivos.

8. Design de Venda de Ingressos Concorrentes / Flash Sale (Ticketmaster / Amazon Prime Day)

Empresas que mais perguntam: Amazon, Shopee, Ticketmaster, Booking.com.

  • O Problema: Projete um sistema de reserva e venda onde 100.000 usuários tentam comprar apenas 5.000 ingressos no mesmo segundo.
  • O que os recrutadores avaliam no nível Sênior:
    • Como evitar (Venda Acima do Estoque): Pessimistic Locking (trava linhas no banco SQL, degrada performance) vs. Optimistic Locking (com versionamento WHERE version = current_version) vs. em Memória no Redis (DECRBY).
    • Virtual / Rate Limiter de Entrada: Fila de espera que libera usuários em lotes controlados para o checkout, evitando que o backend de pagamentos entre em colapso.
    • Reserva Temporária com TTL: O ingresso fica reservado por 10 minutos para preenchimento de dados do cartão; se não pagar, a reserva expira automaticamente e o estoque volta via Redis Keyspace Notifications ou jobs de .
  • Mapeamento AWS: Amazon CloudFront (conteúdo estático) + AWS WAF + Amazon ElastiCache Redis (controle de concorrência e trava atômica) + Amazon SQS FIFO + Amazon RDS / Aurora PostgreSQL.

9. Design de um Encurtador de URLs Global (TinyURL / Bitly)

Empresas que mais perguntam: Meta, Microsoft, Uber, Twitter.

  • O Problema: Projete um serviço que receba URLs longas e gere URLs curtas (ex.: https://bit.ly/3xY7kP), processando centenas de milhares de cliques com latência mínima.
  • O que os recrutadores avaliam no nível Sênior:
    • Geração de Chave Curta: Função Hash (MD5/SHA-256 truncada, propensa a colisão) vs. Gerador de IDs com ($[a-z, A-Z, 0-9]$). Com 7 caracteres em Base62, geram-se $62^7 \approx 3.5 \text{ trilhões}$ de URLs únicas.
    • HTTP 301 (Moved Permanently) vs. HTTP 302/307 (Temporary Redirect):
      • 301: O navegador faz cache do redirecionamento (menor carga no seu servidor, mas perde métricas analíticas de clique).
      • 302: Cada clique bate no seu servidor antes de redirecionar (essencial para coletar dados analíticos e geográficos do clique).
    • Cache maciço de leitura (99% de leituras para 1% de escritas) com expiração LRU (Least Recently Used).
  • Mapeamento AWS: Amazon CloudFront + AWS Lambda@Edge (redirecionamento ultrarrápido na borda) + Amazon DynamoDB (armazenamento chave-valor de baixíssima latência) + Amazon ElastiCache.

10. Design de um Web Crawler Distribuído em Larga Escala (Googlebot)

Empresas que mais perguntam: Google, Microsoft/Bing, LinkedIn.

  • O Problema: Projete um coletor de páginas web capaz de rastrear bilhões de URLs na internet, baixar páginas HTML e extrair novos links para alimentar o índice de um motor de busca.
  • O que os recrutadores avaliam no nível Sênior:
    • Politeness () & Robots.txt: Garantir que o crawler não realize um ataque de negação de serviço (DoS) involuntário contra servidores de terceiros; espaçar requisições para o mesmo domínio via filas separadas por host.
    • Deduplicação de URLs: Como testar se uma URL já foi rastreada entre 10 bilhões de URLs sem estourar a memória RAM (Uso de Filtros de Bloom em disco/memória).
    • Detecção de loops infinitos em páginas dinâmicas (calendários infinitos, links circulares).
  • Mapeamento AWS: Amazon EC2 Instances (computação paralela barata) + Amazon SQS / Apache Kafka (filas de URLs prioritárias) + Amazon S3 (armazenamento dos HTMLs brutos) + Amazon DynamoDB.

11. Design de Infraestrutura e Gateway de Inferência para LLMs (OpenAI / Anthropic API)

Empresas que mais perguntam: OpenAI, Anthropic, Databricks, Meta, startups de IA.

  • O Problema: Projete um gateway corporativo para expor modelos de linguagem (LLMs), gerenciando requisições com respostas em streaming de longa duração e otimização de custos de clusters de GPU.
  • O que os recrutadores avaliam no nível Sênior:
    • Conexões de Longa Duração e Streaming: Uso de Server-Sent Events () ou HTTP/2 streaming em vez de polling para entregar tokens conforme são gerados.
    • Rate Limiting Baseado em Tokens (TPM) e Requisições (RPM): O limitador precisa calcular tanto o número de chamadas quanto o consumo dinâmico de tokens de entrada e saída.
    • Dynamic Batching: Agrupamento de requisições concorrentes para processar paralelamente no hardware da GPU aumentando a vazão do modelo.
    • Cache de Prefixo / Prompt Caching & Roteamento Semântico: Identificação de perguntas idênticas ou embeddings similares para retornar respostas prontas sem acionar a inferência cara na GPU.
    • Circuit Breaking: Detecção de degradação nos nós de GPU e fallback para instâncias menores ou provedores secundários.
  • Mapeamento AWS: Amazon API Gateway + AWS Lambda / ECS Fargate + Amazon ElastiCache (Redis) para caching semântico + AWS Bedrock / Clusters SageMaker com GPUs NVIDIA.

12. Design de uma Plataforma de Métricas e Observabilidade Global (Datadog / Prometheus)

Empresas que mais perguntam: Datadog, Splunk, Elastic, Uber, Netflix.

  • O Problema: Projete um sistema que ingira milhões de métricas por segundo (CPU, memória, logs, latência p99) de milhares de microsserviços e permita consultas analíticas em tempo real.
  • O que os recrutadores avaliam no nível Sênior:
    • Ingestão em Larga Escala: Agente local coletor com buffer em memória e envio em lotes (batching) para mitigar sobrecarga de rede.
    • Time-Series Database (TSDB): Como estruturar índices invertidos com tags de alta sem estourar os limites de memória.
    • Downsampling e Retenção Hierárquica:
      • Dados brutos a cada 1 segundo mantidos por 7 dias;
      • Agregados de 1 minuto mantidos por 30 dias;
      • Agregados de 1 hora mantidos por 1 ano em storage frio.
  • Mapeamento AWS: Amazon Kinesis Data Streams / Amazon MSK (Kafka) + Amazon Timestream / OpenSearch + Amazon S3 (Cold Data Lake com consultas via Amazon Athena) + Amazon CloudWatch.

O que Separa um Candidato Pleno de um Sênior?

AspectoCandidato Pleno (Mid-Level)Candidato Sênior (L5/L6+)
Início da EntrevistaComeça a desenhar caixas e bancos imediatamente.Clarifica ambiguidades, define SLAs, p99 e faz cálculos de escala (back-of-the-envelope).
Banco de DadosEscolhe “PostgreSQL” ou “MongoDB” por costume.Justifica a escolha técnica pelo modelo de acesso, Teorema CAP, estratégia de particionamento e consistência.
Falhas de RedeAssume que conexões de rede são 100% confiáveis.Planeja o que acontece em timeouts, partição de rede, replicação defasada e nós mortos.
ConcorrênciaMenciona vagamente transações no banco.Especifica mecanismos concretos: Idempotency Keys, Optimistic Locking, Scripts Lua no Redis ou filas FIFO.
Custo na NuvemIgnora o custo de hardware e banda.Considera arquitetura econômica: Edge Caching, S3 Lifecycle Tiers, Instâncias Spot e compressão de payloads.

respostas

Uma regra vale para as doze. Em entrevista de nível sênior, o desenho em si é o de menos. O que se avalia é se você pergunta antes de desenhar, se usa números para decidir, se escolhe com um trade-off declarado e se sabe dizer o que quebra. A figura abaixo mostra como os 45 minutos costumam se dividir, e a cor laranja marca o que o entrevistador está medindo em cada fase.

roteiro 45 min
figura 1 · roteiro 45 min

Leia a figura da esquerda para a direita. A linha escura grossa é o que você faz (esclarecer, estimar, desenhar, aprofundar, fechar) e as caixas laranja, ligadas por linhas pontilhadas, são o que a outra pessoa anota enquanto isso. A fase de aprofundar leva 20 dos 45 minutos: é onde quase todo mundo ganha ou perde a vaga, e é por isso que cada pergunta abaixo gasta mais tempo no mecanismo do que na caixa de “alto nível”.

Convenção de cores em todas as figuras desta nota: laranja é o cliente ou quem pede, azul é borda e gateway da AWS, tom terroso (claude) é o código que você escreve, verde é dado, roxo é fila, evento e orquestração, rosa é falha ou bloqueio, cinza é serviço de terceiros. Todo serviço da AWS aparece com o ícone oficial.

1 rate limiter distribuído

1.1 a pergunta e o que ela mede

“Projete um limitador de taxa que controle milhões de requisições por segundo por usuário, IP ou API key, em vários data centers.” O entrevistador quer ver três coisas. Primeiro, se você sabe que um rate limiter é uma decisão de produto (quem é limitado, por quanto, com que mensagem) antes de ser um algoritmo. Segundo, se você enxerga a corrida entre instâncias que leem e escrevem o mesmo contador. Terceiro, se você pensa no que acontece quando o próprio limitador quebra, porque um componente que fica no caminho de toda requisição é um ponto único de falha em potencial.

1.2 como responder em voz alta

“Eu começaria perguntando onde o limite vale e o que acontece quando ele estoura. Limitar por IP, por usuário logado ou por API key são problemas diferentes, e o produto pode querer mensagens e cotas distintas por plano. Pergunto também se o limite precisa ser exato ou aproximado, se vale para o mundo todo ou por região, e se a API é de leitura barata ou de operação cara, como cobrança, porque isso decide o que fazer quando o limitador cai.

Para a escala, suponho algo como 1 milhão de requisições por segundo no pico e dezenas de milhões de chaves ativas. Cada chave guarda poucos bytes (um contador e um carimbo de tempo), então o estado cabe em memória: com 50 milhões de chaves e algumas dezenas de bytes por chave, falamos de poucos GB, o que um cluster em memória aguenta. O gargalo é o número de operações por segundo e a latência extra por requisição, não o volume de dados.

No desenho, eu uso camadas, porque cada uma barra um tipo de abuso pelo menor custo. Na borda, o AWS WAF com regra por taxa derruba o volume bruto por IP antes de chegar na minha conta de computação. No API Gateway, os planos de uso aplicam a cota contratada por API key. Na aplicação, um script atômico no Redis aplica a regra fina de negócio por usuário e por rota. Como algoritmo, escolho token bucket: ele aceita rajadas curtas até o tamanho do balde e depois impõe uma taxa média, o que combina com tráfego real de usuário.

O ponto sênior é a atomicidade. Se cada instância faz GET e depois SET, duas instâncias leem o mesmo saldo e as duas deixam passar. Resolvo com um script Lua que roda inteiro no servidor: lê, repõe os tokens conforme o tempo passou, decide e grava, sem que outro comando entre no meio.

Por fim, falo das falhas. Se o Redis não responde em 20 ms, não posso travar a API. Para rotas de leitura, abro o limitador (fail-open) e uso um limite local em memória, aproximado. Para rotas de cobrança, fecho (fail-closed) e devolvo 503, porque deixar passar custa dinheiro. Entre regiões, não replico contador em tempo real, porque isso soma latência a toda requisição: divido a cota entre as regiões e rebalanceio a cada poucos segundos.”

1.3 por dentro: como funciona

Comece pelo problema mais simples. Um limitador responde “esta chave pode fazer mais uma requisição agora?” em poucos milissegundos, milhões de vezes por segundo. Para responder, ele precisa lembrar quanto cada chave já consumiu. Em uma máquina só, isso é um mapa em memória protegido por um lock, e a aula Rate Limiting (em Go) e o desafio Uber - High-Throughput Rate Limiter mostram essa versão local, com sync.RWMutex e uma goroutine que descarta chaves ociosas. O problema “distribuído” aparece quando há várias instâncias do serviço: cada uma só enxerga as próprias requisições, e o limite de 100 por minuto vira 100 por minuto por instância. Para somar, o estado precisa morar num lugar comum, e o lugar comum clássico é um Redis (veja Redis para o que ele é e por que é rápido).

Há quatro algoritmos que o entrevistador espera que você compare. O contador de janela fixa conta requisições dentro de cada minuto do relógio: é o mais barato, mas permite o dobro do limite na virada da janela (100 requisições no fim de um minuto e 100 no começo do seguinte). A janela deslizante com log guarda o carimbo de cada requisição e é exata, mas gasta memória proporcional ao limite. O contador de janela deslizante aproxima o log combinando a janela atual e a anterior ponderadas, com memória constante e erro pequeno. O token bucket (balde de fichas) guarda dois números por chave, as fichas atuais e o instante da última reposição: cada requisição gasta fichas, e as fichas voltam a uma taxa fixa até o teto do balde. O leaky bucket é o espelho: a saída é uniforme, o que serve para proteger um sistema que não tolera rajada.

Eu prefiro token bucket por padrão porque ele tem duas propriedades úteis para APIs: o tamanho do balde define a rajada permitida, e a taxa de reposição define o consumo médio. Com capacidade 5 e reposição de 0,5 ficha por segundo, um cliente pode mandar 5 requisições de uma vez e depois uma a cada 2 segundos. A reposição não precisa de timer: calcula-se de forma preguiçosa, na próxima requisição, pelo tempo decorrido desde a última.

Agora a corrida. Se o código da aplicação faz GET tokens, calcula e faz SET tokens, o intervalo entre os dois comandos é uma janela em que outra instância pode ler o mesmo valor. A aula Race Condition explica o mesmo fenômeno dentro de um processo; aqui a diferença é que o “mesmo lugar de memória” está do outro lado da rede. A saída é tornar leitura, decisão e escrita um único comando indivisível. O Redis executa um script Lua de uma vez, sem intercalar outros comandos, e é o mesmo recurso que a aula Locks Distribuídos com Redis usa para liberar um lock com segurança. Este é o script que eu usaria (usa TIME do servidor para não depender do relógio das instâncias):

-- KEYS[1] = chave do balde; ARGV = capacidade, fichas por segundo, custo
local t = redis.call('TIME')
local now = t[1] * 1000 + math.floor(t[2] / 1000)
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local cost = tonumber(ARGV[3])
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1])
local ts = tonumber(data[2])
if tokens == nil then
  tokens = capacity
  ts = now
end
local elapsed = math.max(0, now - ts)
tokens = math.min(capacity, tokens + elapsed * rate / 1000)
local allowed = 0
local retry = 0
if tokens >= cost then
  tokens = tokens - cost
  allowed = 1
else
  retry = math.ceil((cost - tokens) / rate * 1000)
end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(capacity / rate * 1000) * 2)
return {allowed, math.floor(tokens), retry}

Eu executei este script num Redis 7.4.4 em container local (não no ElastiCache). Com capacidade 5 e 0,5 ficha por segundo, as cinco primeiras chamadas seguidas voltaram {1, 4..0, 0} e a sexta voltou {0, 0, 1461}, isto é, negada, com 1461 ms de espera sugerida (o valor entra no cabeçalho Retry-After). Em outro teste, 60 chamadas disparadas em paralelo (30 ao mesmo tempo) contra um balde novo de capacidade 5 e reposição desprezível resultaram em exatamente 5 permitidas e 55 negadas, que é o que a atomicidade promete. O PEXPIRE evita o vazamento de memória de chaves de clientes que nunca mais voltam: o balde some sozinho quando já teria enchido de novo. Em cluster, o script só pode tocar chaves declaradas em KEYS, e todas precisam cair no mesmo slot (a documentação do Valkey exige que todo nome de chave acessado seja passado como argumento de chave).

Há duas bordas que o entrevistador adora. A primeira é a : um único cliente muito grande gera milhares de operações por segundo na mesma chave, que mora em um único shard. O Redis processa comandos em uma thread por shard, então uma chave quente satura aquele shard. As saídas são dividir a cota em N sub-baldes (a chave vira user:42:3, e cada instância usa um sub-balde com 1/N da cota) ou aceitar um limite local aproximado nas instâncias. A segunda borda é o relógio: usar TIME do servidor resolve o desencontro entre instâncias, mas, em failover, o novo primário pode ter relógio diferente; por isso o script usa max(0, elapsed).

Sobre falhas, a aula Reliability diz a regra geral: um cache é uma otimização, nunca a fonte da verdade nem um componente obrigatório. O rate limiter é um caso especial, porque ele está no caminho da requisição e decide algo de negócio. Daí o dilema. Fail-open mantém o produto no ar mas, durante a falha, o limite some: é aceitável para leitura, login e feed, onde o pior caso é carga extra. Fail-closed protege o que custa dinheiro ou tem efeito irreversível (cobrança, envio de SMS, e-mail em massa), ao custo de indisponibilidade. A aula Resiliência com Circuit Breaker e Retry ensina a peça que implementa isso: um timeout curto na chamada ao limitador e um que, depois de falhas seguidas, para de tentar e vai direto para a política de fallback, sem esperar o timeout a cada requisição.

Para várias regiões, a ideia contraintuitiva é não sincronizar. O ElastiCache Global Datastore replica de forma assíncrona de um cluster primário para clusters secundários em outras regiões, e os secundários só aceitam leitura, de modo que ele serve para leitura local e recuperação de desastre, não para contadores com escrita ativa nos dois lados. A saída prática é repartir a cota: um cliente com 1000 requisições por minuto recebe, por exemplo, 600 na região A e 400 na região B, cada região conta localmente e rápido, e um processo periódico soma o uso e ajusta as cotas. O erro máximo é a cota repartida de uma região que não está usando, o que é um preço baixo pela latência zero entre regiões.

1.4 mapeamento na AWS

A figura mostra as três camadas e o lugar de cada serviço. O cliente normal (laranja) atravessa todas; os três clientes abusivos (rosa) são barrados cada um na camada que o enxerga melhor.

q1 tres camadas
figura 2 · q1 tres camadas

Siga o pacote laranja de cima para baixo: CloudFront, API Gateway, Lambda e, ao lado, a consulta ao ElastiCache com o EVALSHA do script. Os pacotes rosa mostram o que cada camada barra. O bot é identificado por IP e morre no WAF (403, ou o CAPTCHA/Challenge, que as regras por taxa também permitem). O parceiro estourou o plano contratado e recebe 429 do API Gateway. O usuário logado que faz rajada em /pagamentos só é identificável depois da autenticação, então quem o barra é o script no ElastiCache, na camada de aplicação.

Os porquês de cada serviço, com números que verifiquei na documentação (consulta em 2026-10-22). A regra por taxa do AWS WAF conta requisições por uma janela de avaliação de 60, 120, 300 ou 600 segundos (padrão 300), com limite mínimo de 10, agrega por IP, por IP de um cabeçalho como X-Forwarded-For ou por chaves personalizadas, e aplica qualquer ação exceto Allow. A documentação avisa que o limite é aplicado “perto” do valor configurado, não de forma exata: serve para derrubar volume, não para cota de produto. O WAF no CloudFront é global; no API Gateway, a documentação trata apenas de REST APIs. Os planos de uso do API Gateway valem para REST APIs, identificam o cliente por API key, usam token bucket e a documentação diz explicitamente que throttling e cotas são “best effort”, que não se deve usá-los para controlar custo nem bloquear acesso, e que API key não é mecanismo de autenticação. A cota padrão da conta é de 10.000 requisições por segundo por região, com balde de 5.000, e algumas regiões têm padrão de 2.500 e 1.250. O ElastiCache aceita Valkey e Redis OSS, serverless ou em clusters por nós; a documentação de Lua no ElastiCache diz que um script precisa receber ao menos uma chave, que o lua-time-limit padrão é de 5000 ms (passado disso o serviço tenta SCRIPT KILL e, se falhar, reinicia o engine) e que um script tem no máximo 4 MiB. Por isso o script precisa ser curto.

Por que não outros serviços. Só o API Gateway não basta, porque sua cota é aproximada e por API key. Só o WAF não basta, porque ele não conhece o usuário logado nem a regra de negócio. DynamoDB com escrita condicional funcionaria para contadores baixos e daria durabilidade, mas cada decisão viraria uma escrita paga e com latência de um dígito de milissegundos, o que é lento e caro para 1 milhão de decisões por segundo. Lambda como limitador próprio perde o estado entre invocações. Memcached não tem script atômico.

A figura seguinte compara o que acontece sem e com o script. À esquerda, duas instâncias leem tokens = 1 ao mesmo tempo, as duas passam e o limite 1 vira consumo 2. À direita, o Redis serializa os dois scripts: A passa e B recebe 429 com o Retry-After calculado dentro do script.

q1 corrida vs lua
figura 3 · q1 corrida vs lua

Olhe primeiro o lado “Antes”: os pacotes rosa dos dois GET chegam juntos, e é essa simultaneidade que gera o erro. No lado “Depois” os pacotes verdes entram um depois do outro (o segundo tem atraso), porque o servidor só executa um script por vez.

Agora o modo de falha, que é a pergunta que separa pleno de sênior. A figura é um fluxograma de decisão: o serviço chama o limitador com timeout de 20 ms, e, se não houver resposta no prazo, a rota decide entre abrir e fechar.

q1 fail open closed
figura 4 · q1 fail open closed

O caminho verde (“sim”) é o normal. O caminho rosa começa no losango “Resposta no prazo?”: o ElastiCache está com saúde=erro (a caixa treme), o serviço cai para o segundo losango e escolhe a política pela rota. Fail-open usa um contador local em memória (cada instância limita com uma fração da cota, de forma aproximada), fail-closed responde 503 com Retry-After (o X vermelho marca o pacote barrado). Em ambos os casos um alarme no CloudWatch avisa que o limitador está degradado, porque abrir sem ninguém saber é a forma silenciosa de perder a proteção.

Por fim, as regiões. O quadro mostra duas regiões com cota local (600 e 400 requisições por minuto) e um rebalanceador que soma o uso e ajusta as cotas.

q1 multi regiao
figura 5 · q1 multi regiao

O tráfego laranja e o EVALSHA verde são rápidos e locais a cada região. Os pacotes roxos, lentos e pontilhados, são a parte assíncrona: a cada poucos segundos o rebalanceador lê o uso das duas regiões e grava a nova divisão no DynamoDB (uma tabela global replica entre regiões, também de forma assíncrona). A figura é uma das formas de fazer, não a única: o ponto é que a sincronização fica fora do caminho da requisição.

1.5 dicas para a entrevista

Comece perguntando: o limite é por IP, usuário ou chave? Precisa ser exato ou pode errar 5%? Qual a rota mais sensível? O que o cliente deve ver quando for limitado (429, Retry-After, X-RateLimit-Remaining)? Há planos pagos com cotas diferentes? Números úteis para estimar: 1 milhão de decisões por segundo, com 50 milhões de chaves e algumas dezenas de bytes por chave, dá algo na faixa de GB de memória; uma ida e volta ao Redis na mesma zona de disponibilidade custa em geral frações de milissegundo a poucos milissegundos (ordem de grandeza, depende da rede e da carga, e não medi no ElastiCache).

Os trade-offs que o entrevistador espera ouvir: exatidão contra custo (janela deslizante com log contra contador aproximado), latência contra consistência entre regiões (cota repartida contra contador global), disponibilidade contra proteção (fail-open contra fail-closed) e rajada contra taxa média (tamanho do balde). As respostas que soam júnior são “uso um contador no banco relacional” (cada requisição vira uma escrita em disco), “uso GET e SET” (corrida), “o Redis nunca cai” e “sincronizo o contador entre regiões em tempo real”. Se você travar, volte à pergunta-guia: onde está o estado, quem pode mexer nele ao mesmo tempo e o que acontece se ele sumir. Dizer isso em voz alta já organiza o resto.

1.6 follow-ups prováveis

Como você limita por usuário e por IP ao mesmo tempo? Com duas chaves, user:42 e ip:1.2.3.4, e a requisição só passa se as duas passarem; o script pode receber as duas chaves se elas caírem no mesmo slot, ou você chama o script duas vezes e aceita que uma pode consumir ficha sem a outra deixar passar.

E se um cliente legítimo gera uma chave quente de 50 mil requisições por segundo? Divida a cota em sub-baldes (user:42:0 a user:42:7), sorteie o sub-balde por requisição e aceite um erro pequeno, ou deixe o limite fino na instância e use o Redis só para reajustes periódicos.

Como você testa o limitador? Com teste de carga concorrente contra um Redis real, afirmando que o número de permitidas nunca passa da capacidade mais a reposição do tempo decorrido (foi o que eu fiz acima com 60 chamadas paralelas), e com um teste de falha que derruba o Redis e confere que a rota de cobrança fecha e a de leitura abre.

O que o cliente deve receber? 429 com Retry-After em segundos e, se possível, cabeçalhos com limite, restante e instante de reposição; para 503 de fail-closed, também Retry-After, e o cliente deve usar backoff com jitter (veja Resiliência com Circuit Breaker e Retry).

Veja também Worker Pools e Rate Limiting para limitar dentro de um processo Go e Uber - High-Throughput Rate Limiter para a versão de entrevista de código.


2 chat em tempo real com mensagens offline

2.1 a pergunta e o que ela mede

“Projete um chat 1:1 e em grupo, com confirmação de entrega e leitura, que entregue também as mensagens enviadas enquanto o destinatário estava offline.” O entrevistador procura quatro sinais: se você entende que uma conexão persistente muda o desenho (o servidor passa a ter estado, e esse estado precisa ser achado por outro servidor), se você garante ordem e não perde mensagem, se você trata o usuário offline como o caso normal e não como exceção, e se você sabe o que muda quando o canal tem 50 mil pessoas em vez de 2.

2.2 como responder em voz alta

“Primeiro eu fixo o escopo. Mensagens de texto 1:1 e em grupo, recibos de entrega e leitura, histórico sincronizado, e a garantia que o produto quer: nenhuma mensagem perdida, ordem consistente dentro de uma conversa, e entrega pelo menos uma vez com deduplicação no cliente. Pergunto o tamanho máximo do grupo e se há criptografia de ponta a ponta, porque isso muda o que o servidor pode ler.

Para a escala, suponho 100 milhões de usuários ativos por dia e 40 mensagens enviadas por usuário por dia. São 4 bilhões de mensagens por dia, uns 46 mil por segundo em média e, com um pico de três vezes, perto de 140 mil por segundo. Com uns 200 bytes por mensagem entre texto e metadados, são cerca de 800 GB por dia de dados novos. O número que mais pesa é o de conexões simultâneas: se 30% dos ativos estão conectados, são dezenas de milhões de conexões abertas.

No desenho, o cliente abre uma conexão WebSocket com um gateway. Quando a Ana manda uma mensagem, o gateway aciona o serviço de envio, e o serviço faz três coisas nesta ordem. Grava a mensagem de forma durável, com um id ordenável, antes de qualquer outra coisa. Procura onde o destinatário está conectado numa tabela de sessões. E então entrega pela conexão dele, ou, se não houver conexão, dispara uma notificação push para acordar o aplicativo. Gravar primeiro é o que garante que nenhuma mensagem se perde: a entrega em tempo real é uma otimização sobre um histórico durável.

Para o usuário offline, a mensagem já está no histórico. Quando ele abre o app, reconecta e manda o id da última mensagem que viu, e o servidor devolve tudo que veio depois, em ordem. Por isso não preciso de uma fila por usuário.

A ordem vem de um id por conversa. Dentro de uma conversa, uso um contador sequencial ou um id estilo Snowflake, que é ordenável pelo tempo, e deixo o cliente reordenar por esse id. Em grupo grande, o contador sequencial vira ponto quente, então uso Snowflake e aceito a ordem aproximada por milissegundo.

Para um canal de 50 mil pessoas, não faço 50 mil entregas dentro do serviço de envio. Gravo uma vez, publico um evento no tópico do canal e uma frota de workers entrega em fatias, cada uma lendo uma parte do índice de conexões do canal. Termino com falhas: conexão que morre sem avisar, reconexão com backoff, e o que o cliente faz se o POST de entrega falhar.”

2.3 por dentro: como funciona

Comece pelo que muda quando a conexão é persistente. Em HTTP comum, o cliente pergunta e o servidor responde; o servidor não sabe quem está do outro lado entre duas requisições. Em WebSocket, o cliente abre uma conexão TCP, faz um handshake HTTP com pedido de upgrade, e a partir daí os dois lados mandam frames quando quiserem. A aula WebSocket mostra o handshake, os frames, o ping/pong e a validação de origem, em Go. O que importa para o design é a consequência: cada conexão pertence a um servidor específico, na memória dele. Se a Ana está conectada ao servidor 7 e o Bruno ao servidor 31, o servidor 7 não consegue escrever no socket do Bruno. Alguém precisa saber que o Bruno está no servidor 31, e é por isso que existe uma tabela de sessões (usuário para conexão) e um mecanismo para mandar a mensagem ao servidor certo.

O segundo ponto é o que fazer com quem está offline. Quando o celular está desligado ou sem rede, não há conexão para escrever. A pergunta de design é onde a mensagem espera. Há duas respostas. A primeira é uma fila por destinatário, que guarda o que ainda não foi entregue. A segunda é um histórico durável por conversa, no qual cada usuário guarda um ponteiro “li até aqui”. A segunda é mais simples de operar: a mensagem é gravada uma vez, e cada leitor tem a própria posição, o que também serve para o grupo (uma gravação, 50 mil ponteiros), para sincronizar um segundo dispositivo e para reentregar depois de uma falha. Por isso eu uso o histórico como fonte da verdade e trato a entrega pela conexão como um atalho.

O terceiro ponto é a ordem. A aula explica o layout de 64 bits (41 de tempo em milissegundos, 10 de nó, 12 de sequência) e o ponto que a nota do Gemini simplifica: o Snowflake é ordenável por tempo (k-ordered), não estritamente monotônico no mundo inteiro, porque dois nós com relógios diferentes podem emitir ids fora da ordem real do envio. Dentro de uma conversa, você tem duas opções. Um contador sequencial por conversa, incrementado atomicamente no banco (um UpdateItem com ADD no DynamoDB devolve o novo valor), dá ordem estrita, ao custo de serializar as escritas daquela conversa em um item. Para 1:1 e grupos pequenos isso é ótimo. Para um canal enorme, o item vira ponto quente, e o Snowflake é a saída, com a ordem decidida por (timestamp, nó, sequência) e a convenção de que o cliente ordena por esse id. O vault não tem uma aula sobre ordenação causal; o assunto ainda não tem aula no vault.

O quarto ponto é a garantia de entrega. Redes móveis perdem pacotes e reconectam o tempo todo, então a garantia realista é “pelo menos uma vez”: se não houve confirmação, reenvia. Isso cria duplicatas, e a solução é a Idempotência: o cliente gera um client_msg_id (um UUID) para cada mensagem que digita, o servidor grava com escrita condicional (só se esse id ainda não existe naquela conversa) e o reenvio devolve o mesmo resultado. Os recibos são mensagens pequenas no sentido contrário: “entregue” quando o aparelho do destinatário confirma o recebimento, “lido” quando a tela mostra a mensagem. Cada um atualiza o status da mensagem no histórico e é empurrado ao remetente pelo mesmo caminho de entrega.

Sobre o ciclo de vida da conexão, há limites que moldam o design. Conexões morrem em silêncio: o celular entra num túnel, o NAT esquece o mapeamento, e nenhum lado recebe um aviso. Por isso há heartbeat (ping periódico) e timeout. E, no API Gateway, há limites duros: 10 minutos de ociosidade e 2 horas de duração máxima por conexão. O cliente precisa reconectar com backoff e jitter (a aula Resiliência com Circuit Breaker e Retry explica por que o jitter evita a manada de reconexões depois de uma queda) e, ao reconectar, pedir o que perdeu a partir do último id visto.

O quinto ponto é o canal grande. Com 50 mil participantes, a mensagem de uma pessoa vira 50 mil entregas. Se o serviço de envio fizesse isso sozinho, o autor esperaria segundos e a função estouraria o tempo. O padrão é publicar o evento uma vez e deixar vários consumidores entregarem em paralelo, o que é publish/subscribe com fan-out (a nota pub - sub ainda está vazia no vault, então a explicação está aqui; para o modelo de eventos veja Arquitetura orientada a eventos). As conexões do canal ficam particionadas em fatias, e cada worker cuida de uma fatia.

2.4 mapeamento na AWS

A figura mostra o caminho de uma mensagem entre duas pessoas, numerado na ordem em que acontece.

q2 visao geral
figura 6 · q2 visao geral

Siga os pacotes laranja grossos. A Ana manda a mensagem (1) pelo API Gateway WebSocket, que escolhe a rota sendmessage e aciona a Lambda (2). A Lambda grava primeiro no DynamoDB de mensagens (3, pacote verde grosso) e depois consulta a tabela de conexões (4, ida e volta). Se o Bruno tem conexão, a Lambda faz um POST ao endpoint @connections do API Gateway (5), que escreve o frame na conexão dele. Se não tem, o caminho roxo (5b) publica no SNS, que usa o APNs ou o FCM para acordar o aplicativo.

Por que cada serviço. O API Gateway WebSocket mantém as conexões por você, escolhe a rota pelo conteúdo da mensagem ($request.body.action) e invoca integrações no $connect, $disconnect e nas rotas customizadas. O backend manda dados para um cliente com POST https://{api-id}.execute-api.{região}.amazonaws.com/{stage}/@connections/{connectionId}, assinado com SigV4, e a resposta pode ser um GoneException se a conexão não existe mais. Os limites que verifiquei na documentação: frame de 32 KB, mensagem de 128 KB (acima de 32 KB a mensagem precisa ser quebrada em frames), ociosidade de 10 minutos, duração máxima de 2 horas, 500 novas conexões por segundo por conta e região (ajustável), timeout de integração de até 29 segundos. A documentação não impõe cota de conexões simultâneas: o teto vem do ritmo de novas conexões vezes a duração máxima, e o exemplo dela é de 3,6 milhões de conexões simultâneas com a cota padrão. O DynamoDB serve às duas tabelas: mensagens (chave de partição chat_id, chave de ordenação o id da mensagem, para Query por intervalo) e conexões (chave user_id, com atributo de TTL). Item de DynamoDB tem no máximo 400 KB, muito acima de uma mensagem de texto. O TTL apaga registros expirados “tipicamente em até alguns dias” e sem consumir capacidade de escrita; como o apagamento não é imediato, a leitura deve filtrar registros expirados. O SNS tem push móvel para APNs, FCM e outros serviços de push.

Por que não outros. Uma fila SQS por usuário parece natural, mas não permite reler nem consultar por posição, e o histórico durável já resolve os dois problemas (também resolve vários dispositivos por usuário). Kinesis ou Kafka (veja Kafka) fazem sentido para espalhar eventos para muitos consumidores, e eu os usaria se o volume pedisse, mas para a entrega a um usuário não acrescentam nada que o DynamoDB e o SNS já façam. Polling HTTP gasta bateria e dá latência de segundos.

Uma ressalva honesta de escala: com 30 milhões de conexões simultâneas, o API Gateway WebSocket exigiria aumentos de cota e teria um custo por mensagem e por minuto de conexão que eu não verifiquei neste momento (confira a página de preços). Nessa escala, muitas empresas operam os próprios gateways WebSocket em contêineres (ECS ou EKS atrás de um Network Load Balancer), com um índice de sessões em ElastiCache ou DynamoDB. Eu diria isso na entrevista como a evolução natural do desenho, e não como o ponto de partida.

A figura seguinte mostra a vida de uma conexão, que explica por que o design precisa de heartbeat e de reconexão.

q2 ciclo conexao
figura 7 · q2 ciclo conexao

Leia da esquerda para a direita. O handshake (wss com JWT, veja JWT) cai na rota $connect, que chama o Lambda authorizer e grava o connectionId no DynamoDB com TTL. Na conexão aberta, o heartbeat do app (pacotes laranja pontilhados) evita o corte por ociosidade de 10 minutos. O corte de 2 horas é o pacote barrado com X: não há como evitá-lo, só se recuperar, e é por isso que a reconexão com backoff e jitter e o pedido “me dê o que perdi” fazem parte do protocolo do cliente, e não de um tratamento de exceção. A rota $disconnect remove a conexão, mas o API Gateway a invoca em regime de melhor esforço, então o TTL é a rede de segurança para registros fantasmas.

Agora o ponto central da pergunta, o caminho online e o caminho offline.

q2 online offline
figura 8 · q2 online offline

O fluxo começa pela mensagem já gravada (verde, no topo). O losango pergunta se o Bruno tem conexão. À esquerda, “sim”: POST em @connections, e o app responde com ack (status delivered). Se o POST falha com GoneException (caixa rosa), a conexão morreu sem avisar, e o sistema trata o Bruno como offline, seguindo o caminho pontilhado até o SNS. À direita, “não”: o SNS publica no endpoint do aparelho, o sistema operacional acorda o app, o Bruno abre, reconecta mandando o último id visto e a Lambda de sincronização faz um Query por chat_id com id maior que o último, devolvendo tudo em ordem. Note que a mensagem nunca esteve “numa fila de offline”: o histórico é a fila.

Por fim, o canal grande.

q2 grupo grande
figura 9 · q2 grupo grande

O autor manda um único frame. A Lambda grava uma vez no DynamoDB e publica uma vez no tópico SNS do canal. O SNS fan-out distribui para três filas SQS, uma por fatia de conexões (no exemplo, uns 17 mil de 50 mil), e uma Lambda por fatia lê o índice de conexões do canal e faz POST em @connections para cada uma. Os pacotes rápidos do final mostram o volume saindo. Para espalhar por fatias, o SNS permite política de filtro por atributo da mensagem, e o publicador marca a fatia; os detalhes da configuração eu não verifiquei nesta consulta.

2.5 dicas para a entrevista

Perguntas esclarecedoras: tamanho máximo de grupo, criptografia de ponta a ponta (se sim, o servidor guarda blobs cifrados e a busca no servidor some), vários dispositivos por usuário, retenção do histórico, mídia (imagem e vídeo vão para o S3 com URL pré-assinada, e a mensagem carrega só a referência; veja Upload de Arquivos para AWS S3). Números: 100 milhões de ativos e 40 mensagens por dia dão 46 mil mensagens por segundo na média; 200 bytes por mensagem dão 800 GB por dia; 30% de conexão simultânea são dezenas de milhões de sockets.

Trade-offs a verbalizar: gravar antes de entregar (durabilidade) custa uma escrita extra e um pouco de latência; contador sequencial (ordem estrita) contra Snowflake (ordem aproximada e sem ponto quente); fan-out na escrita contra leitura sob demanda em canal grande; API Gateway gerenciado contra gateway próprio em escala muito alta. Respostas que soam júnior: “uso WebSocket e pronto”, “guardo a mensagem na memória do servidor até o usuário voltar” (perde tudo no deploy), “uma fila por usuário”, “o Snowflake garante ordem total” e “o $disconnect sempre roda”. Se travar, desenhe a história de uma mensagem do envio ao recibo “lido”, passo a passo, e pergunte a si mesmo a cada seta “e se isto falhar aqui?”.

2.6 follow-ups prováveis

Como você garante que o Bruno não receba a mesma mensagem duas vezes? Ele pode receber duas vezes (entrega pelo menos uma vez), mas o app deduplica pelo id da mensagem, e o servidor grava com escrita condicional pelo client_msg_id do remetente.

E se o Bruno tem três dispositivos? A tabela de conexões guarda uma linha por dispositivo (user_id e device_id), a entrega faz fan-out para todos os dispositivos conectados, e cada dispositivo mantém o próprio ponteiro de “visto até aqui” para sincronizar o que perdeu.

Como sincronizar o histórico depois de um mês offline? Paginação por cursor sobre o id da mensagem (a aula Cursor-based explica por que isso é melhor que OFFSET), em lotes, com os mais recentes primeiro na tela.

Como lidar com um deploy sem derrubar milhões de conexões? Drenar aos poucos, com o cliente reconectando com jitter; no API Gateway quem segura as conexões é o serviço gerenciado, e o deploy da Lambda não derruba os sockets. Com gateways próprios, a aula Graceful Shutdown e Signal Handling cobre o desligamento ordenado.


3 feed de notícias

3.1 a pergunta e o que ela mede

“Projete a linha do tempo em que as pessoas postam e os seguidores veem os posts mais recentes e relevantes de quem seguem.” É a pergunta mais clássica de assimetria: poucos usuários têm milhões de seguidores e a maioria tem centenas. O entrevistador quer saber se você identifica a decisão central (onde pagar o custo, na escrita ou na leitura), se você percebe que a resposta certa depende de quem é o autor, e se você pensa nas falhas do cache que sustenta tudo.

3.2 como responder em voz alta

“Eu começo separando as duas operações: publicar um post e abrir o feed. Pergunto qual é a proporção entre elas, se o feed é cronológico ou ranqueado, qual o limite de seguidores e quanto de atraso é aceitável entre o post e o feed dos seguidores. Costuma ser algo como ‘o feed abre em menos de 200 ms e um post aparece para os seguidores em poucos segundos’.

Para a escala, suponho 100 milhões de usuários ativos por dia abrindo o feed 10 vezes: 1 bilhão de leituras por dia, uns 12 mil por segundo na média e, no pico, perto de 35 mil. Posts: 20 milhões por dia, com 200 seguidores em média, o que dá 4 bilhões de cópias de id por dia se eu copiar para cada seguidor, uns 46 mil gravações por segundo. Já um post de um usuário com 30 milhões de seguidores seria 30 milhões de gravações de uma vez.

Esse último número é a decisão. Eu tenho duas estratégias. Fan-out na escrita (push): ao publicar, copio o id do post para a lista de cada seguidor, e a leitura vira buscar uma lista pronta, que é barata. Fan-out na leitura (pull): não copio nada, e na hora de abrir o feed eu busco os posts recentes de todos que o usuário segue e ordeno, que é barato para escrever e caro para ler. Nenhuma serve sozinha: o push explode no usuário famoso e desperdiça trabalho para seguidores inativos, e o pull deixa a leitura lenta para quem segue muita gente.

A resposta sênior é híbrida. Faço push para a imensa maioria dos autores e deixo de fazer push para autores acima de um limite de seguidores, que eu ajusto com dados (a ordem de um milhão é um ponto de partida). Na leitura, monto o feed juntando a lista pré-montada do usuário com os posts recentes das celebridades que ele segue, buscados na hora.

Na AWS, o post vai para o DynamoDB, que é a fonte da verdade, e um evento vai para uma fila. Workers consultam o grafo social e empurram o id do post para as listas dos seguidores no ElastiCache, que guarda só os últimos algumas centenas de ids por usuário. O serviço de timeline lê a lista, junta com as celebridades, busca o conteúdo dos posts em lote e devolve uma página com cursor.

Falhas: se uma lista some do cache, reconstruo com pull uma única vez, com uma trava para que mil requisições não reconstruam a mesma lista ao mesmo tempo. E o fan-out é assíncrono, então um atraso nele afeta o frescor do feed, mas não a gravação do post.”

3.3 por dentro: como funciona

Pense numa redação de jornal. Há duas formas de entregar o jornal de hoje aos assinantes. Na primeira, a redação imprime um exemplar para cada assinante e o entrega na porta (fan-out na escrita): o trabalho todo acontece quando o jornal sai, e ler é instantâneo. Na segunda, a redação deixa a edição numa banca, e cada assinante vai buscá-la quando quiser (fan-out na leitura): imprimir é barato, mas cada leitor faz a viagem. Se o jornal tem 200 assinantes, imprimir 200 exemplares é nada. Se tem 30 milhões, a gráfica para.

A estrutura que torna o push barato é uma lista por usuário com os ids dos posts, mantida em memória. O Redis oferece listas (LPUSH e LTRIM mantêm um tamanho fixo) e conjuntos ordenados (ZADD com o instante do post como pontuação, ZREMRANGEBYRANK para cortar). A lista é mais simples, o conjunto ordenado deixa mesclar por tempo e inserir fora de ordem. Guardar só os ids (8 bytes de um Snowflake mais a sobrecarga da estrutura) e não o conteúdo evita copiar o texto 200 vezes e permite editar ou apagar um post em um lugar só. O conteúdo é buscado por BatchGetItem na hora da leitura. Para estimar memória, suponha uns 20 bytes por entrada contando a sobrecarga (valor que eu não medi): 800 entradas dão 16 KB por usuário, e 100 milhões de usuários dão da ordem de 1,6 TB, o que explica por que se mantém lista só para usuários ativos nos últimos dias e se reconstrói as outras sob demanda. O número 800 do texto original é um parâmetro de projeto que vem de relatos públicos sobre o Twitter, os quais não verifiquei; trate-o como “algumas centenas” e justifique o seu.

A conta do custo de escrita mostra por que o híbrido é necessário. Com 46 mil gravações de lista por segundo de média, uma celebridade com 30 milhões de seguidores precisaria de cerca de 650 segundos (30.000.000 dividido por 46.000) da capacidade inteira de fan-out para um único post, uns 11 minutos, e isso durante o horário em que todo mundo está lendo. Os seguidores veriam o post em ordens de minutos e os demais posts do sistema ficariam atrasados atrás dele na fila. É o problema da chave quente aplicado à escrita.

O pull resolve isso porque o post de uma celebridade existe uma vez só. Mas, na leitura, o serviço precisa saber quais celebridades o usuário segue e consultar os posts recentes de cada uma, o que custa uma consulta por celebridade seguida. Em geral um usuário segue poucas dezenas de contas acima do limite, e essas consultas são paralelas e batem num índice por autor (no DynamoDB, uma Query com chave de partição do autor e chave de ordenação o id do post). Os posts das celebridades são muito lidos, então cabem num cache: a leitura de celebridades se beneficia da aula Outside the API (cache perto do cliente) e de TTL curto.

A mescla é simples: junte os ids da lista do usuário com os ids das celebridades, ordene em ordem decrescente (o Snowflake já é ordenável por tempo, veja Snowflake ID), corte em uma página e busque o conteúdo. A paginação precisa de cursor, não de OFFSET, porque o feed muda entre uma página e outra: a aula Cursor-based explica o motivo, e Limit - Offset mostra o que dá errado quando se pagina uma lista que cresce.

O fan-out é assíncrono por um motivo: quem publica não deve esperar 200 gravações, nem 2 milhões. A fila (SQS) absorve o pico, e o worker lê o evento, consulta quantos seguidores o autor tem e decide. Como a fila entrega “pelo menos uma vez”, o worker precisa ser idempotente: empurrar o mesmo id duas vezes na lista não pode duplicar o post. Com conjunto ordenado isso sai de graça (o membro repetido só atualiza a pontuação); com lista, o LTRIM limita o dano e o cliente pode deduplicar por id (veja Idempotência). O modelo de eventos em geral está em Arquitetura orientada a eventos.

Sobre o que quebra: o cache de listas é uma otimização, como diz Reliability. Se um nó do ElastiCache reinicia, as listas daquele pedaço somem, e a leitura de milhões de usuários cai no caminho de reconstrução ao mesmo tempo, que é o efeito manada descrito na mesma aula. A defesa é uma trava de reconstrução por usuário (só uma requisição reconstrói, as outras esperam ou recebem um feed parcial), um limite na reconstrução (por exemplo, só os seguidos mais ativos) e réplicas com failover em outra zona de disponibilidade para que perder um nó raramente signifique perder a lista. O ranking por relevância, que o enunciado cita, é um segundo estágio sobre os candidatos que este desenho produz (um modelo reordena os 200 ou 500 ids candidatos); o vault ainda não tem aula sobre isso.

3.4 mapeamento na AWS

A figura mostra o desenho completo, com o caminho de escrita em cima e o de leitura embaixo.

q3 visao geral
figura 10 · q3 visao geral

Comece pelo pacote laranja de cima, “Quem posta”: passa pelo CloudFront e pelo Application Load Balancer até o serviço de posts, que faz o PutItem no DynamoDB (verde) e publica o evento no SQS (roxo). Os workers de fan-out leem a fila, consultam o grafo social no Aurora PostgreSQL (“seguidores?”, ida e volta) e empurram o id para as listas no ElastiCache, com pacotes rápidos e grossos porque é o volume dominante. Embaixo, “Quem lê” faz GET /feed e o serviço de timeline lê a lista (LRANGE) e busca o conteúdo em lote no DynamoDB.

Por que cada serviço. O CloudFront (Amazon CloudFront) termina TLS perto do usuário e cacheia mídia e respostas públicas; o feed personalizado não é cacheável na borda, porque cada usuário tem o seu. O Application Load Balancer distribui para tarefas em ECS (ou EKS, ou EC2). O DynamoDB guarda os posts porque o acesso é por chave (autor e id) e o volume cresce sem parar, e escala por chave de partição. O Aurora PostgreSQL (RDS tem a família de engines gerenciadas) guarda o grafo social, onde há relações e consultas como “quem segue quem” e contagens, e uma tabela com índice por (seguido, seguidor) responde bem (veja Índices); para um grafo no nível de bilhões de arestas, o mais comum é particionar (veja Particionamento). O SQS desacopla a publicação do fan-out e absorve picos. O ElastiCache guarda as listas.

Por que não outros. Postar direto em uma tabela de timeline no DynamoDB (uma linha por seguidor) funcionaria e daria durabilidade, mas escrever bilhões de itens por dia custa muito mais do que listas em memória que podem ser reconstruídas. Lambda para os workers serve a volumes baixos; com 46 mil gravações por segundo, workers em contêineres com lotes (pipeline do Redis) usam as conexões de forma mais eficiente. Kinesis ou Kafka (Kafka) entram se você precisar de reprocessamento do fluxo de posts por vários consumidores (busca, recomendação, analytics), mas para o fan-out de timeline o SQS basta.

A decisão de quem recebe push e quem não recebe aparece na figura da escrita.

q3 escrita push pull
figura 11 · q3 escrita push pull

Leia de cima para baixo: o post é gravado (verde), vira evento (roxo), e o worker consulta a contagem de seguidores. O losango decide. À esquerda, o usuário comum: o pacote laranja grosso leva a “um LPUSH por seguidor”, e na última seta oito pacotes verdes e rápidos representam as N gravações por post. À direita, a celebridade (rosa): um único pacote lento e pontilhado, porque não há nada para copiar, o post fica só no DynamoDB e será buscado na leitura.

E a leitura, que é onde o híbrido se paga.

q3 leitura hibrida
figura 12 · q3 leitura hibrida

O serviço de timeline (terroso, à esquerda) faz duas consultas em paralelo, com pacotes verdes de ida e volta: a lista pré-montada no ElastiCache (LRANGE) e os posts recentes das celebridades que o usuário segue no DynamoDB. Os dois conjuntos de ids vão para a mescla, que ordena por Snowflake e corta em 50. Em seguida, BatchGetItem busca o conteúdo e a resposta sai com um cursor. O ramo rosa é o modo de falha: lista ausente (nó reiniciado ou usuário que não abre o app há semanas), o pacote barrado com X, e a reconstrução por pull, feita uma vez e com trava, cujo resultado entra na mescla como se viesse da lista.

3.5 dicas para a entrevista

Pergunte: feed cronológico ou ranqueado? Qual o atraso aceitável entre o post e a aparição no feed? Quantos seguidores no máximo? Há retweet ou compartilhamento (que é outro tipo de evento no mesmo fan-out)? E apagar ou editar um post: como o id some das listas? (Resposta: a leitura filtra ids cujo post foi apagado, e não se vai atrás das cópias.) Números: 100 milhões de ativos por dia e 10 aberturas dão 12 mil leituras por segundo na média; 20 milhões de posts por dia com 200 seguidores dão 46 mil gravações de lista por segundo; uma celebridade com 30 milhões de seguidores gera 30 milhões de gravações por post.

Trade-offs a dizer em voz alta: push gasta escrita para ganhar leitura; pull faz o contrário; o híbrido troca complexidade por controle de custo; o limite de celebridade é um parâmetro e não uma verdade (um autor pode cruzar o limite com o tempo); consistência eventual no feed é aceitável, no post em si não (o post é gravado de forma durável antes de tudo). Respostas que soam júnior: “uso um join entre posts e seguidores no SQL” (não escala), “faço push para todos” (ignora a celebridade), “faço pull para todos” (ignora a latência de leitura) e “cacheio o feed inteiro com TTL de uma hora” (o feed fica velho e a invalidação é o problema de verdade). Se travar, comece pelo caso de um usuário com 200 seguidores e depois pergunte “e se tiver 30 milhões?”: o híbrido sai da própria pergunta.

3.6 follow-ups prováveis

Como você decide o limite de celebridade? Com dados: a soma de gravações de fan-out por segundo contra a capacidade, e a latência de propagação aceitável. Se o post de um autor com X seguidores leva mais que o atraso tolerado para chegar a todos, X passa do limite. Pode haver três níveis: push completo, push só para seguidores ativos e pull.

O que acontece com um usuário que volta depois de seis meses? A lista dele foi descartada; a primeira abertura reconstrói por pull, com um limite de tempo ou de quantidade, e a partir dali o fan-out volta a alimentar a lista.

E o feed ranqueado? O desenho produz candidatos (algumas centenas de ids); um serviço de ranking pontua e reordena, e o cursor de paginação passa a ser sobre a posição no ranking guardado por sessão, e não sobre o id. Isso muda a paginação: o cursor por id deixa de valer.

Como evitar a manada na reconstrução? Trava por usuário (por exemplo, SET lock:rebuild:42 NX PX 5000 no Redis, com o cuidado de liberar com script; veja Locks Distribuídos com Redis), mais um limite de reconstruções simultâneas no serviço.


4 pagamentos e ledger financeiro

4.1 a pergunta e o que ela mede

“Projete uma plataforma de pagamentos que fale com adquirentes e bancos externos, garanta que ninguém seja cobrado em duplicidade e rastreie cada centavo.” Aqui o entrevistador abandona a velocidade como critério e passa a medir correção. Ele quer ver se você sabe que a rede falha entre o seu sistema e o banco e que, portanto, “não sei se cobrou” é um estado real; se você usa idempotência de verdade (e não só a palavra); se você sabe modelar dinheiro com partidas dobradas e dados que nunca são alterados; e se você escolhe entre saga e commit em duas fases sabendo o que cada um custa.

4.2 como responder em voz alta

“Eu começo pelas garantias, porque dinheiro as define. Pergunto: quais métodos de pagamento, qual o volume (digamos 1.000 pagamentos por segundo no pico), qual a latência aceitável (alguns segundos), se há múltiplas moedas, e principalmente qual é a regra de ouro, que costuma ser ‘nunca cobrar duas vezes e nunca perder um centavo, mesmo que isso custe latência ou disponibilidade’. Isso me diz que escolho consistência acima de disponibilidade neste domínio.

O desenho tem quatro peças. Primeiro, a idempotência na borda: o cliente manda um cabeçalho Idempotency-Key, e a primeira coisa que o serviço faz é gravar essa chave numa tabela com escrita condicional (‘só se ainda não existe’). Se a chave já existe, devolvo o resultado guardado ou digo ‘em andamento’, e nunca inicio uma segunda cobrança. Segundo, o fluxo de negócio é uma saga: reservar no ledger, autorizar no adquirente, efetivar. Cada passo tem uma ação compensatória, porque não existe transação que cubra o meu banco e o banco externo. Eu orquestro com Step Functions Standard, que guarda o estado de cada execução, repete passos com backoff e dispara a compensação em caso de recusa. Terceiro, o ledger: um modelo de partidas dobradas, em que cada transação tem lançamentos de débito e crédito que somam zero, as linhas só são inseridas e nunca alteradas, e um erro se corrige com um lançamento de estorno. Isso fica num PostgreSQL com transações ACID (Aurora), porque o saldo precisa ser consistente. Quarto, a conciliação: um job diário compara o ledger com o arquivo que o banco envia no dia seguinte e abre exceção para qualquer diferença.

O caso difícil é o timeout. Se eu chamo o adquirente e a conexão cai antes da resposta, eu não sei se a cobrança aconteceu. Não posso reenviar às cegas nem dar o pagamento por falho. A saída é mandar ao adquirente a mesma chave de idempotência em cada tentativa e, ao estourar o tempo, consultar o status da transação por essa chave antes de decidir. Só depois de saber, efetivo ou compenso.

Sobre falhas e custo: o Step Functions Standard cobra por transição de estado, e um pagamento de oito transições custa uma fração de centavo; o gargalo real é o adquirente, que é externo, então eu limito a taxa de chamadas a ele, uso circuit breaker e uma fila para absorver o pico. Por último, os eventos pós-pagamento (webhook, e-mail, conciliação) saem por uma fila SQS FIFO agrupada por conta, para que os eventos de uma conta cheguem em ordem.”

4.3 por dentro: como funciona

Imagine uma maquininha de cartão. Você aperta pagar, a tela fica girando e a conexão cai. O dinheiro saiu ou não? Nem você nem a maquininha sabem. O que um sistema de pagamentos faz é transformar esse “não sei” num estado explícito, com procedimento para cada resposta possível. Todo o desenho gira em torno disso.

A primeira ferramenta é a idempotência, explicada na aula Idempotência, que traz inclusive uma tabela de chaves em PostgreSQL com a resposta guardada e o cuidado de escopo por cliente e de expiração. A ideia é que uma operação executada duas vezes tenha o mesmo efeito de uma. O que torna isso possível numa API é uma chave que o cliente gera (um UUID por tentativa de pagamento, reenviado em todo retry) e um registro, do lado do servidor, que associa a chave ao resultado. A parte que costuma errar é a atomicidade entre “a chave é nova?” e “marquei que estou processando”: se forem dois comandos, duas requisições simultâneas passam juntas. A solução é um único comando condicional. No DynamoDB, um PutItem com condição attribute_not_exists(pk) falha para a segunda requisição, e ela lê o registro e responde conforme o estado: IN_PROGRESS (devolve 409 ou espera), COMPLETED (devolve a resposta guardada, a mesma de antes). Em SQL, é uma restrição UNIQUE na coluna da chave. O texto do Gemini propõe também locks distribuídos temporários. Eu evitaria: um lock expira, e um processo lento que perdeu o lock continua executando (a aula Locks Distribuídos com Redis mostra a armadilha da liberação indevida e do tempo de vida), enquanto uma escrita condicional no banco é atômica e durável sem prazo de validade. Também lembre que a tabela de idempotência precisa de TTL (o DynamoDB apaga expirados em geral em até alguns dias, e a leitura filtra os vencidos; para TransactWriteItems, o token do cliente vale 10 minutos após o fim da requisição, outro mecanismo, de nível mais baixo).

O segundo conceito é o , uma ideia da contabilidade com séculos. Cada evento financeiro (uma transação) gera dois ou mais lançamentos: um débito em uma conta e um crédito em outra, de modo que a soma de débitos é igual à soma de créditos. O dinheiro nunca “aparece”: ele sai de uma conta e entra em outra. Duas regras completam o modelo. O lançamento é imutável (só se faz INSERT), porque um histórico que se edita não serve de auditoria, e um erro se corrige com um lançamento novo que o reverte. E o saldo de uma conta é a soma dos seus lançamentos (em geral materializado numa tabela de saldos atualizada na mesma transação, para ler rápido, mas com o ledger como verdade). Valores ficam em inteiros na menor unidade (centavos, em bigint), nunca em ponto flutuante.

Eu testei as invariantes num PostgreSQL 17.11 em container local. A restrição de soma zero é um CONSTRAINT TRIGGER ... DEFERRABLE INITIALLY DEFERRED, que verifica no COMMIT (depois que todos os lançamentos da transação entraram):

CREATE FUNCTION check_balanced() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE diff bigint;
BEGIN
  SELECT COALESCE(SUM(CASE direction WHEN 'D' THEN amount_cents ELSE -amount_cents END),0)
    INTO diff FROM entries WHERE tx_id = NEW.tx_id;
  IF diff <> 0 THEN
    RAISE EXCEPTION 'transacao % desbalanceada: diferenca de % centavos', NEW.tx_id, diff;
  END IF;
  RETURN NULL;
END $$;
CREATE CONSTRAINT TRIGGER entries_balanced AFTER INSERT ON entries
  DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION check_balanced();
CREATE TRIGGER entries_immutable BEFORE UPDATE OR DELETE ON entries
  FOR EACH ROW EXECUTE FUNCTION no_change();  -- função que levanta exceção

Resultados: uma transação com débito de 5000 e crédito de 5000 passou; outra com 5000 contra 4900 falhou no COMMIT com “transacao 2 desbalanceada: diferenca de 100 centavos” e foi revertida; um UPDATE em entries falhou com “entries e append-only”; e inserir uma segunda transação com a mesma idem_key falhou por violação da restrição UNIQUE. As tabelas e os índices que uso aparecem na figura 16, e as propriedades que garantem tudo isso estão em ACID e Transação; para o que acontece com duas transações concorrentes na mesma conta, veja MVCC. O deadlock entre transferências cruzadas (A para B e B para A ao mesmo tempo) se evita travando as contas sempre na mesma ordem, o que o desafio Stone - Financial Transfer & Deadlock Prevention resolve em Go, e o desenho de carteira com validações está em PicPay - Digital Wallet & Payment Gateway; para o fluxo de autorização com regras, Nubank - Authorizer.

O terceiro conceito é a saga contra o commit em duas fases. A aula Saga Pattern traz a comparação completa. O resumo para a entrevista: o commit em duas fases (2PC) faz todos os participantes votarem e depois confirmarem, o que dá consistência forte mas exige travas mantidas durante a rede e depende de cada participante falar o protocolo. Um banco externo não vai participar do seu 2PC. A saga divide o processo em passos locais, cada um com uma compensação, e aceita consistência eventual: durante a saga, o sistema passa por estados intermediários visíveis (por isso o lançamento “pendente”). Para pagamentos, isso é natural, porque o próprio cartão funciona assim (autorização e captura são etapas separadas). A orquestração (um coordenador central que dita os passos) é preferível à coreografia (cada serviço reage a eventos) quando o fluxo tem ramos, tempos limite e compensações, porque o estado fica num lugar só e dá para auditar o que aconteceu. O Step Functions é um orquestrador gerenciado, o mesmo papel que o Temporal ou o Conductor têm na lista da aula de saga.

O quarto conceito é a reconciliação. Mesmo com tudo certo, sistemas externos divergem: um estorno que o banco processou e você não registrou, uma cobrança aprovada depois do seu timeout. A conciliação é o seguro: compara o que você acha que aconteceu com o que o banco diz que aconteceu, e trata as diferenças como incidentes. Sem ela, um erro silencioso vira prejuízo que ninguém vê. Ela corre em lote (o arquivo de retorno costuma chegar no dia seguinte, o D+1 do texto original), porque o banco só consolida depois.

4.4 mapeamento na AWS

A figura mostra o caminho de um pagamento do comerciante até o ledger e os eventos pós-pagamento.

q4 visao geral
figura 13 · q4 visao geral

Siga os pacotes laranja grossos: o comerciante manda POST /pagamentos com Idempotency-Key, o API Gateway autentica e a Lambda de entrada faz o PutItem condicional na tabela de idempotência (verde, ida e volta). Se a chave era nova, a Lambda inicia uma execução do Step Functions (pacote roxo grosso). A saga chama o adquirente (cinza, lento, ida e volta, porque é externo e pode demorar), lança no ledger em Aurora PostgreSQL e publica o evento no SQS FIFO, de onde os consumidores tiram webhooks, e-mails e a conciliação.

Por que cada serviço, com o que verifiquei (consulta em 2026-10-22). O Step Functions Standard tem execução de até um ano, histórico consultável por até 90 dias após o término e semântica de execução exatamente uma vez para estados, “a menos que você tenha especificado Retry”, o que, segundo a documentação, o torna adequado a ações não idempotentes como processar pagamentos (mesmo assim, as ações dentro dos passos devem ser idempotentes, porque o retry e o timeout podem repetir a chamada ao adquirente, o que é mais um motivo para a mesma chave seguir em todas as tentativas). Cobra por transição de estado: US$ 0,000025 por transição na região US East (N. Virginia), com 4.000 transições gratuitas por mês. Uma saga de 8 transições custa então US$ 0,0002 por pagamento, algo como US$ 200 por milhão de pagamentos (conta minha, na tarifa dessa região; confira a sua). O Express é cobrado por requisições e duração, dura no máximo 5 minutos e, no modo assíncrono, tem semântica de pelo menos uma vez, por isso não é o tipo para orquestrar pagamentos. Uma curiosidade útil: usar a chave de idempotência como nome da execução evita duplicatas apenas enquanto a execução está em andamento, porque a documentação diz que, concluída a primeira, o segundo início lança uma exceção; a tabela de idempotência segue sendo necessária. O Aurora PostgreSQL (na família RDS) dá as transações ACID e Multi-AZ para o ledger. O SQS FIFO ordena dentro de um grupo de mensagens (MessageGroupId), e aí vale o cuidado de usar o id da conta como grupo, para que os eventos de uma conta saiam em ordem e contas diferentes sejam processadas em paralelo; sem lotes, o limite é de 300 transações por segundo por ação de API por partição, com lotes de 10 chega a 3.000 mensagens por segundo, e o modo de alto throughput tem limites bem maiores que variam por região (por exemplo, até 70.000 TPS em US East N. Virginia, com a observação de que são limites de chamadas de API). A deduplicação do FIFO funciona numa janela de 5 minutos (valor da documentação do SQS que eu não reabri nesta consulta), então ela não substitui a tabela de idempotência, cuja chave vive por dias.

Por que não outros. O Amazon QLDB, que poderia parecer a escolha óbvia para um ledger, teve o suporte encerrado em 31 de julho de 2025, e a AWS recomenda migrar para o Aurora PostgreSQL; não o use em desenho novo. DynamoDB transacional (TransactWriteItems, até 100 ações e 4 MB, com isolamento serializável entre operações transacionais) dá ACID dentro de uma região e é uma opção válida para ledger de acesso por chave, mas saldos agregados e consultas de auditoria por período ficam mais naturais em SQL, e as transações do DynamoDB não valem entre regiões numa tabela global. SQS Standard para o fluxo de pagamento dá pelo menos uma vez e sem ordem, o que complica a ordem por conta. Lambda em cadeia (uma chamando a outra) esconde o estado e a compensação; é o que o Step Functions existe para resolver.

A figura seguinte é a saga em si, como uma máquina de estados.

q4 saga step functions
figura 14 · q4 saga step functions

Leia de cima para baixo. Os passos 1 e 2 são sequenciais (reservar no ledger com um lançamento pendente, autorizar no adquirente com Retry e TimeoutSeconds). O losango tem três saídas. Aprovado leva a “Efetivar” (verde). Recusado leva a “Compensar” (rosa), que faz um lançamento de estorno em vez de UPDATE. Timeout, o caso difícil, vai a “Consultar status” (laranja, lento) e só então decide, com a mesma chave. Os dois ramos desembocam no passo 4, que grava a resposta na tabela de idempotência, e todo retry posterior recebe essa mesma resposta (caixa laranja no fim).

O que acontece quando o cliente repete a requisição por timeout dele é o próximo quadro.

q4 idempotencia retry
figura 15 · q4 idempotencia retry

A Tentativa 1 chega, a Lambda grava a chave como IN_PROGRESS (2) e inicia a saga (3). A saga fala com o adquirente (4) e grava a resposta (5), mas a resposta ao cliente se perde (o pacote barrado com X, “perdida”). A Tentativa 2 usa a mesma chave (6), a Lambda lê o registro COMPLETED (7, ida e volta) e devolve a mesma resposta (8) sem iniciar outra saga. O adquirente só viu uma autorização.

O modelo de dados do ledger:

q4 ledger erd
figura 16 · q4 ledger erd

transactions (com idem_key única) tem de um a muitos entries, e accounts também tem muitos entries; as chaves estrangeiras (ícone de chave vazada) em entries ligam as duas. A caixa de invariantes resume as três regras: débito igual a crédito por transação, só INSERT, e erro se corrige com estorno. O pé-de-galinha de cardinalidade pode não aparecer no desenho renderizado; o rótulo de cada linha diz a cardinalidade.

E a conciliação:

q4 reconciliacao
figura 17 · q4 reconciliacao

O EventBridge Scheduler dispara a Lambda conciliadora todo dia depois que o arquivo do banco chega ao S3 (D+1). A Lambda lê o arquivo e os lançamentos confirmados do dia no Aurora e compara por chave. O losango separa o que bate (verde, muitos pacotes, o caso comum) do que não bate (rosa, um pacote lento): cada diferença cai numa fila de exceções e a equipe de operações decide, e o ajuste vira um lançamento novo, nunca uma edição.

4.5 dicas para a entrevista

Pergunte: quais métodos de pagamento? Autorização e captura são separadas? Há reembolso parcial? Multi-moeda? Qual a taxa de erro aceitável do adquirente? Que prova de auditoria o regulador exige? Números: 1.000 pagamentos por segundo no pico dá 86 milhões por dia; com 8 transições por saga, 8.000 transições por segundo no Step Functions (confira as cotas de início de execução e de transição da sua região antes de afirmar que cabe, o que eu não verifiquei); um lançamento de ledger pesa algumas dezenas a poucas centenas de bytes, então um bilhão de lançamentos cabem em centenas de GB (estimativa grosseira, sem índices).

Trade-offs: consistência contra disponibilidade (aqui, consistência), saga contra 2PC (eventual contra forte, mas 2PC não existe com banco externo), orquestração contra coreografia (clareza contra desacoplamento), banco relacional contra DynamoDB (consulta de auditoria contra escala por chave). Respostas que soam júnior: “uso um lock distribuído para evitar duplicidade”, “se der timeout, reenvio”, “guardo o saldo numa coluna e faço UPDATE saldo = saldo - x” (sem histórico e com disputa), “float para dinheiro”, “o SQS FIFO já garante exatamente uma vez, então não preciso de idempotência” (a deduplicação é de 5 minutos e só do lado do SQS) e “o Step Functions garante exatamente uma vez, então dispensa idempotência” (as ações dentro dos passos podem repetir com Retry). Se travar, volte ao timeout: “o que eu sei e o que eu não sei neste instante?”. Quase todo design de pagamentos sai de responder a isso por etapa.

4.6 follow-ups prováveis

E se o adquirente ficar fora do ar por uma hora? Circuit breaker aberto, pagamentos novos entram numa fila e recebem 202 Accepted com status pendente (se o produto tolera), ou falham rápido com 503 para o comerciante tentar depois; a saga em andamento espera com Retry e backoff ou espera um callback (waitForTaskToken, que o Standard suporta e o Express não). Veja Resiliência com Circuit Breaker e Retry.

Como você garante que o saldo não fica negativo com dois pagamentos simultâneos na mesma conta? Travando a linha de saldo (SELECT ... FOR UPDATE) ou com um UPDATE condicional (WHERE saldo >= valor) na mesma transação do lançamento; as contas são travadas numa ordem fixa para evitar deadlock.

Como você faz um estorno? Um lançamento novo com débito e crédito invertidos, ligado à transação original por um campo de referência, e uma saga própria se o estorno também passa pelo adquirente. Nada se apaga.

Como você observa isso em produção? Métricas por passo (taxa de recusa, de timeout, duração do adquirente) e rastreamento distribuído com o mesmo id de correlação da chave de idempotência; veja Tracing Distribuído e OpenTelemetry e SRE para SLOs. A conciliação diária é o alarme de última linha.


5 streaming de vídeo global

5.1 a pergunta e o que ela mede

“Projete um sistema em que criadores enviam vídeos de vários gigabytes e milhões de pessoas assistem com reprodução fluida, em TV, celular e navegador, com qualidade que muda conforme a conexão.” O entrevistador quer ver se você separa três problemas que parecem um só: receber um arquivo enorme sem travar o servidor, transformar esse arquivo em várias versões e entregar cada pedaço de vídeo a milhões de pessoas com baixa latência e custo controlado. Ele também mede se você percebe que a maior parte do custo não está em processar, e sim em armazenar e transmitir bytes, e se você sabe o que um player de vídeo faz com a informação que recebe.

5.2 roteiro para falar

“Eu começaria perguntando quais dispositivos e resoluções precisam ser suportados, se existe transmissão ao vivo ou só vídeo sob demanda, e quanto tempo o criador aceita esperar até o vídeo ficar disponível. Essas respostas mudam bastante o desenho. Vou assumir sob demanda, com vídeos de até algumas horas e milhões de espectadores.

O primeiro ponto é o upload. Eu não quero que o arquivo passe pelos meus servidores de aplicação. O cliente pede uma URL pré-assinada ao backend, e envia as partes diretamente para o S3 usando multipart upload, o que permite retomar um envio interrompido. O backend só autoriza e registra o vídeo com estado ‘enviando’.

Quando o último pedaço chega, o S3 emite um evento e um fluxo de trabalho (Step Functions) manda um segmentador cortar o vídeo em pedaços curtos, de uns seis segundos, e transcodificar cada pedaço em várias resoluções, por exemplo 1080p, 720p e 480p, em paralelo. O resultado é um conjunto de segmentos e uma lista de versões em HLS ou DASH. Essa lista é o que permite o adaptive bitrate: o player baixa um segmento, mede a banda e escolhe a resolução do próximo.

O terceiro ponto é a entrega. Os segmentos vão para um CDN, com cache nas bordas perto dos espectadores. Como a maior parte do tráfego se concentra em poucos vídeos recentes, a borda absorve quase tudo e a origem recebe uma fração pequena dos pedidos. Vídeos antigos e pouco vistos migram para classes mais baratas de armazenamento, e a tabela de metadados (título, descrição, estado) fica separada dos blobs de mídia, porque são padrões de acesso diferentes.

Por fim, falo do que quebra. Se a transcodificação de um pedaço falha, só esse pedaço é refeito, com retry e backoff, e o vídeo não fica pronto até todas as partes estarem prontas. Se o CDN não responde, o player cai para uma resolução menor no mesmo CDN ou troca de borda, e o usuário vê qualidade menor, não tela preta.”

5.3 por dentro: como funciona

Comece pelo upload, porque ele mostra a regra mais importante do desenho: arquivo grande não passa pelo servidor da aplicação. Um servidor HTTP ocupa uma conexão e memória enquanto o arquivo passa por ele, e a banda de subida é cobrada e limitada no servidor. Com URL pré-assinada, o servidor emite uma permissão temporária e sai do caminho. A aula Upload de Arquivos para AWS S3 cobre o lado do código desse envio. Os limites exatos (tamanho mínimo de parte, quantidade máxima de partes por envio) estão na documentação do S3 e não foram reconferidos para esta nota: é conhecimento geral.

Depois do upload vem a transcodificação. O motivo de cortar o vídeo em segmentos curtos é que o player precisa poder trocar de qualidade a cada segmento, e isso só é possível se cada segmento começar em um ponto que o decodificador consiga ler sem depender do anterior. Na prática, o corte é feito nos limites de (grupo de quadros), e um GOP de cerca de seis segundos é um valor comum. A figura abaixo mostra o que acontece quando os segmentos são processados em paralelo e um deles falha.

02 transcodificacao paralela
figura 18 · 02 transcodificacao paralela

Olhe primeiro o vídeo mestre à esquerda, que é cortado e distribuído em quatro workers, um por pedaço. Os pacotes laranja mostram a distribuição, e os verdes mostram os pedaços que voltaram prontos e foram juntados pelo empacotador, que monta a lista de versões. O pedaço D, em rosa, falhou na resolução 720p; ele não bloqueia os outros, mas o empacotador só fecha a lista quando todos existem, então a pergunta certa é quanto tempo o retry demora. A linha pontilhada rosa é o Step Functions refazendo só essa etapa com backoff.

A entrega depende de cache em camadas, e cada camada tem um papel diferente. A borda (edge) está perto do espectador e guarda o segmento que alguém já pediu. Quando a borda erra (miss), o pedido sobe para uma camada regional, que junta os erros de várias bordas. A última barreira é o , que colapsa pedidos iguais que chegam ao mesmo tempo, de modo que a origem no S3 recebe um GET por segmento, não um por espectador. A figura mostra exatamente isso.

03 cache em camadas
figura 19 · 03 cache em camadas

Os hits são os pacotes laranja que voltam direto da borda, e isso explica por que um lançamento popular não derruba a origem. Os misses são os pacotes amarelos pontilhados, que sobem devagar e se juntam no Origin Shield. Repare que o espectador de Frankfurt, na última linha, paga o caminho inteiro na primeira vez, e as próximas pessoas da mesma região já pegam o segmento perto. Essa é a intuição para explicar por que a CDN não é um enfeite: ela transforma milhões de pedidos em poucos pedidos à origem.

O último mecanismo é o custo de guardar vídeo. Vídeo antigo quase não é visto, e pagar a classe quente do S3 por ele é desperdício. Mas a popularidade de um vídeo não segue uma linha reta: alguns voltam a viralizar meses depois. A figura mostra a curva típica e por que existe uma classe que move o objeto sozinho.

04 curva de tiering
figura 20 · 04 curva de tiering

A curva em tom terroso cai rápido, e as faixas mostram as classes do S3 com seus períodos mínimos de permanência. A curva rosa tracejada é o vídeo que volta a viralizar, e é exatamente o caso que torna a escolha da classe difícil: se você move para a classe fria e ele volta a ser visto, paga a recuperação. Os valores de período mínimo e de cobrança mínima por objeto são de conhecimento geral e não foram conferidos hoje; a curva é ilustrativa, não medida. O Intelligent-Tiering existe para esse caso de padrão imprevisível.

Para o mecanismo de cada componente, vale ler Amazon CloudFront e CDN antes da entrevista, e S3 para as classes de armazenamento.

5.4 mapeamento na AWS

A figura geral junta os três fluxos. O criador (laranja) pede uma URL ao API Gateway, que chama uma Lambda para assiná-la. Em seguida, envia as partes direto ao S3 bruto. A partir do evento de criação, o EventBridge inicia o Step Functions, que cria o job no MediaConvert e registra o estado no DynamoDB. As saídas voltam ao S3 de saída, e o CloudFront entrega aos espectadores.

01 upload e entrega
figura 21 · 01 upload e entrega

Siga o pacote laranja de cima para baixo, da URL até o espectador. Os pacotes verdes são os segmentos que o MediaConvert grava de volta no S3, e a linha pontilhada que vai para as camadas frias mostra a política de ciclo de vida, que não passa pelo fluxo de entrega.

Por que cada serviço. O S3 guarda blobs grandes de forma durável e barata, e é o único lugar onde o upload de vários gigabytes faz sentido sem passar pela aplicação. Não usaria EC2 como porta de entrada, porque a banda e o disco da instância viram gargalo e custo. Lambda assina a URL e dispara o fluxo, mas não transcodifica: uma função tem limite de tempo de execução (conhecimento geral: 15 minutos), e vídeo longo não cabe nele. Para a transcodificação, o MediaConvert entrega a escada de versões e o empacotamento como serviço gerenciado; a alternativa é rodar ffmpeg em contêineres (Fargate, ECS ou EKS), que dá controle total e exige operar a fila e as máquinas. O DynamoDB guarda o estado do vídeo porque a leitura é por chave e o volume de metadados é pequeno e previsível. O CloudFront entrega os segmentos com cache de borda; um CDN de terceiros também serve, e a escolha depende de contrato, de integração e de onde estão os espectadores.

Não há aula no vault sobre MediaConvert, EventBridge ou Step Functions em detalhe; a aula Lambda cobre o limite de execução e Arquitetura orientada a eventos cobre o modelo de eventos.

5.5 dicas de entrevista

Faça perguntas antes de desenhar: sob demanda ou ao vivo, tamanho máximo de arquivo, dispositivos, tempo aceitável até o vídeo ficar disponível e se existe DRM. Ao vivo muda o desenho inteiro, porque os segmentos precisam sair em tempo real e a latência passa a ser um requisito de produto, e você não tem como esperar o arquivo completo.

Para estimar, pegue um bitrate médio e declare a premissa. Com 5 Mbps, uma hora de vídeo ocupa cerca de 2,25 GB (5 milhões de bits por segundo vezes 3.600 segundos, dividido por 8 bits por byte). Multiplique pelas versões de resolução, pelo número de vídeos e pelo tempo de retenção, e você tem a ordem de grandeza do armazenamento. A banda de saída é a parte mais cara, então estime-a separadamente, com visualizações simultâneas de pico.

O trade-off central é o custo de transcodificar tudo em todas as versões contra o custo de transcodificar sob demanda. Transcodificar tudo gasta com vídeos que ninguém vai ver; transcodificar só quando alguém pede aumenta a latência do primeiro acesso. Mostrar as duas opções e escolher uma com motivo é o que o entrevistador procura.

Armadilhas de resposta júnior: fazer o upload passar pelo backend, guardar um único MP4 e servir sem adaptação de qualidade, esquecer o CDN e deixar a origem tomar todo o tráfego, e usar Lambda para transcodificar vídeo longo sem falar do limite de tempo. Se você travar, diga o que faria primeiro: “vou garantir que o upload não passe pelo servidor, depois falo da entrega”.

5.6 follow-ups

Como o desenho muda para transmissão ao vivo? Os segmentos são gerados em tempo real, pequenos e publicados à medida que existem, e a latência de ponta a ponta passa a ser o principal requisito. A transcodificação precisa rodar em fluxo contínuo, não em job por arquivo.

Como reduzir o custo do armazenamento de vídeos antigos? Use classes frias e remova versões de resolução pouco usadas, recriando-as sob demanda se alguém pedir. Declare o custo de recuperação como o preço dessa decisão.

Por que não um único formato com qualidade fixa? Porque a banda do espectador varia no meio do vídeo. Com uma única qualidade, quem perde conexão trava, e quem tem conexão boa paga por bytes que não precisa.

6 banco chave-valor distribuído

6.1 a pergunta e o que ela mede

“Projete um armazenamento chave-valor que escale horizontalmente até petabytes e responda em menos de 10 ms para leituras e escritas.” O entrevistador quer saber se você consegue explicar o caminho de uma chave, desde a chegada até o disco, e em cada passo dizer o que está sacrificando. Ele observa se você fala de particionamento antes de replicação, se entende que é uma escolha de consistência e não um detalhe de implementação, e se sabe o que muda quando um servidor cai no meio de uma escrita. A pergunta também testa uma decisão de engenharia: quando vale construir esse sistema e quando é melhor usar um serviço pronto.

6.2 roteiro para falar

“Antes de desenhar, eu perguntaria o padrão de acesso: é sempre por chave, sem consulta por faixa ou junção? Qual a proporção de leitura e escrita? Precisa de consistência forte em todas as operações ou só em algumas? E qual durabilidade é exigida? Vou assumir acesso só por chave, carga equilibrada entre leitura e escrita, e durabilidade com cópias em zonas diferentes.

Para distribuir os dados, uso consistent hashing com nós virtuais. Cada chave vira um número, e esse número cai em um ponto de um anel. Cada servidor físico ocupa muitos pontos desse anel, os nós virtuais, e isso espalha a carga de forma uniforme. Quando um servidor entra ou sai, só as chaves vizinhas dele mudam de dono, em vez de quase todo o cluster.

Para replicar, cada chave vai para N réplicas em zonas diferentes. Uma escrita só é confirmada quando W réplicas respondem, e uma leitura consulta R réplicas. Se R + W for maior que N, a leitura sempre encontra pelo menos uma réplica com a última escrita confirmada. Com N = 3, W = 2 e R = 2, isso vale; com W = 1 e R = 1, não vale, e a leitura pode trazer um valor antigo. Essa é a alavanca de consistência, e o cliente escolhe o custo por operação.

Para o disco de cada nó, eu escreveria primeiro um log sequencial (), barato e durável, e depois atualizaria a estrutura de índice. Se a carga for de muitas escritas, uma é a escolha clássica; se for de leituras por chave com atualização no lugar, uma com log costuma ser mais previsível. Eu justificaria a escolha pelo padrão de acesso, não por preferência.

Por fim, as falhas. Se um servidor some, a escrita continua com as outras réplicas, e quando ele volta, a sincronização recupera o que perdeu, por leitura de reparo ou comparação de árvores de hash. Se uma partição de rede separa grupos de servidores, o sistema precisa escolher entre responder com dado possivelmente antigo ou recusar a operação, e essa escolha depende de quem é o cliente.”

6.3 por dentro: como funciona

O ponto de partida é distribuir chaves. A solução ingênua é servidor = hash(chave) mod N. Ela funciona enquanto N não muda, mas ao acrescentar um servidor quase todas as chaves mudam de dono, e o sistema inteiro precisa copiar dados. O consistent hashing resolve isso colocando servidores e chaves no mesmo anel: a chave pertence ao primeiro ponto encontrado no sentido horário, e um servidor novo só assume as chaves entre ele e o ponto anterior. Com nós virtuais, cada servidor ocupa muitos pontos, e a lista de réplicas de uma chave precisa pular pontos do mesmo servidor, para que as cópias fiquem em máquinas diferentes.

02 anel de hash
figura 22 · 02 anel de hash

Siga o pedido laranja até C1, que percorre o anel no sentido horário e escolhe A2 e B2 como as outras réplicas. A linha rosa mostra o caso proibido: C2 pertence ao mesmo servidor C, então é pulado.

Por que nós virtuais e não um ponto por servidor? Com um ponto só, a distribuição depende de onde cada servidor caiu no anel, e alguns ficam com muito mais chaves. A figura mede essa diferença em duas situações, ao entrar um quinto servidor e no servidor mais carregado de um cluster de dez. Os números são os que a figura traz; a simulação que os gerou não está descrita na fonte, então leia-os como ordem de grandeza.

03 nos virtuais medidos
figura 23 · 03 nos virtuais medidos

A barra da esquerda mostra a fração de chaves que mudam de dono: 80% com hash mod N, 41% com um por servidor, 21% com 128 nós virtuais, contra o ideal de 20%. A barra da direita mostra que o servidor mais carregado, dividido pela média, cai de 2,26 para 1,13 com 128 nós virtuais. O ganho está na variância da carga, e não na média, e é isso que vale dizer na entrevista.

Agora, a escrita replicada. Um cliente envia o pedido ao roteador, que consulta onde a chave mora. O líder da partição grava no log e replica para as outras zonas. A figura segue o caminho que um artigo técnico da AWS descreve para o DynamoDB; a documentação de uso não detalha esse protocolo.

01 dynamodb por dentro
figura 24 · 01 dynamodb por dentro

Siga o pacote laranja da aplicação ao roteador e ao líder, que replica para a zona a e para a zona c. O OK sai quando duas de três confirmam, e a réplica tracejada da zona c pode chegar depois sem atrasar a resposta. A seta roxa tracejada para o índice secundário global mostra que ele é atualizado depois, por isso consultas por esse índice são eventualmente consistentes.

Quórum tem uma consequência prática que a figura seguinte deixa visível. Com N = 3 e W = 2, a escrita confirmada atinge duas réplicas, e uma leitura que consulta duas réplicas sempre encontra uma delas, porque dois subconjuntos de tamanho dois dentro de três se cruzam. Quando R + W não passa de N, os conjuntos podem não se cruzar, e a leitura pega uma réplica atrasada.

04 quorum
figura 25 · 04 quorum

À esquerda, R + W = 4 e o leitor encontra a versão nova na réplica 2. À direita, R + W = 2, o leitor cai na réplica 3, atrasada, e recebe um valor que já foi substituído. A réplica 3 aparece em estado degradado: não está fora do ar, só atrasada, e o sistema precisa decidir se tolera isso.

Falta o disco. Há duas famílias. Na LSM-tree, a escrita vai para um log e para uma memória ordenada; essa memória é descarregada em arquivos imutáveis, que um processo de compactação funde depois, e as leituras consultam filtros de Bloom antes de abrir arquivos. Na B-tree, a escrita atualiza a página no lugar, e o log sequencial garante a durabilidade.

05 lsm contra btree
figura 26 · 05 lsm contra btree

A linha de cima mostra a LSM, com mais etapas na escrita e um trabalho de compactação que roda em segundo plano. A linha de baixo mostra a B-tree, que atualiza a página no lugar e lê por um caminho só. A figura atribui a B-tree aos nós do DynamoDB, seguindo o artigo de 2022; isso não é detalhado pelo serviço, e não há aula sobre LSM ou B-tree no vault, então trate essa atribuição como a conferir na fonte antes de repeti-la como fato.

Para entender por que nada disso aparece na API, veja o que o cliente vê: você faz PutItem e recebe 200 OK. Não escolhe servidor, não configura o anel, não decide a compactação. Os mecanismos são reais e valem para a entrevista, mas o serviço os esconde, e essa é a diferença entre desenhar o sistema e usá-lo. A aula O que é uma partição de banco de dados (database partition)? cobre o conceito de partição sem depender de produto.

6.4 mapeamento na AWS: o DynamoDB resolve cada peça

O Gemini não deu mapeamento para esta pergunta. Abaixo, cada peça do desenho acima e o que o DynamoDB faz de fato, com a parte que continua sendo sua decisão. Os nomes internos vêm do artigo técnico da AWS de 2022; a documentação de uso é a referência para o que você pode configurar.

Particionamento. Você não escolhe anel nem número de servidores. Escolhe a chave de partição, e a AWS distribui os itens entre partições, dividindo-as quando o volume cresce. O que você controla é o desenho da chave: pouca variedade concentra o tráfego em uma partição, que é o problema de chave quente discutido em outras perguntas desta série. Há limites de capacidade por partição na documentação (conhecimento geral, não reconferido hoje), então o cuidado é com o padrão de acesso, não só com o volume total.

Replicação e consistência. Cada item fica em zonas de disponibilidade diferentes. A leitura padrão é eventualmente consistente, e ConsistentRead=true faz a leitura fortemente consistente para a chave pedida, com custo maior. É a escolha de R e W da seção anterior, exposta como parâmetro por operação. Para escritas, há escrita condicional, que serve para evitar duplicidade, como discutido em Idempotência e na nota Complexas 01-04, e transações, que escrevem vários itens num passo atômico.

Conflitos entre regiões. Com global tables, a AWS replica tabelas entre regiões e resolve conflitos pela última escrita (conhecimento geral, não reconferido hoje). A API não expõe vector clocks. Se o seu domínio precisa de resolução própria, modele o conflito na aplicação, por exemplo com número de versão e escrita condicional.

Reparo, falhas e disco. Sincronização de réplicas, detecção de nós mortos e reposição de dados são da AWS. A escolha entre LSM e B-tree também fica dentro do serviço: você influencia pelo padrão de acesso, mas não escolhe o motor.

A decisão que o entrevistador provavelmente quer ouvir é quando usar DynamoDB em vez de construir. A figura resume o raciocínio em três perguntas.

06 dynamodb ou construir
figura 27 · 06 dynamodb ou construir

Comece pela pergunta de cima: se o dado cabe na memória e pode se perder, é cache ou estado volátil, e o caminho é o ElastiCache. Se não cabe ou não pode se perder, a segunda pergunta separa o acesso por chave e faixa, que vai para o DynamoDB, das consultas livres com junções, que vão para Aurora ou RDS. A terceira pergunta, sobre sair da AWS ou ajustar o motor, é o único motivo para Keyspaces, Cassandra ou um sistema próprio, com o custo de operar tudo isso.

Construir só se justifica quando há requisito que o serviço não cobre, e esse requisito precisa estar escrito.

6.5 dicas de entrevista

Pergunte antes de desenhar: padrão de acesso, proporção de leitura e escrita, necessidade de consistência forte, durabilidade exigida, tamanho do item e se existe consulta por faixa. Essas respostas mudam a escolha de disco, de quórum e até de produto.

Para estimar, comece por volume de dados e taxa de operações. Um petabyte com fator de replicação três, mais uma folga de disco que você declara, dá o número de nós por ordem de grandeza. A taxa de escrita, dividida pela capacidade por nó, dá o número mínimo de partições. Declare as premissas, porque o número serve para discutir, não para cravar.

O trade-off central é consistência contra latência e disponibilidade. Consistência forte em tudo encarece cada operação e reduz a disponibilidade durante partições; eventual em tudo complica a aplicação. A boa resposta escolhe por operação e dá um exemplo: saldo de conta usa leitura forte, contador de visualizações usa eventual.

Armadilhas de resposta júnior: dizer “vou usar Cassandra” sem falar de partição, citar CAP como se fosse uma escolha única para o sistema inteiro, escolher LSM sem explicar o padrão de acesso, confundir quórum com consenso (quórum garante interseção entre conjuntos, não ordem global) e esquecer que um nó lento pode causar mais dano que um nó morto. Se você travar, diga a pergunta que faria primeiro sobre o padrão de acesso e siga a partir da resposta que imaginar.

6.6 follow-ups

Por que não usar um banco relacional com sharding? Sharding resolve o volume, mas você passa a cuidar de rebalanceamento, de transações entre shards e de consultas que cruzam shards. Se o acesso é por chave e não há junções, um serviço gerenciado costuma ser mais barato em operação.

O que acontece com a escrita se duas das três réplicas caem? Com W = 2 e N = 3, a escrita falha se só uma réplica estiver disponível. A troca é explícita: o sistema recusa a operação para não confirmar um dado que pode se perder. Com W = 1, a escrita passa, e a chance de perda aumenta.

Por que R + W maior que N não dá consistência forte absoluta? Porque o quórum garante que a leitura enxerga a última escrita confirmada, mas não ordena escritas concorrentes nem resolve leituras feitas no meio de uma escrita ainda não confirmada. Para isso, o sistema precisa de versões ou de um líder com ordem clara.

7 sincronização de arquivos tipo Drive e Dropbox

7.1 a pergunta e o que ela mede

“Projete um sistema de sincronização de arquivos para desktop e celular, com versionamento, deduplicação e envio eficiente de arquivos grandes que foram pouco alterados.” O entrevistador quer saber se você percebe que o custo de sincronizar não é proporcional ao tamanho do arquivo, e sim à parte que mudou. Ele mede se você sabe dividir o arquivo em pedaços de forma que uma edição pequena altere poucos pedaços, se entende que o mesmo conteúdo pode ser guardado uma única vez para muitos usuários, e se trata o conflito de edição offline como um caso normal, não como exceção. Ele também observa se você distingue sincronizar arquivos de editar um documento em tempo real, que são problemas diferentes.

7.2 roteiro para falar

“Antes de desenhar, eu perguntaria o tamanho típico dos arquivos e o máximo, quantos dispositivos por usuário, se o cliente precisa funcionar offline e se existe edição colaborativa em tempo real. Sincronizar arquivos e editar um documento junto com outras pessoas são problemas diferentes, e eu não tentaria resolver os dois com a mesma peça. Vou assumir arquivos de até alguns gigabytes, edição offline e sincronização de arquivos, sem edição simultânea no mesmo conteúdo.

O primeiro ponto é o chunking. O cliente corta cada arquivo em pedaços de cerca de 4 MB, e o corte não é feito em posições fixas. Ele é feito onde o próprio conteúdo indica uma , usando uma função de rolagem como a de Rabin. Assim, inserir um parágrafo no começo do arquivo não desloca todos os pedaços seguintes, e só o pedaço alterado precisa ser enviado.

O segundo ponto é a deduplicação. Cada pedaço recebe um hash SHA-256 e é guardado uma única vez, com o hash como chave. Antes de enviar, o cliente pergunta ao serviço quais hashes já existem, e só sobe o que falta. Se muitos usuários enviam o mesmo arquivo, o conteúdo entra no armazenamento uma vez.

O terceiro ponto é a versão. Uma versão é uma lista ordenada de hashes de pedaços, guardada em um banco de metadados. Um commit cria uma versão nova a partir de uma versão base, e só é aceito se a versão atual ainda for a mesma base. Se não for, há uma edição concorrente, e o sistema não sobrescreve nada: cria uma cópia em conflito e avisa o usuário.

Por fim, a sincronização entre dispositivos. Quando uma versão muda, o serviço publica um aviso, e cada dispositivo pede as mudanças a partir do último cursor que conhece. O aviso é só um sinal; o conteúdo vem depois, pelos pedaços. Se um dispositivo estiver offline, ele pede o que perdeu quando voltar.”

7.3 por dentro: como funciona

O problema de partida é simples de enunciar. Um arquivo de 100 MB com uma linha alterada no meio não deveria custar 100 MB de upload. Mas a divisão mais óbvia, em blocos de tamanho fixo, quebra essa expectativa: se a alteração acontece no começo, todos os blocos seguintes mudam de posição e de conteúdo, porque cada bloco agora começa com bytes diferentes. A figura compara os dois métodos com o mesmo arquivo e a mesma alteração.

02 fatiamento
figura 28 · 02 fatiamento

Compare as duas colunas bloco a bloco. À esquerda, em blocos fixos, os cinco blocos têm hash novo, em rosa, e os cinco são reenviados. À direita, o corte pelo conteúdo cria um hash novo só para o primeiro bloco, enquanto os outros quatro já existem no servidor, em verde, e não são enviados. A diferença está na regra de corte: ela depende do conteúdo vizinho, e não da posição.

A deduplicação é o passo seguinte, e a figura seguinte mostra a conversa entre o cliente e o serviço. O cliente envia a lista de hashes do arquivo novo, o serviço consulta o índice global de blocos e responde quais faltam. Só os blocos faltantes são enviados ao armazenamento de objetos, e a versão nova é gravada como uma lista de hashes.

03 commit e dedup
figura 29 · 03 commit e dedup

Siga o notebook de Ana, que editou o bloco 3. Ela envia os hashes, recebe de volta que só falta h3’, e sobe apenas esse bloco. O notebook de Bruno, com o mesmo arquivo mas que nunca enviou nada, recebe a lista completa do serviço e não sobe nenhum bloco, em verde, porque o conteúdo já está no armazenamento. A versão v7 é só uma lista ordenada de referências, e isso é o que torna o armazenamento compartilhado barato.

Conflito é o caso em que duas pessoas partem da mesma versão e tentam avançar. A figura mostra duas edições offline sobre a versão v6.

04 conflito
figura 30 · 04 conflito

Os dois dispositivos baixaram a v6 e editaram sem conexão. O notebook A volta primeiro, e o commit só é aceito porque a versão atual ainda é v6: a escrita condicional, a mesma ideia de Race Condition e de versionamento otimista, passa. Quando o celular B volta, a versão atual já é v7, a condição falha, e B recebe uma cópia em conflito, em rosa. Nenhuma edição se perde, e a decisão de qual versão manter fica com a pessoa, não com o algoritmo.

Um detalhe que a pergunta costuma cobrar: os blocos são imutáveis e endereçados pelo hash, então um bloco nunca é sobrescrito. Isso torna a deduplicação segura e facilita a recuperação de versões antigas, porque a versão antiga ainda aponta para blocos que existem.

7.4 mapeamento na AWS

A figura mostra os três grupos do sistema: os dispositivos, o plano de controle e os dados. O notebook (laranja) calcula hashes e sobe os blocos novos ao S3; a conversa de metadados passa pelo API Gateway e pelo serviço de sincronização. Quando uma versão muda, o serviço grava no DynamoDB e publica um aviso no SNS, que chega ao celular.

01 sync na aws
figura 31 · 01 sync na aws

Siga o pacote laranja do notebook até o S3 e, em paralelo, o commit até o serviço, que grava a versão nova no DynamoDB. O aviso roxo segue do SNS até o celular, que depois pergunta, em laranja tracejado, quais mudanças existem a partir do cursor que conhece. Essa última pergunta é o que torna a sincronização tolerante a dispositivos que ficam offline por dias.

Por que S3 para os blocos. Ele guarda objetos imutáveis de forma durável e barata, e a chave pelo hash do bloco é exatamente o que a deduplicação precisa. Um detalhe que o texto original do Gemini não deixa claro: multipart upload é útil para objetos grandes, e a recomendação da AWS é usá-lo a partir de cerca de 100 MB (conhecimento geral, não reconferido hoje). Como o bloco aqui tem 4 MB, um envio simples por URL pré-assinada costuma bastar, e o multipart fica para arquivos que não foram quebrados antes.

Por que DynamoDB para metadados e índice. A pergunta “este hash já existe?” precisa responder rápido e em grande volume, e uma consulta por chave é o caso típico do DynamoDB. A escrita condicional da versão é o mecanismo que impede o conflito silencioso da figura de conflito. Usar só o S3 para isso exigiria listar ou consultar objeto por objeto, mais lento e sem a condição atômica.

Por que SNS para avisar. Um usuário pode ter vários dispositivos, e o aviso precisa chegar a todos. Um tópico SNS com inscrição por dispositivo faz esse fan-out sem que o serviço conheça cada conexão. Se o cliente estiver conectado por WebSocket, o aviso pode seguir por esse canal, e o SNS fica para o caso de app fechado, que depende de notificação push.

Por que Fargate para o serviço de sincronização. Ele valida versão e monta a lista de mudanças, com lógica que pode ficar em execução contínua. Lambda serve bem se o tráfego for irregular e as operações curtas, e a escolha depende do padrão de carga, que deve ser declarado.

Não há aula no vault sobre DynamoDB, SNS ou Fargate; S3 e Lambda cobrem os blocos e a alternativa sem servidor, e Upload de Arquivos para AWS S3 cobre o envio pelo lado do código.

7.5 dicas de entrevista

Pergunte antes de desenhar: tamanho típico e máximo dos arquivos, número de dispositivos por usuário, necessidade de funcionamento offline, se há edição colaborativa em tempo real e quanto tempo de histórico de versões precisa ser guardado. A última pergunta muda o custo de armazenamento mais do que qualquer outra escolha.

Para estimar, faça a conta com a taxa de alteração. Se um usuário altera 1% de um arquivo de 1 GB por dia, e o tamanho do bloco é 4 MB, a alteração típica toca poucos blocos, não o arquivo inteiro. Compare esse upload com o de reenviar tudo, e declare a premissa de quantos blocos uma edição toca em média.

O trade-off central é o tamanho do bloco. Blocos pequenos aumentam a chance de deduplicação e reduzem o envio de cada alteração, mas multiplicam o número de hashes, de linhas de metadados e de requisições. Blocos grandes fazem o inverso. Escolher 4 MB, como o desenho, e justificar a escolha pelo custo de metadados e de requisição já é uma boa resposta.

Armadilhas de resposta júnior: dizer “envio o arquivo inteiro de novo” sem falar de chunking, tratar hash de arquivo como suficiente (sem blocos, qualquer alteração reenvia tudo), esquecer que deduplicação entre usuários tem implicação de privacidade, e resolver conflito por “o último que salva ganha”, que apaga edição sem avisar. Se você travar, descreva o caso mais simples, um usuário e um dispositivo, e depois acrescente os outros.

7.6 follow-ups

Como o celular sabe que houve mudança sem ficar consultando o servidor? Com um cursor e uma conexão de longa duração, ou com push. O aviso é só um sinal; quem busca o conteúdo é o cliente, a partir do último cursor, e isso evita perder atualizações quando o dispositivo volta da inatividade.

Deduplicação entre usuários vaza informação? Sim. Quem envia um bloco pode descobrir, pela resposta do índice, se outro usuário já tem aquele conteúdo. A mitigação exige cifragem por usuário ou prova de posse, e o trade-off é perder parte da deduplicação global. Um sistema que decide isso precisa dizer qual escolheu.

E se duas pessoas editarem o mesmo documento ao mesmo tempo, com o texto aparecendo para as duas? Isso é edição colaborativa, e a resposta muda: a sincronização de arquivos gera cópias em conflito, e o editor colaborativo precisa de uma estratégia de fusão das operações, como as usadas em edição colaborativa em tempo real. São duas peças diferentes, e a entrevista costuma querer ver que você separa as duas.

8 venda de ingressos e flash sale

8.1 a pergunta e o que ela mede

“Projete um sistema de venda em que 100.000 pessoas tentam comprar 5.000 ingressos no mesmo segundo, sem vender mais ingressos do que existem e sem derrubar o processador de pagamento.” O entrevistador quer ver três coisas. A primeira é se você reconhece que o problema de concorrência é de estoque, não de tráfego: o sistema pode aguentar muitas requisições e ainda assim vender errado. A segunda é se você consegue proteger o que é caro e frágil, o checkout e o pagamento, empurrando o excesso de demanda para antes deles. A terceira é se você pensa no que acontece com a reserva de quem não paga, porque o estoque precisa voltar sem intervenção humana.

8.2 roteiro para falar

“Antes de desenhar, eu perguntaria se o ingresso tem assento numerado ou é pista, quanto tempo a pessoa tem para pagar depois de escolher, se o processador de pagamento tem limite de chamadas e se o usuário pode entrar na fila antes de a venda abrir. Assento numerado muda o modelo de estoque, então eu assumo ingresso de pista, 5.000 unidades, dez minutos para pagar e um processador que aceita algumas centenas de cobranças por segundo.

O problema central é o overselling: duas pessoas leem que há um ingresso sobrando e as duas compram. Eu resolveria isso no próprio contador de estoque, com um decremento atômico, em vez de ler, verificar e gravar em três passos. O Redis faz essa operação de forma indivisível e devolve o valor depois da subtração, então quem recebe zero ou menos sabe que perdeu a corrida, e o sistema desfaz a própria subtração.

Como o pico é muito maior que a capacidade do checkout, eu colocaria uma sala de espera antes dele. A página de espera vem de uma CDN, para que a origem não sinta o pico. Quem entra recebe uma posição e, quando chega a sua vez, um token assinado que dá direito a entrar no checkout. O checkout só recebe o que aguenta, e o processador de pagamento recebe ainda menos, porque as cobranças passam por uma fila que controla a taxa.

Uma vez no checkout, a pessoa ganha uma reserva com validade de dez minutos. Essa reserva é um registro no Redis com tempo de expiração. Se a pessoa paga, o pedido vai para o banco relacional, onde uma restrição de unicidade por ingresso garante que nenhum ingresso é vendido duas vezes, nem mesmo se o Redis falhar. Se ela não paga, um reconciliador devolve o ingresso ao estoque.

Por fim, falo do que quebra. Se o Redis perde dados, o banco é a fonte da verdade, e o estoque pode ser recalculado a partir dos pedidos confirmados e das reservas ativas. Se o processador de pagamento fica lento, a fila acumula cobranças e o usuário vê uma mensagem de espera, e não um erro. Em nenhum desses casos o sistema vende o que não tem.”

8.3 por dentro: como funciona

O overselling nasce de uma condição de corrida, e vale ver o mecanismo antes de escolher a solução. O código ingênuo lê o estoque, verifica se é maior que zero e grava o valor menos um. Entre a leitura e a gravação há uma janela de tempo, e duas requisições que caem nessa janela leem o mesmo número. A figura compara o código ingênuo com o atômico, com dois clientes disputando o último ingresso.

03 overselling
figura 32 · 03 overselling

À esquerda, os dois clientes leem estoque igual a um, e os dois gravam zero no banco, então duas vendas saem de um ingresso. À direita, cada cliente executa um decremento, e o Redis atende um de cada vez: o primeiro recebe zero e compra, o segundo recebe menos um e é recusado. A seta tracejada do cliente B, bloqueada, mostra que o segundo comando não encosta no mesmo contador enquanto o primeiro não termina. É a mesma ideia de Race Condition vista no nível de rede.

Há três estratégias clássicas. A trava pessimista (um SELECT ... FOR UPDATE na linha do estoque) é correta, mas com cem mil clientes na mesma linha, todos ficam esperando na fila de bloqueio do banco, e a latência explode. A trava otimista (ler a versão, gravar com WHERE version = ? e repetir se falhar) evita o bloqueio, mas com esse volume quase todas as tentativas concorrentes falham e precisam tentar de novo, o que é um efeito de manada no próprio banco. O decremento atômico no Redis evita as duas coisas: cada requisição é uma operação só, e o servidor a executa sem intercalar outra no mesmo contador.

A regra de negócio fica mais segura quando o decremento e a decisão acontecem no mesmo script. O script subtrai, verifica se o resultado é negativo e, se for, soma de volta o que tirou. Um script Lua roda inteiro no servidor, sem outros comandos no meio, e é isso que a figura de reserva mostra no losango de decisão.

02 reserva com ttl
figura 33 · 02

Siga a reserva da esquerda para a direita. O pedido faz o decremento; se o resultado ficou negativo, o caminho rosa é esgotado, e a operação devolve o que tirou. Se ficou positivo, o hold em verde é criado com expiração de dez minutos. Se a pessoa paga antes do prazo, o pedido vai para o banco; se não paga, o caminho rosa leva o ingresso de volta ao estoque.

O ponto que a figura deixa implícito é como o estoque volta. Há uma tentação de usar as notificações de expiração de chave do Redis para isso, e a documentação do próprio Redis avisa que essas notificações são entregues sem garantia: uma desconexão ou um failover pode perder o evento. Por isso o desenho usa um reconciliador periódico, que varre as reservas vencidas e devolve o estoque. Para saber o que venceu sem varrer tudo, uma forma simples é manter um conjunto ordenado pelo instante de expiração, mas isso é uma escolha de implementação, e não algo que a figura mostra.

A segunda parte do problema é o pico. A figura seguinte mostra por que a sala de espera existe.

04 pico e fila
figura 34 · 04 pico e fila

A curva rosa é a chegada de pedidos, que começa muito acima da capacidade e cai com o tempo; a linha verde é o que o checkout aguenta. Na região laranja, o excedente vai para a sala de espera. O balão da esquerda mostra o que acontece sem sala: o excedente vira timeout e retry, e o retry aumenta o pico. O balão da direita mostra o caso com sala, em que o checkout recebe só o que aguenta. Os valores da curva são ilustrativos, e a forma importa mais que os números.

Repare que a sala de espera não é só uma otimização de custo. Sem ela, o próprio mecanismo de retry do cliente transforma uma falha de um segundo em uma sobrecarga de minutos. A fila, junto com o token, quebra esse ciclo, porque o cliente deixa de bater na porta do checkout e passa a esperar em um lugar que o sistema controla.

8.4 mapeamento na AWS

A figura geral junta a borda, a sala de espera, o checkout e os dados. O WAF limita por IP e barra bots. O CloudFront serve a página de espera a partir do S3, sem tocar no backend. Pedidos de entrada passam pelo API Gateway, que valida o token e aplica limite de taxa. A Lambda da fila incrementa a posição no Redis e devolve um token assinado. Quando o token é válido, a Lambda do checkout reserva o ingresso, grava o pedido no Aurora e publica a cobrança numa fila FIFO que controla a taxa de envio ao processador.

01 flash sale na aws
figura 35 · 01 flash sale na aws

Siga o pacote laranja de cima para baixo: os cem mil pedidos do WAF ao CloudFront, e dali ao API Gateway, que deixa passar só quem tem token para a Lambda do checkout. Os pacotes verdes são as operações no Redis e no Aurora, e o roxo é a cobrança que segue pela fila até o processador. A página de espera é servida pela borda, então o pico de leitura não chega ao banco.

Por que cada serviço. A CDN e o S3 absorvem a leitura da página de espera, que é a parte mais barulhenta do pico e não precisa de computação. O WAF limita por IP, porque o volume bruto de um bot se resolve antes do API Gateway. O API Gateway aplica a limitação de taxa e a validação do token, mas a documentação da própria AWS trata a limitação como melhor esforço, então ela não é a barreira de estoque. O Redis guarda os contadores quentes, porque o decremento atômico é a operação mais barata que o problema exige. O Aurora PostgreSQL é a fonte da verdade, e a restrição de unicidade por ingresso impede a venda dupla mesmo sem o Redis. A fila FIFO serializa as cobranças e impede que o processador receba mais do que aguenta; ela tem limite de throughput e precisa ser dimensionada, e esse número deve ser conferido na documentação antes de citá-lo.

Por que não outros serviços. DynamoDB com escrita condicional também faz o decremento com segurança, e seria a escolha se você quisesse durabilidade e operação sem cluster. O Redis fica com o contador porque a latência e o volume de operações do pico são o que mais pesa aqui, e isso precisa ser medido no seu caso. Aurora sozinho, com trava de linha, funciona para 5.000 ingressos, mas com 100.000 tentativas na mesma linha a fila de bloqueio vira o gargalo. Lambda não guarda estado entre invocações, então ela precisa do Redis ou do banco para o contador; não é o lugar do estoque.

Não há aula no vault sobre SQS, DynamoDB ou WAF; Redis, Locks Distribuídos com Redis e Race Condition cobrem o estoque, Amazon CloudFront a página de espera e Lambda as funções. Rate Limiting cobre o limite de taxa do lado da aplicação.

8.5 dicas de entrevista

Pergunte antes de desenhar: assento numerado ou pista, tempo de pagamento depois da reserva, limite do processador de pagamento, se a fila pode começar antes da abertura da venda e se o usuário pode trocar de ingresso. Assento numerado muda o estoque de contador para um conjunto de vagas, e isso muda quase toda a solução.

Para estimar, comece pela demanda e pela capacidade. Cem mil tentativas em um segundo, com 5.000 ingressos, dá uma taxa de rejeição de 95% se nada for feito antes. Se o checkout processa algumas centenas de pedidos por segundo, a sala de espera precisa segurar o restante por minutos, e você deve dizer quantos minutos e qual o tamanho da fila. Declare essas premissas, porque o número muda o desenho.

O trade-off central é justiça contra velocidade. A sala de espera tem custo de experiência: o usuário espera, e a posição pode parecer injusta se for mal explicada. Mas sem ela o sistema derruba o checkout para todos. A boa resposta mostra que você escolheu conscientemente o que perder, e explica como comunicar a espera.

Armadilhas de resposta júnior: ler o estoque e depois gravar, sem atomicidade; usar trava pessimista e achar que o banco aguenta; esquecer da reserva e do que acontece com quem não paga; contar com a notificação de expiração do Redis para devolver estoque; e não limitar a taxa da cobrança no processador. Se você travar, diga primeiro o problema de estoque, porque é o que o entrevistador quer ouvir, e depois o de pico.

8.6 follow-ups

E se o Redis cair no meio da venda? O Redis não é a fonte da verdade: o banco tem uma restrição de unicidade por ingresso, então nenhum ingresso é vendido duas vezes. O estoque no Redis pode ser recalculado a partir dos pedidos confirmados e das reservas ativas, e a perda de reservas em andamento se trata como expiração, com o cliente pedindo uma nova reserva.

Como evitar cobrar duas vezes quando o pagamento responde com timeout? Use uma chave de idempotência derivada da reserva, de modo que repetir a cobrança com a mesma chave não gera nova cobrança. Se o pagamento foi aprovado e a gravação do pedido falhou, o fluxo precisa de uma compensação, o que se descreve com um saga; a aula Saga Pattern e a aula Idempotência cobrem os dois pontos.

Por que não colocar toda a venda numa fila única? Uma fila serializa o acesso e dá ordem, mas o usuário fica sem resposta sobre a posição, e você ainda precisa de reserva e de restrição de unicidade para o caso em que a mensagem é processada duas vezes. A fila resolve a cobrança e a espera; ela não substitui o controle de estoque.

9 encurtador de URLs global

9.1 a pergunta e o que ela mede

“Projete um encurtador de URLs: alguém envia um link longo e recebe um curto, como bit.ly/3xY7kP, e quem clica nele é redirecionado com a menor latência possível, em volume de centenas de milhares de cliques.” Parece um problema de uma linha, e é exatamente essa a armadilha. O entrevistador quer ver se você percebe que o sistema é quase todo leitura, que a geração do código curto precisa ser única sem virar gargalo, e que a escolha de um código de status HTTP (301 ou 302) decide o que a empresa consegue medir depois. Ele também observa se você pergunta sobre expiração de links, alias personalizado, abuso (links de phishing) e privacidade dos dados de clique.

9.2 roteiro para falar

“Antes de desenhar, eu perguntaria quantos links são criados por dia, qual a proporção entre criação e clique, se os links expiram, se o cliente pode escolher o próprio apelido e se as estatísticas de clique são requisito de produto. Essas respostas mudam o desenho. Vou assumir cem milhões de links novos por mês, o que dá cerca de 40 escritas por segundo, e uma proporção de cem leituras para cada escrita. Ou seja, o caminho de leitura é quem merece a atenção.

Para gerar o código curto, eu descartaria a ideia de pegar um hash da URL e cortar os primeiros caracteres. Com sete caracteres em base62 (letras maiúsculas, minúsculas e dígitos), há cerca de 3,5 trilhões de códigos, mas um hash truncado colide com outro hash mesmo com esse espaço enorme, por causa do : com 23 pessoas numa sala já é mais provável que duas façam aniversário no mesmo dia do que não façam. Em vez disso, uso um contador. Uma função reserva um bloco de mil números de uma vez num DynamoDB, com uma operação atômica de soma, e gera os códigos localmente a partir desse bloco. Como cada número só é entregue uma vez, não preciso checar colisão. Uma permutação reversível embaralha os números antes da conversão para base62, para que o código não revele a ordem de criação.

O redirecionamento usa o código HTTP 302, e não o 301. O 301 diz ao navegador que a mudança é permanente, então ele guarda o destino e nunca mais pergunta ao meu servidor, o que é ótimo para carga e péssimo para contar cliques. O 302 diz que o destino pode mudar, e eu deixo o CloudFront guardar a resposta por cinco minutos. Assim, o servidor de origem recebe um pedido por código a cada cinco minutos, e os registros de acesso do CloudFront guardam cada clique para a análise. A troca tem um custo que eu declararia: um mesmo navegador que clica de novo dentro dos cinco minutos aparece uma vez só na contagem da origem, e essa precisão é a que eu escolho abrir mão.

Por fim, o que quebra. Se o DynamoDB fica lento, a borda continua servindo os links quentes. Se um escritor morre com um bloco de números não usados, a faixa se perde, e isso deixa um buraco inofensivo na sequência. Se um link expira, o registro tem um TTL e a borda precisa ter um prazo de cache menor que o prazo do link, ou então uma invalidação explícita.”

9.3 por dentro: como funciona

Comece pelo redirecionamento, porque ele define o resto. Quando o navegador pede GET /3xY7kP, o servidor não devolve a página de destino. Devolve uma resposta de redirecionamento, um código da família 3xx com um cabeçalho Location apontando para a URL longa, e o navegador faz um segundo pedido para lá. O código 301 significa “movido permanentemente” e o 302 significa “encontrado”, que na prática quer dizer “temporário”. A diferença que importa para o desenho é o cache. Por padrão, um 301 pode ser guardado pelo navegador e por caches intermediários; um 302 só é guardado se o servidor disser explicitamente até quando, por um cabeçalho Cache-Control ou Expires. Essa é uma regra da especificação HTTP que eu conheço de cor e que não reconferi hoje, mas ela é estável há anos. A figura abaixo mostra as duas respostas lado a lado.

encurtador 301 contra 302
figura 36 · encurtador 301 contra 302

Olhe primeiro a decisão à esquerda, que é a pergunta de negócio: precisa contar cliques? Se a resposta é não, o caminho cinza leva ao 301, e o navegador do segundo clique (bloco de cache, em terroso) nem chega ao CloudFront, o que é a economia desejada. Se a resposta é sim, o caminho laranja leva ao 302, e cada clique aparece no CloudFront. Repare no rótulo “1 miss” na linha de baixo: a origem só é consultada uma vez por janela de cache, e o log de cada clique segue para o S3 de qualquer forma.

Agora o código curto. A figura seguinte mostra as duas maneiras de gerar o código e por que a segunda vence. O caminho de cima é o hash: a URL passa por MD5, pega sete caracteres em base62 e, se o código já existe, o sistema refaz o cálculo com um sal e tenta de novo. O caminho de baixo é o contador em blocos, que nunca produz código repetido porque cada número sai de uma faixa reservada.

encurtador geracao de id
figura 37 · encurtador geracao de id

Leia a figura pelos dois caminhos e não pelos caixas. A seta rosa tracejada de “Colisão” de volta ao hash mostra o ciclo que o hash obriga a fazer: cada colisão recalcula e volta ao começo. A anotação ao lado da colisão diz que a primeira delas aparece perto de dois milhões de URLs, bem antes de o espaço de 3,5 trilhões parecer cheio. No caminho de baixo, a chamada ao DynamoDB acontece uma vez a cada mil URLs, e o restante é cálculo local, sem nenhum caminho de volta.

Esses dois pontos explicam a escolha. O hash parece simples, mas exige uma consulta ao banco para cada código gerado, com uma tentativa extra a cada colisão. O contador troca essa busca por uma reserva rara, e a unicidade vem da construção, não da verificação.

9.4 mapeamento na AWS

A figura geral mostra os dois fluxos, o de criação em baixo e o de redirecionamento em cima, que é o que importa para a carga.

encurtador visao geral
figura 38 · encurtador visao geral

Siga o pacote laranja da esquerda para a direita. O navegador chega ao Route 53, que aponta o domínio para o CloudFront com um registro ALIAS. O WAF filtra abuso antes da borda. O CloudFront responde com um 302 se o código estiver no cache; quando não está, chama uma função Lambda que consulta o ElastiCache e, se o código também não estiver lá, lê o DynamoDB. Os pacotes verdes mostram o caminho até o DynamoDB, e a seta cinza pontilhada que sai do CloudFront para o S3 mostra o registro de cada clique, que segue para o Athena e não passa pelo caminho de resposta.

Por que cada serviço. O DynamoDB guarda a tabela de código para URL porque o acesso é sempre por chave, a leitura é uma busca direta e o TTL nativo cuida dos links expirados. Um banco relacional funcionaria em volume, mas exigiria um primário para escrita e sharding quando o volume crescesse, e o modelo não precisa de junções. O ElastiCache entra como segunda camada, e aqui a justificativa é honesta: com TTL curto na borda, muitos códigos quentes ainda chegam à Lambda com frequência, e a memória absorve esse excesso. Se o TTL da borda fosse longo, a camada seria peça a mais. A Lambda do contador roda na criação porque a operação é curta, sem estado, e cabe no limite de execução dela. O S3 com Athena guarda os cliques porque um contador de clique por requisição escreveria no banco a cada redirecionamento, o que transforma a leitura na carga de escrita mais cara do sistema.

Há uma nota de Route 53 e outra de Amazon CloudFront no vault. Não há aula sobre DynamoDB, ElastiCache, Athena nem Fargate, então a descrição desses serviços é conhecimento geral deste texto e não foi reconferida hoje.

9.5 dicas de entrevista

Comece por perguntas de escala e de negócio: quantos links por dia, a proporção de leitura, se os links expiram, se há alias personalizado, se o analytics é de tempo real ou pode ser atrasado de uma hora e se existe bloqueio de links maliciosos. A última pergunta muda o desenho, porque um sistema que encurta links precisa de uma etapa de verificação antes de gravar.

Para estimar, declare a premissa e faça a conta na frente do entrevistador. Cem milhões de links por mês são cerca de 40 por segundo, e com cem leituras para cada escrita, a média fica perto de 4 mil leituras por segundo, com pico bem acima disso. O número que mais importa é a sua fração de cache: se a borda serve 95% dos pedidos, a origem vê uma pequena parte do tráfego.

O trade-off central é o 301 contra o 302, e o entrevistador quer ouvir os dois com o que cada um custa. O 301 reduz carga e custo, mas tira do seu sistema a visão dos cliques repetidos. O 302 devolve a visão e aumenta a carga. Escolher um e declarar o que se perde é a resposta que ele procura.

Armadilhas de resposta júnior: usar hash MD5 truncado sem mencionar colisão, usar auto incremento de banco relacional como gargalo global, gravar no banco um contador de cliques a cada redirecionamento, e esquecer que um encurtador é também um vetor de phishing. Se você travar, diga por onde começaria: “vou desenhar o caminho de leitura primeiro, porque é 99% da carga, e depois falo de como gerar o código”.

9.6 follow-ups

Como expirar um link sem deixar o cache servi-lo depois? O prazo de cache na borda precisa ser menor que o prazo do link, ou então é preciso invalidar o caminho no CloudFront quando o link expira. Invalidação tem custo e demora, por isso prefiro um TTL curto.

O contador não vira ponto único de falha? Ele é um item do DynamoDB com uma operação atômica de soma, e o banco replica os dados entre zonas. Se a função morre com um bloco reservado, a faixa se perde, o que custa uma sequência com buracos e nenhum dado errado.

Como funciona o alias personalizado, em que o cliente escolhe o código? A gravação usa uma escrita condicional, que só aceita o item se a chave ainda não existir. Se já existir, a resposta é um conflito, e o cliente escolhe outro apelido.

10 web crawler distribuído

10.1 a pergunta e o que ela mede

“Projete um coletor de páginas da web capaz de percorrer bilhões de URLs, baixar o HTML de cada uma, extrair os links novos e alimentar um índice de busca.” A frase parece pedir um programa que baixa páginas em paralelo, e o entrevistador quer ver se você percebe que o difícil é outra coisa. Ele mede se você entende que o crawler é um cliente indesejado de sites que não são seus, e que um erro de cortesia vira ataque de negação de serviço sem intenção. Também mede se você sabe deduplicar um universo de bilhões de URLs sem guardar todas na memória, e se consegue lidar com armadilhas da web, como calendários que geram páginas para sempre.

10.2 roteiro para falar

“Antes de começar, eu perguntaria quão frescas as páginas precisam estar, se a busca é geral ou restrita a alguns domínios, qual o orçamento de armazenamento e se o crawler deve respeitar o robots.txt sempre, o que eu assumiria sem discussão. Vou assumir dez bilhões de URLs conhecidas, um milhão de páginas por hora em regime, e que o crawler respeita o robots.txt e o atraso pedido por cada site.

O ponto central é que a fila de URLs não pode ser uma fila comum. Ela precisa ter duas garantias ao mesmo tempo: prioridade, porque páginas que mudam muito devem ser visitadas antes, e cortesia, porque um site nunca pode receber duas requisições do mesmo crawler ao mesmo tempo, nem mais rápido do que pede. Eu separaria a decisão em duas etapas. Primeiro um seletor escolhe a próxima URL com viés para a prioridade alta. Depois, a URL vai para uma fila dedicada ao seu host, e um único fetcher por host consome essa fila, respeitando um relógio que diz quando a próxima visita é permitida.

Antes de baixar qualquer página, cada link passa por um filtro de duplicidade. Uso um , que responde se uma URL provavelmente já foi vista, usando alguns bits por elemento em vez de guardar a string inteira. Ele erra de um lado só: às vezes diz que já viu uma URL nova, o que faz perder uma página, mas nunca diz que não viu uma URL que já viu. Com dez bilhões de URLs e uma taxa de erro de cerca de 1%, a conta dá algo perto de 12 GB, e isso cabe em memória distribuída.

Os HTMLs brutos vão para o S3, compactados e organizados por data, e um evento dispara o parser, que extrai os links e os normaliza. Os links novos voltam para a fronteira. A indexação fica fora do escopo da pergunta, e eu diria isso em voz alta.

O que quebra. Um fetcher que morre devolve a URL à fila depois que a visibilidade expira, então nada se perde. Um site que responde devagar é tratado pelo seu próprio tempo limite, sem atrasar os outros hosts. Uma armadilha como um calendário infinito é detectada pela profundidade ou pelo orçamento de páginas por host, e o host inteiro vai para quarentena.”

10.3 por dentro: como funciona

Comece pela cortesia, porque ela muda a fila. Um crawler que tira URLs de uma fila global e distribui para N máquinas parece eficiente, mas um site popular recebe rajadas de pedidos vindos de dez máquinas ao mesmo tempo. O mecanismo certo tem uma fila por host, e cada host tem um relógio. A figura mostra a fronteira inteira, da prioridade até o fetcher.

crawler fronteira politeness
figura 39 · crawler fronteira politeness

Olhe primeiro as três filas da esquerda, que são as de prioridade, e note que a fila de alta prioridade tem mais espaço e mais pacotes saindo. O seletor no meio sorteia com esse viés e entrega a URL escolhida à regra de roteamento, o losango com hash do host módulo N, que decide qual fila por host recebe a URL. À direita, cada fetcher é dono de um host, e a barreira bloqueada que parte do relógio por host e chega ao fetcher de b.com mostra o caso em que o site pediu dez segundos de intervalo: a próxima visita só é permitida depois disso. O fetcher de b.com não é lento; ele está esperando a hora certa, e isso é o que a cortesia exige.

Agora o filtro de duplicidade e a decisão sobre cada link descoberto. A figura seguinte segue um link desde a página até a fronteira, e cada losango é uma pergunta com uma saída de descarte.

crawler decisao url
figura 40 · crawler decisao url

Leia de cima para baixo, e repare que a normalização vem antes de tudo. Uma mesma página aparece com parâmetros de sessão, com fragmentos ou com a ordem de query trocada, e sem normalizar o filtro trataria cada variação como uma URL nova. O robots.txt é perguntado antes do Bloom porque é uma regra do dono do site, e descartar cedo economiza uma consulta. A última pergunta, sobre profundidade e orçamento, é a que pega o calendário infinito: a figura marca em rosa a quarentena do host, e não só do link, porque a armadilha costuma gerar milhares de variações do mesmo padrão.

Por último, a visão geral mostra como as peças se encaixam, e é ela que o entrevistador costuma querer ver primeiro.

crawler visao geral
figura 41 · crawler visao geral

Siga a seta laranja de cima para a direita, das sementes até o site. A fronteira no DynamoDB guarda, por host, a próxima visita permitida e a prioridade, e o scheduler a consulta para liberar apenas URLs vencidas. As setas roxas são a fila com uma URL por host, que é o que impõe a cortesia. O fetcher também consulta o ElastiCache para o robots.txt, e a seta amarela de mão dupla entre os dois mostra essa consulta sendo feita e respondida. Na parte de baixo, o HTML bruto vai para o S3, o parser lê o evento e devolve apenas os links novos ao DynamoDB, e a seta pontilhada que leva ao indexador mostra que essa parte está fora do escopo.

10.4 mapeamento na AWS

Cada serviço responde a uma parte do problema. A fronteira fica no DynamoDB porque o acesso é por host, a ordenação por hora de próxima visita é uma consulta natural e as atualizações podem ser condicionais, para que dois schedulers não liberem o mesmo host. O scheduler roda em Fargate porque é um processo contínuo, sem pico previsível, e não precisa de máquina própria.

A fila é o SQS FIFO com o host como grupo de mensagens. Essa escolha é a mais importante da arquitetura, e a razão é conhecida (conhecimento geral, não reconferido hoje): no FIFO, mensagens de um mesmo grupo são entregues em ordem, e a próxima só aparece depois que a anterior foi removida ou seu tempo de visibilidade expirou. Isso impõe uma URL por host sem nenhum código extra de controle. Uma fila padrão não daria essa garantia, e o Kafka daria, com uma partição por chave, mas o consumo por host e o controle de atraso ficariam por conta da aplicação. Os limites de throughput do FIFO mudam com o modo e com a região, e eu confirmaria a cota antes de prometer um número.

Os fetchers rodam em EC2 Spot porque são stateless e refazem o trabalho sem perda quando a instância é interrompida. A mensagem volta para a fila quando a visibilidade expira. Lambda seria possível, mas as conexões de rede longas e a concorrência alta de milhares de requisições pendentes tornam o custo por tempo de espera pior que o de instâncias. O S3 guarda o HTML compactado porque é barato em volume e tem classes de armazenamento mais frias para dados antigos. O ElastiCache guarda o robots.txt e a resolução de DNS com TTL de 24 horas, e o filtro de Bloom também fica nele, com o comando de existência do módulo de Bloom. A figura indica a versão mínima do motor, mas eu não verifiquei hoje se ela está disponível no ElastiCache, então trate esse detalhe como pendente.

Não há aula no vault sobre SQS, Fargate, Spot ou filtros de Bloom em detalhe. A relação com Kafka existe na nota de Kafka, e a de cortesia com limitação de taxa está em Rate Limiting e Worker Pools e Rate Limiting.

10.5 dicas de entrevista

Comece perguntando sobre frescor, escopo, orçamento e obediência ao robots.txt, porque cada resposta muda o tamanho da frota e a frequência de visita. Um crawler de notícias precisa de visitas frequentes a poucos sites, e um de busca geral precisa de cobertura ampla com visitas espaçadas.

Estime com números. Dez bilhões de URLs num filtro de Bloom com 1% de erro dá cerca de 12 GB, e a fórmula está no glossário para você refazer a conta na entrevista. Um milhão de páginas por hora são cerca de 280 por segundo, e com um atraso de cortesia de um segundo por host, a quantidade de hosts ativos precisa ser grande para que a frota não fique ociosa. Mostre que você sabe que o gargalo real é a espera por host, e não o número de máquinas.

O trade-off central é cobertura contra cortesia. Quanto mais hosts em paralelo, mais páginas por hora, e mais o crawler se aproxima de um ataque de sobrecarga para os sites pequenos. A resposta boa diz como o relógio por host protege sites pequenos sem travar a frota.

Armadilhas de resposta júnior: colocar uma fila global e um pool de threads sem falar de cortesia, guardar todas as URLs visitadas num conjunto em memória, ignorar o robots.txt ou tratá-lo como sugestão, e não normalizar URLs antes da deduplicação. Se você travar, diga: “vou começar pela regra de que nenhum site recebe duas requisições ao mesmo tempo, porque todo o resto depende dela”.

10.6 follow-ups

Como evitar que um site enorme monopolize a frota? Um limite de URLs ativas por host e um orçamento de páginas por host impedem que um domínio gigante consuma tudo. O seletor de prioridade também pode aplicar um teto por host antes de sortear.

E se o filtro de Bloom ficar cheio? Ele perde precisão e começa a descartar mais URLs novas. Por isso ele é dimensionado para o volume previsto, e um filtro novo é criado a cada janela de tempo, com um período de transição em que as duas versões são consultadas.

Como tratar uma página que muda o tempo todo? A frontier guarda uma estimativa de frequência de mudança por URL e reagenda a próxima visita de acordo com o que foi observado, em vez de usar um intervalo fixo para todas.

11 gateway de inferência para LLMs

11.1 a pergunta e o que ela mede

“Projete um gateway corporativo que exponha modelos de linguagem para várias equipes e clientes, com respostas em streaming que podem durar minutos, limites de uso por cliente e controle de custo sobre um parque de GPUs caro.” O entrevistador quer saber se você percebe que um LLM não se parece com uma API comum. Cada requisição dura segundos ou minutos, consome uma quantidade de recurso que só se conhece no fim, e o recurso é escasso (GPU) e caro. Ele observa se você trata o streaming como parte do desenho e não como detalhe, se mede limite em tokens e não só em requisições, e se sabe o que fazer quando o modelo principal degrada.

11.2 roteiro para falar

“Antes de desenhar, eu perguntaria quais modelos precisam ser expostos, se são modelos de terceiros ou próprios, qual o tempo máximo aceitável de resposta, se o cliente pode cancelar uma geração no meio e se existe requisito de isolamento entre clientes. Vou assumir modelos de terceiros como primário, um modelo próprio em GPU como reserva, streaming obrigatório e isolamento por tenant. Vou assumir também meio milhão de requisições por dia, com média de 2 mil tokens por requisição entre entrada e saída.

Eu começaria pelo streaming, porque ele muda a camada de rede. O modelo gera tokens um a um, e o cliente deve recebê-los assim que existirem. Uso Server-Sent Events, que é uma conexão HTTP que fica aberta e recebe eventos em texto. Por isso o balanceador precisa ter um tempo de ociosidade maior que a pausa entre dois tokens, e o gateway não pode guardar a resposta antes de devolvê-la. Qualquer buffer intermediário transforma o streaming numa espera de vários segundos seguida de um bloco de texto, e isso derruba a experiência.

O limite de uso também precisa ser em tokens. Limitar só em requisições por minuto deixa passar um pedido de dez mil tokens de saída com o mesmo peso de uma pergunta curta. Então o gateway reserva, antes da chamada, os tokens de entrada mais o máximo de saída pedido. Se o saldo do cliente não cobre essa reserva, ele recebe um 429 com o tempo de espera calculado. Depois da resposta, a diferença entre o reservado e o usado é devolvida ao saldo. A leitura e o débito precisam ser atômicos, senão duas requisições simultâneas leem o mesmo saldo e as duas passam.

Para a disponibilidade, eu não aceitaria que a GPU própria fosse o único destino. Um disjuntor acompanha o tempo até o primeiro token () e a taxa de erro de cada destino. Quando o primário passa do limite, o tráfego vai para outra região do mesmo modelo, depois para um modelo menor, depois para a GPU própria, e só como último recurso um 429. Essa degradação precisa ser declarada ao cliente e registrada, porque trocar o modelo muda a qualidade da resposta.

Por fim, o cache. Um guarda respostas de perguntas parecidas, e a parte difícil é o limiar de similaridade, porque duas perguntas com vetores próximos podem pedir coisas diferentes. Eu só ligaria esse cache para respostas determinísticas e sempre separado por tenant, para não vazar uma resposta de um cliente para outro.”

11.3 por dentro: como funciona

Comece pelo streaming, porque é a parte que o HTTP comum não cobre. Uma resposta normal tem um tamanho conhecido ou, no mínimo, termina numa hora previsível. Um stream de tokens tem um tempo que não se conhece antes de começar, e a conexão fica aberta todo esse tempo. O protocolo usado é o Server-Sent Events, que usa o tipo de conteúdo text/event-stream: o servidor escreve linhas com o prefixo data: e uma linha em branco separa cada evento, e o cliente lê os eventos na medida em que chegam. A figura abaixo acompanha um pedido do primeiro ao último token.

llm streaming sse
figura 42 · llm streaming sse

Leia da esquerda para a direita, pelo caminho de ida até o Bedrock. O pacote laranja que entra no balanceador carrega o pedido com cinco eventos de volta, e o rótulo do trecho entre o gateway e o Bedrock registra três pedidos e quatro deltas. O ponto de atenção é o balanceador: ele repassa os eventos à medida que chegam, e o rótulo dele diz que a pausa entre tokens precisa ser menor que o tempo ocioso configurado. A seta rosa de baixo mostra o caso em que o cliente fecha a aba no meio, e a conexão precisa ser fechada até o provedor, senão a geração continua e você continua pagando por tokens que ninguém vai ler.

Agora o limite por tokens, que é o mecanismo mais específico desta pergunta. A figura seguinte segue um pedido pelas três etapas: admitir, executar e acertar.

llm limitador de tokens
figura 43 · llm limitador de tokens

A primeira etapa, à esquerda, é a reserva. O pedido declara 2.000 tokens de entrada e um máximo de 4.000 de saída, e o gateway reserva 6.000 do saldo, em um passo atômico no ElastiCache. Se o saldo não cobre, o pacote barrado vai para o 429 com o tempo de espera. A etapa do meio executa e o provedor gera 700 tokens de saída. Na terceira etapa, o uso real volta, e o gateway devolve ao saldo a diferença: 6.000 reservados menos 2.700 realmente usados, o que dá 3.300 de volta. O ponto que a figura quer ensinar é que a reserva é sempre pelo máximo, porque o gateway não sabe quanto o modelo vai gerar antes de gerar.

A última figura trata da falha e mostra o disjuntor escolhendo o destino.

llm fallback gpu degradada
figura 44 · llm fallback gpu degradada

Siga o pacote laranja do cliente até o gateway, e depois o losango, que é a pergunta do disjuntor: o primário está saudável? O alarme em rosa vindo do CloudWatch, por TTFT acima do limite, é o que abre o disjuntor, e a seta barrada até a região A mostra a rota fechada. A seta sólida até a primeira opção indica o destino preferido, e as tracejadas até as opções seguintes indicam as reservas. A última saída, o 429 com Retry-After, aparece em rosa e só é usada quando nenhum destino tem capacidade.

Por fim, a visão geral junta as peças, e é nela que o entrevistador costuma querer ver os componentes.

llm gateway visao geral
figura 45 · llm gateway visao geral

Siga a seta laranja do cliente até o gateway, que decide a rota e não guarda estado próprio, por isso pode ser replicado. Do gateway saem quatro trocas de ida e volta, para a tabela de chaves e cotas, para o contador de tokens no ElastiCache, para o cache semântico e para o modelo. A seta clara e grossa até o Bedrock leva os tokens em stream, e o caminho tracejado até o endpoint próprio só é usado quando o primário degrada. O CloudWatch recebe as métricas por modelo, que é o que alimenta o disjuntor.

11.4 mapeamento na AWS

O Amazon Bedrock é o primário porque entrega modelos de vários fornecedores por uma API única, com a operação de streaming (ConverseStream) e sem que você administre GPUs. Sua escolha de modelo, de região e de preço fica separada do gateway, o que permite trocar o primário sem reescrever os clientes. A alternativa é um endpoint próprio, e ele entra como reserva porque controla o modelo, a quantidade de GPUs e a latência, mas exige operar a capacidade e pagar as instâncias mesmo ociosas. O SageMaker AI é o caminho para esse endpoint próprio. O uso de perfis de inferência entre regiões do Bedrock aparece no diagrama como primeira opção de fallback; a existência e o comportamento desse recurso são conhecimento geral deste texto e precisam ser conferidos antes de virar fala.

O gateway roda em Fargate porque é stateless, escala pelo número de conexões e não exige máquina própria. Uma observação que a pergunta costuma pedir: a conexão de streaming dura o tempo da geração, e um contêiner com muitas conexões abertas precisa de memória e de um tempo de desligamento gracioso que espere as respostas em andamento. O balanceador é o ALB, e não o API Gateway, porque o streaming é um requisito de conexão longa e o ALB repassa a resposta sem armazená-la. Os limites de integração do API Gateway não foram reconferidos hoje, e não é o foco aqui.

O DynamoDB guarda chaves de API, tenant e cotas porque a consulta é por chave e o volume é modesto. O ElastiCache guarda o contador de tokens porque o limitador precisa de latência de sub-milissegundo e de uma operação atômica, e um script Lua faz a leitura e o débito de uma vez. O cache semântico também fica no ElastiCache, com busca vetorial, cuja versão mínima do motor a figura cita e que eu não verifiquei hoje. O CloudWatch recebe o TTFT, o uso de tokens por minuto e os códigos 429 e 5xx por modelo, e esses números alimentam o disjuntor e o alarme.

Não há aula no vault sobre Bedrock, SageMaker, Fargate ou SSE em detalhe. A parte de limitação está relacionada a Rate Limiting e Worker Pools e Rate Limiting, e a de disjuntor a Resiliência com Circuit Breaker e Retry. O cache de respostas se relaciona com Redis.

11.5 dicas de entrevista

Pergunte sobre os modelos, o tempo máximo de resposta, o cancelamento, o isolamento entre clientes e se há uma exigência regulatória sobre onde os dados podem trafegar. Essa última muda a escolha de região e de cache, e é o tipo de pergunta que mostra experiência com o cliente real.

Estime em tokens, não em requisições. Meio milhão de requisições por dia com 2 mil tokens cada é um bilhão de tokens por dia, cerca de 11.600 por segundo em média. Esse número é o que você compara com a cota do fornecedor, e a conta de custo sai da mesma soma. Declare que o pico pode ser três ou quatro vezes a média e que o limite precisa ser dimensionado para ele.

O trade-off central é a qualidade contra a disponibilidade. Trocar para um modelo menor quando o primário degrada mantém o serviço no ar e piora a resposta, e a decisão de qual cliente aceita essa troca é de produto, não só de engenharia. Mostrar essa decisão é o que o entrevistador procura.

Armadilhas de resposta júnior: limitar só por requisições por minuto, fazer buffer da resposta antes de enviá-la, esquecer de cancelar a geração quando o cliente desiste, usar cache semântico sem limiar e sem separação por tenant, e tratar dynamic batching como função do gateway. O batching dinâmico acontece no servidor de inferência, que agrupa as requisições na GPU; o gateway só escolhe o destino. Se você travar, diga: “vou começar pelo streaming, porque ele define a rede, e depois pelo limite de tokens, porque ele define o custo”.

11.6 follow-ups

Como evitar que um cliente abuse do limite de tokens com requisições paralelas? O saldo é debitado pela reserva e não pelo uso, então requisições paralelas não passam do saldo. O atômico no ElastiCache é o que garante isso mesmo com várias instâncias do gateway.

Como trocar de modelo sem afetar o cliente? O cliente fala com um nome de modelo lógico, e o gateway mapeia esse nome para o destino atual. A troca vira uma mudança de configuração, e o disjuntor decide o destino em tempo de execução.

Por que não cachear tudo? Porque uma resposta gerada com temperatura alta, ou dependente de dados do momento, não se repete. Um cache só vale quando a mesma entrada deve produzir a mesma saída, e isso restringe o alcance do cache.

12 plataforma de métricas e observabilidade

12.1 a pergunta e o que ela mede

“Projete uma plataforma que receba métricas de milhares de microsserviços (CPU, memória, latência p99, contadores de erro), armazene tudo e permita consultas e alertas quase em tempo real.” Parece um problema de banco de dados, mas o entrevistador quer ver se você percebe que o volume de escrita é constante e enorme, que o valor de um dado cai com a idade, e que o custo do sistema cresce com o número de combinações de rótulos e não com o número de métricas. Ele observa se você fala de agente local e de buffer antes de falar de banco, se entende cardinalidade, e se consegue explicar por que dados antigos devem ser agregados e movidos para um armazenamento mais barato.

12.2 roteiro para falar

“Antes de desenhar, eu perguntaria quantos hosts e métricas existem, com que frequência cada ponto é coletado, por quanto tempo cada faixa de dado precisa ficar disponível, quanto tempo de atraso o alerta pode ter e se o cliente pode criar tags livremente. Vou assumir cem mil hosts, duzentas métricas por host, um ponto a cada dez segundos, e alertas com atraso de até um minuto. Isso dá cerca de dois milhões de pontos por segundo, e esse número é a premissa sobre a qual o resto do desenho se apoia.

O primeiro ponto é a coleta. Eu não quero que cada aplicação mande uma requisição por métrica. Um agente em cada host agrega localmente, guarda um ponto por série em janelas de dez segundos, junta em lotes e envia a cada cinco segundos, comprimido. Isso reduz o número de conexões e de requisições em ordens de grandeza, e o agente continua funcionando quando a rede cai, porque guarda os lotes em memória e depois em disco.

O segundo ponto é a ingestão. Os lotes chegam a um balanceador de camada de transporte, que não olha o conteúdo, e depois a um serviço que valida, aplica rótulos padrão e divide os dados em partições por série. A partição é importante porque a mesma série precisa sempre cair na mesma partição, para que a ordem temporal se mantenha. O stream recebe tudo e serve de ponto de entrada para três consumidores: o escritor do armazenamento quente, o agregador de faixas mais longas e o avaliador de alertas, que lê do stream e não do banco, para não competir com as consultas.

O terceiro ponto é a retenção. Os pontos brutos de um segundo ficam sete dias no armazenamento quente, porque é lá que os engenheiros investigam incidentes. Depois, um agregado de um minuto fica trinta dias, e um agregado de uma hora fica um ano num armazenamento frio, em formato colunar, consultado por Athena. A tela de dashboard escolhe a camada pelo intervalo pedido, e isso mantém as consultas longas baratas.

O quarto ponto é o que mais pesa em custo: cardinalidade. Cada combinação de nome de métrica com valores de tag é uma série, e uma série tem custo fixo de índice e de memória. Uma tag como endpoint tem poucas dezenas de valores e cabe. Uma tag como identificador de usuário tem milhões, e multiplica o número de séries por milhões. Eu proibiria ou limitaria tags de alta cardinalidade no próprio agente, e mandaria o detalhe por usuário para um sistema de logs ou de rastreamento, que foi feito para isso.

O que quebra: se a rede cai, o agente guarda o que pode e descarta o mais antigo quando o disco enche, contando os descartes numa métrica própria para que a perda seja visível. Se o armazenamento quente fica lento, o alerta continua funcionando, porque lê do stream. Se uma consulta pede um intervalo de um ano em segundos, o roteador manda para a camada fria, que responde mais devagar, mas não derruba a quente.”

12.3 por dentro: como funciona

Comece pelo agente, porque ele decide o que chega ao resto do sistema. A aplicação registra valores com operações simples, incrementar um contador ou ajustar um medidor, e o agente acumula esses valores em janelas. Isso é uma agregação local: mil incrementos num mesmo intervalo viram um único ponto com a soma. A figura mostra o agente em duas situações, com a rede saudável em cima e com a rede caída em baixo.

metricas agente buffer
figura 46 · metricas agente buffer

Na parte de cima, o caminho é o que você espera: as amostras da aplicação viram um ponto por série na janela, o ponto entra num buffer, o emissor junta lotes e manda para a ingestão. Na parte de baixo, a rede caiu e o emissor começa a falhar. O buffer em memória enche, e o sistema não pode simplesmente parar a aplicação para esperar, porque a telemetria não pode derrubar o serviço que ela observa. Por isso o transbordo vai para um em disco com tamanho limitado. Quando o disco também enche, o mais antigo é descartado e o descarte é contado, e a seta rosa de baixo mostra exatamente essa cadeia. A regra é preferir perder dado antigo a travar a aplicação ou perder tudo sem aviso.

A segunda figura é a mais importante para a discussão de custo, porque mostra o efeito de uma única tag mal escolhida.

metricas cardinalidade
figura 47 · metricas cardinalidade

Compare as duas linhas. Em cima, a métrica tem tags com valores que cabem em poucas dezenas: host, endpoint e status. Multiplicando 5.000 hosts por 50 endpoints por 5 códigos de status, o total é 1,25 milhão de séries, um número que o índice aguenta. Embaixo, a mesma métrica recebe o identificador de usuário, com dois milhões de valores, e a conta vai para 2,5 trilhões de séries. A figura marca em rosa o ponto em que o índice deixa de caber e a ingestão trava. O ensino é que o custo cresce com o produto dos valores de todas as tags, e não com o número de métricas.

Por fim, a pirâmide de retenção mostra por que os dados antigos são agregados e não guardados com a mesma resolução.

metricas piramide retencao
figura 48 · metricas piramide retencao

Leia de cima para baixo, na coluna das camadas. O stream entrega pontos de um segundo para a camada quente, que guarda sete dias, e a seta verde mostra que cada camada gera a seguinte por agregação: sessenta pontos de um segundo viram um de um minuto, e sessenta de um minuto viram um de uma hora. Cada camada tem muito menos pontos por série que a anterior, e é isso que torna os trinta dias ou um ano baratos. À direita, o roteador de consulta escolhe a camada pelo intervalo pedido, e a seta amarela até a camada do meio mostra um painel de trinta dias sendo servido por ela. O número ao lado de cada camada é a conta que o entrevistador quer ver: 604.800 pontos por série no primeiro caso, 43.200 no segundo e 8.760 no terceiro.

12.4 mapeamento na AWS

A visão geral mostra o caminho completo.

metricas visao geral
figura 49 · metricas visao geral

Siga a seta laranja do agente até o NLB, e depois os pacotes roxos do Kinesis Data Streams, que é o ponto onde o fluxo se divide em três consumidores. O escritor quente grava no Timestream for InfluxDB, que recebe o dado de um segundo, e a seta verde de grava os agregados de um minuto e de uma hora. O avaliador de alertas, à direita, lê o stream e envia pelo SNS, sem consultar o banco. Na parte de baixo, o Grafana consulta a camada quente para o recente e o Athena para o histórico, e as duas setas verdes mostram o mesmo painel sendo atendido pelas duas fontes.

Por que cada serviço. O agente roda no host porque ele é o único lugar que vê a métrica antes da rede. O NLB recebe os lotes sem olhar o conteúdo, e isso permite escalar a ingestão sem um balanceador de aplicação na frente. O Kinesis Data Streams é o barramento porque separa a chegada do consumo e permite que três consumidores leiam o mesmo fluxo em paralelo, com a chave de partição sendo o hash da série. O MSK, que é o Kafka gerenciado, é uma alternativa legítima, e a escolha depende de quem já opera Kafka na empresa. O Timestream for InfluxDB guarda a camada quente porque foi desenhado para séries temporais e tem retenção por tabela. Eu preciso declarar um ponto: o Amazon Timestream for LiveAnalytics, que era a versão original do serviço, deixou de aceitar clientes novos, segundo o que eu sei, e eu não verifiquei isso hoje. Por isso desenho com o InfluxDB. O S3 em Parquet com Athena guarda a camada fria porque o armazenamento é barato e a consulta é ocasional. O Grafana fica como interface porque lê as duas fontes com um único painel. O SNS entrega os alertas aos canais.

Não há aula no vault sobre Kinesis, Timestream, Athena, Grafana ou SNS em detalhe. A ideia de agregação local e de buffer se conecta com Kafka e com Rate Limiting na parte de , Existe no vault um desafio de agregador de métricas de alto volume, Datadog - High-Throughput Metric Aggregator Daemon, que vale ler como exercício prático (não reli o conteúdo para esta nota). Para a parte de telemetria, veja Tracing Distribuído e OpenTelemetry e SRE.

12.5 dicas de entrevista

Pergunte sobre o número de hosts e de métricas, o intervalo de coleta, o atraso aceitável de alerta, o tempo de retenção por faixa e se os clientes podem criar tags livremente. Esta última pergunta é a mais importante, porque um sistema sem limite de cardinalidade vai quebrar, e o entrevistador quer ver que você já pensou nisso antes do primeiro incidente.

Estime com números e declare as premissas. Cem mil hosts com duzentas métricas e um ponto a cada dez segundos dão dois milhões de pontos por segundo. Sete dias de pontos de um segundo para uma série são 604.800 pontos, e é por isso que a camada quente é cara e a fria não. Com esses números, o entrevistador vê que você sabe onde está o custo.

O trade-off central é a resolução contra o custo de armazenamento e de consulta. Guardar tudo em um segundo para sempre é caro e quase ninguém consulta depois de uma semana. Agregar cedo demais perde o detalhe de um incidente. A pirâmide é a resposta, e o trade-off fica na escolha das janelas de retenção.

Armadilhas de resposta júnior: colocar cada evento como linha de banco relacional, mandar cada métrica por uma requisição HTTP individual, usar identificador de usuário como tag sem perceber a explosão de séries, fazer o alerta consultar o banco de consulta, e não prever o que acontece quando o agente não consegue enviar. Se você travar, diga: “vou começar pelo agente, porque o volume é decidido ali, e depois pela cardinalidade, porque ela decide o custo”.

12.6 follow-ups

Como detectar uma tag de alta cardinalidade antes que ela quebre o sistema? O agente e a ingestão contam séries novas por métrica e por tag, e um limite por métrica bloqueia ou rejeita as que passam do teto, com um alerta para o time dono.

Por que o alerta não lê do banco de consulta? Porque o banco é para consultas humanas, que podem ser pesadas e concorrentes, e o alerta precisa de latência previsível. Ler do stream dá ao alerta uma cópia do dado chegando, sem competir com o painel.

E se o agregado de uma hora perder um pico de um segundo? O agregado guarda mínimo, máximo, soma e contagem por janela, e não só a média. Assim, um pico continua visível na camada fria, e o detalhe de segundos some só depois de sete dias, quando já não é mais consultado.

correções ao texto do Gemini

Perguntas 1 a 4

O texto original das perguntas 1 a 4 é bom como roteiro, mas tem simplificações que, dita em entrevista, soariam como erro. As correções abaixo valem para esta nota; a nota do Gemini não foi editada.

  1. Rate limiter, “API Gateway Usage Plans” como camada de limite. Os planos de uso existem para REST APIs, identificam o cliente por API key (não por IP) e, segundo a documentação, throttling e cotas são “best effort”: os clientes podem excedê-los, e a AWS manda não depender deles para controlar custo nem bloquear acesso, e sim usar o AWS WAF. API key também não é mecanismo de autenticação. Nesta nota o plano de uso é a camada de cota contratada, e a regra precisa de negócio vive no ElastiCache.
  2. Rate limiter, “AWS WAF Rate-based rules”. A regra do WAF tem janela de 60, 120, 300 ou 600 segundos, limite mínimo de 10 e aplica o limite “perto” do valor, sem garantia de exatidão. Serve a volume por IP, não a cota por usuário.
  3. Rate limiter, “sincronização assíncrona eventual” entre regiões com Redis. O ElastiCache Global Datastore replica de forma assíncrona de um primário para secundários somente leitura; não permite escrita ativa nas duas regiões. A forma de fazer é repartir a cota por região e rebalancear (figura 05).
  4. Chat, “IDs estritamente monotônicos (Snowflake)”. Snowflake é ordenável por tempo (k-ordered), não estritamente monotônico entre nós, e a própria aula Snowflake ID avisa sobre o relógio. Para ordem estrita use um contador por conversa; para canais grandes, Snowflake com ordem aproximada.
  5. Chat, “filas transitórias no SQS/Kafka” para o usuário offline. Uma fila por usuário não deixa reler nem consultar por posição, e não serve a vários dispositivos. O histórico durável no DynamoDB, com o cliente pedindo “depois do último id”, é mais simples e mais robusto. O mapeamento também omite o SNS (ou o serviço de push) para acordar o app: sem ele, o usuário com o app fechado só vê a mensagem ao abrir.
  6. Chat, sobre o Amazon Pinpoint, que às vezes aparece em guias de push: a documentação da AWS informa o fim do suporte em 30 de outubro de 2026 (daqui a poucos dias, na data da consulta); as APIs de push passam a ser do AWS End User Messaging. Nesta nota uso o SNS para push móvel.
  7. Feed, “mais de 1 milhão de seguidores” e “800 posts” como se fossem regras. São parâmetros de projeto, não constantes: o limite sai da conta de capacidade de fan-out e o tamanho da lista, da memória e do comportamento de uso. O número 800 vem de relatos públicos sobre o Twitter que não verifiquei.
  8. Pagamentos, “locks distribuídos temporários” para idempotência. A escrita condicional no banco (ou a restrição UNIQUE) é atômica e não expira; o lock tem prazo e pode ser perdido com o processo ainda executando.
  9. Pagamentos, “SQS FIFO (ordenação estrita e deduplicação)”. A ordenação é por grupo de mensagens, não global, e a deduplicação vale numa janela curta (5 minutos), então não substitui a chave de idempotência.
  10. Pagamentos, ledger. O texto não cita o Amazon QLDB, mas ele costuma ser sugerido para ledgers: teve o suporte encerrado em 31 de julho de 2025. Use Aurora PostgreSQL ou RDS.

Perguntas 5 a 8

O texto original das perguntas 5 a 8 serve de roteiro, mas tem trechos que, ditos em entrevista, soariam como erro. Nada abaixo foi reconferido na documentação hoje (2026-10-22); são correções de conhecimento geral, e cada uma deve ser checada antes de virar fala.

  1. Q7, “versionamento ветores”. A palavra está escrita com caracteres cirílicos e deveria ser “vetores de versão” (version vectors). Vale corrigir o texto de origem para que a palavra não fique com letras de outro alfabeto.
  2. Q5, “AWS Elemental MediaConvert / Lambda / EC2 para transcodificação”. Lambda tem limite de tempo de execução (conhecimento geral: 15 minutos), e vídeo longo não cabe. A nota usa MediaConvert ou contêineres para transcodificar e Lambda só para assinar URLs e disparar o fluxo.
  3. Q5, “Amazon S3 Glacier (Cold storage)”. “Glacier” hoje é uma família de classes com períodos mínimos diferentes (Instant, Flexible, Deep Archive). Diga a classe pelo nome quando for falar de custo.
  4. Q5, “80% do tráfego vem de 20% dos vídeos”. É uma regra empírica de cauda, não uma medida. Na entrevista, apresente como premissa a ser confirmada com dados da plataforma.
  5. Q6, “LSM-Trees e SSTables com Bloom Filters” aparece como estrutura do banco chave-valor que o desenho descreve. LSM é o motor de Cassandra e RocksDB; o artigo técnico da AWS sobre o DynamoDB descreve B-trees com log. Se o entrevistador citar o DynamoDB, a resposta correta é a segunda.
  6. Q6, “Vector Clocks” como mecanismo para detectar conflitos. O Dynamo original (2007) usa vector clocks; o DynamoDB gerenciado não expõe esse mecanismo, e as global tables resolvem conflito pela última escrita. Diga qual dos dois sistemas está em discussão.
  7. Q6, “R + W > N para consistência forte”. A condição garante que a leitura interseciona a última escrita confirmada em um sistema sem falhas nem escritas concorrentes. Não é consistência forte no sentido de linearizabilidade, e o texto deveria dizer isso.
  8. Q7, “Amazon S3 Multipart Upload” para os blocos. Com blocos de 4 MB, um envio simples basta; a recomendação de multipart costuma começar em torno de 100 MB. Use multipart para o arquivo original, se ele for enviado inteiro.
  9. Q8, “Redis Keyspace Notifications” para devolver estoque. As notificações são entregues por Pub/Sub, sem garantia de entrega; uma desconexão ou um failover perde eventos. Use um reconciliador periódico, como na nota.
  10. Q8, “Amazon SQS FIFO” para a fila de cobranças. A ordem vale por grupo de mensagens e o limite de throughput depende do modo; confira a cota antes de dizer um número.

Perguntas 9 a 12

O texto original das perguntas 9 a 12 serve de roteiro, mas tem pontos que, ditos em entrevista, soariam como erro ou ficariam incompletos. Nada abaixo foi reconferido na documentação hoje (2026-10-22); são correções de conhecimento geral, e cada uma deve ser checada antes de virar fala.

  1. Q9, “301 perde métricas analíticas”. A frase está certa no essencial, mas incompleta. A resposta 301 pode ser guardada pelo navegador e por caches intermediários, então cliques repetidos do mesmo navegador somem da contagem. Os cliques que chegam à borda ainda aparecem nos logs do CloudFront. Diga a diferença assim, e não como perda total de analytics.
  2. Q9, “302 com cache” e cache de borda. Um 302 não é guardado sem indicação explícita do servidor. A nota usa Cache-Control: max-age=300 na borda para que a origem receba um pedido por janela, e isso precisa estar declarado na resposta.
  3. Q9, “Lambda@Edge para o redirecionamento”. Funciona, mas para um redirecionamento simples o CloudFront Functions costuma ser a opção mais leve. Confira custo e limites da versão atual antes de escolher.
  4. Q9, “Filtros de Bloom ou hash em disco e memória”. Filtros de Bloom são estruturas de memória por natureza. O disco entra só para persistência ou para o caso de o conjunto não caber. Diga isso na entrevista.
  5. Q10, “Crawl-delay” e robots.txt. A RFC 9309, que padroniza o robots.txt, não inclui a diretiva Crawl-delay. Ela existe em convenções de alguns crawlers e sites. Trate o atraso como política do crawler e respeite o pedido do site quando ele existir.
  6. Q10, “Amazon SQS FIFO” para uma URL por host. A garantia de ordem vale por grupo de mensagens, e os limites de throughput dependem do modo. Confira a cota antes de dizer um número.
  7. Q11, “AWS Bedrock”. O nome correto do serviço é Amazon Bedrock.
  8. Q11, “Dynamic batching” como função do gateway. O agrupamento de requisições na GPU acontece no servidor de inferência, dentro do endpoint. O gateway decide para qual destino a requisição vai, e não agrupa tokens.
  9. Q11, “Cache de prefixo / prompt caching” misturado com cache semântico. São mecanismos diferentes. O prompt caching é um recurso do provedor que reaproveita um prefixo de prompt já processado. O cache semântico é um componente do gateway que reaproveita respostas por similaridade de embedding. A figura mostra só o segundo.
  10. Q12, “Amazon Timestream” sem especificar a versão. Hoje a escolha precisa dizer qual. A figura usa o Timestream for InfluxDB. Pelo que eu sei, o Amazon Timestream for LiveAnalytics deixou de aceitar clientes novos, mas não verifiquei isso hoje, então confirme antes de falar.
  11. Q12, “OpenSearch” como TSDB. O OpenSearch é um motor de busca e análise que pode servir a logs e a algumas métricas, mas não é um banco de séries temporais no sentido usado aqui. Se for citado, diga para que ele serve.

glossário

  • Token bucket: balde com fichas que voltam a uma taxa fixa; cada requisição gasta fichas. Permite rajada até o tamanho do balde.

  • Fail-open e fail-closed: o que o sistema faz quando o limitador (ou outra proteção) falha. Abrir deixa passar; fechar bloqueia.

  • Script Lua no Redis: bloco de código executado de uma vez no servidor, sem intercalação de outros comandos.

  • WebSocket: conexão persistente em que cliente e servidor trocam frames a qualquer momento, depois de um handshake HTTP.

  • connectionId: identificador que o API Gateway dá a cada conexão WebSocket, usado para mandar dados de volta.

  • Snowflake ID: identificador de 64 bits ordenável por tempo e gerado sem coordenação central.

  • Fan-out: transformar um evento em várias entregas. Na escrita (push) copia na hora de publicar; na leitura (pull) busca na hora de ler.

  • Chave quente: uma chave que recebe tráfego desproporcional e satura o shard ou a partição que a guarda.

  • Idempotência: executar duas vezes tem o mesmo efeito que executar uma.

  • Saga: sequência de passos locais com ações compensatórias, no lugar de uma transação distribuída.

  • Ledger de partidas dobradas: registro em que cada transação tem débitos e créditos de soma zero, só com inserções.

  • Conciliação: comparação periódica entre o seu registro e o de um sistema externo.

  • Nó virtual: posição de um servidor físico no anel de hash. Um servidor ocupa muitas posições, o que espalha a carga.

  • Quórum: quantidade mínima de réplicas que precisam responder para uma leitura (R) ou uma escrita (W) valer. Com N réplicas, R + W maior que N garante que os conjuntos se cruzam.

  • WAL (write-ahead log): registro sequencial gravado antes da alteração, para recuperar dados depois de uma queda.

  • LSM-tree: estrutura que grava em memória e em arquivos imutáveis, e funde esses arquivos em segundo plano (compactação).

  • B-tree: árvore de páginas atualizadas no lugar; a leitura percorre um caminho até a página.

  • Chunking: divisão de um arquivo em pedaços. Por posição, o corte é fixo; por conteúdo, o corte depende dos bytes e tolera inserções no meio.

  • Deduplicação por hash: cada pedaço é guardado uma vez, com o hash como chave, e muitos arquivos apontam para ele.

  • Commit condicional: escrita aceita só se a versão atual for a esperada; é a base para detectar conflito sem trava.

  • GOP: grupo de quadros de vídeo que pode ser decodificado a partir do seu início; o corte de segmentos respeita esses limites.

  • HLS e DASH: formatos de streaming que dividem o vídeo em segmentos e publicam uma lista de versões em resoluções diferentes.

  • Adaptive bitrate: o player escolhe a resolução de cada segmento conforme a banda medida.

  • Origin Shield: camada de cache que junta pedidos iguais antes de chegar à origem.

  • Overselling: vender mais unidades do que o estoque tem, por corrida entre leitura e gravação.

  • Decremento atômico: operação que subtrai e devolve o novo valor de uma vez, sem intercalação.

  • Reserva com TTL: registro temporário que expira sozinho, para segurar um ingresso enquanto a pessoa paga.

  • Sala de espera: fila controlada na frente do checkout, que libera entradas na taxa que o sistema aguenta.

  • Base62: codificação de números com 62 símbolos (letras maiúsculas, minúsculas e dígitos). Sete caracteres formam 62 elevado a 7, cerca de 3,5 trilhões de combinações.

  • Paradoxo do aniversário: a probabilidade de colisão entre valores escolhidos ao acaso cresce bem mais rápido que a intuição sugere. Com 23 pessoas, a chance de duas terem o mesmo dia de aniversário já passa de 50%.

  • Código 301 e 302: respostas de redirecionamento, a primeira permanente e a segunda temporária. A diferença prática é o que o navegador e os caches podem guardar.

  • Cache-Control: cabeçalho HTTP que diz por quanto tempo uma resposta pode ser guardada, como max-age=300, que significa cinco minutos.

  • Normalização de URL: transformar variações de uma mesma página (fragmento, ordem de parâmetros, maiúsculas no host) numa forma única antes de comparar.

  • Filtro de Bloom: estrutura probabilística que responde se um elemento provavelmente já foi visto. Nunca dá falso negativo e pode dar falso positivo. Com n elementos e taxa de erro p, o tamanho em bits é aproximadamente m = -n · ln(p) / (ln 2)², o que para dez bilhões de URLs e 1% dá cerca de 96 bilhões de bits, perto de 12 GB.

  • Cortesia (politeness): regra de não sobrecarregar um site, com no máximo uma requisição por vez por host e um intervalo entre elas.

  • Fronteira (frontier): a estrutura que guarda as URLs a visitar, com prioridade e o próximo horário permitido por host.

  • Visibility timeout: tempo em que uma mensagem fica invisível para outros consumidores depois de lida. Se o consumidor morre antes de remover a mensagem, ela volta à fila quando o tempo expira.

  • Spot: instância de computação com desconto, que a AWS pode interromper com aviso curto. Serve bem a trabalho que pode ser refeito.

  • Cardinalidade: número de combinações distintas de nome de métrica e valores de tag. Cada combinação é uma série com custo próprio.

  • Série temporal: sequência de pontos com tempo e valor, identificada por nome e tags.

  • Rollup (agregação temporal): resumo de vários pontos numa janela maior, como a média, o mínimo, o máximo e a contagem de um minuto.

  • Contrapressão: mecanismo pelo qual um componente lento avisa quem o alimenta para reduzir o ritmo, ou para guardar temporariamente o excesso.

  • Spool: área de armazenamento em disco que guarda dados enquanto o destino está indisponível.

  • TTFT (time to first token): tempo entre o envio de um pedido de geração e a chegada do primeiro token. É a métrica que mais sente a degradação do modelo.

  • SSE (Server-Sent Events): protocolo em que o servidor envia eventos em texto por uma conexão HTTP aberta, com o tipo de conteúdo text/event-stream.

  • Reserva de tokens: débito antecipado do saldo pelo máximo que a requisição pode consumir, com devolução da diferença depois da resposta.

  • Disjuntor (circuit breaker): componente que abre quando um destino passa do limite de erro ou de latência, e impede novas chamadas a ele por um tempo.

  • Cache semântico: cache que procura respostas anteriores por similaridade de vetores, e não pela igualdade exata do texto.

fontes

Perguntas 1 a 4

Documentação oficial consultada em 2026-10-22:

Verificado por execução local (Redis 7.4.4 e PostgreSQL 17.11 em contêiner, não no ElastiCache nem no Aurora): o script do token bucket (seção 1) e as invariantes do ledger (seção 4).

Não verificado: preços e cotas de API Gateway WebSocket em escala de dezenas de milhões de conexões, configuração de filtro por fatia no SNS, cotas de início de execução e transição do Step Functions, janela de 5 minutos da deduplicação do SQS FIFO (valor da documentação, página não reaberta), latência real do ElastiCache e consumo de memória por entrada de lista (20 bytes é suposição), e o número 800 do feed.

Perguntas 5 a 8

Nenhuma consulta nova foi feita para esta nota em 2026-10-22: não abri documentação hoje. As referências abaixo são conhecimento geral do autor do texto e precisam ser conferidas antes de citadas em entrevista.

  • DeCandia et al., “Dynamo: Amazon’s Highly Available Key-value Store”, SOSP 2007 (consistent hashing, nós virtuais, quórum, vector clocks).
  • Amazon DynamoDB, artigo técnico da AWS publicado no USENIX ATC 2022 (nós, WAL, B-tree, Multi-Paxos, réplicas por zona); a figura de escrita e a de LSM contra B-tree seguem esse artigo.
  • Documentação do DynamoDB: leitura consistente, escrita condicional, transações e global tables (conflito por última escrita).
  • Documentação do Amazon S3: multipart upload, classes de armazenamento e períodos mínimos de cobrança.
  • Documentação do Amazon CloudFront: origin shield e origin access control.
  • Documentação do Redis: Pub/Sub e keyspace notifications.
  • RFC 8216, HTTP Live Streaming (HLS).
  • Rabin, M. O., “Fingerprinting by Random Polynomials”, 1981 (base para o chunking por conteúdo).

Os números das figuras de consistent hashing (06-03) e a curva de popularidade (05-04) são ilustrativos ou vêm de simulação sem descrição na fonte; não tratá-los como medidas.

Perguntas 9 a 12

Nenhuma consulta nova foi feita para esta nota em 2026-10-22: não abri documentação hoje. As referências abaixo são conhecimento geral do autor do texto e precisam ser conferidas antes de citadas em entrevista.

  • RFC 9110, HTTP Semantics (códigos 301 e 302, cabeçalho Location).
  • RFC 9111, HTTP Caching (guarda de respostas e cabeçalho Cache-Control).
  • RFC 9309, Robots Exclusion Protocol (robots.txt; não padroniza Crawl-delay).
  • WHATWG, HTML Living Standard, seção de Server-sent events (protocolo do EventSource).
  • Bloom, Burton H., “Space/Time Trade-offs in Hash Coding with Allowable Errors”, 1970 (filtro de Bloom).
  • Documentação do Amazon SQS, em especial filas FIFO, grupos de mensagens e visibility timeout.
  • Documentação do Amazon Timestream, para o aviso sobre o LiveAnalytics e a versão for InfluxDB (não verificada hoje).
  • Documentação do Amazon Bedrock, para a operação ConverseStream e os perfis de inferência entre regiões (não verificada hoje).
  • Documentação do Amazon Kinesis Data Streams, para a chave de partição e os consumidores em paralelo.

Os números das premissas (cem milhões de links por mês, cem mil hosts, um bilhão de tokens por dia) são escolhas para exercício, não medidas. As figuras de cardinalidade e de retenção usam contas simples, e não medições de um sistema real.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?