TL;DR

Este capítulo desenha, do zero, um encurtador de URLs com analytics — um SaaS pequeno o bastante para caber numa entrevista de 45 minutos, e rico o bastante para tocar todos os pilares da trilha Cloud. Começamos com os requisitos (como num loop real), evoluímos o desenho em camadas — compute, rede, dados, borda, mensageria, resiliência, segurança, custo —, cada camada ancorada num pilar do Well-Architected e linkada ao galho que a explica em profundidade. No fim, o mesmo sistema reaparece pequeno, na DigitalOcean, para mostrar que arquitetura boa não é sinônimo de arquitetura grande.

O problema, como ele chega numa entrevista

Imagine a pergunta chegando assim: “Desenhe um sistema tipo bit.ly. Usuários encurtam URLs longas, compartilham o link curto, e o dono da conta vê analytics de cliques — de onde vieram, quando, de que dispositivo.” É uma pergunta clássica, mas o que separa uma resposta júnior de uma sênior não é conhecer o algoritmo de encurtamento — é a sequência: primeiro extrair requisitos, depois estimar escala, só então desenhar, e desenhar de um jeito que expõe trade-offs, não que empilha serviços da AWS para impressionar.

É essa sequência que este capítulo segue. Se você pulou direto pra cá sem passar pelos 24 galhos anteriores, tudo bem — mas cada decisão aqui invoca a nota que a fundamenta, então este texto funciona também como um mapa de navegação pra trás.

Requisitos funcionais

#Requisito
F1Usuário autenticado cria um link curto a partir de uma URL longa
F2Qualquer visitante que acessa o link curto é redirecionado pra URL original
F3Dono do link vê analytics: cliques por dia, país (via IP), referrer, dispositivo
F4Usuário pode customizar o slug (meusite.com/promo26) ou receber um gerado
F5Links podem expirar numa data ou após N cliques

Requisitos não funcionais — e por que eles decidem a arquitetura

#RequisitoPor que importa aqui
NF1Redirecionamento é o caminho quente: precisa ser rápido (p99 < 100ms) e disponível (99.9%+)Puxa a leitura pra perto do usuário e pro cache — não pro banco
NF2Leitura (redirects) domina sobre escrita (criação de link) em ordem de grandeza — típico 100:1 ou maisJustifica cache agressivo e réplicas de leitura
NF3Analytics pode ser eventualmente consistente — ninguém liga se o contador de cliques atrasa 30 segundosAbre a porta pra processamento assíncrono, tirando trabalho do caminho quente
NF4Sistema multi-tenant: dados de um cliente nunca vazam pra outroDecide modelo de dados e a fronteira de autorização
NF5Picos de tráfego são possíveis (link viral)Puxa auto scaling e CDN pra absorver o pico sem acordar ninguém

Estimativa de escala (o exercício que a entrevista espera)

Um cálculo de guardanapo, do tipo que se faz em voz alta na entrevista: suponha 10 milhões de links criados por mês e uma proporção de leitura:escrita de 100:1. Isso dá ~333 mil criações/dia (≈4 QPS de escrita, com folga pra picos) e ≈400 QPS de leitura em média — mas o pico pode ser 10-50x isso se um link viralizar. Armazenamento: cada registro de link é pequeno (URL + metadados, digamos 500 bytes); em 5 anos, 600 milhões de links dão ~300GB — trivial para um banco relacional gerenciado. O volume que cresce de verdade é o de eventos de clique (um por redirect), que em 1 ano pode passar de bilhões de linhas — por isso analytics não mora na mesma tabela transacional que os links.

Esse é o ponto onde já dá pra prever a forma do sistema: leitura pesada e latência crítica no redirect, escrita leve na criação, e um rio de eventos separado para analytics. As próximas seções constroem exatamente isso, camada por camada.

Assista: System Design: Encurtador de URL - Desafio Real de Entrevista RESOLVIDO

Canal: Leonardo Zamariola | Duração: ~50min | Idioma: PT-BR

