Encurtar uma URL parece simples: receber um endereço longo, gerar um código curto e redirecionar quem acessar esse código. Em pequena escala, uma aplicação e uma tabela resolvem o problema. Em grande escala, o desafio muda: as leituras superam muito as escritas, códigos não podem colidir, links populares criam picos e o redirecionamento precisa continuar rápido mesmo durante falhas.
Este estudo de caso mostra como conduzir essa discussão em uma entrevista de System Design. Os números são hipóteses para orientar decisões, não uma receita universal.
O que é um URL Shortener?
Um URL Shortener, ou encurtador de URLs, transforma uma URL longa em um endereço curto e fácil de compartilhar.
URL longa: https://exemplo.com/produtos?categoria=livros&campanha=primavera
URL curta: https://dvp.to/aZ3k9B
Quando alguém acessa dvp.to/aZ3k9B, o serviço procura o destino associado ao código aZ3k9B e responde com um redirecionamento HTTP.
Há dois fluxos principais:
criação: URL longa → validação → código curto → armazenamento
redirecionamento: código curto → busca do destino → resposta HTTP 3xx
O segundo fluxo costuma dominar o volume. Essa assimetria favorece uma arquitetura otimizada para leitura.
Defina os requisitos antes da arquitetura
Uma boa solução começa alinhando o escopo. Sem isso, é fácil adicionar componentes sofisticados para problemas que não existem.
Requisitos funcionais
- Criar uma URL curta a partir de uma URL longa;
- Redirecionar o código curto para o destino correto;
- Permitir um alias personalizado, se estiver disponível;
- Aceitar uma data de expiração opcional;
- Desabilitar links maliciosos ou removidos;
- Registrar cliques para estatísticas sem atrasar o redirecionamento.
Requisitos não funcionais
- Baixa latência no redirecionamento;
- Alta disponibilidade para leitura;
- Códigos únicos e difíceis de adivinhar em sequência;
- Durabilidade dos links enquanto estiverem válidos;
- Escala para um volume muito maior de leituras que de escritas;
- Proteção contra abuso, phishing e criação automatizada em massa.
Também é importante esclarecer o que fica fora do escopo inicial. Painel de campanhas, QR Codes, domínio personalizado e relatórios em tempo real podem ser evoluções, mas não são necessários para provar o desenho principal.
Faça estimativas de ordem de grandeza
Suponha os seguintes requisitos para exercitar a capacidade:
novas URLs por mês: 100 milhões
proporção leitura : escrita: 100 : 1
retenção média: 5 anos
tamanho médio de um registro: 1 KB
Isso produz aproximadamente:
escritas médias:
100.000.000 / 30 dias / 86.400 s ≈ 39 escritas/s
leituras médias:
39 × 100 ≈ 3.900 leituras/s
links em 5 anos:
100.000.000 × 12 × 5 = 6 bilhões
armazenamento bruto:
6 bilhões × 1 KB ≈ 6 TB
Picos podem ser várias vezes maiores que a média, especialmente quando um link viraliza. Réplicas, índices e metadados também aumentam o armazenamento real. Ainda assim, a estimativa já revela as decisões importantes: o volume de dados é administrável, o caminho de leitura merece cache e a distribuição de popularidade será desigual.
Modele as APIs
Uma API mínima de criação pode ser:
POST /v1/links
Content-Type: application/json
Idempotency-Key: 1f3b...
{
"url": "https://exemplo.com/artigos/system-design",
"customAlias": "system-design",
"expiresAt": "2027-09-28T00:00:00Z"
}
{
"code": "aZ3k9B",
"shortUrl": "https://dvp.to/aZ3k9B",
"expiresAt": "2027-09-28T00:00:00Z"
}
O redirecionamento pode usar uma rota pública simples:
GET /aZ3k9B
HTTP/1.1 302 Found
Location: https://exemplo.com/artigos/system-design
Uma chave de idempotência impede que o retry de uma requisição de criação produza vários links. O alias personalizado precisa de uma restrição de unicidade e deve rejeitar palavras reservadas como api, admin e login.
301 ou 302?
O 301 Moved Permanently permite cache agressivo por navegadores e intermediários, reduzindo carga. Porém, dificulta trocar o destino, revogar um link rapidamente e registrar cada clique no serviço.
O 302 Found mantém mais controle no servidor e é uma escolha inicial flexível. O custo é receber mais tráfego de redirecionamento. Se os links forem imutáveis e a medição puder ser aproximada, respostas permanentes podem ser habilitadas por política.
Modele os dados pelo padrão de acesso
O acesso principal é uma busca exata pelo código curto. Um modelo lógico suficiente seria:
| Campo | Finalidade |
|---|---|
code | Chave única usada no redirecionamento |
destination | URL completa de destino |
created_at | Auditoria e políticas de retenção |
expires_at | Expiração opcional |
status | Ativo, desabilitado, expirado ou removido |
owner_id | Proprietário, quando houver conta |
redirect_type | Política de resposta, como |
Um banco relacional funciona bem no início: oferece índice único, transações e operação conhecida. Um banco chave-valor distribuído se torna atraente quando o volume e a distribuição geográfica exigem particionamento horizontal simples.
Não é preciso decidir apenas pela categoria “SQL ou NoSQL”. Pergunte:
- O acesso é quase sempre pela chave?
- Precisamos de consultas relacionais ou relatórios no banco principal?
- Qual consistência é necessária ao criar, editar e revogar um link?
- Como ocorrerão backup, restauração, replicação e expansão de capacidade?
Estatísticas de clique possuem outro padrão de acesso e outro volume. Elas não precisam compartilhar a mesma tabela nem a mesma tecnologia dos redirecionamentos.
Como gerar códigos curtos?
Um alfabeto Base62 usa letras minúsculas, maiúsculas e números:
0-9 + a-z + A-Z = 62 símbolos
Com 7 caracteres, o espaço teórico é:
62⁷ ≈ 3,5 trilhões de combinações
Isso é muito maior que os 6 bilhões de links da estimativa. Alguns códigos podem ser reservados, bloqueados ou nunca utilizados, portanto o espaço não deve ser explorado até perto do limite.
ID sequencial codificado em Base62
O sistema obtém um ID único e o converte para Base62.
ID 125 → código "21"
Vantagens: não há colisões, o código é compacto e a geração é barata.
Custos: um contador central pode limitar a escala; a sequência revela aproximadamente o volume do sistema; códigos vizinhos são enumeráveis. Para distribuir a geração, é possível reservar blocos de IDs por instância ou usar um serviço de IDs, mas isso aumenta a operação.
Código aleatório
O serviço gera uma sequência aleatória e tenta inseri-la sob uma restrição de unicidade. Em caso de colisão, gera outra.
Vantagens: reduz a previsibilidade e não depende de um contador global.
Custos: exige uma fonte aleatória adequada, índice único e retry. A chance de colisão cresce conforme o espaço é ocupado, efeito conhecido como paradoxo do aniversário.
Para um código público, não use um gerador previsível se a enumeração revelar destinos sensíveis. Mesmo um código aleatório não substitui autorização: quem possuir uma URL curta pública normalmente poderá acessá-la.
Hash da URL longa
Calcular um hash parece determinístico, mas introduz perguntas difíceis: quantos caracteres manter, como resolver colisões, como criar dois links distintos para a mesma URL e como incorporar expiração ou proprietário. O hash pode participar da solução, mas não elimina a necessidade de identidade e unicidade.
Desenhe o fluxo de escrita
cliente
→ API de criação
→ autenticação e rate limiting
→ validação e normalização da URL
→ geração do código
→ INSERT com restrição única
→ resposta com a URL curta
O serviço deve aceitar apenas protocolos permitidos, normalmente http e https, impor limites de tamanho e normalizar somente o que for seguro. Transformações agressivas podem mudar o significado da URL: parâmetros repetidos, maiúsculas no caminho e fragmentos nem sempre são equivalentes.
A escrita precisa ser confirmada pelo banco antes de responder. Se houver cache, ele pode ser preenchido depois do commit. Publicar primeiro no cache cria o risco de servir um link que não existe na fonte de verdade.
Aliases personalizados usam o mesmo caminho, mas o código é fornecido pelo cliente. A restrição única decide de forma atômica qual requisição venceu a disputa.
Desenhe o fluxo de redirecionamento
usuário
→ DNS e CDN ou edge
→ load balancer
→ serviço de redirecionamento
→ cache distribuído
├─ hit → responde 302
└─ miss → banco → preenche cache → responde 302
O cache segue o padrão cache-aside. O código curto é a chave; o destino e o estado formam o valor. Links populares permanecem em memória e evitam leituras repetidas no banco.
Também vale armazenar por pouco tempo resultados negativos, como códigos inexistentes ou expirados. Isso reduz a carga causada por scanners, erros de digitação e tentativas de enumeração. O TTL negativo deve ser curto para não esconder por muito tempo um link recém-criado.
Invalidação e consistência
Quando um link é alterado ou desabilitado:
1. atualize a fonte de verdade
2. invalide a entrada no cache
3. aceite uma pequena janela de inconsistência ou use uma política mais forte
Se a revogação for uma exigência de segurança, um TTL longo pode ser inaceitável. O sistema pode combinar TTL curto, invalidação por evento e uma lista de bloqueio de rápida propagação. A consistência necessária vem do requisito, não do componente escolhido.
Hot links
Um único código viral pode concentrar tráfego. Réplicas de leitura e particionamento do banco ajudam pouco se todas as requisições chegam à mesma chave. Cache distribuído, cache local curto e CDN absorvem melhor esse padrão.
Ao usar CDN, considere que o redirecionamento pode não chegar ao backend. Isso melhora latência e disponibilidade, mas muda a coleta de métricas e a velocidade de revogação. Logs da borda ou eventos agregados podem recuperar parte da visibilidade.
Separe analytics do caminho crítico
O usuário não deve esperar a gravação de uma estatística para ser redirecionado.
redirecionamento → resposta 302
└───────→ evento de clique → fila ou stream → consumidores → analytics
O evento pode conter código, horário, região aproximada, referenciador e categoria do dispositivo, respeitando minimização de dados e privacidade. Consumidores agregam os eventos em janelas e atualizam um armazenamento analítico.
Esse fluxo normalmente aceita consistência eventual. Duplicatas podem ocorrer quando há retry, então os consumidores devem ser idempotentes ou as métricas devem tolerar uma pequena supercontagem documentada. “Cliques” também precisam de uma definição: requisições de bots, previews de redes sociais e retries não representam necessariamente pessoas.
Escale por etapas
Uma evolução razoável evita começar com a arquitetura final:
Etapa 1: aplicação e banco relacional
- Uma aplicação sem estado;
- Banco com índice único em
code; - Rate limiting básico;
- Backups e observabilidade.
Etapa 2: cache e réplicas
- Cache-aside para redirecionamentos;
- Réplicas de leitura, se o banco continuar recebendo carga relevante;
- Métricas assíncronas;
- Mais instâncias atrás de um load balancer.
Etapa 3: particionamento e distribuição geográfica
- Particionar por hash do código para distribuir dados de forma uniforme;
- Replicar dados para regiões próximas dos usuários;
- Definir qual região aceita escritas e como novos links se propagam;
- Automatizar rebalanceamento, backup e recuperação por partição.
Particionar por hash distribui bem códigos aleatórios, mas dificulta consultas por proprietário. Um índice secundário ou armazenamento separado pode atender o painel do usuário. Isso é uma consequência direta de modelar a fonte principal para o redirecionamento.
Disponibilidade e falhas
O desenho deve explicar o comportamento durante falhas, não apenas no cenário saudável.
| Falha | Resposta esperada |
|---|---|
| Cache indisponível | Consultar o banco com timeout e limitar concorrência |
| Banco primário indisponível | Pausar criações; manter leituras por cache e réplica quando seguro |
| Réplica atrasada | Um link novo pode retornar |
| Fila de analytics indisponível | Redirecionar e descartar ou armazenar eventos em buffer limitado |
| Região indisponível | Direcionar tráfego para outra região com dados suficientemente atuais |
O redirecionamento e a criação podem ter objetivos de disponibilidade diferentes. Durante um incidente, faz sentido manter links existentes funcionando e rejeitar temporariamente novas criações.
Defina SLOs separados, como disponibilidade e percentis de latência para criação e redirecionamento. Monitore hit rate, latência do cache e banco, códigos não encontrados, colisões, atrasos de replicação, tamanho das filas e links bloqueados.
Segurança e prevenção de abuso
Um encurtador esconde o domínio de destino e pode ser usado para phishing, malware e spam. Segurança faz parte do desenho central.
- Aplique rate limiting por conta, IP e outros sinais de risco;
- Bloqueie protocolos perigosos e entradas malformadas;
- Consulte listas de reputação e permita denúncia de links;
- Faça varredura assíncrona e desabilite destinos abusivos;
- Proteja endpoints administrativos com autorização e auditoria;
- Evite buscar a URL fornecida a partir da rede interna sem controles contra SSRF;
- Defina retenção, anonimização e acesso para dados de analytics;
- Mostre uma página de aviso quando o risco não puder ser decidido com segurança.
Não siga redirecionamentos de uma URL desconhecida em um worker com acesso à rede interna. Se a inspeção for necessária, use isolamento, regras de saída e bloqueio de endereços privados e metadados de nuvem.
Vantagens da arquitetura proposta
- Mantém o redirecionamento simples e otimizado para leitura;
- Usa unicidade no armazenamento para resolver corridas de criação;
- Adiciona cache onde a proporção leitura-escrita justifica;
- Separa analytics do caminho de baixa latência;
- Permite escalar aplicação, cache e dados de forma independente;
- Evolui em etapas, de acordo com métricas reais.
Limitações e trade-offs
| Decisão | Benefício | Custo ou risco |
|---|---|---|
| Código aleatório em Base62 | Menos previsível e geração distribuída | Colisões exigem restrição única e retry |
Redirecionamento | Permite edição, revogação e métricas | Mais requisições chegam ao serviço |
| Cache-aside | Reduz latência e carga no banco | Invalidação e dados temporariamente antigos |
| Analytics assíncrono | Não atrasa o usuário | Métricas chegam depois e podem conter duplicatas |
| Particionamento por hash | Distribui a carga por código | Consultas por proprietário exigem outro índice |
| CDN para links populares | Absorve picos perto do usuário | Revogação e contagem de cliques ficam mais difíceis |
Erros comuns
Começar pelos componentes, não pelos requisitos
Adicionar filas, múltiplas regiões e vários bancos antes de estimar o tráfego produz um diagrama complexo sem justificativa. Comece com um fluxo funcional e evolua a partir dos gargalos.
Dizer apenas “vamos usar um hash”
Um algoritmo não responde como colisões serão detectadas, qual será o tamanho do código, se a saída é previsível nem como dois links para o mesmo destino serão tratados.
Ignorar hot keys
Distribuição uniforme dos códigos não implica distribuição uniforme dos acessos. Um link viral pode concentrar a maior parte do tráfego e precisa ser absorvido por camadas de cache.
Registrar analytics de forma síncrona
Uma escrita analítica no caminho do redirecionamento aumenta latência e cria mais uma dependência capaz de derrubar a função principal.
Tratar cache como fonte de verdade
Entradas podem expirar, ser removidas ou ficar antigas. O banco durável continua responsável pelo vínculo e por seu estado.
Esquecer abuso e revogação
Um sistema que cria links rapidamente, mas não consegue bloquear phishing com rapidez, está incompleto.
Perguntas de entrevista
- Como você estimaria tráfego, armazenamento e tamanho necessário do código?
- Quais são os trade-offs entre ID sequencial, código aleatório e hash da URL?
- Quando escolher
301ou302para o redirecionamento? - Como impedir que uma colisão associe o código ao destino errado?
- Como o sistema se comporta quando cache, banco ou fila estão indisponíveis?
- Como você trataria um único link recebendo milhões de acessos por minuto?
- Como editar ou revogar um link que está em caches e CDNs?
- Como contar cliques sem aumentar a latência do redirecionamento?
- Como particionar os dados e quais consultas ficam mais difíceis?
- Quais controles reduzem phishing, spam, enumeração e SSRF?
Próximos conteúdos
Continue por Redis, para aprofundar cache, TTL, eviction e hot keys, e por SQL ou NoSQL, para escolher o armazenamento a partir do padrão de acesso. Depois, estude idempotência para tornar a criação segura diante de retries e duplicatas.