“Design a URL shortening service like Bitly or TinyURL.”

Essa é geralmente a primeira pergunta de system design que candidatos encontram. Parece simples — recebe URL longa, retorna URL curta, redireciona quando acessam. Dá pra fazer em 50 linhas de código, certo?

Certo. Pra 100 usuários. Agora faz isso pra 100 bilhões de URLs com 100.000 redirects por segundo e vamos ver o quão “simples” é.

O URL Shortener é o exercício perfeito pra fechar a série porque combina decisões aparentemente simples que revelam profundidade quando você puxa o fio:

  • Como gerar IDs curtos únicos sem colisão? (hashing vs counter)
  • Como servir redirects em <10ms? (caching agressivo)
  • Como contar cliques sem afetar latência? (analytics async)
  • Como expirar URLs sem scan do banco inteiro? (TTL design)

São decisões que aparecem em qualquer sistema. Dominar esse design te dá ferramentas pra todos os outros.

Vamos aplicar o framework pela última vez.

Fase 1: Esclarecer requisitos

Requisitos funcionais

FuncionalidadeDetalhe
Criar short URLRecebe URL longa, retorna short.url/abc123
RedirectAcesso à short URL redireciona pra URL original
Custom aliasUsuário pode escolher slug personalizado
ExpiraçãoURLs podem ter TTL configurável
AnalyticsContagem de cliques, geo, device, referer

Requisitos não-funcionais

RequisitoTarget
Escala1 bilhão de URLs criadas, 100B redirects/mês
Ratio read/write~1000:1 (pra cada URL criada, ~1000 redirects)
Latência de redirect< 10ms (p99)
Latência de criação< 100ms
Disponibilidade99.99% (redirect offline = links quebrados everywhere)
UnicidadeColisão = zero. Dois URLs nunca podem mapear pro mesmo short code
DurabilidadeURL criada nunca pode ser perdida (a menos que expire)

Fora do escopo

  • Link preview / unfurling
  • QR code generation
  • A/B testing de destinos
  • User management (planos, billing)

A restrição interessante

O short code precisa ser curto (é literalmente o propósito do serviço). Se usamos caracteres [a-zA-Z0-9] = 62 possibilidades por posição:

6 caracteres: 62^6 = ~56 bilhões de combinações
7 caracteres: 62^7 = ~3.5 trilhões de combinações

Com 1 bilhão de URLs, 6 caracteres é suficiente (usa ~1.8% do espaço). 7 caracteres dá margem confortável pra décadas.

Fase 2: Estimativas

Criação (write)

URLs criadas/mês: 100.000.000 (100M)
URLs criadas/segundo: 100M / (30 × 86.400) ≈ ~40/s (média)
Pico: ~200/s

Tamanho médio de uma URL: ~200 bytes (original) + ~50 bytes (metadata)
Storage/mês: 100M × 250 bytes = ~25 GB/mês
Storage/5 anos: ~1.5 TB

1.5 TB em 5 anos. Cabe num único PostgreSQL confortavelmente. Escala de write é trivial (~40/s). O desafio não é write.

Redirect (read)

Redirects/mês: 100.000.000.000 (100 bilhões)
Redirects/segundo: 100B / (30 × 86.400) ≈ ~38.500/s (média)
Pico: ~100.000/s

Bandwidth: 100K req/s × ~500 bytes (request + headers) = ~50 MB/s

100.000 reads/segundo no pico. Aqui está o desafio. O banco não aguenta 100K queries/segundo por row lookup. Precisa de cache na frente.

Cache sizing

Distribuição de acessos segue power law:
  20% das URLs respondem por 80% do tráfego

URLs "hot" (acessadas frequentemente): ~20M URLs
Tamanho por entry no cache: short_code + long_url ≈ 300 bytes
Cache total: 20M × 300 bytes = ~6 GB

6 GB cabe em um único Redis node. Esse é o tipo de sistema onde caching resolve quase tudo.

Fase 3: High-level design

Arquitetura geral

┌─────────────────────────────────────────────────────────────────────┐
│                         WRITE PATH (Create)                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  Client ──→ API Server ──→ ID Generator ──→ URL DB                   │
│                                │                                      │
│                                ▼                                      │
│                          Cache (warm)                                  │
│                                                                       │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                        READ PATH (Redirect)                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  Client ──→ CDN/Load Balancer ──→ Redirect Service                   │
│                                        │                              │
│                                   ┌────┴────┐                        │
│                                   ▼         ▼                        │
│                              Cache (hit?)  URL DB                     │
│                                   │                                   │
│                                   ▼                                   │
│                          HTTP 301/302 Redirect                        │
│                                   │                                   │
│                                   ▼ (async)                           │
│                          Analytics Pipeline                            │
│                                                                       │
└─────────────────────────────────────────────────────────────────────┘

