Software Engineering · Scalability|13 min de lectura|

Arquitectura de Backend Escalable: Patrones para Crecer sin Romper

La mayoría de los problemas de escala no aparecen en el lanzamiento — aparecen cuando el producto tiene éxito. El sistema que funcionó perfectamente con 100 usuarios diarios empieza a crujir con 10,000. Las decisiones de arquitectura que parecían pragmáticas al principio (consultas sin índices, sesiones en memoria, lógica en el request-response) se convierten en cuellos de botella concretos. Esta guía documenta los patrones de arquitectura que permiten a los backends escalar de forma sostenible.

Diagnóstico primero: dónde está realmente el cuello de botella

Escalar sin diagnóstico es tirar dinero. Antes de añadir instancias o rediseñar la arquitectura, identificar el cuello de botella real. Los cuatro lugares más frecuentes en orden de probabilidad:

  • Base de datos (70% de los casos): queries sin índices, N+1 queries, lock contention, connection pool agotado. La base de datos escala peor que la aplicación y es el cuello de botella en la mayoría de los sistemas.
  • IO de red y latencia de terceros (15%): llamadas síncronas a APIs externas en el request-response principal, sin caching ni circuit breaker.
  • Código de aplicación (10%): loops O(n²), serialización ineficiente, generación de objetos en memoria a escala.
  • Infraestructura (5%): CPU throttling en contenedores, límites de red, nodos sobresaturados.

Connection Pooling: el primer problema que nadie configura bien

Una aplicación sin connection pool crea una nueva conexión a la base de datos en cada request. A baja carga, el overhead es manejable. A alta carga, el tiempo de establecer conexiones TCP + TLS + autenticación PostgreSQL puede ser el 30-40% de la latencia total del request. Con PgBouncer como proxy de pooling, la aplicación reutiliza conexiones existentes.

ini
; pgbouncer.ini — configuración para una API de producción
[databases]
production = host=postgres-primary port=5432 dbname=produccion

[pgbouncer]
pool_mode = transaction     ; pool por transacción (más eficiente que session)
max_client_conn = 1000      ; conexiones máximas de la aplicación al proxy
default_pool_size = 25      ; conexiones reales al PostgreSQL
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
log_connections = 0         ; desactivar en producción (genera I/O)
log_disconnections = 0

Con pool_mode = transaction, PgBouncer asigna una conexión de PostgreSQL solo durante la duración de la transacción, no durante toda la sesión del cliente. 1,000 conexiones de aplicación pueden usar solo 25 conexiones reales a PostgreSQL si cada transacción es corta. Esto es lo que permite a sistemas con miles de requests concurrentes operar con una base de datos que acepta 100 conexiones máximas.

Caching en capas: la arquitectura correcta

El caching no es una sola herramienta — es una estrategia en capas, cada una con un tradeoff diferente entre costo, complejidad y hit rate.

typescript
// Caching en capas: in-process → Redis → base de datos
import { LRUCache } from 'lru-cache';
import Redis from 'ioredis';

const localCache = new LRUCache<string, Product>({
  max: 1000,          // máximo 1000 items en memoria del proceso
  ttl: 30_000,        // 30 segundos de TTL en caché local
});

const redis = new Redis(process.env.REDIS_URL!);

async function getProduct(id: string): Promise<Product> {
  // Capa 1: caché local en memoria (sub-milisegundo)
  const local = localCache.get(id);
  if (local) return local;

  // Capa 2: Redis (1-2ms)
  const cached = await redis.get(`product:${id}`);
  if (cached) {
    const product = JSON.parse(cached);
    localCache.set(id, product);  // poblar caché local
    return product;
  }

  // Capa 3: base de datos
  const product = await db.product.findUnique({ where: { id } });
  if (!product) throw new NotFoundError(`Product ${id} not found`);

  // Escribir en Redis con TTL de 5 minutos
  await redis.setex(`product:${id}`, 300, JSON.stringify(product));
  localCache.set(id, product);
  return product;
}
El caché local en memoria del proceso (LRU) es el más rápido pero tiene stale data risk en deployments multi-instancia. Para datos que cambian frecuentemente (precios, inventario), usar solo Redis con TTL corto. Para datos que cambian poco (catálogo, configuración de cuenta), el caché local en memoria con TTL de 30-60 segundos elimina decenas de millisegundos de latencia.

CQRS: separar lecturas de escrituras para escalar cada lado

Command Query Responsibility Segregation separa el modelo de datos para escrituras (Commands) del modelo para lecturas (Queries). En muchos sistemas de producción, las lecturas superan a las escrituras en ratio 10:1 o más. Con CQRS, puedes escalar el read model independientemente (réplicas de solo lectura, projections desnormalizadas) sin afectar el write model.

typescript
// CQRS: handler de escritura y modelo de lectura separados

