StackPractices
advanced Por Mathias Paulenko

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

PreguntaSi 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

  1. Dual-write del monolito a la base vieja y la nueva.
  2. Backfill de los datos históricos a la base nueva.
  3. Cambia las lecturas a la base nueva.
  4. 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

TrimestreObjetivo
Q1Extraer un servicio no crítico (notificaciones, reports) con Strangler Fig
Q2Implementar API Gateway y service discovery
Q3Extraer un servicio medio-crítico con Branch by Abstraction
Q4Parallel run para un servicio de criticidad alta (recomendaciones, búsqueda)
Año 2Extraer los servicios core; el monolito queda como capa fina de orquestación
Año 3Retirar 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

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.