Módulo 3: El patrón strangler fig

El facade delante del legacy

Descripción

En la lección anterior viste el ciclo completo del strangler fig y lo corriste en miniatura. Ahora empezamos a construirlo por fases, y la primera es la más importante y la que más gente se salta: poner el facade delante del legacy. Antes de desviar un solo request, antes de construir una línea del servicio nuevo, necesitas un lugar donde la decisión "¿viejo o nuevo?" pueda tomarse. Ese lugar es el facade: un proxy que se convierte en el único punto de entrada del tráfico que vas a migrar.

Lo maravilloso del facade —y la razón por la que es el primer paso— es que se puede desplegar sin cambiar absolutamente nada. En el momento en que lo instalas, el traffic_percent está en 0: el facade recibe todas las requests y las reenvía, todas, al legacy. Las respuestas son idénticas a como eran antes. El sistema se comporta exactamente igual. Pero ahora existe algo que antes no existía: un punto de control por el que pasa el 100% del tráfico, donde puedes empezar a contar, a medir, y —cuando estés listo— a desviar. Instalaste la perilla en 0; girarla es todo lo que sigue.

Esta lección lo ejecuta con una prueba de transparencia. Vamos a comparar, request por request, la respuesta del legacy llamado directamente contra la respuesta del mismo legacy a través del facade, y a verificar que son idénticas byte a byte. Esa verificación es lo que te da la confianza para desplegar el facade en producción sin miedo: matemáticamente, no cambiaste el comportamiento.

Conexión con el módulo. La lección 1 te dio el ciclo completo; esta hace su primera fase: el facade como punto de entrada, desplegado con cero riesgo. La lección 3 hace la segunda fase (construir el servicio nuevo al lado); la 4 hace la tercera (desviar el tráfico por porcentaje) usando este mismo facade con el traffic_percent ya distinto de 0. Todo lo que sigue en el módulo asume que este paso está hecho: sin el facade no hay dónde decidir viejo vs nuevo. Fíjate en la frontera: el facade de aquí es un proxy externo —se interpone en la frontera de red del endpoint—. Cuando no hay frontera externa donde interponerlo (una función interna del monolito), el punto de decisión se inserta en el código con una capa de abstracción, y eso es el módulo 4. Aquí, GET /products es un endpoint: tiene su frontera, y ahí va el facade.

Una analogía: el mesero que toma todos los pedidos

Imagina un restaurante con una sola cocina —vieja, lenta, pero funciona— y quieres, con el tiempo, reemplazarla por una cocina nueva. No puedes cambiar las dos cocinas de golpe en plena hora de comida. Lo primero que haces no tiene nada que ver con cocinar: pones un mesero en la puerta que toma todos los pedidos.

Antes, los comensales entraban y le gritaban su pedido directo a la cocina vieja. Ahora, todos le dicen su pedido al mesero, y el mesero lo lleva a la cocina. ¿Cambió algo para el comensal? Nada: pide lo mismo, recibe lo mismo, con el mismo sabor y el mismo tiempo, porque el mesero simplemente lleva cada pedido a la cocina vieja, tal cual. Desde la mesa, el restaurante es idéntico.

Pero para ti, dueño del restaurante, cambió todo. Ahora tienes un punto único donde pasan todos los pedidos. Puedes contar cuántos pedidos entran, cuáles son los platillos más pedidos, a qué hora hay pico. Y —cuando la cocina nueva esté lista— puedes decirle al mesero: "los pedidos de ensalada, llévalos a la cocina nueva; el resto, a la vieja". El mesero es el facade: no cocina, solo decide a qué cocina va cada pedido. Instalarlo no cambió la comida de nadie; te dio el control para cambiarla después, plato por plato.

El facade del software es ese mesero. GET /products es el pedido. Las cocinas son el legacy_catalog y el modern_catalog. Y el hecho de que instalar el mesero no cambie la comida de nadie es exactamente la prueba de transparencia que vamos a ejecutar.

Ejemplo trabajado: la prueba de transparencia del facade a 0%