// --- Write side: normalizado, consistente ---
async function createOrder(cmd: CreateOrderCommand): Promise<string> {
  return db.transaction(async (trx) => {
    const order = await Order.create({
      userId: cmd.userId,
      status: 'pending',
    }, { transaction: trx });

    await OrderItem.bulkCreate(
      cmd.items.map(item => ({ orderId: order.id, ...item })),
      { transaction: trx }
    );

    await OutboxEvent.create({
      event_type: 'OrderCreated',
      payload: JSON.stringify({ orderId: order.id, userId: cmd.userId }),
    }, { transaction: trx });

    return order.id;
  });
}

// --- Read side: desnormalizado, optimizado para la UI ---
// Este modelo se construye desde los eventos y puede ser
// una tabla en PostgreSQL, un documento en MongoDB, o un índice en Elasticsearch
interface OrderSummaryReadModel {
  orderId: string;
  userName: string;
  totalAmount: number;
  itemCount: number;
  statusLabel: string;
  createdAtFormatted: string;
}

Database sharding y hot spots

El sharding horizontal (distribuir datos entre múltiples instancias de base de datos por sharding key) es la solución de última línea para escala de base de datos. Antes de llegar ahí, hay pasos intermedios:

  • Read replicas: escalar las lecturas con réplicas de solo lectura es simple y resuelve la mayoría de los problemas de escala de base de datos sin complejidad de sharding.
  • Table partitioning: particionar tablas grandes (logs, eventos, facturas) por rango de fecha. PostgreSQL native partitioning reduce el costo de queries y facilita el archivado de datos históricos.
  • Optimizar queries antes de escalar infraestructura: un índice faltante puede generar un full table scan que tarda 5 segundos en una tabla de 10 millones de filas. Con el índice correcto, la misma query tarda 2 milisegundos.

Rate limiting: proteger el backend de sí mismo

El rate limiting no es solo protección contra abuso externo — es protección contra picos de tráfico legítimo que pueden saturar el backend. El algoritmo Token Bucket permite picos de tráfico hasta cierto límite mientras mantiene una tasa promedio sostenible.

typescript
// Rate limiting con Redis usando el algoritmo sliding window
async function checkRateLimit(
  userId: string,
  limitPerMinute: number
): Promise<{ allowed: boolean; remaining: number }> {
  const key = `rate_limit:${userId}:${Math.floor(Date.now() / 60000)}`;
  const current = await redis.incr(key);

  if (current === 1) {
    await redis.expire(key, 60); // expirar al final del minuto
  }

  return {
    allowed: current <= limitPerMinute,
    remaining: Math.max(0, limitPerMinute - current),
  };
}

Preguntas frecuentes

¿Cuándo es momento de pasar de una instancia de base de datos a réplicas de lectura?
Cuando las métricas de base de datos muestran CPU > 60% sostenido, o cuando el tiempo de respuesta de las queries de lectura empieza a afectar los percentiles altos de latencia (p95, p99) de la API. La señal más clara: las lecturas analíticas o reportes están afectando la latencia de las escrituras transaccionales en la misma instancia.
¿Redis o Memcached para caching empresarial?
Redis en prácticamente todos los casos. Redis soporta estructuras de datos avanzadas (sorted sets, hashes, streams), tiene persistencia opcional, soporta Pub/Sub, y tiene clustering nativo. Memcached es marginalmente más rápido en el caso de uso de cache pura (key-value string), pero la versatilidad de Redis justifica la diferencia. En la práctica, la mayoría de los sistemas que empiezan con caching eventualmente necesitan las features adicionales de Redis.
¿Qué es N+1 query y cómo lo evito?
El problema N+1 ocurre cuando cargas N entidades y luego haces 1 query adicional por entidad para obtener datos relacionados. Ejemplo: cargar 100 órdenes y luego hacer 100 queries adicionales para obtener el nombre del cliente de cada orden. La solución: eager loading (JOIN en la query inicial), DataLoader (batching de queries en GraphQL), o un índice de lectura desnormalizado que incluye los datos relacionados necesarios.
¿Cómo escalo horizontalmente una API stateful (con sesiones)?
La sesión es el estado que impide el scaling horizontal naïve. La solución: mover el estado de sesión fuera del proceso a un store externo compartido (Redis). Con sesiones en Redis, cualquier instancia de la API puede manejar cualquier request — el load balancer puede distribuir sin sticky sessions. JWT con estado mínimo en el token (no sesión completa) es la alternativa si no quieres gestionar el store de sesiones.
¿Cuándo implementar CQRS y cuándo es over-engineering?
CQRS es over-engineering para la mayoría de las aplicaciones de CRUD simples. Es valioso cuando: el modelo de lectura y el de escritura tienen shapes fundamentalmente distintos (la API devuelve datos desnormalizados de múltiples tablas), las lecturas escalan 10x más que las escrituras, o necesitas diferentes niveles de consistencia para lecturas y escrituras. El criterio: si el mismo modelo de datos sirve bien para leer y escribir, CQRS añade complejidad sin beneficio real.

¿Tu backend empieza a mostrar problemas de escala? Podemos hacer un diagnóstico técnico e identificar los cuellos de botella reales antes de proponer cambios de arquitectura.

Habla con el equipo

Artículos relacionados

IQS

Equipo de Ingeniería — IQS

Ingenieros de software, cloud y DevOps con experiencia en proyectos empresariales.