Resolve exatamente esta pergunta de entrevista do zero, em português — vale assistir como companheiro deste capítulo pra ver a mesma sequência (requisitos → esquema de geração de slug → réplicas de leitura) sendo verbalizada em tempo real, com as hesitações e correções que uma entrevista de verdade tem. Trecho de destaque [39:00]: “Seria legal que tivéssemos réplica de leitura para questão de performance e eh o link longo ter índice.”

🎬 Assistir no YouTube

O desenho evolutivo

Camada 0 — a versão ingênua (e por que ela quebra)

A resposta mais simples — uma API, um banco relacional, pronto — funciona para 100 usuários e quebra na primeira menção de “pico de tráfego” ou “múltiplos data centers”. Ela serve como ponto de partida honesto numa entrevista (mostra que você sabe simplificar antes de complicar), mas o entrevistador vai empurrar os requisitos não funcionais até ela ceder. É aí que cada camada seguinte entra como resposta a uma pressão específica, não como enfeite.

Camada 1 — compute e a API

O núcleo é uma API que expõe três rotas: POST /links (cria), GET /{slug} (redireciona — o caminho quente) e GET /links/{id}/stats (lê analytics). Rodar isso em containers — não em VMs geridas à mão, não em uma única função Lambda monolítica — é a escolha natural para um serviço com tráfego constante e lógica de negócio não trivial: containers gerenciados (ECS Fargate ou EKS) dão o meio-termo entre o controle de uma VM e a operação zero de serverless. A API roda atrás de um load balancer que distribui entre múltiplas tarefas em múltiplas zonas — o padrão de elasticidade e balanceamento coberto em Compute II, que também decide a política de auto scaling: escalar por CPU não basta para um pico de redirect; escalar por requisições-por-tarefa captura melhor o padrão real.

O caminho de criação de link (F1, F4) é onde a lógica de negócio mora: gerar ou validar o slug, checar duplicidade, gravar. O caminho de redirect (F2) é deliberadamente burro — só lê o cache ou o banco, resolve a URL, dispara um evento assíncrono, e devolve um 302. Essa assimetria é a decisão de arquitetura mais importante da nota inteira: o redirect nunca deve esperar por nada que não seja essencial ao redirect.

Para processamento assíncrono mais pesado (gerar relatórios agregados de analytics, expirar links por cron), funções Lambda entram como o complemento certo: código que roda sob demanda, disparado por evento, sem servidor pra manter de pé o tempo todo. A mistura containers-para-API-síncrona + serverless-para-jobs-assíncronos é um padrão maduro, não uma indecisão — cada peça no lugar onde seu modelo de custo e escala faz sentido.

Assista: Beginner System Design Interview: Design Bitly w/ a Ex-Meta Staff Engineer

Canal: Hello Interview | Duração: ~60min | Idioma: EN

Um ex-staff engineer da Meta conduz a mesma entrevista de encurtador de URL do início ao fim, insistindo — como este capítulo — que o requisito não é “baixa latência” genérico, e sim baixa latência especificamente no redirect. Boa referência canônica em inglês pra comparar o vocabulário técnico com a versão PT-BR acima. Trecho de destaque [07:37]: “it’s not just low latency it’s specifically low latency on redirects and we want to quantify it too”

🎬 Assistir no YouTube

Camada 2 — rede: onde tudo isso mora

Nada do que foi dito acima faz sentido sem uma VPC desenhada com intenção. O desenho clássico de duas camadas se aplica direto aqui: subnets públicas em pelo menos duas AZs hospedam só o load balancer (a única coisa que precisa ser alcançável da internet); subnets privadas, também espalhadas por duas ou três AZs, hospedam as tarefas da API, o banco de dados e o cache. Nada nas subnets privadas tem IP público — elas alcançam a internet (para, por exemplo, chamar uma API de geolocalização de IP) através de um NAT Gateway, e nunca o contrário.

