🏗️ System Design — conceptos y casos
Diseño de sistemas de verdad: estimaciones back-of-the-envelope, escalabilidad, load balancing, caché, CDN, bases de datos a escala, particionado, consistencia, y el framework completo para resolver casos clásicos. Con ejemplos y práctica.
System Design — conceptos y casos
System design es la disciplina de diseñar sistemas que aguantan carga, fallos y crecimiento, entendiendo los trade-offs de cada decisión. Es la asignatura que separa a quien «programa endpoints» de quien «diseña sistemas». Se estudia con dos ingredientes: conceptos (los building blocks) y casos (aplicarlos a problemas reales).
- Todo es un trade-off: consistencia vs disponibilidad vs latencia vs coste. 2. Se diseña para el fracaso: los fallos son normales, no excepciones. 3. Los números aproximados (back-of-the-envelope) valen más que la precisión. 4. Se piensa en garantías, no en features. 5. La evolución incremental gana a la revolución.
Paso 0: números y estimaciones (back-of-the-envelope)
Antes de diseñar hay que calcular a mano cuánta carga tendrá el sistema. Estos números aproximados son los que distinguen a un buen ingeniero.
1 segundo = 10⁶ microsegundos
1 petición HTTP ≈ 1-10 ms en un servidor
1 consulta SQL ≈ 1-10 ms
1 lectura de SSD ≈ 0.1-1 ms
1 lectura de RAM ≈ 0.1 µs (10.000× más rápida que SSD)
RTT por internet ≈ 10-100 ms (según distancia)
1 servidor típico: ~10.000-100.000 peticiones/seg (si es simple)
~1.000-10.000 con base de datos detrás
Cálculo de ejemplo — un URL shortener:
Q1: ¿cuántas escrituras por segundo?
100M URLs nuevas / año ≈ 300K / día ≈ 3-4 / segundo pico
Q2: ¿cuántas lecturas (redirects)?
10:1 lectura:escritura ≈ 30-40 / segundo
Q3: ¿cuánto almacenamiento en 10 años?
100M × 10 años = 1.000M URLs ≈ 1.000M × ~500 bytes ≈ 500 GB
→ cabe en un solo disco; no necesitas sharding todavía.
💡 Regla: estima siempre en órdenes de magnitud (10×, 100×, 1000×), no en cifras exactas. Si la respuesta cambia por un factor de 10 según tu estimación, el diseño cambia.
Paso 1: los building blocks (bloques de construcción)
Estos son los componentes estándar con los que se arma todo sistema grande.
Escalabilidad: vertical vs horizontal
| Vertical (scale up) | Horizontal (scale out) | |
|---|---|---|
| Qué haces | Más CPU/RAM en la misma máquina | Más máquinas |
| Límite | Físico (máximo de una máquina) | Prácticamente ilimitado |
| Coste | Caro y no lineal | Barato por máquina (commodity) |
| Complejidad | Baja | Alta (repartir la carga, coordinar) |
| Ejemplo | Pasar de 16 GB a 64 GB RAM | Pasar de 1 servidor a 10 |
Vertical: [servidor 64GB] → tope físico
Horizontal: [sv1] [sv2] [sv3] ... → +1 máquina = +capacidad
⚠️ La escala horizontal añade el problema del estado: si cada petición puede ir a cualquier máquina, los datos compartidos (sesiones, caché, BBDD) deben vivir fuera de la máquina (Redis, BBDD compartida) o replicarse.
Load balancer (balanceador de carga)
Distribuye las peticiones entre varias máquinas. Es el primer «bloque» de cualquier sistema con más de un servidor.
┌─────────────┐
Clientes ───────▶│ Load │──▶ [App 1]
│ Balancer │──▶ [App 2]
└─────────────┘──▶ [App 3]
| Método | Cómo reparte | Uso |
|---|---|---|
| Round robin | A cada uno en turno | Simple |
| Weighted | Según capacidad | Máquinas distintas |
| Least connections | Al que menos conexiones tiene | Cargas variables |
| IP hash / sticky | Misma IP → misma máquina | Sesiones locales |
El balanceador además hace health checks (saca a las máquinas caídas) y soporta failover (si uno muere, los demás asumen).
💡 Implementaciones: Nginx (HTTP), HAProxy (TCP/HTTP), ELB/ALB (AWS). A nivel DNS, también existe round-robin DNS (devuelve varias IPs). En Kubernetes es el Service + Ingress.
Caché (caching)
Una caché guarda resultados caros para no volver a calcularlos. Es la técnica que más rendimiento da por dólar de esfuerzo.
Cliente ──▶ [Caché: ¿está?] ──sí──▶ devolver copia (rápido)
│ no
▼
[Backend/BBDD] ──▶ guardar en caché ──▶ devolver
Dónde se pone:
| Nivel de caché | Dónde | Ejemplo |
|---|---|---|
| Cliente | En el navegador/app | HTTP Cache-Control |
| CDN | En el borde de la red | CloudFront, Cloudflare, Akamai |
| Servidor web | Antes de la app | Nginx proxy_cache |
| Aplicación | En el proceso | Diccionario en memoria |
| Distribuida | Fuera de la app | Redis, Memcached |
Patrones de caché:
- Cache-aside: la app consulta la caché; si no está (cache miss), lee de la BBDD y la llena. El estándar.
- Write-through: escribe a la vez en caché y BBDD (consistente, más lento).
- Write-back: escribe solo en caché y vuelca a BBDD luego (rápido, riesgo de pérdida).
- Read-through: la propia caché carga de la BBDD al fallar.
⚠️ Invalidación: el problema eterno de la caché es saber cuándo está vieja. Soluciones: TTL (expira sola), invalidación manual al escribir, o versionado de claves. Una caché mal invalidada sirve datos obsoletos (stale reads) — aceptable para feeds, fatal para saldos.
Ratio de acierto (hit rate): % de peticiones servidas desde caché. Un 90%+ ya transforma la carga de tu BBDD.
CDN (Content Delivery Network)
Una CDN es una red de caché geográficamente distribuida: tu contenido (imágenes, JS, HTML) se sirve desde el servidor más cercano al usuario.
Usuario en Madrid ──▶ CDN nodo Madrid (cerca, rápido)
Usuario en Tokio ──▶ CDN nodo Tokio (cerca, rápido)
│ todos sirven el MISMO contenido cacheado del origen
▼
[Servidor origen]
💡 Reduce latencia (menos distancia) y carga del origen (el contenido estático ni llega a tu servidor). Ideal para assets estáticos e imágenes. No para datos dinámicos personalizados.
Bases de datos a escala
Cuando una sola base de datos no basta, se aplican dos técnicas que ya viste en Fundamentos: BBDD y Distribuidos:
- Replicación (copias): separa lecturas de escrituras.
- Particionado / sharding (partes): reparte los datos entre nodos.
Replicación: Escrituras ──▶ [Maestro] ──▶ [Réplica] ──▶ lecturas
[Réplica] ──▶ lecturas
Sharding: Usuarios 0-1000 ──▶ [Shard A]
Usuarios 1001-2000 ──▶ [Shard B]
Usuarios 2001-3000 ──▶ [Shard C]
⚠️ La decisión más difícil: elegir la clave de partición (shard key). Si eliges mal (ej: por país y el 90% está en un país), tienes hot spots y el rendimiento se hunde. Y cambiarla después es carísimo. Piensa: «¿qué consultas son las más comunes y cómo las reparto uniformemente?»
Paso 2: el framework de diseño de un caso
Para resolver cualquier caso de system design, usa este framework de 4 pasos:
1. Requisitos y estimaciones
Antes de dibujar nada: pregunta.
¿Qué hace el sistema? ¿Quiénes lo usan?
¿Cuántos usuarios? ¿Cuántas peticiones por segundo?
¿Lecturas o escrituras? ¿Proporción?
¿Cuántos datos se guardan? ¿Cuánto crecerán en 10 años?
¿Requisitos no funcionales: latencia, disponibilidad, consistencia?
2. Diseño de alto nivel
Dibuja la arquitectura con bloques, sin detalles:
[Cliente] ──▶ [Load Balancer] ──▶ [Servidores API]
│ │
▼ ▼
[CDN (estático)] [Caché (Redis)]
│
▼
[BBDD primaria]
│
▼
[Réplicas / colas / workers]
3. Componentes y datos
Detalla cada bloque: esquema de la base de datos, APIs, algoritmos clave.
Tabla urls:
id (PK) | url_original | short_code (UNIQUE, INDEXADO) | creado_en
API:
POST /api/url {url} → {short_code}
GET /:short → 301 redirect a url_original
4. Escalar y fallos
«¿Qué pasa si se duplica el tráfico? ¿Y si se cae un nodo? ¿Y si se cae la BBDD?»
Escalar: más servidores de app (ya son stateless → solo añadir)
caché en Redis para lecturas calientes
réplicas de lectura
Fallos: balanceador detecta app caída → redirige
BBDD caída → con réplicas, promoción (failover)
CDN sigue sirviendo el estático
Caso resuelto: URL shortener
Aplicamos el framework al caso más clásico.
1. Requisitos: crear URLs cortas (write-heavy es poco, read-heavy sí), redirects rápidos (menos de 50 ms), sin colisiones.
2. Alto nivel:
[Cliente] ──▶ [LB] ──▶ [API servers] ──▶ [Redis caché]
──▶ [BBDD PostgreSQL]
3. Datos: tabla urls con short_code único e indexado. La búsqueda por short_code es O(log n) por el índice.
4. Generación del código corto: dos opciones clásicas.
-- Opción A: ID autoincremental → codificar en base62
-- id=1000000 → base62 → "4c92" (más corto que decimal)
-- 6 caracteres base62 = 62^6 ≈ 56.800 millones de combinaciones
-- Opción B: hash de la URL
SELECT substr(md5(random()::text), 1, 7); -- aleatorio, con retry si colisiona
Escalar: los redirects son lecturas → se cachean en Redis (TTL largo, el 99% de los redirects son de URLs populares). La BBDD se replica para lecturas. Si el tráfico crece mucho, se sharding por short_code (hash).
Caso resuelto: chat (sistema de mensajería)
1. Requisitos: mensajes en tiempo real, historial, multiusuario.
2. Alto nivel:
[Cliente] ──▶ [WebSocket / Long-poll] ──▶ [Chat server] ──▶ [Cola] ──▶ [Worker] ──▶ [BBDD]
│
▼
[Persistencia]
3. Decisiones clave:
- Tiempo real: WebSocket para el canal activo (bidireccional, sin polling). Alternativas: Server-Sent Events (unidireccional), long-polling (compatibilidad).
- Orden de mensajes: cada conversación tiene un
conversation_id; el orden se garantiza con un número de secuencia por conversación (no global). - Presencia: estado en Redis (TTL que se renueva con heartbeats).
- Persistencia: tabla
mensajes (id, conversation_id, user_id, texto, creado_en)con índice enconversation_id. - Notificaciones push cuando el usuario no está conectado (FCM/APNs).
4. Escalar:
- Chat servers stateless → añadir más detrás del LB
- El canal WebSocket se enruta con sticky sessions o un "presence registry"
(Redis con mapa conversation → server)
- Historial viejo → a almacenamiento frío (archivo/S3) o columna
- Sharding por conversation_id para millones de conversaciones
Catálogo de casos clásicos
Practica resolviendo estos (por dificultad):
| Caso | Conceptos que entrena |
|---|---|
| URL shortener | BBDD, base62, caché, redirección |
| Rate limiter | Algoritmos de límites (token bucket, sliding window), Redis |
| News feed | Push vs pull, fanout, caché, ranking |
| Chat | WebSocket, orden, presencia, colas |
| Búsqueda (search engine) | Inverted index, ranking, crawling |
| Video streaming | CDN, streaming adaptativo (HLS/DASH), segmentos |
| Notificaciones | Colas, deduplicación, workers |
| Key-value store | Replicación, sharding, consistencia, LSM/B-tree |
| Ticketing (reservas) | Consistencia fuerte, locks, transacciones, overbooking |
| Uber / maps | Geo-indexing, eventos, sincronización |
Terminología que debes dominar
| Término | En una frase |
|---|---|
| Throughput | Operaciones por segundo (QPS/TPS) |
| Latencia | Tiempo de una operación |
| Escalabilidad | Capacidad de crecer con la carga |
| Disponibilidad (SLA) | % de tiempo que el sistema responde (99.9% = ~8.7h/año caído) |
| Load balancer | Reparte carga entre servidores |
| Caché | Copia de datos caros de recalcular |
| Hit rate | % de peticiones servidas por caché |
| CDN | Caché distribuida geográficamente |
| Replicación | Copias de los datos (lecturas/escrituras separadas) |
| Sharding / particionado | Cada nodo guarda una parte |
| Shard key | La clave que decide a qué partición va cada dato |
| Fanout | Multiplicar un dato a muchos destinos (feed) |
| Idempotencia | Repetir la operación da el mismo resultado (clave para colas) |
| Back-of-the-envelope | Estimación a mano, aproximada |
| Cold / hot data | Datos poco usados / muy usados |
Práctica propuesta
- Calcula back-of-the-envelope para un sistema de tweets: usuarios, tweets/día, lecturas/seg, almacenamiento en 10 años.
- Dibuja en papel la arquitectura de URL shortener sin mirar la solución. Luego compárala con la de arriba.
- Implementa un rate limiter con token bucket en tu lenguaje (Redis si lo tienes a mano).
- Monta una caché Redis delante de un endpoint tuyo y mide la mejora con
ab(ApacheBench) owrk. - Explica a alguien (o por escrito) la diferencia entre replicación y sharding con un dibujo.
- Elige una shard key para: usuarios, mensajes, pedidos. Justifica cada elección.
Para profundizar
- Designing Data-Intensive Applications: el libro #1. Los capítulos 1-6 son obligatorios.
- System Design Primer (Donne Martin): índice enciclopédico con casos resueltos y tarjetas Anki.
- System Design Interview (Alex Xu): los casos resueltos más famosos.
- ByteByteGo: conceptos con diagramas claros.
- High Scalability: arquitecturas reales y lecciones aprendidas.
- Grokking the System Design Interview: el framework de 4 pasos en formato curso.
- Sistemas distribuidos (fundamentos): la teoría base de CAP, replicación y particionado.
- Sigue con 🏗️ System Design avanzado y 🌐 Distribuidos.