Módulo 3: El patrón strangler fig

Presentación del módulo: el patrón strangler fig

Por qué este módulo existe aquí

Hasta ahora esta guía te preparó el terreno. En el módulo 1 te convenciste, con números y no con dogma, de que la reescritura grande de un sistema vivo casi siempre fracasa y de que el camino que gana es el incremental. En el módulo 2 pusiste el legacy bajo una red de seguridad: escribiste characterization tests que fijan el comportamiento actual del código —incluidos sus bugs— para poder tocarlo sin volar nada por accidente. Tienes la convicción y tienes la red. Lo que te falta es la primera técnica de migración de verdad, y es la que le da nombre a toda la práctica: el patrón strangler fig.

Aquí está la idea completa, en una frase: en vez de reemplazar el sistema viejo de un golpe, pones un facade —un proxy, un enrutador— delante de él, construyes el reemplazo al lado sin tocar el legacy, y desvías el tráfico incrementalmente del viejo al nuevo, empezando por un chorrito y subiendo hasta el 100% solo cuando el nuevo demuestra que aguanta. Mientras tanto el sistema nunca se apaga, y la vieja ruta queda como red de seguridad: si el nuevo falla, la request cae al legacy y el usuario ni se entera. Cuando el 100% del tráfico pasó limpio por lo nuevo y nadie llama ya al viejo, lo retiras. Ningún apagón, ningún big-bang, cada paso reversible.

Este módulo enseña esa mecánica a fondo, aplicada al catálogo de Mercado. Recuerda el caso: Mercado es un monolito legacy con catalog, orders, payments y shipping viviendo en un solo código, sobre una sola base de datos. Vamos a modernizar el catálogo —concretamente el endpoint GET /products— sin tocar el resto del monolito: le ponemos un facade delante, construimos un servicio modern al lado, y desviamos el tráfico del legacy al modern por porcentaje, con fallback, observando las dos rutas, hasta apagar la vieja ruta.

Conexión con el módulo. Esta es la lección-mapa. No entramos todavía al detalle de cada paso; instalamos la metáfora (la higuera estranguladora, el desvío de una carretera), el ciclo completo (facade → construir al lado → desviar 0→100 → retirar) y el vocabulario (strangler_router, traffic_percent, facade, fallback, canary, feature flag, burn-down). Y corremos un ejemplo-mapa: un strangler_router mínimo que enruta el GET /products de Mercado por porcentaje, para ver el tráfico desviarse antes de entrar al detalle. La lección 2 pone el facade; la 3 construye el nuevo al lado; la 4 desvía por porcentaje con fallback; la 5 compara las tres formas de desviar; la 6 monta el fallback y la observabilidad; la 7 hace el corte final y retira el legacy; y la 8 lo integra todo en el catálogo de Mercado. Fíjate en la frontera: aquí el strangler es externo y por tráfico (un proxy decide viejo vs nuevo request por request). Cambiar la implementación por dentro del código, sin un proxy externo, es el módulo 4 (branch by abstraction); extraer el servicio a su propio proceso con un anti-corruption layer es el módulo 5; y caracterizar el legacy con tests fue el módulo 2.

Y la promesa de siempre: nada se afirma "de memoria", todo se ejecuta. Cada simulación corre con Python 3.14 y solo la biblioteca estándar, con datos fijos, así que la salida que ves en cada bloque "Qué esperar" es la salida literal de correr el código. Puedes copiarlo y reproducirlo idéntico.

Una analogía: el desvío de una carretera

Tienes una carretera vieja que ya no da abasto. Está llena de baches, se inunda cuando llueve, y cada vez pasan más autos por ella. Vas a construir una carretera nueva. Tienes dos formas de hacerlo, y son de naturalezas opuestas.

La forma big-bang. Cierras la carretera vieja de la noche a la mañana, mandas todo el tráfico por la nueva desde el primer minuto, y rezas. Si la carretera nueva tiene un puente mal calculado, un cruce mal señalizado o un tramo que se inunda, te enteras cuando el 100% de los autos ya está encima, en hora pico, sin salida. El desastre es total porque la exposición fue total. Y si algo sale muy mal, ni siquiera puedes volver atrás fácil: la carretera vieja ya la cerraste.

