Diseñar un rate limiter distribuido
{tokens, lastRefill} por clave; en cada request recarga por elapsed×rate (limitado), permite si ≥1 y decrementa. Devuelve 429 + Retry-After + X-RateLimit-*.Diseño de sistemas
Un recorrido senior de diseño de sistemas backend, mapeado sobre NestJS y Node.js. Este es el material de "cómo piensas sobre construir un backend a escala" — decisiones antes del código, para la peor consulta, la peor red y el peor pico de tráfico.
El diseño de sistemas es toma de decisiones antes del código. Un flujo estructurado te mantiene calmado bajo presión:
| Paso | Qué haces | Tiempo |
|---|---|---|
| C — Clarificar | Requisitos funcionales y no funcionales, escala (RPS, DAU), ratio lectura/escritura, necesidades de consistencia. La mayoría de candidatos se apresuran aquí y diseñan el sistema equivocado. | ~15% |
| R — HLD aproximado | Dibuja el diagrama de alto nivel: clientes, gateway, servicios, almacenes de datos, colas. Un mapa de ciudad, no de calles. | ~20% |
| D — Inmersión profunda | Zoom en un componente (el modelo de datos, el rate limiter, la cola) — interfaces, casos borde. | ~35% |
| D — Discutir compromisos | Nombra alternativas y por qué elegiste una. La señal senior. | ~20% |
| S — Resumir | Resumen, señala riesgos, maneja seguimientos. | ~10% |
Casi todos los servicios Nest comparten el mismo esqueleto — memorízalo como tu plantilla HLD:
| Capa | Responsabilidad | Equivalente en Nest |
|---|---|---|
| Presentación | Borde HTTP/GraphQL/WS: parse, valida, serializa. | Controladores, resolvers, gateways, DTOs, pipes |
| Aplicación / Dominio | Casos de uso y reglas de negocio — la capa más probable. | Servicios, clases de caso de uso, modelos de dominio |
| Datos / Repositorio | Combina remoto + local, caché, mapea filas → dominio. | Proveedores de repositorio sobre TypeORM/Prisma/Mongoose |
| Integración | APIs externas, brokers de mensajes, caché. | Clientes HTTP, ClientProxy, cache-manager |
Para dominios complejos y de larga duración, escala a arquitectura hexagonal: el dominio depende de ports (interfaces + tokens DI) y los adaptadores vinculan implementaciones concretas — para que la infraestructura sea intercambiable y el dominio sea puro. No pagues ese costo de indirección en CRUD delgado.
Un cliente de producción hacia otro servicio necesita más que fetch: timeouts (nunca esperar para siempre), reintentos con backoff exponencial + jitter (solo para fallos idempotentes/transitorios — red, 5xx, no 4xx), un circuit breaker (deja de golpear una dependencia caída) y un bulkhead (límite de llamadas concurrentes para que una dependencia lenta no agote tu pool).
Envuelve las llamadas en un resultado/Observable con timeout(), propaga un correlation id (traceparent) para tracing y mapea DTOs externos a tu dominio en el límite (una capa de anticorrupción) para que los cambios de esquema no se propaguen a tu código.
Elige el almacén para el patrón de acceso: relacional (Postgres) para integridad transaccional + joins; documento (Mongo) para esquemas flexibles; clave-valor (Redis) para datos calientes/efímeros; columna amplia (Cassandra/Dynamo) para series de tiempo pesadas de escritura. La replicación escala lecturas + alta disponibilidad (cuidado con el lag de réplica); la fragmentación escala escrituras (elige una buena clave de fragmentación; los joins cross-shard duelen).
Capas de caché: en memoria (rápido, por instancia, perdido al reiniciar) y Redis compartido (sobrevive reinicios, consistente entre réplicas). Estrategias: cache-aside (predeterminado), read/write-through, write-behind. Invalida vía TTL, delete-on-write o claves versionadas; protege contra stampede con un lock/single-flight. Nombra stale-while-revalidate.
Node escala al mantenerse no bloqueante y sin estado. Mantén el trabajo de CPU fuera del event loop (worker threads / colas). Escala horizontalmente: ejecuta un proceso por núcleo (cluster) o, más comúnmente, N réplicas de contenedor detrás de un load balancer. Externaliza todo el estado (sesiones, caché, subidas) a Redis/DB/object storage para que cualquier réplica pueda servir cualquier request.
Desacopla trabajo con asincronía. Las colas (BullMQ/SQS/RabbitMQ) distribuyen trabajo a consumidores en competencia (consumir-una-vez); los streams/logs (Kafka) son duraderos, reproducibles, ordenados-por-partición, multi-consumidor. Usa colas para trabajos (email, procesamiento de imágenes); usa streams para event sourcing y fan-out a muchos consumidores.
La entrega es al-menos-una-vez (timeout de visibilidad → re-entrega en crash), por lo que los consumidores deben ser idempotentes. Los reintentos usan backoff + jitter → una DLQ con alertas. Back-pressure: auto-escala workers por profundidad de cola.
Empieza con un monolito modular: un módulo por contexto acotado, cada uno con sus tablas, llamadas cross-módulo a través de una pequeña API pública + adaptador. Un despliegue, refactors atómicos, llamadas transaccionales en proceso — impón límites en CI con dependency-cruiser.
Extrae un microservicio solo por una necesidad concreta: escalado independiente, despliegue, aislamiento de fallos o propiedad de equipo. Si los límites eran limpios, intercambias el servicio público en proceso por un cliente de transporte que implementa la misma interfaz. La consistencia entre servicios usa sagas (acciones compensatorias) + el transactional outbox, no 2PC.
Diseña para el fallo como predeterminado. El toolkit: timeouts en todas partes, reintentos (solo idempotentes, backoff + jitter), circuit breakers, bulkheads, degradación graceful (sirve caché obsoleto / una respuesta reducida en lugar de un error) y claves de idempotencia para que los efectos secundarios reintentados se ejecuten a-más-una-vez.
Falla rápido y de forma visible: valida en el borde, límite el tamaño de request y la concurrencia, y propaga la cancelación (AbortSignal). Decide fail-open vs fail-closed por feature (un rate-limiter Redis caído: fail-open para disponibilidad, fail-closed para endpoints sensibles a abuso).
Elige un modelo de aislamiento temprano (difícil de revertir): Silo (DB por tenant — aislamiento/cumplimiento más fuerte, más caro), Pool (tablas compartidas + tenant_id — más barato, riesgo de vecino ruidoso), Bridge (esquema por tenant). Los tiers frecuentemente mezclan (enterprise = silo, SMB = pool).
Resuelve el tenant desde subdomain/claim JWT, establecelo en el contexto de request (CLS) y impleméntalo en todas partes — el RLS de Postgres hace que un filtro olvidado no pueda filtrar filas. Scope las claves de caché por tenant; añade cuotas/límites de tasa por tenant para vecinos ruidosos.
No puedes operar lo que no ves. Instrumenta cuatro señales: salud (probes de liveness/readiness), métricas (RED — tasa/errores/duración — más internos de Node: event-loop lag, heap, GC), trazas (OpenTelemetry, propagadas vía traceparent) y logs estructurados con correlation ids.
Define SLOs (por ejemplo, latencia p99 < 300ms, 99.9% de disponibilidad), rastrea un presupuesto de errores y alerta en salvaguardas — no en CPU raw. Trata un despliegue como "listo" solo cuando las tasas de crash/error se mantienen durante el rollout.
Seis prompts clásicos de diseño de sistemas backend, cada uno como Concepto → Ejemplo → Advertencia → Respuesta senior. Estos son los que los entrevistadores buscan — ensaya los compromisos en voz alta.
{tokens, lastRefill} por clave; en cada request recarga por elapsed×rate (limitado), permite si ≥1 y decrementa. Devuelve 429 + Retry-After + X-RateLimit-*.pipeline(readStream → transform → res); subida multipart para archivos muy grandes.pipeline (backpressure automático + limpieza); valida tamaño + tipo con números mágicos con ParseFilePipe; procesa derivados (thumbnails, escaneo de virus) asíncronamente vía una cola.