Componentes

ComponenteResponsabilidade
API ServerCriar short URLs, validação
ID GeneratorGerar short codes únicos
URL DBPersistir mapeamento short→long
Cache (Redis)Lookup rápido pra redirects
Redirect ServiceResolver short code e redirecionar
Analytics PipelineContar cliques, metadata
Cleanup ServiceExpirar URLs com TTL

Deep Dive 1: Geração de Short Codes — O problema mais importante

Como gerar abc123 a partir de https://www.example.com/very/long/path/to/something?

Parece bobo, mas a escolha aqui impacta unicidade, performance, e distribuição do sistema inteiro.

Approach 1: Hashing (MD5/SHA256 + truncar)

input:  "https://www.example.com/long/path"
md5:    "e4d909c290d0fb1ca068ffaddf22cbd0"
base62: "e4d909c290" → truncar pra 7 chars → "e4d909c"

Problemas:

  1. Colisão: truncar hash aumenta probabilidade de colisão. Com 7 chars base62 e 1B URLs, a probabilidade não é negligível (birthday paradox).

  2. Resolver colisão: se e4d909c já existe no DB:

    Retry: hash(original_url + "1") → novo hash → truncar → checar colisão → retry...
    

    Loop até encontrar slot livre. Funciona mas adiciona latência imprevisível.

  3. Mesma URL = mesmo hash: se dois usuários encurtam a mesma URL, recebem o mesmo short code. Pode ser feature (deduplicação) ou bug (cada um deveria ter analytics separado).

Veredito: funciona pra MVP. Não escala bem com bilhões de URLs (colisões frequentes degradam performance).

Approach 2: Counter + Base62 encoding

Counter auto-increment: 1, 2, 3, ... 1000000
Base62 encode: 
  1 → "1"
  62 → "10"  
  1000000 → "4c92"
  56800235583 → "zzzzzzz" (máximo com 7 chars)

Como funciona:

CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def encode_base62(num):
    if num == 0:
        return CHARSET[0]
    result = []
    while num > 0:
        result.append(CHARSET[num % 62])
        num //= 62
    return ''.join(reversed(result))

def decode_base62(s):
    num = 0
    for char in s:
        num = num * 62 + CHARSET.index(char)
    return num

Vantagens:

  • ✅ Zero colisões (cada número gera código único)
  • ✅ Previsível e determinístico
  • ✅ Decodificação possível (short code → número → busca direta no DB)

Problema: como gerar o counter de forma distribuída?

Counter distribuído: as opções

Opção A: Auto-increment no DB

INSERT INTO urls (long_url) VALUES ('https://...') RETURNING id;
-- id = 7483921
-- short_code = base62(7483921) = "1Ri7B"

Simples. Mas o DB vira single point of contention. Com ~40 writes/s é ok. Com burst de 200/s pode ser gargalo (lock no sequence).

Opção B: Pre-allocated ranges

Coordinator distribui ranges pra cada server:
  Server A: range [1, 1.000.000]
  Server B: range [1.000.001, 2.000.000]
  Server C: range [2.000.001, 3.000.000]

Cada server consome seu range localmente (in-memory counter):
  Server A cria URL → counter = 547.832 → base62 → short code
  Sem coordenação necessária (cada server tem range exclusivo)
  Quando range acaba → pede novo range ao coordinator

Vantagens:

  • Zero colisão (ranges não se sobrepõem)
  • Sem coordenação por-request (counter local é O(1))
  • Coordinator só é chamado a cada ~1M URLs (raro)

Desvantagem: IDs não são sequenciais globalmente (Server A pode estar no 500K enquanto Server B está no 1.2M). Não é problema — ninguém precisa de ordem global.

Opção C: Snowflake-style ID (timestamp + machine + sequence)

| 41 bits: timestamp | 5 bits: datacenter | 5 bits: machine | 12 bits: sequence |

Gera IDs únicos sem coordenação. Mas produz números de 64 bits que em base62 têm 11 chars. Mais longo que necessário pra URL shortener (queremos 6-7).

Escolha recomendada: Pre-allocated ranges. Combina simplicidade, zero colisão, e escala horizontal.

Approach 3: Custom alias (slug escolhido pelo user)

