De Monolito a Microservicios — Estrategias de Migración
Guía práctica para descomponer monolitos: strangler fig, branch by abstraction y extracción incremental que reducen riesgo y preservan la continuidad del negocio.
Introducción
Migrar un monolito a microservicios es una de las refactorizaciones más arriesgadas que puedes emprender. Hecha mal, te deja un monolito distribuido: más lento, más complejo y más difícil de operar. Hecha bien, te da autonomía de equipo, escalado independiente y entregas más rápidas. Esta guía cubre las estrategias de extracción que permiten descomponer con seguridad.
Antes de Empezar
Valida la decisión
| Pregunta | Si la respuesta es “no”, reconsidera |
|---|---|
| ¿Tienes más de 50 ingenieros? | Un monolito con límites modulares puede bastar |
| ¿Los deploys duelen (más de uno por semana)? | Arregla el CI/CD primero |
| ¿Puedes medir el costo del monolito? | Cuantifica el dolor antes de justificar la migración |
| ¿Tienes expertise operativo? | Los microservicios exigen madurez SRE/DevOps |
| ¿El negocio aguanta un proyecto largo? | La migración tarda 1-3 años en sistemas grandes |
Identifica el primer servicio a extraer
Elige un servicio que sea:
- De bajo riesgo: ni pagos ni autenticación.
- De alto dolor: lo que cambia a menudo, despliega lento o ahoga al resto.
- De límite claro: poco estado compartido con el monolito.
- De valor independiente: útil incluso funcionando solo.
Buenos primeros candidatos: notificaciones, reporting o un servicio de feature flags.
Patrones de Migración
1. Strangler Fig (higuera estranguladora)
Sustituye funcionalidad del monolito gradualmente, redirigiendo tráfico a los nuevos servicios mientras el monolito sigue corriendo.
Phase 1:
[Users] → [Monolith] → [Database]
Phase 2:
[Users] → [API Gateway] → [Monolith] → [Database]
└──────> [New Service] → [New DB]
Phase 3:
[Users] → [API Gateway] → [Service A] → [DB A]
└──────> [Service B] → [DB B]
└──────> [Monolith] → [DB Legacy]
Phase 4:
[Users] → [API Gateway] → [All Services]
Implementación:
# Lógica de routing del API Gateway
class Router:
def route(self, request):
if self.feature_flags.is_enabled("use-new-catalog", request.user_id):
return self.catalog_service.handle(request)
return self.monolith.handle(request)
Por qué funciona: puedes hacer rollback al instante con un feature flag. El monolito es tu red de seguridad hasta que el servicio nuevo queda probado.
2. Branch by Abstraction
Crea una capa de abstracción dentro del monolito y luego intercambia la implementación.
# Paso 1: crear la abstracción en el monolito
class NotificationSender(ABC):
@abstractmethod
def send(self, user, message):
pass
# Paso 2: el monolito sigue usando la implementación vieja
class MonolithNotificationSender(NotificationSender):
def send(self, user, message):
... # lógica vieja del monolito
# Paso 3: construir el nuevo servicio con la misma interfaz
class ServiceNotificationSender(NotificationSender):
def __init__(self, client):
self.client = client # cliente gRPC/HTTP al servicio nuevo
def send(self, user, message):
self.client.send_notification(user.id, message)
# Paso 4: intercambiar implementaciones con feature flag
sender = ServiceNotificationSender() if flags.enabled("new-notifications") else MonolithNotificationSender()
Por qué funciona: el monolito nunca se entera de que habla con un servicio. El rollback se resuelve en una línea.
3. Parallel Run
Ejecuta las dos implementaciones a la vez, compara sus salidas, pero sirve a los usuarios solo la vieja.
# Tráfico en sombra al servicio nuevo
old_result = monolith_recommendations.get(user_id)
new_result = recommendations_service.get(user_id)
# Loguea diferencias; aún no sirvas new_result
if old_result != new_result:
metrics.increment("recommendation.divergence")
logger.warning("Divergence detected", old=old_result, new=new_result)
return old_result
Cuándo usarlo: cuando la corrección es crítica (pagos, recomendaciones, precios) y necesitas confianza antes del cambio.
4. Patrones de migración de datos
Base de datos compartida (temporal)
[Monolith] ──> [Shared DB]
[New Service] ─┘
Úsala: durante semanas, no meses. El servicio nuevo lee de la BD compartida mientras planificas la migración de la propiedad del dato.
Change Data Capture (CDC)
[Monolith DB] ──> [Debezium] ──> [Kafka] ──> [New Service DB]
Úsala: para mantener los datos del servicio nuevo sincronizados sin tocar el monolito. Debezium lee el binlog y publica los cambios.
Sincronizar y cambiar
- Dual-write del monolito a la base vieja y la nueva.
- Backfill de los datos históricos a la base nueva.
- Cambia las lecturas a la base nueva.
- Deja de escribir en la vieja.
# Dual-write en el monolito
def create_order(order):
db.execute("INSERT INTO orders ...") # viejo
new_service_client.create_order(order) # nuevo
Hoja de Ruta
| Trimestre | Objetivo |
|---|---|
| Q1 | Extraer un servicio no crítico (notificaciones, reports) con Strangler Fig |
| Q2 | Implementar API Gateway y service discovery |
| Q3 | Extraer un servicio medio-crítico con Branch by Abstraction |
| Q4 | Parallel run para un servicio de criticidad alta (recomendaciones, búsqueda) |
| Año 2 | Extraer los servicios core; el monolito queda como capa fina de orquestación |
| Año 3 | Retirar el monolito; toda la funcionalidad vive en servicios |
Lo Que Funciona
- Nunca hagas un rewrite big-bang. La extracción incremental mantiene tus opciones abiertas.
- Mide antes y después: frecuencia de deploy, lead time, tasa de fallos.
- Mantén el monolito deployable: una extracción que rompe el CI/CD lo frena todo.
- Invierte en tests pronto: los contract tests entre monolito y servicios detectan breaking changes antes de que salgan.
- Muestra progreso a los stakeholders: necesitan valor visible, no solo actividad de ingeniería.
- Acepta que parte del código nunca se mueve. Un módulo que cambia una vez al año quizá no merece extracción.
Errores Comunes
- Extraer por capas técnicas (UI, lógica de negocio, datos) en vez de por capacidades de negocio.
- No tomarte en serio la consistencia de datos. Las transacciones distribuidas necesitan sagas, no esperanza.
- Subestimar el último tramo: esos servicios finales suelen ser los más duros y acoplados.
- Retirar el monolito demasiado pronto, cuando los servicios aún son inestables.
- Invertir poco en developer experience: el desarrollo local y el testing se complican bastante con microservicios.
Ver También
- Martin Fowler — Strangler Fig Application: el ensayo original que bautizó el patrón.
- AWS Prescriptive Guidance — Strangler Fig Pattern: guía aplicada de reescritura incremental.
- microservices.io — Refactoring a monolith: el catálogo de patrones detrás de la mayoría de los consejos de migración.
Temas Avanzados
Escenario detallado: extracción de notificaciones de un monolito Rails
Sistema: monolito Rails (450k líneas, 35 devs, PostgreSQL)
Objetivo: extraer notification-service como primer microservicio
Plazo: 8 semanas
Semana 1-2: Analizar el código actual de notificaciones
- 12 tipos de notificación identificados (confirmación de pedido, envío, reset de password, etc.)
- 3 canales de entrega: email (SendGrid), SMS (Twilio), push (Firebase)
- Lógica de notificaciones repartida entre 18 modelos de Rails
- Tabla compartida: notifications (escrita por 8 controladores distintos)
Semana 3: Crear la capa de abstracción (Branch by Abstraction)
# app/services/notification_sender.rb
class NotificationSender
def self.send(user_id, type, payload)
if FeatureFlags.enabled?("use-notification-service", user_id)
NotificationServiceClient.send(user_id, type, payload)
else
MonolithNotificationDelivery.send(user_id, type, payload)
end
end
end
# Sustituir todas las llamadas directas en el monolito
# Antes: NotificationMailer.order_confirmation(user).deliver_now
# Después: NotificationSender.send(user.id, :order_confirmation, {order: order})
Semana 4-5: Construir notification-service (Go)
- Servidor gRPC escuchando en :50051
- Lee de su propia base de datos PostgreSQL
- Integra SendGrid, Twilio y Firebase vía adaptadores
- Soporta templates, reintentos y dead-letter queue
// proto/notifications.proto
service NotificationService {
rpc Send(SendNotificationRequest) returns (SendNotificationResponse);
rpc GetStatus(GetStatusRequest) returns (NotificationStatus);
}
Semana 6: Migración de datos con CDC
- Desplegar conector Debezium sobre el PostgreSQL del monolito
- Stream de cambios de la tabla notifications a un topic de Kafka
- notification-service consume de Kafka y escribe en su propia BD
- 1 semana en modo sombra (sin tráfico servido)
[Monolith DB] -> [Debezium] -> [Kafka] -> [notification-service] -> [New DB]
Semana 7: Traslado gradual del tráfico
- Día 1-2: 5% de las notificaciones vía el servicio nuevo (feature flag)
- Día 3-4: 25% del tráfico, vigilando error rate y latencia
- Día 5-6: 50% del tráfico
- Día 7: 100% del tráfico
Dashboard de monitorización:
- notification.success_rate (objetivo: > 99.5%)
- notification.latency_p95 (objetivo: < 200ms)
- notification.retry_count (objetivo: < 5% del total)
- notification.dead_letter_count (objetivo: < 0.1% del total)
Semana 8: Limpieza
- Quitar MonolithNotificationDelivery de Rails
- Borrar la tabla notifications del monolito (tras backup)
- Retirar el conector Debezium
- Documentar el servicio nuevo en la wiki de arquitectura
Resultados tras 1 mes:
| Métrica | Monolito | notification-service |
|---------|----------|----------------------|
| Frecuencia de deploy | Semanal | On-demand (3x/semana) |
| Tasa de errores | 2.1% | 0.3% |
| Latencia p95 | 450ms | 120ms |
| Cambios de template | PR + deploy | Actualización de config (sin deploy) | Preguntas frecuentes
¿Cuánto dura una migración de monolito a microservicios?
En un sistema con 100+ ingenieros, cuenta con 1-3 años. El primer servicio tarda meses; el décimo, semanas. El cuello de botella rara vez es técnico: es la alineación organizacional y la confianza en los tests.
¿Deberíamos parar el desarrollo de funcionalidades durante la migración?
No. El negocio no se detiene. Corre la migración como un track paralelo: 70% funcionalidades, 30% migración. Si la migración consume el 100% de la capacidad, estás extrayendo demasiado agresivamente.
¿Qué pasa si acabamos con un monolito distribuido?
Un monolito distribuido aparece cuando los servicios comparten base de datos o despliegan juntos. La cura es la misma que la prevención: database-per-service y pipelines de despliegue independientes. Arreglarlo duele, así que prévenlo desde el principio.
¿Cómo manejo las librerías compartidas durante la migración?
Extrae el código compartido en una librería versionada (gem, paquete npm, JAR) de la que dependan tanto el monolito como los servicios nuevos. Mantenla mínima (value objects, DTOs, utilidades) y deja la lógica de dominio dentro del servicio que la posee. Versiona semánticamente, publica en un registro privado y deja que la librería sobreviva al monolito.
¿Cómo hago tests durante la migración?
Los contract tests entre el monolito y cada servicio nuevo detectan breaking changes antes de que salgan. Añade una fase de parallel run para todo lo crítico en corrección, y mantén verde la suite del monolito todo el camino, porque sigue siendo tu línea base de regresión hasta que el servicio queda probado.
¿Cómo consigo apoyo para una migración de varios años?
Entregando valor cada trimestre en vez de prometer una gran revelación. El primer servicio extraído debería notarse en las métricas que los stakeholders ya miran: frecuencia de deploy, lead time, tasa de incidentes. Una migración sin victorias visibles en sus primeros seis meses es una migración que acaba cancelándose.
Recursos Relacionados
Arquitectura de Microservicios — Cuándo Usarla y Cuándo No
Guía práctica de microservicios: beneficios, trade-offs, patrones comunes y cuándo elegirlos sobre monolitos. Cubre estrategias de descomposición y complejidad operativa.
GuideDomain-Driven Design (DDD): Guía Práctica
Aprende los fundamentos de DDD: bounded contexts, entidades, value objects, aggregates, y cómo modelar dominios de negocio complejos en código.
GuideMonolito Modular — Una Arquitectura Pragmática
Guía práctica de Monolitos Modulares: combina la simplicidad de los monolitos con la modularidad de los microservicios mediante bounded contexts y límites estrictos entre módulos.
RecipePatrones de Comunicación entre Microservicios
Elige entre patrones de comunicación síncronos y asíncronos para arquitecturas de microservicios resilientes.
RecipeDescubrimiento de servicios
Implementa service discovery con health checks, resolución DNS-based y service registries para ambientes en vivo de microservicios.
GuideDiseño de API Gateway: Resiliencia, Enrutamiento y Seguridad
Guía práctica para diseñar API gateways: enrutamiento, autenticación, rate limiting, circuit breakers, caché y patrones de despliegue para APIs resilientes.