Arquitectura

El patrón Strangler Fig en la práctica: migración gradual de un monolito PHP a microservicios mediante REST API

Ruslan Ismailov Publicado 14 min de lectura
E

Introducción: qué es Strangler Fig y por qué es el mejor enfoque para proyectos PHP

El patrón Strangler Fig fue descrito por Martin Fowler en 2004 y recibe su nombre de una planta tropical que va envolviendo gradualmente al árbol huésped hasta reemplazarlo por completo. Aplicado a la arquitectura de software, la idea es sencilla: en lugar de reescribir el sistema desde cero, se van extrayendo partes de la funcionalidad hacia nuevos servicios, manteniendo el monolito en funcionamiento durante todo el proceso.

Para proyectos PHP, especialmente los construidos con Laravel o Symfony, este enfoque resulta especialmente relevante. La mayoría de las grandes aplicaciones PHP han acumulado funcionalidad durante años y su base de código se ha vuelto difícil de mantener. La reescritura completa es la clásica trampa del «segundo sistema»: enormes riesgos, congelación de nuevas funcionalidades y plazos impredecibles. Strangler Fig permite avanzar de forma iterativa, preservando el valor de negocio en cada paso.

La ventaja clave del patrón es la posibilidad de ejecutar el monolito y los nuevos microservicios en paralelo a través de un único punto de entrada, redirigiendo el tráfico de forma gradual. El REST API actúa aquí como contrato natural de comunicación, y el API Gateway como elemento central de orquestación.

Análisis del monolito: cómo definir los límites de los futuros servicios

Antes de extraer el primer servicio, es necesario realizar un domain mapping: analizar el dominio de negocio e identificar los límites naturales entre módulos. En el contexto de un monolito Laravel, esto implica estudiar la estructura de modelos, controladores y sus dependencias.

Herramientas de análisis

  • Análisis de relaciones en la base de datos: las tablas con el menor número de claves foráneas que apuntan a otros dominios son las primeras candidatas a ser extraídas.
  • Mapa de calor de cambios: los módulos que cambian de forma independiente entre sí son más fáciles de aislar.
  • Métrica coupling/cohesion: una alta cohesión interna y un bajo acoplamiento externo indican un buen límite para el futuro servicio.
  • Event Storming: una sesión colaborativa con el equipo para identificar eventos de dominio y agregados.

Indicadores prácticos de un buen límite de servicio

  • El módulo tiene su propio ciclo de vida de datos y no comparte transacciones con otros módulos.
  • Los equipos que trabajan con el módulo pueden desplegarlo de forma independiente.
  • El REST API del módulo puede describirse sin exponer la estructura interna de otros dominios.
  • El módulo tiene un «propietario» claramente definido dentro del equipo.

Los primeros candidatos típicos en proyectos PHP son: autenticación y gestión de sesiones, notificaciones (email, push, SMS), almacenamiento de archivos, facturación y búsqueda. Estos módulos suelen tener una alta frecuencia de despliegue y dependencias mínimas del núcleo del negocio.

El rol del API Gateway: enrutamiento entre el monolito y los nuevos servicios

El API Gateway es el corazón del patrón Strangler Fig. Es quien recibe todas las peticiones entrantes y decide si dirigirlas al antiguo monolito PHP o al nuevo microservicio. Esto permite que los clientes (frontend, aplicaciones móviles) trabajen con un único endpoint sin tener que conocer los cambios internos.

Opciones de implementación del API Gateway

  • Nginx con configuración dinámica: opción minimalista para empezar, adecuada para reglas sencillas de enrutamiento por prefijo de ruta.
  • Kong Gateway: solución potente con soporte de plugins, rate limiting, validación JWT y logging detallado.
  • AWS API Gateway / GCP Cloud Endpoints: soluciones en la nube con escalabilidad integrada.
  • Traefik: reverse proxy ligero con integración nativa en Docker y Kubernetes, muy cómodo para la contenedorización de servicios.

El principio básico de configuración es el siguiente: para cada servicio que se extrae, se añade una regla de enrutamiento que intercepta las peticiones a una ruta determinada y las redirige al nuevo servicio. El resto de peticiones van al monolito por defecto.