flowchart TB
    subgraph VPC["VPC — us-east-1"]
        subgraph AZa["AZ-a"]
            subgraph PubA["Subnet pública"]
                ALB["Application Load Balancer"]
            end
            subgraph PrivA["Subnet privada"]
                API_A["API — task"]
                DB_A[("RDS — primary")]
                Cache_A[("ElastiCache — nó")]
            end
        end
        subgraph AZb["AZ-b"]
            subgraph PubB["Subnet pública"]
                ALB2["ALB — replica lógica"]
            end
            subgraph PrivB["Subnet privada"]
                API_B["API — task"]
                DB_B[("RDS — standby")]
                Cache_B[("ElastiCache — réplica")]
            end
        end
        NAT["NAT Gateway"]
    end
    Internet(("Internet")) --> ALB
    ALB --> API_A
    ALB --> API_B
    API_A --> DB_A
    API_B --> DB_A
    DB_A -.replicação síncrona.-> DB_B
    API_A --> Cache_A
    API_B --> Cache_A
    PrivA -.saída.-> NAT
    PrivB -.saída.-> NAT
    NAT --> Internet

Repare que o ALB é um único recurso lógico gerenciado pela AWS que já opera em múltiplas AZs — o diagrama separa por clareza didática, não porque existam dois load balancers reais. O ponto que importa: nenhuma zona de disponibilidade, sozinha, é dona de nada crítico. Isso é a semente da camada de resiliência, que volta mais abaixo.

Security groups fecham o cerco: o ALB aceita 443 de qualquer origem; a API aceita tráfego só do security group do ALB; o banco aceita conexão só do security group da API. Três portas de entrada, cada uma justificada pela anterior — não uma regra “permitir tudo interno” preguiçosa.

Camada 3 — dados: banco, cache e o rio de eventos

Aqui o desenho se ramifica em três armazenamentos, cada um respondendo a um requisito diferente:

Banco relacional para os links. Um links (id, slug, url_original, dono, criado_em, expira_em) é dado transacional clássico — precisa de unicidade de slug, integridade referencial com o dono, e consistência forte na escrita. Um banco gerenciado — RDS PostgreSQL, por exemplo — em modo Multi-AZ é a escolha, coberta em Bancos gerenciados. Multi-AZ mantém uma réplica síncrona numa segunda AZ que assume automaticamente se a primária cair — é infraestrutura, não lógica de aplicação, o que resolvida.

Verificado 2026-07-24 — RDS Multi-AZ

A documentação da AWS distingue dois modos: Multi-AZ DB instance (uma standby que só faz failover, não serve leitura) e Multi-AZ DB cluster (duas standbys que também servem leitura, com failover tipicamente mais rápido). Para este capstone, o modo instance já resolve o requisito de disponibilidade; o modo cluster vale a pena se o volume de leitura justificar réplicas adicionais. Fonte: docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html.

Cache na frente do redirect. O requisito NF1 (p99 < 100ms) não sobrevive a uma consulta ao banco a cada clique. Um cache in-memory — ElastiCache Redis — guarda o par slug → url_original com TTL curto, e a API só bate no banco em cache miss. Isso é literalmente o padrão que resolve o requisito, e é por isso que a leitura:escrita 100:1 estimada acima importa: quanto mais assimétrica a proporção, mais o cache paga a própria conta.

Um rio de eventos para analytics. Cada redirect bem-sucedido dispara um evento (slug, timestamp, ip, user_agent, referrer) para uma fila — SQS, ou um tópico Kafka/Kinesis se o volume justificar stream em vez de fila ponto-a-ponto —, coberta em Mensageria e eventos gerenciados. Um consumidor assíncrono (Lambda, ou um worker containerizado) lê a fila, enriquece o evento (geolocaliza o IP, classifica o dispositivo) e grava agregados num data store de analytics — pode ser o mesmo Postgres numa tabela separada para uma escala modesta, ou um data warehouse dedicado se o volume de cliques crescer muito além dos links em si. O ponto crítico de arquitetura: o redirect dispara o evento e não espera a confirmação de que ele foi processado — é fire-and-forget na direção da fila, o que mantém o caminho quente rápido mesmo que o consumidor de analytics esteja atrasado ou temporariamente fora do ar.