Vamos a montar el facade y a demostrar que, a 0% desviado, es transparente. Tenemos el legacy_catalog —la implementación vieja del GET /products, la única que existe hoy, con su lógica de filtrado por query—. Y montamos un StranglerFacade con traffic_percent=0 que, por ahora, reenvía todo al legacy pero ya lleva la cuenta de por dónde fue cada request (su observabilidad mínima). Luego pasamos un conjunto de requests por los dos caminos —legacy directo y vía facade— y verificamos que las respuestas coinciden.

# El facade se despliega SIN cambiar comportamiento: intercepta todo el trafico
# de GET /products y hoy lo reenvia 100% al legacy. Es transparente y auditable.

def legacy_catalog(request):
    # La implementacion vieja: la unica que existe hoy.
    q = request.get("q", "")
    products = ["ssd", "usb-hub", "webcam", "hdmi"]
    if q:
        products = [p for p in products if q in p]
    return {"status": 200, "source": "legacy", "products": products}

# --- El strangler facade: unico punto de entrada. traffic_percent=0 => todo legacy. ---
class StranglerFacade:
    def __init__(self, traffic_percent=0):
        self.traffic_percent = traffic_percent
        self.routed = {"legacy": 0, "modern": 0}   # observabilidad: cuenta el reparto

    def handle(self, request):
        # Con traffic_percent=0 no hay ruta nueva todavia: todo va al legacy.
        # Pero AHORA existe el punto donde maniana se decidira viejo vs nuevo.
        self.routed["legacy"] += 1
        return legacy_catalog(request)

# --- Prueba de transparencia: el facade a 0% responde IGUAL que el legacy directo. ---
sample = [
    {"id": 1, "path": "GET /products", "q": ""},
    {"id": 2, "path": "GET /products", "q": "usb"},
    {"id": 3, "path": "GET /products", "q": "hdmi"},
    {"id": 4, "path": "GET /products", "q": "zzz"},   # sin resultados
]

facade = StranglerFacade(traffic_percent=0)
print("Comparacion: legacy directo  vs  a traves del facade (0% desviado)\n")
print(f"{'q':<9}{'legacy directo':<26}{'via facade':<26}{'igual?':>7}")
print("-" * 68)
all_equal = True
for req in sample:
    direct = legacy_catalog(req)
    through = facade.handle(req)
    same = direct == through
    all_equal = all_equal and same
    q = req["q"] or "(vacio)"
    print(f"{q:<9}{','.join(direct['products']) or '-':<26}"
          f"{','.join(through['products']) or '-':<26}{('SI' if same else 'NO'):>7}")

print("-" * 68)
print(f"\nTodas las respuestas identicas: {all_equal}")
print(f"Reparto observado por el facade: {facade.routed}")
print("\n  Desplegaste el facade sin cambiar una sola respuesta (cero riesgo),")
print("  y ahora tienes el punto de control para empezar a desviar trafico.")

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

Comparacion: legacy directo  vs  a traves del facade (0% desviado)

q        legacy directo            via facade                 igual?
--------------------------------------------------------------------
(vacio)  ssd,usb-hub,webcam,hdmi   ssd,usb-hub,webcam,hdmi        SI
usb      usb-hub                   usb-hub                        SI
hdmi     hdmi                      hdmi                           SI
zzz      -                         -                              SI
--------------------------------------------------------------------

Todas las respuestas identicas: True
Reparto observado por el facade: {'legacy': 4, 'modern': 0}

  Desplegaste el facade sin cambiar una sola respuesta (cero riesgo),
  y ahora tienes el punto de control para empezar a desviar trafico.

Lee la columna igual?: SI en las cuatro filas. Sin query, con query que filtra (usb, hdmi), y con query que no devuelve nada (zzz) —el caso borde—, la respuesta a través del facade es idéntica a la del legacy directo. Y la línea de abajo lo confirma en una sola verificación: Todas las respuestas identicas: True. Eso es la transparencia: el facade no es una reimplementación, es un pasamanos. Toma la request y la entrega tal cual a la cocina vieja.

Fíjate también en Reparto observado por el facade: {'legacy': 4, 'modern': 0}. Las cuatro requests fueron al legacy, cero al modern —lógico, traffic_percent es 0 y ni siquiera existe un modern_catalog todavía—. Pero ese contador es la semilla de la observabilidad: desde el minuto uno, el facade sabe por dónde fue cada request. Cuando en la lección 4 empieces a desviar tráfico, este mismo contador te dirá cuántas fueron al nuevo y cuántas al viejo, sin instrumentar nada más.

