Pular para conteúdo

Visão geral

São 09:59 do dia em que a turnê de estádios entra à venda na BoxOffice. Dois milhões de fãs estão atualizando a página do evento; existem 60 mil lugares. Nos próximos dez minutos a plataforma precisa renderizar o catálogo para milhões de visualizações de página, reservar e vender lugares sem venda duplicada, cobrar cartões por meio de um provedor de pagamentos externo, gerar 60 mil ingressos e e-mails e liberar os lugares cujas reservas expiraram. Às 03:00, a mesma plataforma atende algumas centenas de requisições por segundo.

A borda (aula 4) decide para onde vai cada requisição. Esta aula trata do que acontece quando ela chega: onde o código roda, como ele é dividido em serviços, como esses serviços conversam, o que roda fora da requisição, como a frota cresce e encolhe sozinha e como depender com segurança de sistemas que você não controla — o provedor de pagamentos, o provedor de e-mail, qualquer API de terceiros.

A história até aqui

Capítulo 5 da história da disciplina. A borda da aula 4 espalha os 2 milhões de requisições das 10:00 por três réplicas do catalog, e o lab 3 mediu o ganho. Mas uma réplica só pode ser adicionada, derrubada ou substituída se não guardar nada de que outra réplica precise: uma reserva de lugar ou uma sessão de login na memória de uma réplica morre com ela. E a BoxOffice agora tem mais serviços além do catalog: alguém precisa verificar o token do fã uma vez, por todos eles. Esta aula responde: o que uma réplica precisa ser, como o código é dividido em serviços, e como esses serviços dependem uns dos outros e de provedores que não controlamos?

Objetivos de aprendizagem

Ao final desta aula, você deve ser capaz de:

  • Explicar por que servidores de aplicação devem ser stateless e mover cada tipo de estado (sessões, arquivos, caches, agendamentos) para o lugar certo.
  • Estimar quantas réplicas um serviço precisa para um dado pico, usando a capacidade por instância e uma utilização-alvo.
  • Comparar um monólito, um monólito modular e microsserviços, e justificar fronteiras de serviço com bounded contexts e a lei de Conway.
  • Escolher um estilo de API (REST, gRPC, GraphQL, assíncrono) para uma interação e evoluir um contrato sem quebrar seus consumidores.
  • Projetar trabalho em segundo plano com filas, workers e schedulers que sejam seguros diante de retries e de múltiplas réplicas.
  • Decidir entre containers e serverless, e configurar autoscaling (target tracking, HPA, baseado em fila, agendado) para uma carga com picos.
  • Integrar um provedor externo com segurança: idempotency keys, webhooks verificados, retries com backoff, reconciliação e um plano para quedas do provedor.

Roteiro

  1. Esta visão geral — a paleta de computação, servidores de aplicação stateless, escalabilidade horizontal e quantas réplicas você precisa.
  2. Serviços — monólito, monólito modular e microsserviços; bounded contexts de DDD; lei de Conway; a arquitetura dentro de um serviço; auth, busca, notificações e analytics como serviços.
  3. Estilos de API — REST, gRPC, GraphQL e APIs assíncronas para o mesmo recurso da BoxOffice; contratos, OpenAPI e versionamento.
  4. Trabalho assíncrono — workers e jobs em segundo plano, design de jobs, schedulers, locks distribuídos e eleição de líder.
  5. Serverless e autoscaling — functions as a service, cold starts e concorrência; políticas de autoscaling, HPA e por que o escalonamento reativo fica atrás de uma venda relâmpago.
  6. Integrações externas — APIs de terceiros, provedores de pagamento e de e-mail: webhooks com assinaturas, idempotency keys, retries, reconciliação e quedas do provedor.
  7. Quiz — verifique seu entendimento.

Slides

Clique nos slides e use ← → para navegar; F abre em tela cheia e P abre a visão do apresentador, com as notas. Abrir os slides em uma nova aba


A paleta de computação

No System Design Playground, a categoria COMPUTE agrupa todos os componentes que executam o seu código. Cada um é uma resposta diferente à pergunta "onde esta parte da lógica executa, e como ela escala?":