sequenceDiagram
    participant U as Visitante
    participant ALB as Load Balancer
    participant API as API (redirect)
    participant Cache as ElastiCache
    participant DB as RDS (Postgres)
    participant Q as Fila (SQS)
    participant W as Worker de analytics
    participant AN as Tabela de analytics

    U->>ALB: GET /promo26
    ALB->>API: encaminha
    API->>Cache: GET slug:promo26
    alt cache hit
        Cache-->>API: url_original
    else cache miss
        API->>DB: SELECT url_original WHERE slug=...
        DB-->>API: url_original
        API->>Cache: SET slug:promo26 (TTL)
    end
    API-->>U: 302 Location: url_original
    API->>Q: publica evento de clique (assíncrono)
    Note over API,U: usuário já foi redirecionado — não espera a fila
    W->>Q: consome evento
    W->>W: enriquece (geo-IP, device)
    W->>AN: grava agregado

Camada 4 — borda: DNS e CDN

Nem todo tráfego precisa chegar à VPC. Uma CDN na frente do domínio curto, coberta em DNS, CDN e borda, resolve dois problemas de uma vez: reduz a latência do redirect para usuários geograficamente distantes (a resposta pode ser cacheada perto do visitante, já que a maioria dos redirects para um mesmo slug retorna a mesma URL) e absorve picos de tráfego sem tocar a API — se um link viraliza, é a rede de borda da CloudFront que aguenta o primeiro impacto, não o auto scaling da API correndo atrás do prejuízo. O DNS do domínio custom (meusite.com) é gerenciado via Route 53, com um registro apontando para a distribuição CDN, que por sua vez tem o ALB como origem.

Para a API de gestão (criação de link, dashboard de analytics) — que é autenticada e não se beneficia de cache público — o tráfego pode ir direto ao API Gateway em vez da CDN, o que também dá de graça rate limiting, validação de payload e um ponto único para aplicar autenticação antes de a requisição chegar à lógica de negócio.

flowchart LR
    User(("Usuário")) -->|GET /slug| DNS["Route 53"]
    DNS --> CDN["CloudFront"]
    CDN -->|cache hit| User
    CDN -->|cache miss / origin| ALB["ALB"]
    ALB --> API["API — redirect"]

    Admin(("Dono do link")) -->|POST /links, autenticado| APIGW["API Gateway"]
    APIGW --> AuthZ["Autorização — Cognito/JWT"]
    AuthZ --> API2["API — gestão"]

Camada 5 — identidade e segurança

Multi-tenancy (NF4) é decidido aqui, não na camada de dados: cada link carrega um tenant_id, e toda query passa por esse filtro — mas a garantia estrutural vem de cima, na camada de autenticação e autorização coberta em Identidade e acesso (IAM) e aprofundada em Segurança na cloud a fundo. Usuários finais se autenticam via Cognito (ou equivalente), recebem um JWT com o tenant_id embutido, e o API Gateway valida esse token antes mesmo de a requisição chegar à lógica de negócio — a autorização nunca é responsabilidade só do código de aplicação.

Um exemplo do tipo de decisão que essa camada obriga: a política IAM que a task da API assume para escrever na fila de eventos deve ser mínima, não um sqs:* genérico:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["sqs:SendMessage"],
      "Resource": "arn:aws:sqs:us-east-1:123456789012:link-clicks-queue"
    }
  ]
}

Least privilege aplicado literalmente: a task de redirect só pode enviar mensagem para essa fila — não ler, não deletar, não tocar em nenhuma outra fila da conta. É o mesmo princípio de IAM que separa a role da API (que fala com o banco e a fila) da role do worker de analytics (que fala com a fila e a tabela de analytics, e nada mais).

Assista: AWS re:Invent 2021 - Fundamentos de Arquitetura SaaS (Software as a Service)

Canal: AWS Events | Duração: ~28min | Idioma: PT-BR

A palestra oficial da AWS sobre arquitetura SaaS multi-tenant, em português, aprofunda exatamente o requisito NF4 desta nota: isolamento de tenant não é só “filtrar por tenant_id na query” — é uma propriedade que a arquitetura inteira precisa garantir, camada por camada. Trecho de destaque [04:56]: “que tenha isolamento de tenant onde o tenant 1 não consiga ver dados do tenant 2”

🎬 Assistir no YouTube