POST /v1/urls
{
  "long_url": "https://...",
  "custom_alias": "meu-link-legal"
}

Validação necessária:

  1. Slug permitido? (não é palavra reservada, profanidade, etc)
  2. Slug disponível? SELECT 1 FROM urls WHERE short_code = 'meu-link-legal'
  3. Se disponível → reservar atomicamente

Race condition: dois users pedem mesmo slug simultaneamente:

INSERT INTO urls (short_code, long_url) VALUES ('meu-link-legal', '...')
ON CONFLICT (short_code) DO NOTHING
RETURNING id;

-- Se id retornado: sucesso
-- Se nada retornado: slug já existe → erro 409 Conflict

Unique constraint no DB garante atomicidade. Sem race condition.

Deep Dive 2: Storage e Caching

Database schema

CREATE TABLE urls (
    id BIGINT PRIMARY KEY,           -- counter-based
    short_code VARCHAR(10) UNIQUE NOT NULL,
    long_url TEXT NOT NULL,
    user_id BIGINT,                  -- quem criou (nullable pra anônimos)
    created_at TIMESTAMP DEFAULT NOW(),
    expires_at TIMESTAMP,            -- NULL = nunca expira
    click_count BIGINT DEFAULT 0,    -- counter denormalizado
    
    INDEX idx_short_code (short_code),
    INDEX idx_expires (expires_at) WHERE expires_at IS NOT NULL
);

Por que PostgreSQL (e não NoSQL)?

  • Volume de write é baixo (~40/s). Não precisa de escala horizontal de escrita.
  • Read vai ser servido pelo cache na esmagadora maioria.
  • Unique constraint garante zero colisão.
  • Queries de analytics (top URLs, por user) são relacionais.

Com 1.5 TB em 5 anos, um PostgreSQL com bom hardware (NVMe SSD) serve sem problemas. Se precisar escalar além: shard por id (range-based, já que IDs são sequenciais).

Caching strategy

O sistema é extremamente read-heavy (1000:1). Cache hit rate esperado: >99%.

Redirect request: GET /abc123
Redis: GET url:abc123
    ├── HIT (99%+ dos casos): retorna long_url → redirect
    └── MISS: 
         query DB → retorna long_url
         SET url:abc123 {long_url} EX 86400 → cacheia por 24h
         redirect

Cache policy:

AspectoDecisãoMotivo
EvictionLRU (Least Recently Used)URLs não acessadas há tempo são cold
TTL24 horas (renovado a cada hit)Balance entre memória e hit rate
Warm-upAo criar URL, já escreve no cachePrimeira visita não sofre cold start
InvalidaçãoAo deletar/expirar URLRemove do cache imediatamente

Cache hit rate com 6 GB:

Total URLs: 1 bilhão
URLs acessadas nas últimas 24h: ~20 milhões (2%)
Cache cabe: ~20M entries × 300 bytes = 6 GB
Hit rate: ~99.5% (quase todo acesso é pra URLs recentes/populares)

Com 99.5% de cache hit, o DB recebe apenas ~500 queries/segundo (0.5% de 100K/s). Confortável.

CDN como primeira camada

URLs extremamente populares (viral no Twitter, email marketing pra milhões) podem ser cacheadas direto no CDN:

Client → CDN Edge (cache hit?) → Origin (Redirect Service + Redis)

CDN retorna HTTP 301 cacheado. O redirect nem chega no nosso server. Reduz load drasticamente pra links virais.

Cuidado: se usar 301 (permanent redirect), browsers cacheiam e não passam mais pelo seu server (perde analytics). Se usar 302 (temporary redirect), browser volta sempre ao server (mantém analytics mas não ganha cache de browser).

Trade-off:

  • 301: melhor performance, perde controle (não pode mudar destino, perde tracking)
  • 302: mantém controle total, mais load no server

Escolha comum: 302 por padrão (analytics importa), 301 opcional pra URLs que nunca mudam.

Deep Dive 3: Redirect — os 10ms mais importantes

O redirect precisa ser rápido. Cada milissegundo de latência aqui é sentido por todo mundo que clica num link encurtado.

Fluxo otimizado

1. Request: GET /abc123
   [0ms]

2. Parse short_code do path
   [<0.1ms]

3. Redis lookup: GET url:abc123
   [0.5-1ms]

4. Se encontrou: retorna HTTP 302 Location: {long_url}
   [<0.1ms]

5. Se não encontrou: query DB
   [2-5ms]

Total (cache hit): ~1-2ms
Total (cache miss): ~5-10ms