Componente O que executa Exemplo na BoxOffice Escala com
App Server Handlers de requisição/resposta atrás de um load balancer catalog, inventory, order requisições por segundo, CPU
Worker Jobs retirados de uma fila, fora de qualquer requisição de usuário geração do PDF do ingresso, envio de e-mail profundidade da fila, consumer lag
Serverless Funções curtas iniciadas pela plataforma a cada evento redimensionar a imagem de um artista no upload; receber webhooks invocações simultâneas
Auth Service Login, tokens, sessões, identidade auth emite JWTs para fãs e equipe logins por segundo (picos na abertura das vendas)
Search Consultas full-text e por facetas sobre um índice "coldplay são paulo" → eventos consultas por segundo, tamanho do índice
Scheduler Trabalho disparado por tempo (cron, jobs atrasados) abrir a venda às 10:00; liberar reservas expiradas número de jobs, precisão
Notifications Fan-out para provedores de e-mail, push e SMS "seus ingressos chegaram" mensagens por segundo, limites do provedor
Analytics Ingestão e agregação de eventos dashboard de vendas por minuto eventos por segundo

O restante desta página trata do mais comum deles — o app server — e da propriedade que torna todo o resto possível: ser stateless.


App servers stateless

Um servidor é stateless (sem estado) quando não guarda, entre uma requisição e outra, nada de que outra réplica precisaria: qualquer réplica pode atender qualquer requisição, porque tudo o que precisa sobreviver fica fora do processo — em um banco de dados, um cache, um object store ou no próprio cliente. É essa única propriedade que permite ao load balancer mandar cada requisição para qualquer lugar, permite adicionar réplicas em segundos e permite matar uma réplica sem perder nada.

flowchart LR
    fan([Fã]) --> lb["Load balancer"]
    lb --> a1["catalog #1"]
    lb --> a2["catalog #2"]
    lb --> a3["catalog #3"]
    a1 --> redis[("Redis<br/>sessões, cache")]
    a2 --> redis
    a3 --> redis
    a1 --> pg[("PostgreSQL")]
    a2 --> pg
    a3 --> pg
    a3 --> s3[("Object store<br/>imagens, PDFs")]

As réplicas são caixas intercambiáveis; o estado fica nos armazenamentos compartilhados à direita. Cada tipo de estado que tende a "grudar" em um servidor tem uma nova casa padrão:

Estado que gruda no servidor Por que quebra a escalabilidade Onde deveria ficar
Sessão HTTP em memória (carrinho, login) A próxima requisição pode cair em outra réplica → "você foi deslogado" Um token assinado no cliente (JWT) ou um session store (Redis)
Arquivos enviados no disco local Outras réplicas não os enxergam; somem quando o pod morre Object store (S3, GCS, MinIO)
Cache dentro do processo Cada réplica tem uma cópia diferente e desatualizada Um cache distribuído (Redis) — ou aceitar TTLs curtos por réplica
Jobs agendados dentro da aplicação (@Scheduled) Com 10 réplicas o job roda 10 vezes Um scheduler com lock ou líder
Contadores e rate limits em memória Cada réplica conta só a sua parte Redis (INCR) ou o API gateway
Conexões WebSocket Uma conexão é estado, presa a uma réplica Mantenha-a, mas publique as mensagens via pub/sub para que qualquer réplica alcance qualquer usuário

BoxOffice: a reserva de lugar

Quando um fã escolhe o lugar C-14, o serviço inventory cria uma reserva (hold) que expira em 10 minutos. Uma primeira versão tentadora guarda a reserva em um ConcurrentHashMap dentro da réplica. Funciona no notebook e falha em produção: o próximo clique vai para outra réplica, que não conhece a reserva; um restart esquece todas as reservas; duas réplicas vendem o mesmo lugar. A reserva precisa morar no armazenamento compartilhado que garante "um lugar, uma reserva" — uma linha no PostgreSQL com uma unique constraint (ou uma chave no Redis gravada com NX e uma expiração). A aula 6 (bancos de dados) mostra a transação que torna isso seguro.