# Ejemplo de configuración Nginx para el patrón Strangler Fig
upstream monolith {
    server php-monolith:80;
}

upstream auth_service {
    server auth-go-service:8080;
}

upstream notification_service {
    server notification-service:8081;
}

server {
    listen 80;

    # El nuevo servicio Auth intercepta las peticiones
    location /api/v1/auth/ {
        proxy_pass http://auth_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Servicio de notificaciones
    location /api/v1/notifications/ {
        proxy_pass http://notification_service;
        proxy_set_header Host $host;
    }

    # Todo lo demás va al monolito PHP
    location / {
        proxy_pass http://monolith;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Es fundamental implementar un circuit breaker a nivel del Gateway: si el nuevo servicio no está disponible, las peticiones deben hacer fallback automáticamente al monolito. Esto garantiza una degradación cero del servicio ante cualquier problema durante la migración.

Implementación paso a paso: extracción del primer servicio

Una descomposición exitosa del monolito requiere seguir una secuencia de pasos estricta. La improvisación aquí es peligrosa: cada paso debe ser reversible.

Paso 1: Descripción del API público del futuro servicio

Antes de escribir el código del nuevo servicio, es necesario describir su REST API en formato OpenAPI 3.0. Esto crea un contrato que tanto el nuevo servicio como los consumidores del API deben cumplir. El contrato fija los endpoints, los esquemas de solicitud y respuesta, y los códigos de error.

Paso 2: Creación del nuevo servicio

El nuevo servicio implementa el API descrito. En esta etapa puede acceder internamente a la base de datos del monolito (si la separación de BD aún no ha ocurrido) a través de una réplica de solo lectura dedicada o un usuario separado con permisos limitados.

Paso 3: Funcionamiento en paralelo (Shadow Mode)

Antes de cambiar el tráfico, inicia el nuevo servicio en modo «sombra»: el Gateway duplica las peticiones simultáneamente al monolito y al nuevo servicio, pero devuelve al cliente únicamente la respuesta del monolito. Las respuestas del nuevo servicio se registran y se comparan. Esto permite detectar discrepancias sin riesgo para los usuarios.

Paso 4: Cambio gradual del tráfico (Canary Deployment)

Comienza con un 5–10% del tráfico dirigido al nuevo servicio, monitorizando el error rate, la latencia y las métricas de negocio. Ve aumentando la proporción gradualmente hasta el 100%. Una vez estabilizado, elimina la lógica correspondiente del monolito.

Paso 5: Eliminación del código muerto del monolito

Este paso se omite con frecuencia, pero es crítico. El código que permanece en el monolito tras la migración crea una falsa sensación de fiabilidad y acumula deuda técnica. Tras redirigir el 100% del tráfico al nuevo servicio, el código correspondiente en el monolito debe ser eliminado.

Gestión de datos: estrategias para separar la base de datos

El trabajo con los datos es la parte más compleja de la descomposición del monolito. En la mayoría de las aplicaciones PHP, todos los datos se almacenan en una única base de datos y muchas tablas son utilizadas simultáneamente por varios dominios.

Estrategia «Database per Service»

Cada microservicio debe tener su propia base de datos o esquema. Esto garantiza la independencia en el despliegue y el escalado. Sin embargo, la transición a este modelo requiere tiempo y pasos intermedios.

Tácticas intermedias

  • Shared Database, Separate Schema: en la etapa inicial, los servicios comparten el mismo servidor PostgreSQL pero usan esquemas distintos. Esto reduce la complejidad operacional mientras se mantiene el aislamiento lógico.
  • Database Views: para los servicios que necesitan datos de tablas «ajenas», se crean vistas de solo lectura. Esto fija el contrato público de los datos.
  • Change Data Capture (CDC): herramientas como Debezium rastrean los cambios en las tablas del monolito y los replican a la nueva base de datos a través de una cola de mensajes (Kafka, RabbitMQ). Proporciona consistencia eventual sin acoplamiento directo entre servicios.
  • Dual Write: durante el período de transición, la aplicación escribe los datos simultáneamente en la base antigua y en la nueva. Requiere un manejo cuidadoso de los fallos parciales.

Trabajo con tablas compartidas

La tabla users es el ejemplo clásico de tabla compartida en los monolitos PHP. Prácticamente todos los módulos acceden a ella. La estrategia de separación consiste en extraer únicamente los campos que pertenecen al dominio que se está extrayendo. Por ejemplo, los campos de autenticación (password_hash, remember_token, last_login_at) pasan al auth-service, mientras que los campos de perfil (name, avatar, bio) permanecen en el user-profile-service o en el monolito.

Ejemplo práctico: extracción de la autenticación de Laravel a Go

Analicemos un caso concreto: un monolito Laravel con la tabla users y la autenticación estándar de Laravel. El objetivo es extraer la autenticación a un servicio independiente escrito en Go.

¿Por qué Go para el auth-service?

Go proporciona baja latencia, un consumo mínimo de memoria y una gestión sencilla de la concurrencia, características críticas para un servicio de autenticación de alto rendimiento. Además, esto demuestra que el patrón Strangler Fig no te ata a un único lenguaje de programación.

Estructura del nuevo auth-service en Go

// auth-service/main.go
package main

import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "time"

    "github.com/golang-jwt/jwt/v5"
    _ "github.com/lib/pq"
    "golang.org/x/crypto/bcrypt"
)

type LoginRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

type LoginResponse struct {
    Token     string    `json:"token"`
    ExpiresAt time.Time `json:"expires_at"`
}

type AuthHandler struct {
    db        *sql.DB
    jwtSecret []byte
}

// POST /api/v1/auth/login
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
    var req LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
        return
    }

    var userID int64
    var passwordHash string

    // Leemos del esquema auth replicado desde el monolito
    err := h.db.QueryRow(
        `SELECT id, password FROM auth.users WHERE email = $1 AND deleted_at IS NULL`,
        req.Email,
    ).Scan(&userID, &passwordHash)

    if err == sql.ErrNoRows {
        http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
        return
    }
    if err != nil {
        http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
        return
    }

    if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
        http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
        return
    }

    expiresAt := time.Now().Add(24 * time.Hour)
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub": userID,
        "exp": expiresAt.Unix(),
        "iss": "auth-service",
    })

    tokenString, err := token.SignedString(h.jwtSecret)
    if err != nil {
        http.Error(w, `{"error":"token generation failed"}`, http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(LoginResponse{
        Token:     tokenString,
        ExpiresAt: expiresAt,
    })
}

func main() {
    db, err := sql.Open("postgres", "postgres://auth_user:secret@postgres:5432/app_db?sslmode=require")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    handler := &AuthHandler{
        db:        db,
        jwtSecret: []byte("your-secret-key"),
    }

    mux := http.NewServeMux()
    mux.HandleFunc("POST /api/v1/auth/login", handler.Login)

    log.Println("Auth service listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Período de transición: Dual Write en Laravel

Durante la migración, el monolito Laravel sigue trabajando con la tabla users, pero todas las operaciones con contraseñas se duplican en el esquema auth. En el código Laravel esto se implementa mediante un Observer o un decorador sobre la fachada Auth. Una vez confirmada la estabilidad del nuevo servicio, el Dual Write se desactiva y Laravel delega la autenticación al servicio Go mediante una llamada interna al REST API.

Pruebas durante la migración

Migrar sin pruebas fiables es como caminar por la cuerda floja sin red de seguridad. Estrategias clave:

Pruebas de contrato del REST API

Utiliza Pact o Dredd para las pruebas de contrato: el consumidor (por ejemplo, el frontend o el monolito) describe el contrato esperado y el proveedor (el nuevo servicio) verifica su cumplimiento. Esto protege frente a breaking changes durante la evolución del API.

Comparación en Shadow Mode

Durante el período de funcionamiento paralelo, compara automáticamente las respuestas del monolito y del nuevo servicio. Las discrepancias se registran y generan alertas. Para un monolito PHP en Laravel, es cómodo usar un middleware que duplique las peticiones y compare las respuestas JSON ignorando los campos con timestamp.

Pruebas de regresión

Las pruebas E2E deben cubrir los flujos críticos que pasan por el API Gateway, independientemente de si son procesados por el monolito o por el nuevo servicio. Herramientas: Postman/Newman para pruebas de API, Playwright para e2e.

CI/CD para un entorno híbrido

El funcionamiento en paralelo del monolito y los microservicios añade complejidad adicional a los procesos de CI/CD. Algunos principios clave:

Pipelines independientes

Cada microservicio debe tener su propio pipeline de construcción, pruebas y despliegue. El monolito no debe ser una dependencia para el despliegue de un nuevo servicio. En GitLab CI o GitHub Actions esto se implementa mediante archivos de workflow separados con condiciones de activación basadas en cambios en los directorios correspondientes.

Versionado del API y configuración del Gateway

Los cambios en la configuración del API Gateway deben formar parte del Infrastructure as Code (Terraform, Pulumi) y pasar por el mismo proceso de revisión que el código. Los nuevos enrutamientos se activan mediante feature flags, lo que permite revertir cambios sin necesidad de un nuevo despliegue.

Ejemplo de estructura del repositorio

monorepo/
├── monolith/                  # Monolito Laravel PHP
│   ├── app/
│   ├── .github/workflows/monolith-ci.yml
│   └── Dockerfile
├── services/
│   ├── auth-service/          # Servicio auth en Go
│   │   ├── main.go
│   │   ├── .github/workflows/auth-service-ci.yml
│   │   └── Dockerfile
│   └── notification-service/  # Siguiente servicio
│       └── ...
├── infrastructure/
│   ├── nginx/                 # Configuración del API Gateway
│   │   └── strangler.conf
│   ├── terraform/             # IaC
│   └── docker-compose.yml     # Desarrollo local
└── contracts/                 # Contratos OpenAPI
    ├── auth-api.yaml
    └── notification-api.yaml

Health Checks y Circuit Breaker en el pipeline

Tras el despliegue del nuevo servicio, el pipeline de CI/CD debe verificar su endpoint de health antes de cambiar el tráfico. Si el health check falla, el Gateway continúa dirigiendo las peticiones al monolito. Esto garantiza zero-downtime ante cualquier problema.

Métricas de éxito de la migración

Realiza un seguimiento de los siguientes indicadores a lo largo de toda la migración:

  • Porcentaje de tráfico en los nuevos servicios: el indicador objetivo crece del 0% al 100% para cada dominio extraído.
  • Latencia P95/P99: el nuevo servicio no debe degradarse en tiempo de respuesta respecto al monolito.
  • Error Rate: se monitoriza de forma independiente para el Gateway, el monolito y cada microservicio.
  • Deployment Frequency: debe aumentar a medida que se extraen servicios; cada servicio se despliega de forma independiente.
  • Mean Time to Recovery (MTTR): los incidentes en un servicio aislado no deben afectar al resto del sistema.
  • Tamaño y complejidad del monolito: el número de líneas de código, clases y rutas en el monolito debe ir disminuyendo.
  • Cobertura con pruebas de contrato: todos los API públicos de los nuevos servicios deben estar cubiertos con pruebas Pact.

El patrón Strangler Fig es exitoso no cuando se extrae el último servicio, sino cuando cada paso intermedio ha aportado un valor medible: ha reducido riesgos, acelerado el despliegue o simplificado el escalado de un dominio concreto.

Conclusión

El patrón Strangler Fig es el camino más seguro y pragmático para descomponer un monolito PHP. Permite a los equipos avanzar de forma iterativa, validando cada decisión en el entorno de producción sin poner en riesgo el funcionamiento del sistema en su conjunto.

Los principios clave que hemos analizado: comienza con un exhaustivo domain mapping, usa el API Gateway como elemento central de orquestación, implementa siempre el Shadow Mode antes de cambiar el tráfico, y aborda la separación de la base de datos de forma anticipada, no reactiva. Para PHP y Laravel, este camino resulta especialmente natural: el REST API es el lenguaje nativo de comunicación, y el ecosistema de herramientas (Docker, Kubernetes, Traefik, Pact) cubre completamente las necesidades operacionales.

La extracción del auth-service en Go es solo el primer paso. Le seguirán el notification-service, billing, search. Cada servicio extraído hace el monolito un poco más pequeño y al equipo un poco más libre. En eso reside la fuerza del patrón Strangler Fig: no una revolución, sino una evolución con resultados medibles en cada etapa.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →