Nest

Diseño de sistemas

Guía de Arquitectura Backend

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.

01 · Pensar en sistemas — el marco CRDDS

El diseño de sistemas es toma de decisiones antes del código. Un flujo estructurado te mantiene calmado bajo presión:

PasoQué hacesTiempo
C — ClarificarRequisitos 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 aproximadoDibuja el diagrama de alto nivel: clientes, gateway, servicios, almacenes de datos, colas. Un mapa de ciudad, no de calles.~20%
D — Inmersión profundaZoom en un componente (el modelo de datos, el rate limiter, la cola) — interfaces, casos borde.~35%
D — Discutir compromisosNombra alternativas y por qué elegiste una. La señal senior.~20%
S — ResumirResumen, señala riesgos, maneja seguimientos.~10%
Señal senior Di el compromiso en voz alta: "Usaré cache-aside con un TTL corto — estoy intercambiando un poco de obsolescencia por una gran ganancia de latencia."

02 · Arquitectura en capas en NestJS

Casi todos los servicios Nest comparten el mismo esqueleto — memorízalo como tu plantilla HLD:

CapaResponsabilidadEquivalente en Nest
PresentaciónBorde HTTP/GraphQL/WS: parse, valida, serializa.Controladores, resolvers, gateways, DTOs, pipes
Aplicación / DominioCasos de uso y reglas de negocio — la capa más probable.Servicios, clases de caso de uso, modelos de dominio
Datos / RepositorioCombina remoto + local, caché, mapea filas → dominio.Proveedores de repositorio sobre TypeORM/Prisma/Mongoose
IntegraciónAPIs 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.

03 · La capa de integración — llamando a otros servicios

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.

Nombra el patrón Timeout + retry-with-backoff + circuit breaker + bulkhead = el cuarteto de resiliencia.

04 · Almacenamiento y caché

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.

05 · Escalando Node.js

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.

Trampa de estado distribuido El rate limiting, caché, programación y salas de websocket predeterminan memoria por instancia. A escala deben usar Redis (almacenamiento de throttler, KeyvRedis, locks distribuidos, adaptador socket.io Redis) o son incorrectos entre réplicas.
Vertical vs horizontal Vertical (caja más grande) compra tiempo; horizontal (más cajas) es la respuesta real — y solo funciona si eres sin estado.

06 · Asíncrono y mensajería

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.

Guía de decisiones Baja latencia bidireccional → gRPC/WebSocket. Distribución de trabajo → cola. Historial de eventos reproducible / muchos consumidores → Kafka. Push unidireccional al navegador → SSE.

07 · Límites de servicios — de monolito a microservicios

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.

Anti-patrón Los microservicios prematuros compran dolor de sistemas distribuidos (fallos de red, consistencia eventual, overhead de ops) sin beneficio. Gánatelos.

08 · Resiliencia y diseño de fallos

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

09 · Multi-tenancy

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.

Pecado cardinal Filtración de datos entre tenants — un filtro faltante, un tenant mal resuelto o una clave de caché sin el tenant. RLS + claves con scope de tenant son las salvaguardas.

10 · Observabilidad y SLOs

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.

Inmersiones

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.

Diseño de sistemas

Diseñar un rate limiter distribuido

Concepto Limita requests por cliente por ventana. Algoritmos: token bucket (permite ráfagas, suaviza a un promedio — el predeterminado de API), leaky bucket (drenaje constante), fixed window (barato, duplica en el límite), sliding window log (exacto, pesado en memoria), sliding window counter (mejor precisión/memoria).
Ejemplo Token bucket en Redis: almacena {tokens, lastRefill} por clave; en cada request recarga por elapsed×rate (limitado), permite si ≥1 y decrementa. Devuelve 429 + Retry-After + X-RateLimit-*.
Advertencia El read-modify-write entre réplicas es una carrera — dos requests leen 1 token restante y ambas pasan. La memoria por instancia ingenua (el throttler predeterminado de Nest) es incorrecta entre réplicas.
Respuesta senior Haz la recarga→verificación→decremento atómica con un script Lua de Redis (o INCR+EXPIRE). Ejecútalo en el borde/gateway para límites globales baratos. Decide fail-open vs fail-closed si Redis está caído; usa el tiempo del servidor Redis para evitar desfase de reloj.
Diseño de sistemas

Diseñar un acortador de URLs