Este é o fator VI do Twelve-Factor App: "execute a aplicação como um ou mais processos stateless"1. Isso não significa que o sistema não tenha estado — significa que o estado fica concentrado em poucos componentes construídos para guardá-lo, que você então escala com técnicas diferentes (réplicas, partições, caches: aulas 6 e 7).

Sticky sessions são uma muleta

Um load balancer pode prender cada usuário a uma réplica (session affinity). Isso esconde o problema sem resolvê-lo: a carga fica desigual (uma réplica recebe os usuários mais pesados) e, quando essa réplica morre ou é removida no scale-in, seus usuários perdem o estado. Use afinidade apenas para tráfego orientado a conexão (WebSockets) ou como passo temporário enquanto o estado é movido para fora.

No código do BoxOffice: nenhuma sessão em nenhuma réplica (lab 4)

O Hands-on 4 — Gateway não guarda sessão de login em lugar nenhum. O JWT do fã viaja em toda requisição, e o gateway confere a assinatura com a chave pública do auth, buscada uma vez no seu endpoint JWKS: nenhuma réplica se lembra do fã, então o gateway pode mandar cada requisição para qualquer uma das três réplicas do catalog (lb://catalog), e perder uma não perde nada. O preço é a revogação: um token continua válido até expirar (uma hora aqui, jwt.ttl), como explica Identidade.

services/gateway/src/main/java/boxoffice/gateway/SecurityConfig.java (stateless security)
/** Which routes are open, which need a valid token, which need the admin scope. */
@Bean
SecurityWebFilterChain security(ServerHttpSecurity http) {
    return http
            .csrf(ServerHttpSecurity.CsrfSpec::disable)        // stateless API, no cookies
            .authorizeExchange(ex -> ex
                    // open: getting a token, and the public keys to check one
                    .pathMatchers("/auth/register", "/auth/login", "/auth/.well-known/**").permitAll()
                    // open: browsing the catalog (HEAD is a GET without the body)
                    .pathMatchers(HttpMethod.GET, "/catalog/**").permitAll()
                    .pathMatchers(HttpMethod.HEAD, "/catalog/**").permitAll()
                    // admin only: changing the catalog (scope claim "user admin")
                    .pathMatchers("/catalog/**").hasAuthority("SCOPE_admin")
                    // health, metrics, routes: the edge never forwards /actuator from outside
                    .pathMatchers("/actuator/**").permitAll()
                    // everything else (and every future route): any valid token
                    .anyExchange().authenticated())
            .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
            .build();
}

Escalar para cima ou para fora

Há duas formas de dar mais capacidade a um serviço:

Escala vertical (scale up) Escala horizontal (scale out)
Como Uma máquina maior: mais vCPUs, mais memória Mais máquinas (réplicas) atrás de um load balancer
Limite O maior tipo de instância; o preço cresce mais rápido que o tamanho Os armazenamentos compartilhados atrás das réplicas
Disponibilidade Uma caixa só: a falha dela é a sua indisponibilidade As réplicas cobrem umas às outras
Downtime para mudar Normalmente um restart Nenhum — adiciona e remove réplicas em funcionamento
Exige Nada do código App servers stateless
Bom para Bancos de dados, correções rápidas, sistemas pequenos App servers, workers, qualquer coisa stateless

App servers são escalados para fora quase por padrão, porque ser stateless torna isso trivial. Bancos de dados normalmente são escalados para cima primeiro, depois com réplicas e partições — é por isso que a camada de dados, e não a de aplicação, é onde a maioria dos gargalos acaba aparecendo.

Quantas réplicas?

A pergunta que todo design precisa responder com um número. Parta da capacidade de uma réplica, medida com um teste de carga (hands-on 2), e da carga de pico da sua estimativa (aula 2):

\[ N = \left\lceil \frac{\text{RPS de pico}}{\text{RPS por réplica} \times \text{utilização-alvo}} \right\rceil + \text{folga} \]

Por que uma utilização-alvo (tipicamente 50–70%)? Porque a latência não é linear na carga. À medida que um servidor se aproxima de 100% ocupado, as requisições começam a enfileirar e o tempo de espera explode — o "joelho" da curva (filas). O alvo deixa folga para rajadas e para a perda de uma réplica.

Exemplo resolvido — o serviço catalog na abertura das vendas

Suponha (números ilustrativos): uma réplica do catalog tem 10 vCPUs e gasta 20 ms de CPU por requisição, então consegue completar no máximo 10 / 0,020 = 500 requisições/s. A borda faz cache da maior parte da página estática, e a API recebe 4.000 requisições/s no pico.

  • Com 100% de utilização você precisaria de 4.000 / 500 = 8 réplicas — sem folga nenhuma, toda rajada enfileira.
  • Com alvo de 65%: 4.000 / (500 × 0,65) ≈ 12,3 → 13 réplicas.
  • Some uma réplica de folga, N + 1 (ou uma zona inteira, se você roda em três zonas): 14 réplicas.

O gráfico abaixo deixa o joelho visível. Ele modela cada réplica como um servidor de 10 núcleos com 20 ms de trabalho por requisição (uma fila M/M/10) e distribui 4.000 requisições/s igualmente entre N réplicas:

2026-11-01T21:54:58.598158 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

Com 8 réplicas a fila cresce sem limite (100% ocupado). Com 9 réplicas (89% ocupadas) a latência média fica em torno de 31 ms — 60% acima dos 20 ms de trabalho real; a partir de cerca de 12 réplicas, adicionar mais quase não muda a média. E a cauda (p99) se degrada muito antes e muito mais rápido que a média (percentis) — mais um motivo para manter a utilização moderada.

Pense primeiro: por que não rodar logo 100 réplicas?

Três motivos. Custo: você paga capacidade ociosa o dia inteiro para cobrir dez minutos de pico — a motivação para o autoscaling. Armazenamentos compartilhados: 100 réplicas × um pool de 20 conexões com o banco = 2.000 conexões, mais do que um servidor PostgreSQL deveria aceitar (cada conexão é um processo com memória própria); o gargalo simplesmente migra para o banco. Retornos decrescentes: quando a fila desaparece, réplicas extras não deixam uma requisição mais rápida — os 20 ms de trabalho continuam lá.

Experimente no playground

Abra o cenário scale-out: um load balancer na frente de N réplicas da aplicação.

  1. Rode o perfil load com 2 réplicas e anote o p99 e a taxa de erros.
  2. Adicione réplicas uma de cada vez e observe o p99 cair até se estabilizar — o ponto em que a fila desaparece.
  3. Coloque o caos em medium para matar réplicas aleatoriamente: os health checks tiram as réplicas mortas do pool, e as restantes só absorvem a carga se você deixou folga.

Abrir o simulador completo


O ciclo de vida de uma réplica

Escalar para fora significa que réplicas são criadas e destruídas o tempo todo — por deploys, pelo autoscaler, por falhas de nós. Uma réplica que se comporta bem durante seu ciclo de vida é tão importante quanto uma réplica stateless:

stateDiagram-v2
    [*] --> Starting : agendada em um nó
    Starting --> Ready : startup + readiness probe passam
    Ready --> NotReady : readiness falha (ex.: banco inacessível)
    NotReady --> Ready : readiness volta a passar
    Ready --> Draining : SIGTERM (deploy, scale-in)
    Draining --> [*] : requisições em andamento concluídas
    Ready --> [*] : crash / liveness falha → restart
  • Inicie rápido, aqueça antes de receber tráfego. Um serviço na JVM pode precisar de vários segundos para iniciar; a readiness probe só deve passar quando os pools de conexão estiverem abertos e os caches aquecidos. Inicializações lentas tornam o autoscaling lento (serverless e autoscaling).
  • Desligue de forma graciosa. Ao receber SIGTERM, pare de aceitar novas requisições, termine as que estão em andamento e só então saia. No Spring Boot, server.shutdown=graceful com spring.lifecycle.timeout-per-shutdown-phase=20s faz isso.
  • Seja descartável (Twelve-Factor IX): nada importante deve depender de uma réplica viver muito tempo. Health checks (liveness, readiness, startup) são tratados em confiabilidade.

Formatos de computação

O mesmo serviço stateless pode rodar sobre substratos muito diferentes. A escolha muda quem gerencia o quê, quão rápido você escala e como você paga:

Formato Unidade que você implanta Velocidade de escala Você gerencia Paga por Uso típico
Máquina virtual imagem de VM minutos SO, runtime, aplicação horas provisionadas legado, hardware especial, bancos de dados
Containers no Kubernetes imagem de container segundos (pod) a minutos (nó) o cluster (ou um gerenciado), a aplicação nós, mesmo ociosos o padrão para serviços
Containers serverless (Cloud Run, Fargate, Container Apps) imagem de container segundos, até zero a aplicação por requisição ou por vCPU-segundo APIs com períodos ociosos
Funções (FaaS) (Lambda, Cloud Functions) uma função de menos de um segundo a segundos, até zero uma função por invocação e duração cola entre eventos, webhooks, trabalho em rajadas

A aula 11 (cloud-native) cobre containers e Kubernetes em profundidade; esta aula cobre as decisões que dependem da carga de trabalho: serverless e autoscaling.

O mapa de conceitos de compute

Cada componente de computação responde onde um pedaço de lógica roda e como ele escala; os componentes externos são a lógica que você chama mas não executa. O mapa liga cada componente da paleta (verde) aos conceitos em que ele se apoia (azul):

flowchart LR
    app["app"]:::comp -.- c1["stateless, escala horizontal,<br/>ciclo de vida da réplica"]:::tech
    app -.- c2["REST, gRPC, GraphQL,<br/>APIs assíncronas"]:::tech
    app --> queue["queue"]:::comp
    queue --> worker["worker"]:::comp
    worker -.- c3["jobs idempotentes,<br/>lei de Little, DLQ"]:::tech
    scheduler["scheduler"]:::comp --> queue
    scheduler -.- c4["CronJob, locks,<br/>eleição de líder"]:::tech
    serverless["serverless"]:::comp -.- c5["cold start, concorrência,<br/>atraso do autoscaling"]:::tech
    app --> shared["auth · search ·<br/>notifications · analytics"]:::comp
    shared -.- c6["bounded contexts,<br/>alimentados por eventos, donos dos dados"]:::tech
    app --> ext["payment · email ·<br/>thirdparty"]:::comp
    ext -.- c7["idempotency keys, webhooks,<br/>reconciliação"]:::tech
    classDef comp fill:#1e9e5a22,stroke:#1e9e5a
    classDef tech fill:#2f6fed22,stroke:#2f6fed
Componente da paleta Pergunta que responde Conceitos Ensinado em Cenários
app Onde roda a lógica de requisição/resposta, e como ela escala? réplicas stateless, dimensionamento a 50–70%, ciclo de vida, contratos de API esta visão geral, Estilos de API monolith, scale-out
worker O que roda fora da requisição do usuário? jobs numa fila, workers = taxa × tempo por job, pelo menos uma vez, DLQ Trabalho assíncrono async-queue
scheduler O que roda num horário — uma vez, e não uma vez por réplica? CronJob, lock distribuído, eleição de líder, fencing tokens, expiração preguiçosa Trabalho assíncrono — schedulers notification-system
serverless O que deve escalar por requisição, e até zero? FaaS, cold starts, concorrência = taxa × duração, custo por uso Serverless e autoscaling serverless-burst
auth Quem está chamando? identidades e tokens, comprar antes de construir, validação no gateway Serviços flash-sale (rajadas de login)
search · analytics Como manter visões derivadas fora do caminho da requisição? dados derivados, alimentados por eventos, reconstruíveis a partir da fonte da verdade Serviços search-autocomplete, event-fanout
notifications · email Como as mensagens chegam aos fãs apesar dos limites do provedor? workers guiados por fila, rate limits do provedor, bounces, um provedor secundário Serviços, Integrações — quedas do provedor notification-system
payment · thirdparty Como depender de sistemas que não controlamos? adapters, idempotency keys, resultados UNKNOWN, webhooks, reconciliação Integrações payment-system, cascading-failure, retry-storm
(a camada inteira) Quantas réplicas, e quando? HPA, KEDA, escala pela profundidade da fila, escala agendada, atraso do autoscaling Serverless e autoscaling — atraso scale-out, flash-sale

Na entrevista

Descreva a camada de computação em uma frase e siga para as partes interessantes: "A camada de API é stateless — sessões são JWTs, arquivos vão para o object store — então ela fica atrás de um load balancer e faz autoscaling por CPU e requisições por segundo; eu a dimensionaria com cerca de 65% de utilização-alvo, o que dá 13–14 réplicas para o nosso pico de 4 mil RPS." Entrevistadores querem ouvir que você sabe para onde foi o estado, e um número. Gaste o tempo restante nas partes com estado (banco de dados, cache, fila), onde estão os trade-offs de verdade.

Boas práticas

  • Torne cada réplica identificável — coloque o nome da instância num cabeçalho da resposta e em cada linha de log, para que a única réplica ruim entre 14 possa ser encontrada e retirada. Na BoxOffice: o InstanceHeaderFilter do lab 3.
  • Fixe a CPU e a memória de cada contêiner e meça a capacidade sob esses limites — "RPS por réplica" na fórmula acima só é um número quando o tamanho da réplica é fixo. Na BoxOffice: todo serviço no docker-compose.yaml tem deploy.resources.limits (1 CPU, 768 MB), e o lab 2 mede sob eles.
  • Mantenha uma réplica (ou uma zona) de folga acima da utilização-alvo — com 13 réplicas a 65%, perder uma deixa as outras 12 em cerca de 70%, ainda abaixo do joelho.
  • Exponha probes de liveness e readiness separadas — a readiness falha quando uma dependência está inacessível (pare de mandar tráfego), a liveness só quando o processo travou (reinicie-o); uma probe só para as duas reinicia réplicas saudáveis durante um soluço do banco. Na BoxOffice: management.endpoint.health.probes.enabled: true em todo serviço, desde o lab 1.
  • Escreva o orçamento de conexões — réplicas × tamanho do pool de cada serviço precisa ficar abaixo do que o banco aceita (o exemplo das 2.000 conexões acima). Na BoxOffice: DB_POOL_SIZE por serviço e o PgBouncer no lab 6.

Armadilhas comuns

  • Estado escondido: um mapa em memória, um arquivo local ou um job @Scheduled dentro do processo que silenciosamente assume uma única réplica.
  • Escalar a camada stateless contra uma parede: mais réplicas da aplicação multiplicam as conexões com o banco de dados, que é o gargalo real.
  • Dimensionar pela média: uma frota dimensionada para as 03:00 ou para a média do dia derrete às 10:00 no dia da venda.
  • Sem folga para falhas: usar 100% da capacidade significa que perder uma réplica (ou uma zona) vira indisponibilidade.
  • Inicialização lenta e pesada: uma réplica que precisa de 90 s para ficar pronta não ajuda em um pico que dura 5 minutos.

Pontos-chave

  • A camada de computação é fácil de escalar porque é stateless: sessões, arquivos, caches, agendamentos e contadores ficam em armazenamentos construídos para guardar estado.
  • App servers escalam para fora; dimensione-os como pico / (capacidade por réplica × utilização-alvo) mais folga, e mantenha a utilização em torno de 50–70%, porque a latência explode perto de 100%.
  • Réplicas são descartáveis: início rápido, readiness antes do tráfego, desligamento gracioso.
  • As próximas páginas decidem como dividir o código em serviços, como eles conversam, o que roda em segundo plano, como a frota escala sozinha e como depender de provedores que você não controla.

O que quebra em seguida

Réplicas stateless são baratas de adicionar porque empurraram todo o estado para outro lugar, e para a BoxOffice esse lugar é o PostgreSQL: reservas, pedidos e usuários agora moram no único banco que todas as réplicas compartilham. Às 10:00 ele recebe cerca de 33.000 tentativas de reserva por segundo contra as ~1.100 que consegue atender, e dois fãs que clicam no lugar A-12 no mesmo milissegundo chegam a duas réplicas diferentes, então nenhum lock na memória de uma réplica protege o lugar. Escolher, usar e escalar o armazenamento que precisa dizer "um lugar, uma reserva" é a aula 6 — Bancos de dados.



  1. WIGGINS, A. The Twelve-Factor App, fatores VI (processos) e IX (descartabilidade). 2011. ↩