Escalar Sistemas Backend Sin Sobreingeniería: La Checklist de Arquitectura YAGNI
Cuándo escalar y cuándo no. Monolito + cola como la arquitectura más infravalorada, disparadores reales de scaling, cuellos de botella de base de datos, estrategia de caché y checklist YAGNI.
La epidemia de sobreingeniería
Lo hemos visto demasiadas veces: un equipo de 3 ingenieros construyendo una arquitectura de microservicios para una aplicación con 200 usuarios. Cluster de Kubernetes, service mesh, bus de eventos, API gateway, 5 bases de datos separadas y un stack de monitorización más complejo que la propia aplicación. Seis meses después, están depurando problemas de sistemas distribuidos en lugar de construir funcionalidades.
Lo contrario también ocurre: un monolito sirviendo 50.000 usuarios concurrentes, donde cada petición tarda 5 segundos porque la base de datos es el cuello de botella y no hay capa de caché. El equipo sigue añadiendo servidores de aplicación, pero la base de datos es el techo.
Ambos son fallos arquitecturales. Este artículo cubre cuándo escalar, qué escalar y cómo evitar escalar cosas que no lo necesitan. Para patrones de scaling específicos, consulta nuestra guía de arquitectura de sistemas y nuestra guía de arquitectura event-driven.
La checklist de arquitectura YAGNI
Antes de añadir cualquier complejidad arquitectural (nuevo servicio, nueva base de datos, nuevo message broker, nueva capa de caché), pregúntate:
| Pregunta | Si SÍ | Si NO |
|---|---|---|
| ¿Tenemos un problema de rendimiento medido ahora mismo? | Corrige el problema específico | No añadas complejidad para un problema hipotético |
| ¿Resolvería esto un problema que ya hemos encontrado? | Considéralo, con evidencia | No resuelvas problemas que aún no has tenido |
| ¿Podemos resolverlo con un enfoque más simple primero? | Usa el enfoque más simple | La solución compleja puede ser necesaria |
| ¿10x más de tráfico rompería la arquitectura actual? | Planifica (pero no construyas todavía) | La arquitectura actual es suficiente |
| ¿El equipo es lo bastante grande para mantener esto? | Procede si al menos 2 personas pueden encargarse | No añadas cosas que nadie pueda mantener |
| ¿Podemos revertir si no funciona? | Riesgo aceptable | Demasiado arriesgado |
La respuesta por defecto a "¿deberíamos añadir esto?" es no. Añade complejidad cuando tengas evidencia de que es necesaria, no cuando imagines que podría serlo.
Monolito + Cola: la arquitectura más infravalorada
Una aplicación monolítica con una cola de jobs en segundo plano maneja el 90 % de las cargas SaaS. Un solo codebase, un solo despliegue, una sola base de datos y una cola para trabajo en segundo plano.
┌─────────────────────────────────────────┐
│ Monolito │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ API │ │ Admin │ │
│ │ Routes │ │ UI │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ┌────▼──────────────▼─────┐ │
│ │ Services │ │
│ │ (lógica de negocio) │ │
│ └────┬────────────────────┘ │
│ │ │
│ ┌────▼──────────────────────┐ │
│ │ Base de datos (PostgreSQL) │ │
│ └───────────────────────────┘ │
│ │
│ ┌───────────────────────────┐ │
│ │ Job Queue (BullMQ/Redis) │ │
│ │ - Envío de emails │ │
│ │ - Indexación de búsqueda │ │
│ │ - Generación de informes │ │
│ │ - Importación de datos │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────────┘
Por qué funciona
- Un solo codebase: el refactoring es seguro. El buscar-y-reemplazar funciona. Los tipos fluyen por toda la aplicación.
- Un solo despliegue: despliega una vez. Sin coordinación de servicios. Sin rollbacks distribuidos.
- Base de datos compartida: los joins funcionan. Las transacciones son ACID. Sin dolores de cabeza de eventual consistency.
- Job queue: los trabajos largos (emails, importaciones, indexación) no bloquean el procesamiento de peticiones.
- Depuración simple: las stack traces son completas. Los logs están en un solo sitio. Sin necesidad de tracing distribuido.
Cuándo Monolito + Cola deja de funcionar
| Síntoma | Causa raíz | Solución |
|---|---|---|
| Tiempo de respuesta API > 2s | Consultas de base de datos demasiado lentas | Añadir índices, optimizar consultas, read replicas |
| CPU base de datos > 80 % sostenido | Volumen o complejidad de consultas demasiado alto | Read replicas, capa de caché, optimización de consultas |
| Los despliegues dan miedo | Codebase demasiado grande, tests demasiado lentos | Mejores tests, feature flags, despliegues canary (NO microservicios) |
| Una funcionalidad rompe todo | Sin fronteras entre módulos | Mejor organización del código (NO microservicios) |
| El equipo no puede trabajar en paralelo | Conflictos de código, infierno de merges | Propiedad de módulos, trunk-based development |
Fíjate: ninguno de estos síntomas se resuelve añadiendo microservicios. Se resuelven con mejor ingeniería dentro del monolito. Los microservicios resuelven un problema diferente.
Cuándo separar: los disparadores reales
Separa en servicios distintos solo cuando:
1. La escala del equipo lo exige. Cuando tienes 20+ ingenieros y las fronteras de equipo no se alinean con el codebase. El equipo A es dueño de la funcionalidad X y el equipo B de la funcionalidad Y, y necesitan desplegar independientemente. Esta es la razón principal de los microservicios.
2. Requisitos de scaling diferentes. El servicio de búsqueda necesita 10x el compute del servicio de catálogo de productos. Ejecutarlos en el mismo proceso desperdicia recursos. Servicios separados pueden escalar independientemente.
3. Requisitos tecnológicos diferentes. El servicio de inferencia ML necesita Python y GPUs. El servicio API necesita Node.js. Ejecutar ambos en un solo proceso no es práctico.
4. Aislamiento de fallos. Un crash del servicio de pagos no debería tirar el servicio de catálogo de productos. Cuando el aislamiento de fallos es crítico para la continuidad del negocio, procesos separados ayudan.
5. Fronteras de cumplimiento normativo. El procesamiento de pagos compatible con PCI necesita estar separado del resto de la aplicación por razones de auditoría. Un servicio separado con su propia frontera de seguridad simplifica el cumplimiento.
Lo que NO es una razón para separar
- "Los microservicios son buena práctica" (son un compromiso, no una buena práctica)
- "Podríamos necesitar escalar" (escala cuando lo necesites, no antes)
- "Arquitectura limpia" (las fronteras de módulos dentro de un monolito son más limpias que las fronteras de servicios)
- "Desarrollo motivado por el CV" (Kubernetes en tu CV no ayuda a tus usuarios)
La base de datos es generalmente el cuello de botella
En el 80 % de los problemas de rendimiento, la base de datos es el cuello de botella. No el código de la aplicación. No la red. La base de datos.
Diagnóstico
-- Encontrar consultas lentas (PostgreSQL)
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
-- Encontrar índices faltantes
SELECT schemaname, tablename, seq_scan, idx_scan,
CASE WHEN seq_scan > 0 THEN round(100.0 * idx_scan / (seq_scan + idx_scan), 1) ELSE 100 END AS idx_pct
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_scan DESC
LIMIT 20;
-- Encontrar bloat de tablas
SELECT tablename, pg_size_pretty(pg_total_relation_size(tablename::regclass)) AS total_size,
pg_size_pretty(pg_relation_size(tablename::regclass)) AS data_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::regclass) DESC
LIMIT 20;
Orden de corrección
- Añadir índices faltantes (minutos para implementar, impacto masivo)
- Optimizar consultas lentas (reescribir consultas N+1, añadir cláusulas WHERE, usar EXPLAIN ANALYZE)
- Añadir caché (Redis para datos leídos frecuentemente, modificados raramente)
- Read replicas (dirigir consultas de lectura a réplicas, escrituras al primario)
- Connection pooling (PgBouncer entre la aplicación y la base de datos)
- Scaling vertical (instancia más grande, más RAM, almacenamiento más rápido)
- Scaling horizontal (sharding, solo cuando todo lo demás se ha agotado)
La mayoría de equipos saltan al paso 7 sin probar los pasos 1-3. Los pasos 1-3 son gratuitos o baratos y suelen resolver el problema por completo.
Estrategia de caché
Qué cachear
| Datos | ¿Caché? | TTL | Invalidación |
|---|---|---|---|
| Sesiones de usuario | Sí | Duración de la sesión | Al cerrar sesión |
| Catálogo de productos | Sí | 5-15 minutos | Al evento de actualización de producto |
| Resultados de búsqueda | Tal vez | 1-5 minutos | Al actualizar el índice |
| Datos específicos del usuario | Con cuidado | Corto (1-5 min) | Al acción del usuario |
| Config / ajustes | Sí | Largo (1 hora) | Al cambio del admin |
| Agregados calculados | Sí | 15-60 minutos | Al cambiar los datos subyacentes |
| Datos en tiempo real (stock) | No | N/A | Siempre consultar en directo |
Qué NO cachear
- Datos de usuario con implicaciones de privacidad: un contenido de carrito cacheado servido al usuario equivocado es una fuga de datos.
- Datos que cambian rápidamente: niveles de stock, precios en directo, pujas de subasta. El TTL del caché no puede seguir el ritmo.
- Operaciones de escritura intensiva: si los datos cambian más a menudo de lo que se leen, el caché añade complejidad sin beneficio.
Patrón de invalidación de caché
// Invalidación dirigida por eventos (recomendado)
eventBus.on('product.updated', async (product) => {
await cache.delete(`product:${product.id}`);
await cache.delete(`product-list:${product.categoryId}`);
// No intentar actualizar el caché aquí. Dejar que la próxima lectura lo rellene.
});
// Read-through con stale-while-revalidate
async function getProduct(id: string): Promise<Product> {
const cached = await cache.get(`product:${id}`);
if (cached && !isExpired(cached)) {
return cached.data;
}
// Obsoleto pero lo servimos mientras se refresca
if (cached && isExpired(cached)) {
refreshInBackground(id); // async, sin await
return cached.data; // servir obsoleto
}
// Cache miss
const product = await db.products.findById(id);
await cache.set(`product:${id}`, product, { ttl: 300 });
return product;
}
La conversación de scaling con las partes interesadas
Los ingenieros quieren escalar por razones técnicas. Las partes interesadas quieren escalar por razones de negocio. La conversación necesita un lenguaje compartido.
| La parte interesada dice | Lo que quiere decir | Respuesta de ingeniería |
|---|---|---|
| "Necesitamos manejar 10x el tráfico" | Campaña de marketing próxima, o wishful thinking | "¿Cuál es el plazo previsto? Hagamos un test de carga de la capacidad actual primero." |
| "El sitio va lento" | Una página va lenta, no todo el sitio | "¿Qué página? Déjame perfilarla." |
| "Necesitamos microservicios" | Leyeron un artículo o lo oyeron en una conferencia | "¿Qué problema estamos resolviendo? Déjame mostrarte la arquitectura actual." |
| "¿Podemos aguantar el Black Friday?" | Preocupación genuina sobre picos de tráfico | "Hagamos un test de carga a 3x el pico actual y veamos dónde rompe." |
Empieza siempre con medición. "El sitio va lento" no es un requisito de scaling. "La página de listado de productos tarda 4 segundos con 500 usuarios concurrentes, el objetivo es 500 ms" es un requisito de scaling con un objetivo medible.
Errores comunes
-
Microservicios con 100 usuarios. Si tu monolito aguanta la carga y tu equipo tiene menos de 10 personas, los microservicios añaden complejidad operacional sin beneficio.
-
Caché sin estrategia de invalidación. Añadir un caché es fácil. Mantenerlo consistente con la base de datos es difícil. Planifica la invalidación antes de añadir el caché.
-
Escalar la aplicación cuando la base de datos es el cuello de botella. Añadir 5 servidores de aplicación más no ayuda si todos están esperando a la misma consulta lenta de base de datos.
-
Sin tests de carga. "Creemos que necesitamos escalar" no es evidencia. Haz un test de carga a 3-5x el pico de tráfico actual. Mira dónde rompe realmente.
-
El tabú del scaling vertical. Una instancia de base de datos más grande cuesta 200 $/mes más y tarda 10 minutos en provisionar. Eso es casi siempre más barato que una semana de trabajo arquitectural.
-
Sharding antes de intentar todo lo demás. El sharding de base de datos es la solución de scaling más compleja. Prueba índices, optimización de consultas, caché, read replicas y scaling vertical primero.
-
Construir para una escala que nunca alcanzarás. Si tu SaaS tiene 500 usuarios y crece un 10 % al mes, no necesitas manejar 1 millón de usuarios. Necesitas manejar 5.000 usuarios el año que viene.
Puntos clave
-
Monolito + cola maneja el 90 % de las cargas SaaS. Un solo codebase, una sola base de datos, un solo despliegue, y una cola de jobs para trabajo en segundo plano. No separes hasta tener evidencia de que lo necesitas.
-
La base de datos es generalmente el cuello de botella. Añade índices, optimiza consultas, añade caché, luego considera read replicas. La mayoría de problemas de rendimiento se resuelven en los pasos 1-3.
-
Separa servicios por escala de equipo, no por escala técnica. Los microservicios resuelven el problema de "20 ingenieros no pueden trabajar en un solo codebase", no el de "necesitamos más rendimiento".
-
Mide antes de escalar. Haz test de carga a 3-5x el pico actual. Perfila las páginas lentas. Encuentra el cuello de botella real. Luego corrige ese punto concreto.
-
Cachea lecturas, no escrituras. Cachea datos que se leen frecuentemente y cambian raramente. Invalida con eventos de cambio. Sirve datos obsoletos mientras se refrescan en segundo plano.
-
Escala de la forma más barata primero. Una instancia de base de datos más grande (minutos, 200 $/mes) es mejor que un mes de trabajo arquitectural. El scaling vertical no es un fracaso.
Ayudamos a equipos a escalar sus sistemas de manera apropiada como parte de nuestra práctica de software a medida y consultoría. Si necesitas ayuda con decisiones de rendimiento o scaling, habla con nuestro equipo o solicita un presupuesto.
Temas cubiertos
Guías relacionadas
Guía Empresarial de Sistemas de IA Agéntica
Guia tecnica de sistemas de IA agentica en entornos empresariales. Descubre la arquitectura, capacidades y aplicaciones de agentes IA autonomos.
Leer guíaComercio Agéntico: Cómo Dejar que los Agentes IA Compren de Forma Segura
Cómo diseñar comercio iniciado por agentes IA con gobernanza. Motores de políticas, puertas de aprobación HITL, recibos HMAC, idempotencia, aislamiento de tenants y el Agentic Checkout Protocol completo.
Leer guíaLos 9 Puntos Donde Tu Sistema de IA Filtra Datos (y Cómo Sellar Cada Uno)
Un mapa sistemático de cada lugar donde se filtran datos en sistemas de IA. Prompts, embeddings, logs, llamadas a herramientas, memoria de agentes, mensajes de error, caché, datos de fine-tuning y handoffs entre agentes.
Leer guía¿Listo para construir sistemas de IA listos para producción?
Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.
Iniciar una conversación