Caso 8 · TinyURL/Bitly

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.

~100M
Links / año
~270K/día · ~3/s avg.
~30/s pico durante campañas.
~100B
Redirects / año
~270M/día · ~3K/s avg.
~30K/s pico (links virales).
~7
Caracteres base62
62⁷ ≈ 3.5 trillón IDs únicos.
Usar 6 chars hasta 56B; expandir luego.
~500 B
Tamaño por link
~50 GB / 100M links.
~500 GB total con metadata + stats.
~99% hit
Cache redirect
Power-law: top 1% recibe 80% del tráfico.
~30K req/s al edge, ~300/s al origen.
~10 GB
Cache caliente
~20M links top en RAM.
~10 ms p99 de lookup en Redis.
100 GB/día
Click events
~36 TB/año · ~3K events/s avg.
Sink: BigQuery / ClickHouse.
99.99%
SLO redirect
~52 min/año · ~4 min/mes.
~8 s/día permitidos.

Endpoints

MétodoPathDescripciónNotas
POST/api/linksCrea 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}/statsClicks, 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

flowchart LR User(["Usuario / cliente"]) --> Edge(["Edge / CDN
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

CDN edge99%+ del tráfico nunca llega al origen. Latencia <10 ms global.
302 vs 301302 mantiene control: si el destino cambia o se desactiva, puedo invalidar. 301 lo cachea el browser para siempre.
Redis cache (tier 2)Segundo nivel para misses del CDN. Sub-ms y permite expiración fina.
KV store (Dynamo / Cassandra)O(1) por hash, escala lineal. SQL no aporta nada cuando solo es lookup por short_code.
Counter base627 chars = 62⁷ ≈ 3.5 trillones IDs. Sin colisiones por construcción.
Range allocationWorkers reservan bloques (100K IDs) para evitar coordinación por request.
Bloom filter blocklistSafe browsing check sin lookup remoto cada vez. Falsos positivos validados con API.
Kafka click_eventsAnalytics desacoplada del redirect. Si stream falla, el redirect sigue.

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)

DynamoDB / Cassandra / BigtableKV store

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.

Redis (ElastiCache / Memorystore)cache hot links

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.

CDN (CloudFront / Cloud CDN / Cloudflare)edge redirect

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.

Kafka / Kinesisclick_events stream

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.

BigQuery / ClickHouseanalytics warehouse

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.

Bloom filter en Redissafe browsing check

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.

links (KV)
short_codeVARCHAR(10) PK
long_urlTEXT
owner_idUUID NULL
expires_atTIMESTAMPTZ
created_atTIMESTAMPTZ
deletedBOOLEAN
DynamoDB / Cassandra
Lookup O(1) por short_code
id_ranges
range_idBIGINT PK
startBIGINT
endBIGINT
workerVARCHAR
stateENUM
Reservar bloques de 100K IDs
Evita coordinación por request
click_events (stream)
short_codeVARCHAR
tsTIMESTAMPTZ
ip_countryCHAR(2)
device, ua
referrerTEXT
Tópico Kafka particionado por short_code
Sink: BigQuery o ClickHouse
blocklist + cache
blocklist:hashSET (Bloom)
hot:{code}STRING
rl:{user}:{1m}COUNTER
Bloom filter para safe browsing
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

TemaDecisiónPor qué
Generación de IDCounter + base62, o hash + check.Counter evita colisiones por construcción.
StorageKey-value (DynamoDB/Cassandra) + cache.Lookup por shortCode es O(1) y muy hot.
Redirect302 desde edge cache.Latencia mínima y posibilidad de invalidar.
AnalyticsEventos a stream, agregación async.No bloquea el redirect.
AbusoRate limit + bloomfilter de URL maliciosos.Protege a usuarios y reputación.
IdempotencyMisma 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

Colisiones por hash

Si haces hash sin verificar, dos URLs pueden chocar. Verifica antes de escribir.

Hot link

Un link viral satura un shard. Replica reads y cachea agresivo.

Spam y malware

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×.

Hot · CDN + RAM
Top 1% de links virales en CDN edge cache, redirect en sub-10 ms global.
CloudFront / Cloud CDN + Redis (~10 GB)
~$0.02–0.08/GB egress · ~$36/GB Redis
Warm · KV store
Todos los links activos (no expirados, no borrados) en KV con O(1) lookup.
DynamoDB / Cassandra / Bigtable
~$0.25/GB/mes provisionado · ~$1.25/M reads
Cold · object storage IA
Links borrados (soft delete por audit), click events > 90 días para reportes históricos.
S3 Standard-IA + BigQuery long-term
~$0.0125/GB/mes
Frozen · archive
Click events > 1 año (compliance, BI longitudinal), links permanently deleted (legal).
S3 Glacier / Coldline
~$0.004/GB/mes

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.

flowchart LR User(["Usuario / cliente"]) --> Edge(["Edge / CDN
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.