La forma strangler. Construyes la carretera nueva al lado de la vieja, sin cerrar la vieja. Cuando está lista, pones un desvío —un señalamiento en la entrada que decide, auto por auto, por cuál carretera lo mandas—. Al principio mandas el 10% de los autos por la nueva y el 90% sigue por la vieja. Observas: ¿el puente aguanta?, ¿el cruce fluye?, ¿alguien se queja? Si todo va bien, subes al 50%. Observas otra vez. Subes al 100%. Y solo cuando todos los autos pasaron por la nueva sin problemas, durante un buen rato, cierras la vieja y la demueles. Si en cualquier momento la carretera nueva falla, el desvío manda ese auto de vuelta por la vieja —que sigue ahí, abierta— y nadie se queda tirado.

Aquí están los cuatro elementos que este módulo va a construir, ya visibles en la carretera:

  • El facade es el señalamiento en la entrada: el único punto por donde pasan todos los autos, donde se decide la ruta.
  • Construir al lado es levantar la carretera nueva sin cerrar la vieja.
  • Desviar por porcentaje es mandar primero el 10%, luego el 50%, luego el 100%, observando en cada nivel.
  • El fallback es que la carretera vieja siga abierta: si la nueva falla, el auto vuelve por la vieja.

Y hay una imagen aún más precisa, la que le da nombre al patrón: la higuera estranguladora de Queensland. Una semilla germina arriba, en las ramas de otro árbol. Echa raíces que bajan por el tronco hasta la tierra, y ramas que suben hacia la luz. Con los años, la higuera envuelve por completo al árbol anfitrión, le quita la luz y los nutrientes, y lo va reemplazando —hasta que el árbol viejo se muere y se pudre por dentro, dejando a la higuera hueca en pie, ya autosuficiente—. En ningún momento del proceso hubo un claro sin árbol: la higuera creció sobre el viejo y lo reemplazó sin talarlo. Martin Fowler tomó esa imagen en 2004 para nombrar el patrón, y es la metáfora que sostiene toda esta guía.

Ejemplo trabajado: un strangler_router mínimo desviando el GET /products de Mercado

No vamos a describir el desvío de tráfico: lo vamos a ejecutar, aunque sea en su versión más pequeña. La idea es tener las tres piezas mínimas —el legacy, el servicio nuevo, y el router que decide entre ellos por porcentaje— y ver, con nuestros ojos, el tráfico moverse del viejo al nuevo cuando subimos el traffic_percent del 0 al 100.

El strangler_router decide por request usando un bucket estable: convierte el id de la request en un número del 0 al 99 con un hash (crc32), y si ese número cae por debajo del traffic_percent, la request va al modern; si no, al legacy. Que el bucket sea estable importa: la misma request cae siempre en el mismo lado, así que la decisión es reproducible y —cuando el bucket se calcula sobre el usuario, como veremos en la lección 5— un mismo usuario no salta de ruta a media sesión.

import zlib

# --- El monolito legacy de Mercado: responde GET /products hoy. ---
def legacy_catalog(request):
    return {"source": "legacy", "products": ["ssd-1tb", "usb-c-hub", "webcam"]}

# --- El servicio nuevo, construido al lado. Responde lo mismo (por ahora). ---
def modern_catalog(request):
    return {"source": "modern", "products": ["ssd-1tb", "usb-c-hub", "webcam"]}

# --- El strangler facade: decide viejo vs nuevo por porcentaje de trafico. ---
def bucket(request_id):
    # Bucket estable 0..99 por request: la misma request cae siempre igual.
    return zlib.crc32(str(request_id).encode()) % 100

def strangler_router(request, traffic_percent):
    if bucket(request["id"]) < traffic_percent:
        return modern_catalog(request)
    return legacy_catalog(request)

# --- Subimos el porcentaje 0 -> 10 -> 50 -> 100 sobre el mismo lote de trafico. ---
N = 1000
requests = [{"id": i, "path": "GET /products"} for i in range(1, N + 1)]

