De monolito a microservicios: como migrar sin romper todo
Una guia practica sobre como arranco con un monolito, monitoreo trafico por modulo guardando en base de datos, y voy extrayendo microservicios con Redis cuando realmente hace falta.
Por que arrancar con un monolito esta bien
La industria tiene un amor incondicional por los microservicios desde el dia cero. Pero para la mayoria de los proyectos, arrancar con un monolito es la decision mas racional: menos infraestructura, menos complejidad operativa, iteraciones mas rapidas. No tiene sentido resolver problemas de escala que todavia no existen.
Mi approach personal: arranco monolitico, shipeo rapido, mido el trafico real, y solo pienso en dividir cuando hay evidencia concreta de un cuello de botella. La sobreingenieria prematura en arquitectura es tan danina como en codigo.
- Menos infraestructura inicial: un solo proceso, un solo deploy, una sola DB.
- Iteracion rapida: cambios en cualquier parte del sistema sin coordinacion entre servicios.
- Debugging simple: todo el stack en un mismo lugar, traces lineales.
- Costo operativo bajo: no pagan DevOps extra ni infra de comunicacion entre servicios.
Arquitectura tipica de un monolito
Renderizando grafico...
Todos los modulos conviven en un mismo proceso y comparten base de datos. Un solo deploy, una sola instancia.
Estructura tipica de la API monolitica
Aunque corra en un solo proceso, el codigo esta dividido por dominio: cada modulo tiene su propio router, controller y service. Auth maneja todo lo relacionado a autenticacion, Products tiene sus rutas, su logica de negocio y su acceso a la DB. El monolito es un proceso unico, pero la arquitectura interna es modular.
Esta separacion no es solo estetica. Es la que hace posible la extraccion futura. Cuando el modulo Orders tiene limites claros y no esta acoplado con el resto, puede moverse a su propio servicio sin reescribir desde cero.
Flujo de request dentro del monolito
Renderizando grafico...
Aunque es monolito, cada modulo tiene su propia capa de routing, logica y acceso a datos. Los limites estan claros desde el inicio.
Monitoreando trafico por modulo: el approach rustico
Antes de cualquier decision de arquitectura necesito datos. Mi approach: un middleware simple que intercepta cada request y loguea lo minimo necesario en una tabla de la misma base de datos del monolito. Nada de Datadog, nada de Prometheus. Solo SQL y lo que ya tengo.
La tabla tiene campos basicos: modulo, endpoint, metodo HTTP, tiempo de respuesta en milisegundos y status code. Con eso puedo responder las preguntas que importan: cual modulo concentra mas carga, cual tiene los tiempos mas altos, cual esta creciendo.
Es deliberadamente rustico porque en la etapa inicial no quiero dependencias externas ni infra extra. La herramienta de monitoreo mas util es la que ya tengo desplegada.
- Tabla: modulo, endpoint, metodo, tiempo_ms, status_code, created_at.
- El middleware corre antes del controller: registra siempre, incluso en errores.
- Queries de analisis simples: GROUP BY modulo, AVG(tiempo_ms), COUNT(*).
- No agrega dependencia externa: reutiliza la conexion a DB que ya existe en el monolito.
Middleware de monitoreo integrado al flujo
Renderizando grafico...
El middleware intercepta cada request, loguea en la misma DB y el sistema sigue operando normal. El analisis se hace con queries directas.
Identificando el cuello de botella
Con algunas semanas de datos, el patron aparece. Alguno de los modulos concentra el 60% de los requests, o tiene tiempos promedio tres veces mas altos que el resto. Eso es el primer candidato.
Pero la decision de extraer no es solo por volumen. Lo que importa es si la carga de ese modulo esta degradando la experiencia del resto. Si el modulo Reports es lento pero no interfiere con el checkout, quizas alcanza con optimizar internamente. Si esta bloqueando el event loop y ralentizando el login, ahi si hay un problema real.
- Buscar modulos con mayor cantidad de requests por minuto.
- Identificar endpoints con tiempos de respuesta anormales o con alta varianza.
- Evaluar si la carga de un modulo consume recursos que impactan a otros (CPU, conexiones a DB).
- No extraer solo porque tiene mucho trafico: extraer cuando afecta al resto del sistema.
Arbol de decision para identificar candidatos a extraccion
Renderizando grafico...
La decision de extraer no es automatica. La carga alta solo justifica extraccion si impacta al rendimiento del resto del sistema.
Extrayendo el primer microservicio
Una vez identificado el candidato, arranca la extraccion. Como el modulo ya tiene limites claros dentro del monolito, la mayor parte del trabajo es mover codigo, no reescribirlo. Creo un nuevo servicio con su propio repo, su propia base de datos y su propio proceso de deploy.
La estrategia de datos es lo mas delicado. El modulo extraido necesita su propia DB, pero la data historica esta en la DB del monolito. Dependiendo del caso puede ser una migracion completa, un periodo de escritura doble, o simplemente que el servicio nuevo arranca limpio y el monolito sigue siendo el source of truth para datos viejos.
Mientras el microservicio se estabiliza, el monolito puede actuar como proxy: recibe los requests del modulo extraido y los redirige al nuevo servicio. Esto permite rollback rapido si algo sale mal.
- Crear el nuevo servicio con su propia base de datos desde el inicio.
- Mover la logica existente, no reescribir desde cero si ya funciona.
- Definir la estrategia de migracion de datos antes de hacer el cutover.
- El monolito deja de manejar ese dominio directamente una vez que el servicio esta estable.
Arquitectura durante la extraccion del primer modulo
Renderizando grafico...
El modulo extraido vive en su propio proceso con su propia DB. Durante la transicion el monolito puede actuar como proxy antes del cutover definitivo.
Redis como capa de comunicacion
El microservicio extraido necesita comunicarse con el monolito. Mi eleccion por defecto es Redis, especificamente pub/sub para eventos asincronicos y BullMQ para tareas que necesitan persistencia y retry. Por que Redis? Porque ya lo uso para cache en casi todos los proyectos, es rapido, y maneja bien tanto eventos fire-and-forget como comunicacion mas estructurada.
El patron basico: cuando sucede algo relevante en el monolito (por ejemplo, se creo un pedido), publica un evento en Redis. El microservicio correspondiente esta suscripto a ese canal y lo procesa de forma independiente. Si el microservicio esta caido, los mensajes se acumulan en la queue y los procesa cuando vuelve.
HTTP directo entre servicios lo reservo para casos donde necesito una respuesta sincrona inmediata. Para todo lo demas, el bus de Redis da desacoplamiento y amortigua naturalmente los picos de carga.
Un caso concreto donde BullMQ brilla son los procesos async pesados: generacion de contenido con IA, video, imagenes, deep research. En vez de que la app principal los ejecute y bloquee sus propios hilos, los encola en Redis y workers dedicados los procesan de forma independiente.
- Redis pub/sub: eventos asincronicos donde no necesito respuesta inmediata.
- BullMQ (cola sobre Redis): tareas con retry automatico, delay y persistencia.
- HTTP entre servicios: solo cuando necesito respuesta sincrona y no puede ser async.
- Redis como bus desacopla servicios: cada uno corre a su propio ritmo.
Redis como bus de mensajes entre servicios
Renderizando grafico...
Redis maneja los eventos asincronicos. HTTP directo solo aparece cuando el resultado es necesario inmediatamente para continuar el flujo.
El patron de migracion progresiva
La extraccion no es un evento de big bang donde un dia paso de monolito a microservicios. Es un ciclo: monitoreo, detecto el modulo con mas impacto, lo extraigo, lo estabilizo, y vuelvo a monitorear. Cada vuelta del ciclo deja al monolito un poco mas liviano.
El monolito rara vez desaparece del todo, y esta bien. Los modulos que tienen poco trafico y no generan cuellos de botella no justifican la complejidad operativa de ser servicios independientes. Con el tiempo el monolito se convierte en el core que maneja los dominios menos exigentes, rodeado de microservicios especializados en lo que mas carga genera.
Ciclo de migracion progresiva
Renderizando grafico...
Cada iteracion reduce la carga del monolito y agrega un servicio independiente. No hay big bang: cada extraccion es pequena y controlada.
Arquitectura final: monolito reducido + microservicios
Despues de varios ciclos de extraccion, el resultado es un monolito mas pequeno manejando los dominios base, mas algunos microservicios especializados comunicados a traves de Redis. Cada servicio tiene su propia base de datos, su propio pipeline de deploy y puede escalar de forma independiente.
Esto no es la arquitectura de Netflix con 500 servicios. Es algo pragmatico: 3 a 5 servicios extraidos basados en evidencia real, con el resto de la logica viviendo en un monolito que ahora es mas rapido porque tiene menos responsabilidades.
Arquitectura final: monolito core + microservicios via Redis
Renderizando grafico...
Resultado final: monolito reducido como core, microservicios especializados comunicandose via Redis, cada uno con su propia base de datos.
Comparacion: monolito vs microservicios vs migracion progresiva
Ninguno de los tres enfoques es universalmente correcto. La tabla muestra los tradeoffs reales dependiendo de donde este parado el proyecto y el equipo.
| Aspecto | Monolito puro | Microservicios desde el dia 1 | Migracion progresiva |
|---|---|---|---|
| Complejidad inicial | Baja | Alta | Baja (arranca como monolito) |
| Costo de infraestructura | Bajo | Alto desde el inicio | Crece con la demanda real |
| Velocidad de iteracion | Alta al principio, baja al escalar | Baja al principio por el overhead | Alta siempre, extraccion cuando hace falta |
| Debugging | Simple (un proceso) | Complejo (sistema distribuido) | Mixto, crece gradualmente |
| Escalabilidad | Limitada (escala todo junto) | Granular desde el inicio | Granular donde hace falta |
| Riesgo de sobreingenieria | Bajo | Alto | Bajo (basado en datos reales) |
Cuando NO hacer esto
Este approach no es para todos los proyectos. Si el monolito anda bien y los tiempos de respuesta son aceptables, no toques nada. Agregar la complejidad operativa de microservicios a un sistema que no tiene problemas de escala es sobreingenieria pura.
Tampoco tiene sentido extraer sin datos. Todo el approach se basa en medir primero y decidir despues. Sin monitoreo, cualquier decision de arquitectura es una adivinanza. Y las adivinanzas en arquitectura son caras.
- Si el monolito no muestra problemas de rendimiento en produccion: no extraer.
- Si el equipo es chico: la complejidad operativa de varios servicios puede ser peor que el problema original.
- Si no tenes monitoreo implementado: primero medi, despues decide.
- Si el negocio no lo justifica: no optimices infraestructura que no esta generando un problema real.
Checklist antes de arrancar con la extraccion
Renderizando grafico...
Antes de arrancar con cualquier extraccion, chequea estos tres puntos. Si alguno falla, hay una alternativa menos costosa.
Ver también
Fuentes
- Martin Fowler: MonolithFirstEl argumento clasico de por que arrancar con monolito antes de distribuir.
- Strangler Fig Pattern (Martin Fowler)El patron de migracion gradual que inspira este enfoque de extraccion progresiva.
- Redis Pub/Sub DocumentationDocumentacion oficial de pub/sub en Redis.
- BullMQ: Queue Library para Node.jsLibreria de colas basada en Redis para Node.js, ideal para comunicacion entre servicios con retry.
- Redis StreamsAlternativa a pub/sub para mensajeria con persistencia y grupos de consumidores.
- Sam Newman: Building Microservices (2nd ed.)Referencia general sobre patrones de microservicios y estrategias de migracion.