Concepto Mapea un código corto → URL larga con ~100:1 lectura:escritura. 7 caracteres base62 ≈ 3.5T códigos. Almacén KV, lecturas con caché pesada, analytics asíncronos.
Ejemplo Generación de código: counter+base62 (sin colisiones, pero secuencial/adivinable + cuello de botella del counter), hash+primeros-7 (necesita reintento por colisión) o un Key Generation Service que pre-genera claves aleatorias no usadas offline.
Advertencia 301 (permanente) se cachea en el navegador y pierde analytics de clics; los códigos secuenciales son enumerables; el redirect no debe bloquear en analytics.
Respuesta senior KGS para códigos no adivinables; caché Redis cache-aside (~95% de hit) + CDN para redirects calientes; 302 cuando necesitas analytics; emite eventos de clic asíncronos a Kafka→warehouse para que el redirect se mantenga rápido. DynamoDB/Cassandra a escala.
Diseño de sistemas

Diseñar un servicio de notificaciones

Concepto Ingesta de eventos → plantilla → enrutamiento por preferencia de usuario → colas por canal → workers de canal (email/SMS/push) → webhooks de estado → métricas. Fan-out multi-canal.
Ejemplo Un evento se distribuye a muchos destinatarios/dispositivos vía una cola, particionada por usuario para orden + paralelismo; plantillas versionadas + i18n; centro de preferencias con horas silenciosas y resúmenes.
Advertencia La entrega al-menos-una-vez significa duplicados; ENVIADO ≠ ENTREGADO ≠ LEÍDO; una caída del proveedor no debe tirar todo; legal (unsubscribe/TCPA/GDPR).
Respuesta senior Consumidores idempotentes con clave en (event_id, recipient, channel); reintentos con backoff → DLQ; circuit breaker + fallback de canal (push→SMS para críticos); carriles de prioridad para que los OTPs salten delante del marketing. Exactly-one-vez es impracticable — diseña para al-menos-una-vez + dedup.
Diseño de sistemas

Diseñar un backend de chat

Concepto Los gateways WebSocket mantienen conexiones vivas (nodos sin estado + un registro Redis que mapea usuario→nodo). Mensajes almacenados en un almacén de columna amplia con clave por conversation_id + seq.
Ejemplo Un secuenciador del lado del servidor particionado por conversation_id asigna un seq monótono (no confíes en los relojes del cliente); los clientes reordenan/dedup por (conversation_id, seq). Presencia en Redis con heartbeats TTL.
Advertencia Orden en una partición, exactly-one-vez sobre móvil inestable, fan-out a grupos enormes (amplificación de escritura), reconexión/resumen, sincronización multi-dispositivo.
Respuesta senior Al-menos-una-vez + dedup por message_id estable; fan-out en escritura para grupos pequeños, en lectura / Kafka para grandes (híbrido por tamaño — el problema del celebridad); resume desde el último seq al reconectar; socket.io Redis adapter para fan-out entre nodos.
Diseño de sistemas

Diseñar una cola de trabajos/tareas

Concepto Productor → broker → consumidores en competencia extraen. Un timeout de visibilidad oculta un trabajo mientras un worker lo mantiene; el éxito lo elimina, un crash lo hace reaparecer (re-entrega) — el motor de al-menos-una-vez.
Ejemplo BullMQ (Redis) para trabajos con delay/repetibles/prioridad; SQS (gestionado, FIFO para orden+dedup); Kafka para un log reproducible. Prioridades vía colas separadas; trabajos con delay vía sorted set.
Advertencia Timeout de visibilidad más corto que la duración p99 → re-entrega prematura + doble procesamiento; las colas estándar no garantizan orden; una DLQ sin alertas pierde trabajo silenciosamente.
Respuesta senior Consumidores idempotentes (clave de idempotencia + dedup), timeout de visibilidad > p99 (o heartbeat-extend), reintentos con backoff → DLQ con alertas, auto-escala workers por profundidad de cola, FIFO/particionamiento cuando el orden importa.
Diseño de sistemas

Diseñar subida de archivos grandes y streaming

Concepto No enrutes archivos grandes a través de la memoria de la app. Streméalos, y prefiere subida directa a object storage con la app emitiendo credenciales.
Ejemplo Emite un pre-signed S3 URL para que el cliente suba directo a object storage; para descargas/transformaciones, pipeline(readStream → transform → res); subida multipart para archivos muy grandes.
Advertencia Almacenar un archivo completo en búfer explota memoria y bloquea el loop; sin backpressure se filtra memoria; tipo/tamaño no validados son un agujero de DoS + seguridad.
Respuesta senior Pre-signed URLs para descargar ancho de banda; stream con 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.