Otimizações

1. Connection pooling ao Redis:

Abrir conexão TCP pro Redis a cada request adiciona ~1ms. Connection pool reutiliza conexões existentes.

2. Redirect service stateless:

Nenhum estado local. Qualquer instância pode servir qualquer request. Escala adicionando mais pods/containers.

3. HTTP keep-alive:

Client mantém conexão TCP aberta pro server. Elimina overhead de TCP handshake + TLS handshake a cada request.

4. Resposta mínima:

HTTP/1.1 302 Found
Location: https://www.example.com/very/long/original/url
Content-Length: 0

Zero body. Só o header Location. Menor resposta possível = menor latência de transmissão.

Deep Dive 4: Analytics Pipeline

Cada redirect é um evento de analytics: quem clicou, de onde, quando, qual device.

O problema

100.000 redirects/segundo. Se cada um fizer um write síncrono no analytics DB:

100K writes/s no PostgreSQL = vai derrubar
E o redirect ficaria mais lento (esperando write de analytics)

Solução: fire-and-forget + async pipeline.

O redirect NÃO espera analytics ser processado. Registro é assíncrono.

Arquitetura de analytics

Redirect acontece
    ▼ (fire-and-forget)
Kafka topic: "click_events"
    ├──→ Consumer: Real-time counter (Redis INCR)
    │      └── Incrementa click_count:{short_code}
    ├──→ Consumer: Analytics aggregator
    │      └── Agrega por hora, geo, device, referer
    │      └── Escreve em batch no Analytics DB (cada 10s)
    └──→ Consumer: Raw event archive
           └── S3/Blob (pra queries ad-hoc futuras)

Evento de click

{
  "short_code": "abc123",
  "timestamp": "2026-05-24T10:30:15.432Z",
  "ip": "189.100.x.x",
  "country": "BR",
  "city": "São Paulo",
  "user_agent": "Mozilla/5.0 (iPhone; CPU iOS 17_0)...",
  "referer": "https://twitter.com/...",
  "device_type": "mobile",
  "os": "iOS",
  "browser": "Safari"
}

Geo-lookup

IP → país/cidade. Não dá pra fazer request externo a cada click (latência). Solução: MaxMind GeoIP database local (~50 MB, atualizada semanalmente). Lookup é in-memory, <0.1ms.

Aggregation schema

CREATE TABLE click_stats_hourly (
    short_code VARCHAR(10),
    hour TIMESTAMP,         -- truncado pra hora
    click_count INT,
    unique_visitors INT,    -- HyperLogLog approximation
    top_countries JSONB,    -- {"BR": 4500, "US": 1200, ...}
    top_referers JSONB,     -- {"twitter.com": 3000, "direct": 2000}
    device_breakdown JSONB, -- {"mobile": 70, "desktop": 25, "tablet": 5}
    
    PRIMARY KEY (short_code, hour)
);

Por que agregar por hora (e não raw)?

Raw events: 100K/s × 200 bytes = 20 MB/s = ~52 TB/mês. Caro pra queries. Agregado por hora: 1 row por URL por hora. Queries são rápidas, storage é mínimo.

Raw fica no S3 pra deep analytics; aggregated fica no PostgreSQL pra dashboard em real-time.

Contadores aproximados: HyperLogLog

“Quantos visitantes únicos?” parece exigir armazenar cada visitor ID. Com 1M clicks, seria 1M entries só pra uniqueness.

HyperLogLog: conta elementos únicos usando ~12 KB de memória, independente do número de elementos. Erro: <2%.

Redis: PFADD unique_visitors:abc123:2026-05-24 "visitor_hash_1"
Redis: PFADD unique_visitors:abc123:2026-05-24 "visitor_hash_2"
Redis: PFCOUNT unique_visitors:abc123:2026-05-24
→ 847293 (±2%)

847.293 visitors únicos rastreados com 12 KB de memória em vez de ~6 MB (se armazenasse todos os hashes).

Deep Dive 5: Expiração e Cleanup

URLs com TTL precisam parar de funcionar após expirar. Como fazer isso sem escanear o banco inteiro a cada segundo?

Approach 1: Check on read (lazy expiration)

Redirect request → busca URL → verifica expires_at → se expirado: 404

Prós: zero overhead de background jobs. Simples. Contras: URL expirada ainda ocupa espaço no DB e cache até ser acessada. Se nunca for acessada de novo, fica lá pra sempre.

Approach 2: Background cleanup job