Camada 6 — resiliência multi-AZ

A resiliência não é uma camada nova de infraestrutura — é uma releitura do que já foi desenhado, perguntando “o que acontece se isso morrer?” em cada peça. É o assunto de Resiliência e continuidade, e aqui ele se traduz assim:

ComponenteFalha consideradaMitigação já embutida no desenho
Task da APIUma tarefa trava ou é substituídaMúltiplas tasks atrás do ALB, em ≥2 AZs; health check remove a instância doente
Banco RDSAZ inteira caiMulti-AZ com standby síncrona; failover automático (minutos, não horas)
Cache ElastiCacheNó de cache caiCache miss é seguro por design — API cai pro banco; réplica de cache reduz o impacto
Fila de eventosWorker de analytics fica indisponívelMensagens ficam retidas na fila (retenção configurável); nenhum clique é perdido, só atrasado
AZ inteira (rede, compute)Data center fica inacessívelToda camada crítica (API, banco, cache) já está replicada em ≥2 AZs por desenho, não como acréscimo
Região inteira (evento raro)Fora do escopo do MVPDiscutir como próximo passo: backup cross-region do RDS + réplica de leitura em outra região, ver DR em 20

O RTO/RPO que esse desenho entrega no cenário comum (falha de AZ) é de minutos, não de horas — porque o failover é automático e a réplica já está quente. Um RTO de segundos ou uma tolerância a falha de região inteira já é outro orçamento de engenharia, e é honesto dizer isso na entrevista em vez de prometer “zero downtime” sem custo.

Camada 7 — custo, a variável que ninguém pede mas todo mundo cobra

Nenhuma dessas escolhas é grátis, e uma resposta sênior nomeia o trade-off — não só o serviço. NAT Gateway cobra por hora e por GB processado; um par de NAT Gateways (um por AZ, para não recriar um ponto único de falha) é frequentemente uma das linhas mais caras de uma VPC pequena. Cache reduz custo de banco (menos IOPS, instância menor) mas soma seu próprio custo — só compensa quando a proporção leitura:escrita justifica. Multi-AZ no RDS dobra o custo de compute do banco (paga-se pela standby, mesmo ociosa) em troca de failover automático — o tipo de troca que FinOps pede pra explicitar, não esconder atrás de “é o certo a fazer”. Para um MVP com poucos usuários, a resposta honesta pode ser: comece sem Multi-AZ, sem NAT redundante, meça o tráfego real, e adicione essas redundâncias quando o SLA prometido a clientes exigir.

A passada pelos 6 pilares — checklist final

PilarComo este desenho responde
Excelência operacionalIaC de ponta a ponta (ver IaC) para reproduzir o ambiente; observabilidade (métricas de latência do redirect, profundidade da fila) via Observabilidade
SegurançaJWT + Cognito na borda, security groups em camadas, IAM de privilégio mínimo por serviço, tenant_id como fronteira de dados — Segurança a fundo
ConfiabilidadeMulti-AZ em compute, banco e cache; fila absorve pico de escrita assíncrona; nenhum componente crítico é single-AZ — Resiliência
Eficiência de performanceCache na frente do redirect, CDN na borda, auto scaling por carga real — Compute II e CDN
Otimização de custoServerless para jobs esparsos, cache dimensionado pela proporção real leitura:escrita, Multi-AZ como escolha explícita e não automática — FinOps
SustentabilidadeAuto scaling evita superprovisionamento ocioso; serverless paga só o que roda — reflexo direto do pilar de custo, ponto que o próprio Well-Architected reconhece

A mesma ideia na DigitalOcean

Nem toda entrevista — e nem todo cliente real — precisa do vocabulário inteiro da AWS. O mesmo sistema no DO, coberto em DigitalOcean a fundo, perde peças sem perder função:

