Por que essa pergunta é quase universal em entrevista
URL shortener é a questão canônica de System Design porque toca em todos os trade-offs essenciais em 45 minutos: geração de ID única em escala, read-heavy caching, counter distribuído, analytics assíncrona. Se você domina isso, Twitter feed e Instagram também caem.
O teste não é se você "sabe fazer" — é se você consegue defender decisões quando o entrevistador aumenta escala progressivamente: "e se forem 100M URLs? 1B? 10B?"
Passo 1: estabelecer escala (back-of-envelope)
Sempre começa com premissas. Nunca pule. Premissas típicas:
- 100M URLs criadas/mês (~40/s média, ~120/s pico)
- Razão read:write 100:1 — cada URL criada é clicada 100x
- ~10k QPS de redirect médio, ~30k pico
- Retenção 5 anos = 6B URLs totais
- Short code 7 chars base62 = 62⁷ ≈ 3.5 trilhões (espaço largo pra 6B)
Passo 2: geração de short code — 3 estratégias
📋 Como gerar short codes únicos em escala?
Garante unicidade sem colisão, sem round-trip extra pra checar DB. Counter pode ser auto_increment (até 5k/s) ou Snowflake/Redis batched (milhões/s).
Alt: —
Alt: —
Alt: —
Passo 3: storage — schema e escolha de DB
-- Postgres / MySQL — serve confortavelmente até ~1B rows
CREATE TABLE short_urls (
short_code VARCHAR(7) PRIMARY KEY, -- PK compacta, index clustered
long_url TEXT NOT NULL,
user_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ, -- opcional; TTL
click_count BIGINT DEFAULT 0, -- updated async
last_clicked TIMESTAMPTZ
);
CREATE INDEX idx_short_urls_user ON short_urls (user_id, created_at DESC);
CREATE INDEX idx_short_urls_expires ON short_urls (expires_at) WHERE expires_at IS NOT NULL;| Escala | DB sugerido | Por quê |
|---|---|---|
| < 100M URLs | Postgres single primary + read replica | Simples, transacional, aguenta até ~5k write/s |
| 100M-1B | Postgres com table partitioning por mês | Partition pruning acelera queries; drop partition é limpeza instantânea |
| 1B-10B | Cassandra ou Vitess (MySQL sharded) | Escala linear em writes; consistente com quórum |
| > 10B | DynamoDB (key-value) + CDC pra analytics | Escala horizontal sem ops de sharding; custo justifica em escala |
Passo 4: caching — onde está o leverage
Com 100:1 read:write e power law no tráfego (poucas URLs dominam), cache em frente ao DB é decisão óbvia. Sem cache: 10k QPS no DB — viável mas caro. Com cache: 10k QPS no Redis, 50-500 QPS no DB.
# Redis cache-aside pattern
def resolve(short_code: str) -> str | None:
cached = redis.get(f"url:{short_code}")
if cached:
return cached
row = db.query("SELECT long_url FROM short_urls WHERE short_code = %s", short_code)
if not row:
return None
redis.setex(f"url:{short_code}", 86400, row.long_url) # TTL 1 dia
return row.long_urlTTL matters: sem TTL, cache cresce infinitamente. Com TTL de 1 dia + LRU eviction, Redis de 16GB cabe ~16M URLs hot. Power law garante hit rate > 95%.
Passo 5: analytics de cliques — fire and forget
async def redirect_handler(short_code: str, request):
long_url = await resolve(short_code)
if not long_url:
return Response(status=404)
# Fire-and-forget
asyncio.create_task(emit_click_event({
"short_code": short_code,
"ts": time.time(),
"ip": request.headers.get("X-Forwarded-For"),
"ua": request.headers.get("User-Agent"),
"referer": request.headers.get("Referer"),
}))
return RedirectResponse(long_url, status_code=301)
async def emit_click_event(event: dict):
await kafka_producer.send("url.click", event)
# Worker offline enriquece (geo via IP), grava em BigQuery/SnowflakePasso 6: o que entrevistador aumenta progressivamente
| Pergunta | Resposta esperada |
|---|---|
| "E se forem 100B URLs?" | Sharding por hash(short_code). 100+ shards. Consistent hashing para rebalance. DynamoDB ou Cassandra. |
| "E se quiser custom alias?" | Nova tabela custom_aliases (code → url), lookup primeiro em custom depois em generated. Validação contra palavrões. |
| "E se URL tiver malware?" | Scan async (Google Safe Browsing) no write. Bloom filter na hot path para known-bad. Quarantine queue. |
| "E se DB cair?" | Read replica pra redirect (read-only degrade), fila pra writes (Kafka buffer), graceful degrade. RTO/RPO definidos. |
Armadilhas que eliminam candidato
Take-aways
URL shortener é canônica porque a resposta sênior mostra 4 habilidades simultaneamente: (1) quantificar com back-of-envelope, (2) escolher geração de ID correta, (3) isolar hot path (redirect) de cold path (analytics), (4) adicionar cache onde há power law.