-- A cada 5 minutos:
DELETE FROM urls 
WHERE expires_at IS NOT NULL 
  AND expires_at < NOW()
LIMIT 10000;  -- batch pra não lockar tabela

Prós: limpa storage, libera short codes pra reuso. Contras: precisa de index eficiente em expires_at, query pode ser cara se muitas URLs expiram ao mesmo tempo.

Approach 3: Combinação (o melhor dos dois mundos)

1. Check on read (lazy): redirect retorna 404 imediatamente se expirado
2. Cache com TTL alinhado: Redis TTL = expires_at - now()
   → Cache entry expira automaticamente no Redis
3. Background job (diário): limpa URLs expiradas do DB em batch
   → Faz em horário de baixo tráfego (3-5 AM)
   → Processa em chunks de 10K pra não lockar

Reuso de short codes?

Quando URL expira, o short code abc123 fica disponível de novo?

Opção A: Nunca reutilizar. Simples, evita confusão (link antigo em cache de alguém redireciona pra lugar errado). Com 62^7 = 3.5 trilhões de combinações, espaço não é problema.

Opção B: Reutilizar após grace period (30 dias). Libera codes “bonitos” pra custom aliases. Complexo: precisa garantir que caches foram limpos.

Recomendação: nunca reutilizar, a menos que o negócio exija explicitamente.

Deep Dive 6: Rate Limiting e Abuse Prevention

URL shortener é alvo fácil de abuso: spam, phishing, DDoS via redirect amplification.

Problemas de abuso

  1. Spam: criar milhões de short URLs apontando pra sites maliciosos
  2. Phishing: esconder URL de phishing atrás de URL “confiável” do seu domínio
  3. Amplification attack: criar short URL pra site pesado, mandar milhões de bots clicarem → DDoS no destino via seu serviço
  4. Enumeration: tentar todos os short codes (/a, /b, …, /zzzzzzz) pra descobrir URLs privadas

Defesas

Rate limiting por IP e por user:

Criação: máximo 100 URLs/hora por IP (anônimo) ou 1000/hora por conta
Redirect: máximo 1000/minuto por IP (previne bot crawling)

Implementação: Redis sliding window
  Key: ratelimit:create:{ip}:{hour}
  INCR + EXPIRE 3600
  Se > 100: retorna 429 Too Many Requests

URL safety check na criação:

Antes de aceitar URL longa:
  1. Check contra lista de domínios maliciosos (Google Safe Browsing API)
  2. Check contra blocklist interna
  3. DNS resolution: domínio existe?
  4. Se suspeito: queue pra review manual, cria com flag "pending"

Anti-enumeration:

Short codes previsíveis (counter sequencial: 1, 2, 3…) são enumeráveis. Solução: embaralhar o counter antes de encodar:

def generate_short_code(counter):
    # Scramble com bitwise operations (reversível)
    scrambled = scramble(counter)  # 7483921 → 39284756
    return encode_base62(scrambled)  # "mK4Rp"

def resolve_short_code(code):
    scrambled = decode_base62(code)  # "mK4Rp" → 39284756
    counter = unscramble(scrambled)  # 39284756 → 7483921
    return lookup_by_id(counter)

Códigos parecem aleatórios mas são determinísticos. Não é criptografia (não precisa ser seguro contra atacante motivado), só evita crawling trivial.

Deep Dive 7: Alta disponibilidade

URL shortener não pode cair. Links estão em emails, tweets, cartões de visita, QR codes físicos. Se o serviço fica offline, nenhum link encurtado do mundo funciona.

Multi-region deployment

              ┌─── DNS (GeoDNS/Anycast) ───┐
              │                              │
              ▼                              ▼
    ┌──────────────────┐        ┌──────────────────┐
    │   Region: US-East │        │  Region: EU-West  │
    │                    │        │                    │
    │  LB → Redirect    │        │  LB → Redirect    │
    │  Service (N pods) │        │  Service (N pods) │
    │       │           │        │       │           │
    │  Redis Cluster    │        │  Redis Cluster    │
    │       │           │        │       │           │
    │  PostgreSQL       │◄──────►│  PostgreSQL       │
    │  (primary)        │  async │  (read replica)   │
    └──────────────────┘  repli- └──────────────────┘
                           cation

DNS GeoDNS: roteia usuário pro datacenter mais próximo. Brasileiro vai pro US-East (ou SA-East se tiver), europeu vai pro EU-West.

Read replica: redirect é read-only. Replica é suficiente. Se primary cai, promote replica. Writes (criar URL) são raros e podem ter latência ligeiramente maior.