print(f"Facade delante de GET /products - {N} requests por nivel\n")
print(f"{'traffic_percent':>16}{'-> modern':>12}{'-> legacy':>12}   reparto")
print("-" * 62)
for pct in (0, 10, 50, 100):
    counts = {"modern": 0, "legacy": 0}
    for req in requests:
        resp = strangler_router(req, pct)
        counts[resp["source"]] += 1
    bar = "#" * (counts["modern"] * 30 // N)
    print(f"{pct:>15}%{counts['modern']:>12}{counts['legacy']:>12}   |{bar:<30}|")

print("\n  0%  -> el facade existe, pero nada cambio: todo va al legacy.")
print("  100% -> el legacy ya no recibe una sola request. Se puede retirar.")

Qué esperar. Al correr el archivo, la salida es exactamente esta:

Facade delante de GET /products - 1000 requests por nivel

 traffic_percent   -> modern   -> legacy   reparto
--------------------------------------------------------------
              0%           0        1000   |                              |
             10%         109         891   |###                           |
             50%         520         480   |###############               |
            100%        1000           0   |##############################|

  0%  -> el facade existe, pero nada cambio: todo va al legacy.
  100% -> el legacy ya no recibe una sola request. Se puede retirar.

Lee la tabla de arriba hacia abajo, porque ahí está el módulo entero en cuatro renglones.

En 0%, el facade ya está puesto —todas las 1000 requests pasan por strangler_router—, pero el traffic_percent es 0, así que ninguna cae por debajo de ese umbral y todas van al legacy. La barra de reparto está vacía. Esto es clave y es la lección 2: puedes desplegar el facade sin cambiar absolutamente nada. El sistema se comporta idéntico a como se comportaba, pero ahora existe el punto de control.

En 10%, subes el umbral y 109 requests (de 1000) caen por debajo de 10 y van al modern; las otras 891 siguen en el legacy. Ese es tu canary: una fracción pequeña del tráfico probando la ruta nueva mientras la enorme mayoría sigue en la ruta segura. Si el modern tuviera un problema, solo el 11% de los usuarios lo vería, no el 100%.

En 50%, el reparto es casi mitad y mitad (520 al nuevo, 480 al viejo). Y en 100%, las 1000 van al modern y el legacy no recibe una sola request. Fíjate en la última barra, llena, contra la primera, vacía: eso es la higuera envolviendo al árbol. En el 0% el árbol viejo lo sostiene todo; en el 100% la higuera ya se sostiene sola y el árbol viejo se puede retirar —que es exactamente el corte final de la lección 7—.

Que el reparto salga en 109, 520 y no en 100, 500 exactos no es un error: el bucket por hash reparte de forma pareja pero no milimétrica sobre 1000 requests. Lo importante no es el número exacto, sino que el tráfico se mueve del viejo al nuevo de forma controlada cuando subes una sola perilla, el traffic_percent. Toda la mecánica del módulo es refinar esa perilla: cómo decidir el bucket (lección 5), qué hacer cuando el nuevo falla (lección 6), y cuándo llegar a 100 y apagar el viejo (lección 7).

El ciclo completo del strangler fig

Ese ejemplo tocó las cuatro fases del patrón sin nombrarlas del todo. Vale la pena verlas explícitas, porque son la columna vertebral de las siete lecciones que siguen:

Fase                     Lección   Qué haces                        Estado del legacy
───────────────────────  ────────  ───────────────────────────────  ─────────────────
1. Poner el facade       L2        proxy como unico punto de entrada  100% activo
2. Construir al lado      L3        servicio nuevo, mismo contrato     100% activo
3. Desviar el trafico     L4-L6     0 -> 10 -> 50 -> 100, con fallback  bajando
4. Retirar el legacy      L7        borrarlo cuando burn-down = 0       0%, apagado

Y así encajan las fases en el flujo del módulo:

flowchart LR
    C["cliente"] --> F["strangler facade<br/>(traffic_percent)"]
    F -->|bucket >= percent| L["legacy_catalog<br/>(el viejo)"]
    F -->|bucket < percent| M["modern_catalog<br/>(el nuevo, al lado)"]
    M -.->|si falla| L
    L -.->|burn-down a 0| X["retirar<br/>el legacy"]

Léelo así: el cliente ya no llama al legacy directo, llama al facade. El facade calcula el bucket de la request y la manda al legacy o al modern según el traffic_percent. Si el modern falla, la línea punteada la devuelve al legacy (el fallback). Y cuando el burn-down de llamadas al legacy llega a cero —porque el traffic_percent llegó a 100 y ninguna request cae ya en la ruta vieja—, el legacy se retira.

El mapa: dónde está este módulo en la guía

Este módulo es el corazón mecánico de la guía: la primera técnica que de verdad mueve tráfico del viejo al nuevo. Así se conecta con el resto:

flowchart TD
    M1["M1 · Por qué no reescribir<br/>(la conviccion)"]
    M2["M2 · Caracterizar el legacy<br/>(la red de seguridad)"]
    M3["M3 · El patrón strangler fig<br/>(desviar trafico con un facade)"]
    M4["M4 · Branch by abstraction<br/>(migrar por dentro, sin proxy)"]
    M5["M5 · Extraer un servicio<br/>(anti-corruption layer)"]
    M6["M6 · Migrar datos sin downtime"]
    M7["M7 · Medir el progreso"]
    M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7

Léelo así: en M1 te convenciste de que incremental gana; en M2 pusiste la red de seguridad; aquí, en M3, aprendes a desviar tráfico con un facade externo; en M4 verás la variante para cuando no puedes poner un proxy externo y hay que migrar la implementación por dentro del código; en M5 extraes el servicio a su propio proceso; en M6 migras sus datos; y en M7 mides el avance hasta apagar lo viejo. El strangler de este módulo es la técnica que más se ve "en la naturaleza": la mayoría de las migraciones de monolito a servicios que funcionan lo usan como espina dorsal.

Y la frontera con las guías hermanas del ecosistema: esta guía es la técnica de migración, no el destino. A dónde migras el catálogo —si el modern termina siendo un microservicio, un servicio orientado a eventos o una API— lo enseñan architectural-styles-and-boundaries, event-driven-architecture y api-design-and-integration. La decisión de migrar —el ADR, el costo, la reversibilidad registrada— es architecture-decisions-and-tradeoffs. Aquí solo enseñamos cómo desviar el tráfico del viejo al nuevo, sea cual sea la forma final del nuevo.

Errores comunes

Creer que el strangler es solo "poner un proxy" y ya. Qué pasa: el equipo instala el facade, desvía el 10% del tráfico, y da la migración por "en marcha" —pero nunca sube del 10%, o nunca retira el legacy—. Por qué pasa: la parte visible y satisfactoria del patrón es el arranque (¡ya hay tráfico en el nuevo!); las partes tediosas son subir el porcentaje con cuidado y, sobre todo, apagar el viejo. Cómo detectarlo: si llevas meses con "algo de tráfico en el nuevo" pero el legacy sigue prendido al 100% de su capacidad, no estás estrangulando, estás coexistiendo para siempre. Cómo corregirlo: el strangler es un ciclo con final. Las cuatro fases (facade, construir al lado, desviar 0→100, retirar) tienen que completarse, y la cuarta —retirar el legacy— es la que paga la migración. La lección 7 la trata a fondo; guárdala como la meta desde el día uno.

Confundir el strangler (externo, por tráfico) con branch by abstraction (interno, por código). Qué pasa: el equipo intenta poner un proxy HTTP delante de una función interna que ni siquiera está expuesta como endpoint, y se enreda. Por qué pasa: los dos patrones logran lo mismo —migrar de una implementación vieja a una nueva de forma incremental— y es fácil mezclarlos. Cómo detectarlo: pregunta "¿lo que quiero desviar tiene una frontera de red o de proceso donde puedo interponer un proxy?". Si el catalog es un endpoint (GET /products), sí: strangler. Si es una función llamada desde adentro del mismo proceso, sin frontera externa, no hay dónde poner el proxy: eso es branch by abstraction, el módulo 4. Cómo corregirlo: usa el strangler cuando exista un punto de interposición externo (un endpoint, un gateway, un balanceador); usa branch by abstraction cuando la migración es por dentro del código. Este módulo es el primero; el 4 es el segundo.

Ejercicios

Ejercicio 1 — Las cuatro fases en la carretera. Con la analogía de la carretera, empareja cada fase del strangler fig con lo que ocurre en la carretera: (a) poner el facade, (b) construir al lado, (c) desviar por porcentaje, (d) retirar el legacy. Luego di qué elemento de la analogía representa el fallback y por qué es lo que hace seguro subir el porcentaje.

Ver solución
  • (a) Poner el facade → el señalamiento en la entrada que decide, auto por auto, por cuál carretera va. No cambia el destino de nadie todavía (a 0% todos siguen por la vieja), pero ahora existe el punto donde se decide.
  • (b) Construir al lado → levantar la carretera nueva sin cerrar la vieja. Las dos coexisten; la nueva aún no recibe autos.
  • (c) Desviar por porcentaje → mandar primero el 10% de los autos por la nueva, observar, subir al 50%, al 100%.
  • (d) Retirar el legacy → cerrar y demoler la carretera vieja una vez que todos los autos pasan por la nueva sin problemas.

El fallback es que la carretera vieja siga abierta mientras subes el porcentaje. Es lo que hace seguro el proceso: si la carretera nueva falla (un puente que no aguanta, un cruce mal señalizado), el desvío manda ese auto de vuelta por la vieja, que sigue ahí. Sin ese fallback, cada auto que mandas por la nueva es una apuesta sin red; con él, el peor caso de un fallo del nuevo es que ese usuario reciba la respuesta del viejo —más lenta, quizás, pero correcta—. Por eso el legacy no se apaga hasta el final (fase d): mientras subes el porcentaje, es la red de seguridad.

Ejercicio 2 — Lee el reparto. En el ejemplo trabajado, a traffic_percent=10 el reparto fue 109 al modern y 891 al legacy sobre 1000 requests. (a) ¿Por qué no salió exactamente 100 y 900? (b) Si vuelves a correr el mismo código con los mismos 1000 ids, ¿el reparto será igual o distinto? (c) ¿Qué propiedad del bucket garantiza que una misma request caiga siempre en la misma ruta?

Ver solución

(a) Porque el bucket se calcula con un hash (crc32) del id, y un hash reparte los ids de forma pareja pero no milimétrica. De 1000 ids, aproximadamente el 10% cae por debajo de 10, pero "aproximadamente" no es "exactamente": salieron 109. Sobre muestras más grandes la proporción se acerca más al 10%; sobre muestras chicas puede desviarse más. Lo que importa es que la fracción es controlable con el traffic_percent, no que sea exacta al decimal.

(b) Será exactamente igual. El bucket es una función determinista del id (crc32(str(id)) % 100): el mismo id produce siempre el mismo bucket, así que el mismo lote de ids produce siempre el mismo reparto. Por eso el ejemplo es reproducible: no hay azar, hay un hash fijo. (Si usáramos random sin semilla fija, el reparto cambiaría en cada corrida; el hash estable lo evita.)

(c) La propiedad es que el bucket es determinista y estable: depende solo del id (o del usuario, en la lección 5), no del momento ni del azar. Esto garantiza que una misma request —o un mismo usuario— caiga siempre en la misma ruta. Es esencial para el canary por usuario: un cliente no debe ver el catálogo nuevo en una request y el viejo en la siguiente dentro de la misma sesión; el bucket estable se lo garantiza.

Ejercicio 3 — Strangler o no. Para cada situación, di si el patrón strangler fig (facade externo + desvío de tráfico) es aplicable, o si conviene otra técnica de la guía, y por qué: (a) modernizar el endpoint GET /products de Mercado, que ya está expuesto por HTTP; (b) reemplazar una función calculate_shipping() que se llama desde adentro del monolito, sin frontera de red; (c) migrar la tabla products de una base de datos a otra sin cambiar el código que la usa.

Ver solución
  • (a) GET /products → strangler fig, ideal. Es un endpoint HTTP: tiene una frontera de red clara donde puedes interponer un facade/proxy que decida, request por request, si va al legacy o al modern por porcentaje. Es el caso central de este módulo.
  • (b) calculate_shipping() interna → branch by abstraction (módulo 4). Es una función llamada desde adentro del mismo proceso, sin un punto de interposición externo. No hay dónde poner un proxy HTTP. La técnica correcta es insertar una capa de abstracción en el código y migrar la implementación detrás de ella con un flag —lo que enseña el módulo 4—. El strangler y branch by abstraction son primos: mismo objetivo (migración incremental), distinto punto de interposición (red vs código).
  • (c) Migrar la tabla products sin tocar el código → migración de datos sin downtime (módulo 6). Esto no es desviar tráfico de un servicio a otro, sino mover datos de un almacén a otro mientras el sistema sigue leyendo y escribiendo. La técnica es expand-contract con dual-write, backfill y parallel-run —el módulo 6—. El strangler mueve tráfico entre implementaciones; la migración de datos mueve la información que ambas usan.

La lección de fondo: el strangler fig es una técnica de un kit, la que aplica cuando hay una frontera externa donde interponer un proxy. Saber cuándo no aplica —y qué usar en su lugar— es parte de dominarla.

Resumen y siguiente paso

En esta lección instalaste la técnica que le da nombre a toda la práctica: el patrón strangler fig. Viste su idea completa —facade delante, construir al lado, desviar el tráfico del 0 al 100, retirar el legacy— y sus dos metáforas: la carretera nueva que se construye al lado con un desvío que manda primero el 10% de los autos, y la higuera estranguladora que envuelve al árbol y lo reemplaza sin talarlo. Y lo ejecutaste en su versión mínima: un strangler_router que enrutó 1000 requests del GET /products de Mercado, con el traffic_percent subiendo del 0 (todo al legacy) al 100 (todo al modern, el legacy sin una sola llamada), viendo el tráfico desviarse renglón por renglón.

Antes de avanzar deberías poder: nombrar las cuatro fases del strangler y en qué lección se desarrolla cada una; explicar por qué a 0% el facade no cambia nada pero ya es útil; leer un reparto de tráfico y entender por qué el bucket estable lo hace reproducible; y distinguir cuándo aplica el strangler (frontera externa) de cuándo aplica branch by abstraction (por dentro del código).

La lección 2 hace la primera fase con calma: poner el facade delante del legacy. Vas a ver por qué el facade se puede desplegar sin cambiar una sola respuesta —con cero riesgo— y vas a ejecutar la prueba de transparencia: comparar, request por request, la respuesta del legacy directo contra la respuesta a través del facade, y verificar que son idénticas byte a byte. Ese despliegue invisible es lo que te da, sin apostar nada, el punto de control desde el cual todo lo demás será posible.

Recursos

  • Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. El texto que dio nombre al patrón. Fowler cuenta cómo tomó la imagen de las higueras estranguladoras que vio en un viaje a Queensland, y por qué reemplazar por partes le gana a reescribir de golpe. La lectura fundacional de todo el módulo. En inglés.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3, "Splitting the Monolith" — el capítulo que trata el strangler fig como la técnica central para migrar un monolito por rebanadas, con el proxy que desvía llamadas de forma incremental. La referencia de cabecera para los módulos 3 al 6. En inglés.
  • Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. La ficha del patrón en el catálogo de microservicios: contexto, problema, solución y las fuerzas en juego. Corta y directa. En inglés.
  • Paul Hammant, "Legacy Application Strangulation: Case Studies" (2013) — paulhammant.com/2013/07/14/legacy-application-strangulation-case-studies. Casos reales de estrangulamiento de aplicaciones legacy, del autor que más ha escrito sobre el patrón en la práctica (y sobre su primo, branch by abstraction, del módulo 4). En inglés.