El punto de la lección está en la combinación de esas dos salidas: cambiaste la topología del sistema —ahora todo pasa por un facade que no estaba— sin cambiar ni una respuesta ni un byte. En términos de despliegue, esto es oro: puedes poner el facade en producción, verificar que la prueba de transparencia da True, y saber que no rompiste nada. El facade es el paso de menor riesgo de toda la migración, y es el que habilita todos los demás.

Profundización: por qué el facade primero, y qué NO debe hacer

El orden importa, y no es casual que el facade vaya antes que el servicio nuevo. Si construyeras primero el modern_catalog y solo después el facade, tendrías un servicio nuevo sin forma de recibir tráfico de a poco: solo podrías conectarlo con un cambio grande y arriesgado. Al revés —facade primero— cuando el servicio nuevo esté listo, ya tienes el mecanismo para darle tráfico gota a gota. El facade es la infraestructura de desvío; se instala vacía (a 0%) y se llena después.

Aquí está la topología, antes y después de instalar el facade:

ANTES (sin facade):
   cliente ───────────────> legacy_catalog     (llamada directa, sin control)

DESPUES (facade a 0%):
   cliente ──> StranglerFacade ──> legacy_catalog
                     │
                     └── traffic_percent = 0  (todo al legacy, aun)
                     └── routed = {legacy: N, modern: 0}  (ya cuenta)

La regla de oro del facade en esta fase es de disciplina: el facade no debe hacer nada más que enrutar. No valida, no transforma, no agrega lógica de negocio, no cachea, no "aprovecha para arreglar de paso" el formato de la respuesta. En cuanto el facade empieza a hacer cosas, deja de ser transparente —la prueba de transparencia daría False— y pierdes la propiedad que lo hace seguro. Un facade que solo enruta se puede desplegar sin miedo; un facade que además transforma es un cambio de comportamiento disfrazado de infraestructura, y ahí es donde los strangler se rompen. Toda la lógica nueva vive en el modern_catalog (lección 3), nunca en el facade.

Esto conecta con el peor destino de un facade, que veremos como error común: el facade que, lección tras lección, se le va agregando "una cosita más" —un poco de validación aquí, una transformación allá, un caso especial acá— hasta que se convierte en otro monolito, tan enredado como el que querías reemplazar. El facade tiene que quedarse flaco. Su única razón de existir es decidir a cuál cocina va cada pedido.

Errores comunes

Aprovechar el facade para "mejorar de paso" las respuestas. Qué pasa: al construir el facade, el equipo ve una oportunidad —"ya que todo pasa por aquí, normalicemos el formato de fecha", "agreguemos este campo que siempre nos faltó"—. Por qué pasa: el facade es un punto tentador; toca todo el tráfico, parece el lugar perfecto para cambios transversales. Cómo detectarlo: la prueba de transparencia deja de dar True. Si la respuesta vía facade difiere de la del legacy directo aunque sea en un campo, el facade dejó de ser transparente. Cómo corregirlo: en la fase del facade, cero cambios de comportamiento. El facade solo enruta. Las mejoras al formato o los campos nuevos son trabajo del modern_catalog —ahí sí, porque el servicio nuevo puede comportarse distinto y tú controlas el porcentaje de tráfico que lo ve—. Mantén la transparencia como un test que corre en cada despliegue del facade: si se pone en rojo, el facade se ensució.

Construir el servicio nuevo antes que el facade. Qué pasa: el equipo se emociona con la reimplementación, construye un modern_catalog completo, y solo al final se pregunta cómo darle tráfico de a poco —y descubre que no hay mecanismo—. Por qué pasa: escribir el servicio nuevo es la parte divertida; instalar un proxy que no hace nada visible es la parte aburrida, y se pospone. Cómo detectarlo: tienes un servicio nuevo "listo" pero la única forma de activarlo es un cambio grande (apuntar el 100% del tráfico de golpe), que es exactamente el big-bang que el strangler quiere evitar. Cómo corregirlo: el facade va primero, aunque se sienta inútil a 0%. Es la infraestructura que permite el desvío gradual; sin él, tener el servicio nuevo no te sirve de nada para migrar con seguridad. Facade vacío primero, servicio nuevo después, tráfico gota a gota al final.