Redis per-region: cada região tem seu próprio Redis. Writes no Redis são feitos localmente após DB write. Se região cai, outra região assume.

Failover

Cenário: região US-East fica offline

1. DNS health check detecta (timeout 10s)
2. DNS remove US-East do rotation (TTL 30s)
3. Próximos requests vão pra EU-West
4. EU-West tem read replica → serve redirects normalmente
5. Creates são redirecionados pra nova primary (failover de DB)

Impacto total: ~30-60 segundos de latência elevada, zero downtime real

Comparação: URL Shortener vs sistemas anteriores

AspectoURL ShortenerYouTubeWhatsAppUberTwitter
Complexidade de writeTrivial (40/s)Alta (transcoding)Alta (delivery guarantee)Alta (matching)Alta (fan-out)
Complexidade de readMédia (cache-heavy)Alta (CDN/streaming)Baixa (1:1)Média (tracking)Alta (timeline merge)
Data modelSimples (key→value)Complexo (multi-format)Complexo (E2E, ordering)Complexo (geospatial)Complexo (graph + cache)
Principal desafioRead latency + analyticsStorage + bandwidthDelivery + connectionsGeolocation + real-timeFan-out + scale
Cache hit rate>99%>95% (CDN)N/ALow (data moves)>95% (timeline)

O URL Shortener é o “hello world” do system design — mas um hello world que toca em caching, hashing, analytics pipelines, rate limiting, e HA multi-region.

Trade-offs e decisões

DecisãoAlternativaPor que essa escolha
Pre-allocated ranges (ID)Hash + collision checkZero colisão, O(1) generation, distributed
Base62 encodingBase64, UUIDURL-safe, legível, curto
PostgreSQLDynamoDB/CassandraWrite volume baixo, queries de analytics, unique constraints
Redis (single layer)Multi-tier cache6 GB cabe em 1 node, simplicidade
302 Redirect (padrão)301 RedirectMantém controle: analytics, pode mudar destino
Kafka (analytics)Sync writeRedirect não pode esperar analytics
HyperLogLog (uniques)Exact count12 KB vs 6 MB, <2% erro
Lazy + batch expirationSó lazy OU só batchBest of both: UX imediato + storage cleanup
Scrambled counterPure sequentialAnti-enumeration sem custo de colisão
Never reuse codesReuse após expiryEvita cache confusion, espaço é abundante

Como escalar além

  1. Link preview service: ao criar URL, fetch a página destino e armazenar título + thumbnail (como Slack faz ao colar um link)
  2. A/B testing: mesma short URL redireciona pra destinos diferentes baseado em % de tráfego
  3. Smart routing: device=mobile → URL mobile, device=desktop → URL desktop
  4. Branded domains: empresas usam seu próprio domínio (links.empresa.com.br) com seu backend
  5. Real-time dashboard: WebSocket streaming de clicks conforme acontecem (dashbard ao vivo pra campanhas de marketing)

Resumo da série

Com esse artigo, concluímos os 6 sistemas da série. Aqui está o mapa mental dos conceitos que cobrimos:

ConceitoOnde apareceu
CDNYouTube (streaming), URL Shortener (redirect cache)
Message Queue (Kafka)YouTube (transcoding), WhatsApp (delivery), Twitter (fan-out), URL Shortener (analytics)
WebSocketWhatsApp (messaging), Uber (tracking), Twitter (real-time)
Caching (Redis)Todos — é universal
GeospatialUber (H3, matching)
Fan-outTwitter (write vs read vs hybrid)
Consistent HashingYouTube (storage), Twitter (timeline sharding)
Rate LimitingURL Shortener (abuse prevention)
Eventual ConsistencyYouTube (upload visibility), Twitter (timeline), URL Shortener (analytics)
Strong ConsistencyWhatsApp (delivery), Uber (matching — 1 ride = 1 driver)
Pre-signed URLsYouTube (upload bypass)
Snowflake IDsTwitter (time-ordered distributed IDs)
HyperLogLogURL Shortener (unique visitors)
Count-Min SketchTwitter (trending topics)
E2E EncryptionWhatsApp (Signal Protocol)

Se você dominar esses conceitos e souber quando e por que usar cada um, está preparado pra qualquer entrevista de system design.


Esse foi o último artigo da série System Design na Prática. Se a série te ajudou, compartilha com outros devs que estão se preparando pra entrevistas. E se quiser que eu cubra outro sistema específico, me encontra no LinkedIn.