Diseña un acortador de URLs — recorre la arquitectura completa
Un acortador de URLs parece trivial hasta que tienes que generar mil millones de códigos únicos sin que un solo contador se vuelva tu cuello de botella.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Diseña un acortador de URLs — recorre la arquitectura completa
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Aclara primero los requisitos
Antes de dibujar ninguna caja, hay que fijar qué necesita hacer realmente el sistema, porque la arquitectura correcta para un acortador de URLs cambia mucho según los números. Los requisitos funcionales suelen ser: dada una URL larga, devolver una corta; dada una URL corta, redirigir a la original; opcionalmente, dejar que un usuario elija un alias personalizado; opcionalmente, dejar que un enlace expire. Los requisitos no funcionales son donde viven las decisiones de diseño interesantes: las redirecciones deben ser rápidas (bastante por debajo de 100ms) y de alta disponibilidad, los códigos cortos deben ser únicos, y el tráfico de lectura superará ampliamente al de escritura — un enlace se crea una vez y se clickea muchas veces, así que una proporción realista de lectura:escritura anda entre 10:1 y 100:1 o más.
Una estimación aproximada de capacidad ancla el resto del diseño: digamos 100 millones de enlaces nuevos creados por día (~1,160 escrituras/seg en promedio, varias veces eso en pico) y una proporción de lectura:escritura de 100:1, lo que pone el tráfico sostenido de redirección bien dentro de las decenas de miles de peticiones por segundo. Con un promedio de ~500 bytes por registro incluyendo metadatos, cinco años de creación de enlaces caen alrededor de 90 terabytes — nada que una sola máquina sostenga cómodamente, y nada que necesite algo más exótico que un almacén clave-valor escalado horizontalmente con una caché delante.
#Generando códigos cortos únicos
El problema central es convertir una URL larga en un identificador corto y único, y hay tres enfoques que vale la pena conocer, porque los entrevistadores normalmente quieren que los compares en vez de que elijas uno a ciegas.
Contador + codificación base62. Mantén un ID entero globalmente único y monótonamente creciente, y luego codifícalo en base62 (`[0-9a-zA-Z]`, 62 caracteres) para obtener una cadena alfanumérica corta. Base62 es la opción práctica sobre base64 porque evita por completo caracteres no seguros para URLs (`+`, `/`), así que nunca necesitas percent-encoding en el enlace resultante. Con 7 caracteres en base62 obtienes 62^7 ≈ 3.5 billones de códigos posibles — margen de sobra. El detalle está en generar ese ID entero sin que un único contador de base de datos se vuelva un cuello de botella de escritura o un punto único de falla. La solución estándar — a veces llamada patrón "ticket server" — hace que cada servidor de aplicación reclame de una sola vez un rango de IDs (digamos, 1,000 a la vez) desde una pequeña tabla contadora mediante una única actualización atómica, y luego reparta IDs de ese rango localmente sin volver a tocar la base de datos hasta agotarlo. Esto convierte millones de peticiones de ID en un número mucho menor de idas y vueltas de asignación de rango.
Hash de la URL. Toma un hash (MD5 o SHA-256) de la URL larga y codifica en base62 los primeros caracteres. Esto es determinista — la misma URL siempre mapea al mismo código, lo cual puede ser una ventaja (deduplicación natural) o un problema (no puedes dejar que dos usuarios distintos obtengan dos enlaces cortos distintos para la misma URL, por ejemplo, para llevar analíticas separadas). También necesita manejo de colisiones: truncar un hash a 6-8 caracteres hace posibles las colisiones, así que igual necesitas una verificación de unicidad contra la base de datos y un plan B (añadir caracteres, o usar una semilla distinta) cuando ocurra una.
Pool de llaves pregeneradas. Un servicio en segundo plano genera continuamente cadenas base62 aleatorias y únicas, y guarda las no usadas en un almacén clave-valor rápido. Cuando llega una petición de escritura, el servidor de API simplemente extrae una llave ya verificada como única del pool — sin contención de contador, sin manejo de colisiones de hash, y la generación ocurre completamente fuera del camino crítico de la petición. La contrapartida es operativa: ahora corres y monitoreas un servicio de generación de llaves, y debes proteger el pool para que dos servidores no reclamen la misma llave a la vez (típicamente resuelto con un pop atómico, como `SPOP` de Redis).
Para la mayoría de los diseños, la asignación de rangos basada en contador es la respuesta que vale la pena defender porque es lo más simple que escala, pero nombrar las alternativas de hash y de pool pregenerado — y sus modos de falla específicos — es lo que distingue una respuesta memorizada de una entendida.
#El camino de escritura (crear una URL corta)
El servidor de API valida la URL enviada (una URL real, sintácticamente válida, no algo que apunte a infraestructura interna — este endpoint es un blanco clásico de SSRF si alguna vez la haces fetch del lado del servidor para validarla), resuelve un ID (de su rango reclamado localmente, o verificando la unicidad de un alias personalizado pedido), lo codifica, y escribe el registro `{short_code, long_url, owner_id, created_at, expires_at}` en el almacén de datos primario. Escribir también hacia la caché en el momento de la creación es opcional pero se paga solo casi de inmediato si los enlaces nuevos tienden a clickearse poco después de crearse (normalmente así es — piensa en un enlace compartido en redes sociales, que recibe la mayoría de sus clicks en las primeras horas).
#El camino de lectura (redirección)
Como el tráfico de redirección domina, el camino de lectura es donde el diseño se gana su presupuesto de latencia. Una caché delante del almacén primario (Redis o Memcached, particionada por `short_code`) absorbe la abrumadora mayoría de las búsquedas, ya que el tráfico de clicks sobre enlaces acortados sigue una distribución muy sesgada — una fracción pequeña de enlaces concentra la mayoría de los clicks (una curva tipo Zipf), así que incluso una caché modesta con una política de desalojo sensata (LRU) captura la mayor parte del conjunto caliente. En un cache miss, el servidor de API recurre al almacén primario, sirve la redirección, y puebla la caché para la próxima vez.
Dos decisiones de diseño que vale la pena mencionar explícitamente porque los entrevistadores las escuchan:
- 302 vs. 301. Un 301 (redirección permanente) le dice al navegador del cliente que caché el destino de la redirección él mismo, así que el navegador nunca volverá a tu servidor por ese enlace — genial para reducir carga del servidor, terrible para tu capacidad de cambiar el destino después, medir analíticas de clicks, o expirar el enlace. Un 302 (redirección temporal, o 307 si necesitas preservar el método) mantiene cada click fluyendo a través de tu servicio, que casi siempre es lo que realmente quieres para un acortador de URLs.
- El registro de clicks no debe ser síncrono con la redirección. Escribir una fila de analítica en una base de datos en cada redirección pondría una escritura en tu camino más caliente y crítico en latencia. En su lugar, dispara el evento de click hacia una cola de mensajes (Kafka, o incluso un buffer en memoria ligero que se vacía periódicamente) y deja que un consumidor separado lo agregue — el usuario recibe su redirección de inmediato, y la analítica se pone al día de forma asíncrona.
#Almacén de datos y sharding
El patrón de acceso aquí es casi vergonzosamente simple: una búsqueda puntual por llave primaria (`short_code`) sin joins ni consultas complejas, que es exactamente la forma para la que está construido un almacén clave-valor o de columnas anchas (DynamoDB, Cassandra), y particiona limpiamente hasheando `short_code`. Una base de datos relacional también funciona a escala moderada — es solo una tabla con un índice sobre `short_code` — y muchos sistemas reales corren esto sobre Postgres mucho más tiempo del que la gente espera antes de que el sharding se vuelva necesario; la lección anterior sobre escalado vertical vs. horizontal aplica directamente aquí.
#Una implementación real de base62
BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"BASE = 62def encode_base62(num: int) -> str:if num == 0:return BASE62_ALPHABET[0]digits = []while num > 0:num, remainder = divmod(num, BASE)digits.append(BASE62_ALPHABET[remainder])return "".join(reversed(digits))def decode_base62(short_code: str) -> int:num = 0for char in short_code:num = num * BASE + BASE62_ALPHABET.index(char)return num# encode_base62(125_000_000) -> "8M0T2"# decode_base62("8M0T2") -> 125000000
class IdRangeAllocator:"""Entrega a cada servidor de aplicación un bloque contiguo de IDs enterosúnicos por adelantado, para que codificar un enlace corto nuevo casinunca requiera una ida y vuelta a la base de datos — solo reclamar unrango nuevo la necesita, aproximadamente una vez cada range_sizepeticiones en vez de una vez por petición."""def __init__(self, db, range_size: int = 1000):self.db = dbself.range_size = range_sizedef allocate_range(self):row = self.db.execute("""UPDATE id_counterSET current_max = current_max + %sWHERE name = 'url_shortener'RETURNING current_max - %s AS range_start, current_max AS range_end""",(self.range_size, self.range_size),).fetchone()return row["range_start"], row["range_end"]
🧠 Check your understanding
0/2 · 0/2 answered1. ¿Por qué se prefiere generalmente una redirección 302 sobre una 301 para el endpoint de redirección de un acortador de URLs?
2. ¿Qué problema específico resuelve la asignación de IDs por rangos (el patrón 'ticket server') comparado con un único contador auto-incremental compartido?