Camada AWSEquivalente DOO que muda
ECS Fargate + ALB + LambdaApp PlatformUm serviço só junta deploy, load balancing e auto scaling; menos peças pra configurar, menos controle fino
VPC com subnets públicas/privadasVPC do DOModelo mais simples, sem a mesma granularidade de route tables por subnet
RDS Multi-AZManaged Database (Postgres) com standbyStandby síncrona existe, mas o cardápio de topologias é menor que o da AWS
ElastiCacheManaged Valkey/RedisEquivalente direto, catálogo mais enxuto de tamanhos
S3SpacesAPI compatível com S3; CDN integrada nativamente no produto
SQS/KinesisManaged Kafka ou fila própriaSem um serviço de fila leve equivalente ao SQS — Kafka é overkill para uma fila simples; alternativa é rodar um worker com uma lib de fila sobre o Postgres, ou usar Kafka mesmo se o time já domina
CloudFront + Route 53Spaces CDN + DNS do DOCobertura de edge menor, mas suficiente pra a maioria dos casos fora de escala hiperglobal
IAM granular por serviçoTokens de API + roles de projetoModelo mais simples, com menos granularidade de política por recurso

Verificado 2026-07-24 — limites do App Platform

Documentação da DigitalOcean: build usa até 8 vCPUs / 15GB RAM / 24GB de disco, timeout de build de 1h, cada app suporta até 250 containers (100 com autoscaling baseado em requisição). O sistema de arquivos local do App Platform é efêmero e limitado a 4GB — reforça por que uploads de usuário vão pra Spaces, nunca pro disco do container. CDN embutida via Cloudflare (roteamento) e via Spaces CDN (estáticos). Fonte: docs.digitalocean.com/products/app-platform/details/limits/.

A honestidade de paridade importa: o DO não tem um equivalente direto a SQS/SNS como fila gerenciada leve — Managed Kafka existe, mas é uma ferramenta mais pesada para o mesmo trabalho. Para um MVP nesse porte, um approach pragmático no DO é rodar o worker de analytics consumindo uma tabela de outbox no próprio Postgres gerenciado, em vez de introduzir Kafka só para resolver um requisito de fila simples.

Como verbalizar isso numa entrevista

A ordem que funciona na frente de um entrevistador segue a mesma ordem deste capítulo: requisitos → estimativa → desenho simples → evolução guiada pelos NFRs → pilares como checklist de fechamento. Três frases que carregam peso desproporcional quando ditas no momento certo:

  • “O redirect é o caminho quente — tudo que não é essencial a ele eu tiro do caminho síncrono.” (justifica cache + fila assíncrona de uma vez)
  • “Eu escolho Multi-AZ aqui porque o SLA prometido justifica dobrar o custo do banco — mas eu diria isso em voz alta pro time de produto antes de assumir.” (mostra consciência de custo sem parecer que você está economizando por preguiça)
  • “Eu desenharia isso primeiro sem CDN e sem Multi-AZ, mediria o tráfego real, e adicionaria essas peças quando os números pedissem.” (mostra maturidade: você sabe simplificar, não só empilhar serviços)

Armadilhas comuns

  • Colocar o registro do evento de clique síncrono no caminho do redirect — isso viola o próprio requisito de latência que motivou o desenho.
  • Esquecer o tenant_id como filtro em toda query — é o tipo de furo que só aparece em produção, com dados de clientes diferentes vazando.
  • Tratar Multi-AZ como decisão binária e automática — é uma decisão de custo, não uma “boa prática” sem preço.
  • Propor CDN + Multi-AZ + auto scaling agressivo + Kafka sem que nenhum requisito peça isso — parece impressionante, mas sinaliza que você não sabe dizer não a complexidade desnecessária.
  • No DO, tentar recriar a granularidade de IAM da AWS onde ela não existe — é melhor nomear a diferença do que fingir paridade.

O que vem a seguir

Este capstone fecha a trilha Cloud como texto — mas duas frentes continuam abertas para quem quer aprofundar: (1) praticar a mesma pergunta com variações — “e se o pico for 100x maior?”, “e se precisar de DR cross-region?” — usando este desenho como ponto de partida; e (2) revisar o galho de certificação SAA, que testa exatamente esse tipo de julgamento de arquitetura em formato de prova. Vale também revisitar Panorama multi-cloud e portabilidade para pensar em quão preso a um provedor este desenho ficou — e o que custaria migrá-lo.

Fontes