Link shortener escalable
Convertir URLs largas en códigos cortos únicos y redirigir en milisegundos, con analítica y protección contra abuso.
Recursos
Link, ShortCode, Owner, ClickEvent.
Operaciones
Crear, redirigir, ver stats, expirar, borrar.
Escala
Lecturas mucho mayores a escrituras (1000:1 típico).
Riesgo
Colisiones, abuso/spam, links a malware, hot keys.
Requisitos
Pequeño en superficie pero con SLOs estrictos: el redirect es público y muy hot.
Funcionales
- Crear short link con TTL opcional.
- Redirigir 301/302 a la URL destino.
- Stats: clicks, geo, referrer, device.
- Custom alias para tier paid.
- Soft delete y blocklists.
No funcionales
- Redirect p95 < 50 ms.
- 99.99% disponibilidad del redirect.
- Read:Write > 1000:1.
- IDs únicos sin colisiones a escala.
- Anti-abuso: rate limit + block de phishing.
Cómo aclarar requisitos en la entrevista
Casos de uso típicos. Marketing campaign con UTM tracking, links en SMS / Twitter / WhatsApp, QR codes que apuntan a un destino dinámico (puedes cambiarlo después), A/B test de landing pages, vanity URLs personalizados (brand.co/promo), branded short domains de empresa, deep links a apps móviles.
Preguntas que debes hacer:
• ¿Custom alias permitidos para usuarios paid?
• ¿Stats: solo conteo o granular (geo, referrer, device, hora)?
• ¿Bulk creation API para clientes enterprise?
• ¿Multi-region failover o single region?
• ¿Password protect o auth para acceder al destino?
• ¿Deep links a apps móviles con fallback web?
• ¿301 vs 302? 301 lo cachea el browser, 302 das control.
• ¿TTL / expiración de links?
• ¿Anti-malware check obligatorio o best-effort?
Frase clave: "Asumo links permanentes (sin TTL), 302 redirect, stats granulares async, sin custom aliases. Si me pides paid features (custom domains, password) lo extiendo al final."
Estimaciones de capacidad
Resoluciones año/día/seg. El número clave es el cache hit ratio del redirect.
~30/s pico durante campañas.
~30K/s pico (links virales).
Usar 6 chars hasta 56B; expandir luego.
~500 GB total con metadata + stats.
~30K req/s al edge, ~300/s al origen.
~10 ms p99 de lookup en Redis.
Sink: BigQuery / ClickHouse.
~8 s/día permitidos.
Endpoints
| Método | Path | Descripción | Notas |
|---|---|---|---|
| POST | /api/links | Crea short link. | Idempotency-Key. |
| GET | /{shortCode} | Redirige 301/302 al destino. | Cache agresivo. |
| GET | /api/links/{shortCode} | Devuelve metadata sin redirigir. | |
| GET | /api/links/{shortCode}/stats | Clicks, geo, referrers. | Pipeline analítico. |
| DELETE | /api/links/{shortCode} | Borrar/desactivar. | Soft delete. |
Cómo defiendo estas APIs en la entrevista
Dos paths fundamentalmente distintos. El path de creación (POST /links) es complejo: validación, ID generation, blocklist check, persist. El path de redirect (GET /{shortCode}) es trivial: lookup + 302. Estoy diseñando el redirect para que sea brutal de simple porque es el que aguanta 100B/año.
302 vs 301. Uso 302 (temporary). 301 (permanent) sería más eficiente para browsers (cachean para siempre) pero pierdo control: si el destino se vuelve malicioso, no puedo redirigir nuevos clicks. 302 me deja invalidar siempre.
Soft delete con DELETE. No borro físicamente — pongo flag deleted=true. Esto preserva analytics, permite undelete y evita reusar el short_code (que podría llegar como link en algún sitio guardado).
Frase clave: "El redirect es el corazón. GET /{code} debe tardar < 10 ms en p95 con 99% cache hit. La creación puede tardar más — es 1000× menos común."
Ejemplo
POST /api/links
{ "longUrl": "https://example.com/articulo-largo", "ttlDays": 90 }
201 Created
{
"shortCode": "k8Pq2x",
"shortUrl": "https://x.co/k8Pq2x",
"expiresAt": "2026-07-30T00:00:00Z"
}
GET /k8Pq2x
→ 302 Found
Location: https://example.com/articulo-largo
Arquitectura
redirect cache"]) Edge -->|miss| API API{{"🌐 API GATEWAY + Redirect Service"}} --> Cache Cache -->|miss| KV Creator(["Creator API client"]) --> API API --> IDGen["ID Generator
Snowflake / counter base62"] IDGen --> KV API --> Block["Block / Safe Browsing"] User -. click event .-> Stream Stream --> Stats subgraph Cache["💾 Redis · cache hot"] direction TB r1["hot:shortCode"] r2["blocklist (Bloom)"] r3["rl:user:1m"] end subgraph KV["💾 KV Store · DynamoDB / Cassandra"] direction TB k1["links"] k2["id_ranges"] end subgraph Stream["📨 Kafka · click_events"] direction TB s1["click_events"] end subgraph Stats["🗄️ Analytics DB · BigQuery"] direction TB a1["click_events_archive"] a2["clicks_daily"] end classDef gateway fill:#fbbf24,stroke:#f59e0b,stroke-width:3px,color:#0f172a,font-weight:bold classDef edge fill:#1e1b4b,stroke:#a78bfa,stroke-width:2px,color:#e0e7ff classDef service fill:#0c4a6e,stroke:#38bdf8,stroke-width:2px,color:#e0f2fe classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class API gateway class Edge edge class IDGen,Block service class User,Creator client
Cómo lo explico en la entrevista
Redirect (read path). Cliente clickea https://x.co/abc123. El navegador hace GET. El edge CDN intercepta — si tiene esa entrada cacheada (>99% hit ratio), responde 302 con la URL larga sin contactar al origen. Latencia <10 ms. Esto es lo que hace que el sistema escale a billones de redirects/año.
Cache miss → KV lookup. Si miss en CDN, va al Redirect Service. Lookup en Redis primero (~1 ms). Si miss, query al KV store (DynamoDB / Cassandra) por short_code, lookup O(1). Verifica blocklist (bloom filter en Redis) para no servir phishing. Devuelve 302 al cliente y popula la cache.
Analytics async. En paralelo emite un click_event al Kafka stream con timestamp, user-agent, geo, referrer. Esto va a BigQuery / ClickHouse. No bloquea el redirect — si analytics está caído, el redirect sigue funcionando.
Crear (write path). Cliente autenticado hace POST /api/links con la URL larga. ID Generator entrega un base62 de 7 chars (counter centralizado o range-based). Validamos el destino contra Safe Browsing API. Guardamos en KV. Cache opcional. Devolvemos el short URL.
Frase clave: el sistema es 99% read-heavy, así que el secreto está en tener tres tiers de cache (edge CDN, Redis, in-memory) y un KV store con O(1) lookup. Las escrituras son raras y no necesitan optimización agresiva.
Por qué cada componente
Cuellos de botella
- Hot link viral satura un solo shard de cache.
- ID counter contention si single-instance sin range allocation.
- Safe Browsing API rate limits durante creación masiva.
- Cold cache después de un deploy o cache wipe.
- Analytics pipeline lag bajo picos virales.
Mejoras / cómo escala
- Tres tiers de cache: in-memory + edge + Redis para 99.9% hit ratio.
- Snowflake / range pre-allocation por worker, sin coordinación por request.
- Async Safe Browsing con cache de resultados por dominio.
- Cache warm-up post-deploy con top 1% de links.
- Sampling en analytics para picos, agregando 100% al final del día.
- Multi-region con KV replicado, redirect siempre local.
Por qué estos servicios cloud (vs alternativas)
Por qué: lookup O(1) por short_code, escala lineal a billones de rows, latencia P99 ~10 ms. Particionado natural por hash, sin coordinación cross-shard.
vs Postgres: 100M+ rows con random reads y power-law devuelve hot tablespace. vs MongoDB: aceptable pero performance menos predecible. vs MySQL: mismo problema que Postgres a esta escala.
Por qué: sub-ms latency para top 1% de links que reciben 80% del tráfico (power-law). LRU + TTL keep RAM bajo control.
vs Memcached: aceptable pero sin persistencia opcional ni primitivas avanzadas. vs cache solo en CDN: a veces necesitas filtros (block, expired) que el CDN no entiende.
Por qué: 99%+ del tráfico nunca llega al origen. Edge ejecuta el 302 directamente con cache hit. Latencia <10 ms global, costo predecible.
vs origin directo: ~3K QPS al origen vs ~30/s con cache; bandwidth y latencia disparan. vs Varnish self-hosted: sin POPs globales.
Por qué: analytics async desacoplado del redirect. Si el pipeline falla, el redirect sigue funcionando. Múltiples consumers (BigQuery sink, ClickHouse, fraud detection) sobre mismo stream.
vs llamada síncrona a la DB: bloquearía el redirect (latencia inaceptable). vs SQS: queue, no permite múltiples consumers independientes.
Por qué: billones de eventos al año, queries OLAP por geo/referrer/device. BigQuery serverless; ClickHouse más barato pero con ops propio.
vs Postgres analytics: insuficiente para 100B clicks. vs Druid: aceptable, más operativo. vs Snowflake: aceptable, más caro.
Por qué: verifica si un dominio está en blocklist sin llamar a Safe Browsing API en cada request. False positive 0.1% es aceptable; falsos positivos van a verificación dura.
vs llamar a Safe Browsing en cada redirect: rate limits + latencia +200 ms. vs hash table en RAM: 100M dominios × 32 bytes = 3.2 GB; bloom filter usa ~150 MB.
Diseño de base de datos
KV con cache agresivo, eventos a stream, base62 sin colisiones.
Lookup O(1) por
short_codeEvita coordinación por request
short_codeSink: BigQuery o ClickHouse
Hot cache con LRU + TTL refresh
Cómo defiendo este diseño de base de datos
KV store, no relacional. El patrón es 100% lookup por short_code. No hay joins, no hay range scans. Postgres puede hacerlo pero un KV (DynamoDB / Cassandra / Bigtable) lo hace más barato a 100M rows porque está diseñado exactamente para este patrón.
ID generation con range allocation. Workers reservan bloques de 100K IDs en una tabla id_ranges. Esto evita coordinación por request (cada worker usa su rango local) y previene colisiones. Cuando un worker acaba su rango, pide otro.
Click events fuera de la KV. Lookups dummy de la KV son ~1 ms; agregar writes a la misma KV mata el path de lectura. Eventos van a Kafka, sink en BigQuery / ClickHouse para analytics OLAP.
Bloom filter para anti-malware. Un Bloom de 1B URLs maliciosas con 0.1% false positive rate ocupa ~1.5 GB. Si Bloom dice "maybe malicious", verifico con la API real (cache result). Si dice "definitely not", redirigo sin más checks. Reduce calls externas en 99%.
Frase clave: "Read-heavy con power-law: top 1% recibe 80% del tráfico. Por eso tres tiers de cache (CDN edge, Redis hot, KV cold). Para writes, ID range allocation evita el bottleneck de un counter centralizado."
Decisiones técnicas
| Tema | Decisión | Por qué |
|---|---|---|
| Generación de ID | Counter + base62, o hash + check. | Counter evita colisiones por construcción. |
| Storage | Key-value (DynamoDB/Cassandra) + cache. | Lookup por shortCode es O(1) y muy hot. |
| Redirect | 302 desde edge cache. | Latencia mínima y posibilidad de invalidar. |
| Analytics | Eventos a stream, agregación async. | No bloquea el redirect. |
| Abuso | Rate limit + bloomfilter de URL maliciosos. | Protege a usuarios y reputación. |
| Idempotency | Misma URL larga puede mapearse al mismo short. | Optimización opcional, controlable por user. |
Cómo defiendo estas decisiones técnicas
Constraint: redirect < 50 ms con 99.99% disponibilidad. Define todo: edge cache para latencia y carga, KV para O(1) lookup, async analytics para no bloquear, bloom filter para validación rápida.
Counter base62 vs hashing. Counter (Snowflake-style) garantiza no-colisiones por construcción y es predictible. Hashing requiere collision check en cada insert (lookup + write). A escala, counter es más simple y rápido.
Métricas que validan. Cache hit ratio CDN (target > 99%, alerta a < 95% — costo se dispara). Redirect p95 (target < 50 ms). Click event drop rate (target 0%; sampling al 100% en peak es OK temporal). Malware redirect rate (target 0; cualquier > 0 es incidente).
Cuándo cambio de opinión. Custom domains (vanity URLs) → necesito virtual hosting con cert management automático (Let's Encrypt). Stats real-time obligatorias → Redis HyperLogLog para approximate counts. Multi-region active-active → KV con replicación geo.
Frase clave: "El sistema parece trivial pero tiene tres patrones críticos: power-law en reads (cache aggressive), análisis async (no bloquees el redirect), y safety (bloom filter + signed URLs en uploads)."
Pitfalls
Si haces hash sin verificar, dos URLs pueden chocar. Verifica antes de escribir.
Un link viral satura un shard. Replica reads y cachea agresivo.
Sin filtro, te conviertes en relay de phishing. Integra blocklists.
Script de respuesta: “Genero IDs con un counter base62 para evitar colisiones por construcción y guardo el mapping en KV con cache. El redirect es 302 servido desde edge cache para latencia mínima. Los clicks van por stream a analytics, fuera del path crítico. Agrego rate limit, blocklists y soft delete. Mido p95 del redirect y tasa de cache hit.”
Tiers de almacenamiento
Power-law puro: top 1% recibe 80% del tráfico. Tiers explotan eso para reducir costo total ~10×.
Cómo defenderlo en la entrevista
El KV completo cabe en SSD. 100M links × 500 B = 50 GB en DynamoDB. No tiene sentido archivar links activos — son tan pequeños que el costo de Standard es trivial. Lo que sí archivar: click events, que crecen sin límite.
Click events son el grueso del storage. 100B/año × 200 B/event = 20 TB/año. Mantenerlos en BQ active es caro; partition por fecha y dejar que BQ los muevda a long-term storage tras 90 días.
Frase clave: "Top links en CDN porque cache hit ratio > 99% define el costo del negocio. KV completo en Standard porque es small. Click events tier-down agresivo porque no se usan tras 90 días pero no se pueden borrar (audit)."
Versión alternativa: con Load Balancer (no API Gateway)
Para link shortener la latencia es product feature; LB es claramente mejor para el redirect.
redirect cache"]) Edge -->|miss| LB LB{{"⚖️ L7 LOAD BALANCER
health checks"}} --> Pod["Redirect Service Pods
+ Bloom filter (in-memory)
+ Rate limit (Redis)"] Pod --> Cache Cache -->|miss| KV Creator(["Creator API client"]) --> LBC LBC{{"⚖️ L7 LB Creator path"}} --> CPod["Creator Pods
+ Auth (JWT)"] CPod --> IDGen["ID Generator"] IDGen --> KV User -. click event .-> Stream Stream --> Stats subgraph Cache["💾 Redis hot"] direction TB r1["hot:shortCode"] r2["blocklist"] end subgraph KV["💾 DynamoDB / Cassandra"] direction TB k1["links"] k2["id_ranges"] end subgraph Stream["📨 Kafka"] direction TB s1["click_events"] end subgraph Stats["🗄️ BigQuery"] direction TB a1["click_events_archive"] end classDef lb fill:#34d399,stroke:#10b981,stroke-width:3px,color:#0f172a,font-weight:bold classDef edge fill:#1e1b4b,stroke:#a78bfa,stroke-width:2px,color:#e0e7ff classDef service fill:#0c4a6e,stroke:#38bdf8,stroke-width:2px,color:#e0f2fe classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB,LBC lb class Edge edge class Pod,CPod,IDGen service class User,Creator client
API Gateway vs Load Balancer: cómo defender cada uno
El redirect nunca debe ir por API Gateway. 3K QPS avg, 30K QPS pico, p95 target < 50 ms. Gateway agregaría 5–20 ms (10–40% del budget) por ningún beneficio — el redirect no necesita auth, ni rate limit complejo, ni transformación. Edge CDN + LB simple es lo correcto.
El Creator API es otra historia. Si vendes el servicio (Bitly Pro, Branch.io, Rebrandly), Gateway sí encaja: API keys, plans, quotas, monetization. Path separado (api.x.co/v1/links) con Gateway, redirect path (x.co/{code}) con LB.
Frase clave: "Redirect = LB siempre. Creator API = Gateway si es producto público. Es el caso más claro de los 10: la respuesta es 'usar ambos en paths distintos'."
Cuándo elegir API Gateway
- Creator / Management API con API keys y plans monetizados.
- Bulk creation API con quotas por enterprise customer.
- Webhook signing para eventos a clientes.
- Multi-tenant con isolation por cliente.
Cuándo elegir Load Balancer
- Redirect path (siempre): < 50 ms p95 es no-negociable.
- High QPS (~30K/s pico): gateway-per-request es prohibitivo en costo.
- No auth en redirect: es un GET público, Gateway no aporta.
- Eventos analytics: ingest masivo fire-and-forget.
- Health check trivial: verifica que el pod responde con 200.