Saltarse la prueba de transparencia "porque el facade solo reenvía". Qué pasa: el equipo asume que el facade es trivialmente transparente y lo despliega sin verificar. Por qué pasa: "solo reenvía, ¿qué puede salir mal?". Cómo detectarlo: pequeñas diferencias que nadie notó —el facade re-serializa el JSON y cambia el orden de las claves, o pierde un header, o convierte un null en cadena vacía—. Estas diferencias son invisibles hasta que un cliente que dependía del formato exacto se rompe. Cómo corregirlo: la prueba de transparencia no es opcional, es barata: compara la respuesta directa contra la del facade para un conjunto de requests que cubra los casos borde (query vacía, sin resultados, caracteres raros) y verifica igualdad total. Es un test de una tarde que te ahorra un incidente. Que el facade "solo reenvíe" es justo lo que hay que probar, no asumir.

Ejercicios

Ejercicio 1 — El mesero transparente. En la analogía del restaurante, el mesero (facade) toma todos los pedidos y los lleva a la cocina vieja. (a) ¿Qué "prueba de transparencia" haría el dueño para asegurarse de que instalar al mesero no cambió la experiencia de los comensales? (b) Da un ejemplo de algo que el mesero podría "aprovechar para hacer de paso" y que rompería la transparencia. (c) ¿Por qué esa mejora, aunque sea buena, no debe hacerla el mesero en esta fase?

Ver solución

(a) El dueño compararía, para un mismo pedido, el plato que sale cuando el comensal le grita directo a la cocina vieja contra el plato que sale cuando pasa por el mesero: mismo platillo, mismo sabor, mismo tiempo de espera, misma presentación. Si son idénticos en una muestra de pedidos —incluidos los raros, como "sin cebolla" o "para llevar"—, el mesero es transparente. Es exactamente la comparación legacy directo == vía facade del ejemplo.

(b) El mesero podría "aprovechar para" agregar una guarnición que siempre le pareció que faltaba, corregir el término de la carne que "seguro el comensal quiso", o cambiar el plato en que se sirve. Cualquiera de esas cosas hace que el plato vía-mesero difiera del plato directo-a-cocina: rompe la transparencia.

(c) Porque en esta fase el objetivo es instalar el punto de control sin cambiar la experiencia de nadie —ese es justo lo que lo hace seguro de desplegar—. Las mejoras (la guarnición nueva, el formato distinto) son legítimas, pero pertenecen a la cocina nueva (modern_catalog), donde puedes controlar a qué porcentaje de comensales llega. Si el mesero mejora los platos, todos los comensales ven el cambio de golpe, sin canary y sin fallback: es el big-bang que queríamos evitar, colado por la puerta de atrás.

Ejercicio 2 — Diagnostica la transparencia. Un equipo despliega un facade y corre la prueba de transparencia. Para tres de cuatro requests da SI, pero para la request con query vacía (q="") da NO: el legacy directo devuelve ["ssd","usb-hub","webcam","hdmi"] y el facade devuelve ["hdmi","webcam","usb-hub","ssd"]. (a) ¿El facade es transparente? (b) ¿Cuál es la causa más probable? (c) ¿Por qué esto podría romper a un cliente aunque "los productos son los mismos"?

Ver solución

(a) No. Basta una fila en NO para que el facade no sea transparente. La transparencia es igualdad total de la respuesta, no "los mismos elementos en cualquier orden".

(b) La causa más probable es que el facade re-procesa la lista —por ejemplo, la deserializa a un set y la vuelve a serializar, o la reordena— en vez de reenviar la respuesta del legacy tal cual. El legacy devuelve los productos en un orden fijo (el de su lista interna); el facade los devolvió en otro orden. El facade está tocando la respuesta, cuando debería pasarla intacta.

(c) Porque un cliente puede depender del orden: una app móvil que muestra "el primer producto" como destacado, un test automatizado que compara la respuesta exacta, o un contrato con un socio que espera un orden estable. "Los mismos productos en distinto orden" es una respuesta distinta para cualquier consumidor que dependa del orden, y en un sistema legacy no sabes quién depende de qué. La regla es dura por eso: el facade reenvía byte a byte, no "lo equivalente". La corrección es hacer que el facade devuelva exactamente lo que el legacy le dio, sin re-serializar.

Ejercicio 3 — El contador del facade. El StranglerFacade del ejemplo lleva routed = {"legacy": N, "modern": 0} desde la fase de 0%. (a) ¿Para qué sirve ese contador si a 0% siempre marca modern: 0? (b) ¿Qué te dirá ese mismo contador cuando el traffic_percent esté en 30%? (c) ¿Por qué es útil que la observabilidad viva en el facade y no en cada servicio por separado?

Ver solución

(a) A 0% el contador parece inútil (modern siempre 0), pero su valor es que ya está instalado: cuando gires la perilla, no tendrás que agregar instrumentación nueva, ya estará contando. Además, desde 0% te da datos útiles: cuánto tráfico total recibe el endpoint, que es la base para dimensionar el servicio nuevo. Instalar la observabilidad junto con el facade —aunque un lado esté en cero— es parte de desplegar el facade "listo para desviar".

(b) A 30%, el contador marcará aproximadamente {"legacy": 70% de las requests, "modern": 30% de las requests}. Es tu medida directa del reparto real de tráfico —no el porcentaje que pediste, sino el que de verdad ocurrió— y la base del burn-down de la lección 7: ver legacy bajar hacia 0 es ver la migración avanzar.

(c) Porque el facade es el único punto por el que pasa todo el tráfico: contar ahí te da una vista completa y consistente del reparto, en un solo lugar. Si la observabilidad viviera repartida en cada servicio, tendrías que sumar métricas de dos sistemas con relojes y formatos distintos para saber el reparto, y el legacy —que no querías tocar— tendría que instrumentarse. El facade centraliza la decisión y la medición de la decisión: sabe a dónde mandó cada request porque él la mandó. Es el mejor lugar del sistema para medir el progreso de la migración.

Resumen y siguiente paso

En esta lección hiciste la primera fase del strangler fig: poner el facade delante del legacy. Viste, con el mesero que toma todos los pedidos, que el facade es un punto de entrada único que decide a qué cocina va cada uno sin cambiar la comida de nadie. Y lo ejecutaste con la prueba de transparencia: comparaste, request por request, el legacy directo contra el legacy vía facade —incluido el caso borde de query sin resultados— y verificaste Todas las respuestas identicas: True. Ese es el paso de menor riesgo de toda la migración: cambiaste la topología del sistema sin cambiar una sola respuesta, y a cambio obtuviste el punto de control y la observabilidad desde los cuales todo lo demás será posible.

Antes de avanzar deberías poder: explicar por qué el facade va antes que el servicio nuevo; enunciar la regla de oro del facade (solo enruta, no transforma) y por qué romperla rompe la transparencia; correr una prueba de transparencia y saber por qué un cambio de orden ya la reprueba; y describir para qué sirve el contador del facade incluso a 0%.

La lección 3 hace la segunda fase: construir el servicio nuevo al lado. Ahora que el facade está puesto y es transparente, vas a levantar un modern_catalog con una implementación interna distinta —un store con stock, no la lista plana del legacy— pero el mismo contrato público. Vas a ejecutar la coexistencia de los dos, con un canary de 20%, y a verificar que el servicio nuevo cumple el contrato que el facade espera. El legacy quedará intacto; el nuevo vivirá a su lado; y el facade —el que acabas de construir— será el que elija a cuál llamar.

Recursos

  • Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. Fowler describe el "event interception" y el rol del proxy que se interpone entre el cliente y el sistema viejo: el facade de esta lección. En inglés.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — la sección sobre el "HTTP proxy" y cómo desplegar el proxy sin desviar tráfico todavía (equivalente a nuestro facade a 0%) para reducir el riesgo del cambio. En inglés.
  • Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. La ficha del patrón, con el rol del "strangler facade" como componente central que intercepta las requests. En inglés.
  • Paul Hammant, "Legacy Application Strangulation: Case Studies" (2013) — paulhammant.com/2013/07/14/legacy-application-strangulation-case-studies. Casos donde el primer movimiento es siempre interponer el punto de intercepción sin cambiar comportamiento. En inglés.