Módulo 3: El patrón strangler fig
El corte final y retirar el legacy
Descripción
Llegaste a la última fase del strangler fig, y es la que casi nadie completa: el corte final y retirar el legacy. Con el gate de la lección 6 llevaste el traffic_percent hasta 100 —todo el tráfico va al modern, el modern atiende sin fallbacks—. Muchos equipos declaran victoria aquí y siguen con otra cosa. Es un error, y esta lección explica por qué: mientras el legacy siga encendido, la migración no terminó. Sigues pagando por dos sistemas, sigues manteniendo dos rutas, y el monolito que querías reducir sigue del mismo tamaño. El strangler solo paga cuando el árbol viejo se muere y se retira; una higuera que nunca ahoga al anfitrión es solo dos árboles compitiendo por la misma luz, para siempre.
Retirar el legacy tiene un momento y un criterio. El momento se ve en el burn-down: la gráfica descendente de llamadas al legacy a medida que subes el traffic_percent. En 0% el legacy atiende todo; en 50%, la mitad; en 100%, si el modern está completo, cero. El criterio de retiro es simple y duro: el legacy se apaga cuando el burn-down llega a cero —nadie lo llama, ni siquiera por fallback—. Y "retirar" significa algo concreto e incómodo: borrar el código. No comentarlo, no dejarlo en un if False, no "apagarlo por si acaso pero dejarlo ahí". Borrarlo, y simplificar el facade que ya no tiene nada que decidir.
Esta lección ejecuta el burn-down completo —0→10→25→50→75→100— hasta ver las llamadas al legacy llegar a cero, y aplica el gate de retiro. Y nombra el enemigo que este paso combate: la migración eterna, el estado en que el legacy lleva años "prendido por si acaso" con cero tráfico, costando dinero y atención sin dar nada a cambio.
Conexión con el módulo. Las lecciones 4 a 6 desviaron el tráfico y decidieron cuándo subir; esta hace el corte final (fase 4) y retira el legacy. Con ella, el ciclo del strangler se cierra: facade (L2) → construir al lado (L3) → desviar 0→100 (L4-L6) → retirar (L7). La lección 8 integra todo en el catálogo de Mercado. Fíjate en la frontera: aquí usamos el burn-down de llamadas al legacy como la señal para cortar, la última milla del strangler. La disciplina general de medir el progreso de una migración —fitness functions que impiden que el legacy vuelva a crecer, métricas de avance a lo largo de todo un programa de migración, evitar la migración eterna a escala de sistema— es el módulo 7. Aquí, el corte de una rebanada; allá, gobernar la migración completa.
Una analogía: quitar los andamios cuando el edificio se sostiene solo
Cuando construyes o remodelas un edificio, pones andamios: estructuras temporales que sostienen mientras el edificio nuevo aún no aguanta su propio peso. Los andamios son indispensables durante la obra. Pero tienen un destino claro: se quitan cuando el edificio se sostiene solo. Un edificio terminado con los andamios todavía puestos no es "más seguro por si acaso" —es una obra que nadie declaró terminada, que estorba, que cuesta mantener, y que le dice a todo el que pasa que el trabajo no se acabó—.
Imagina un constructor que, por miedo, deja los andamios puestos para siempre "por si el edificio se cae". El edificio lleva años en pie, sólido, lleno de gente que vive y trabaja en él —y los andamios siguen ahí, oxidándose, ocupando la banqueta, costando la renta del alquiler de los andamios cada mes—. En algún momento hay que tomar la decisión que el constructor pospone: el edificio se sostiene solo, quita los andamios.
El legacy, durante la migración, es el andamio: sostiene mientras el modern aún no aguanta todo el tráfico —es el fallback, la red—. Cuando el modern atiende el 100% sin caerse (burn-down en cero), el edificio se sostiene solo. Retirar el legacy es quitar los andamios. Dejarlo prendido "por si acaso" con cero tráfico es el constructor que nunca los quita: paga la renta de dos sistemas para siempre, y nunca declara terminada la obra.
Ejemplo trabajado: el burn-down y el gate de retiro
Vamos a ejecutar el burn-down completo. El modern ya está arreglado (maneja todos los casos, incluido webcam), así que no hay fallbacks. Subimos el traffic_percent por el ramp 0→10→25→50→75→100 y en cada escalón medimos cuántas llamadas recibe el legacy. Al final, el gate de retiro comprueba si el legacy recibió cero llamadas —y si es así, lo retira—.
import zlib
def legacy_catalog(request):
return {"source": "legacy", "product": request["q"]}
def modern_catalog(request):
# Ya arreglado: maneja todos los casos, incluido "webcam".
return {"source": "modern", "product": request["q"]}
def bucket(request_id):
return zlib.crc32(str(request_id).encode()) % 100
def run(requests, traffic_percent):
calls = {"modern": 0, "legacy": 0}
for req in requests:
if bucket(req["id"]) < traffic_percent:
modern_catalog(req)
calls["modern"] += 1
else:
legacy_catalog(req)
calls["legacy"] += 1
return calls
N = 1000
requests = [{"id": i, "q": "webcam" if i % 10 == 0 else "ssd"} for i in range(1, N + 1)]
# --- Burn-down: las llamadas al legacy deben bajar a CERO antes de retirarlo. ---
RAMP = [0, 10, 25, 50, 75, 100]
print(f"Burn-down de llamadas al legacy durante el ramp ({N} req/dia)\n")
print(f"{'dia':>4}{'traffic':>9}{'-> modern':>11}{'-> legacy':>11} llamadas al legacy")
print("-" * 62)
last = None
for day, pct in enumerate(RAMP, start=1):
calls = run(requests, pct)
bar = "#" * (calls["legacy"] * 28 // N)
print(f"{day:>4}{pct:>8}%{calls['modern']:>11}{calls['legacy']:>11} |{bar:<28}|")
last = calls
# --- El gate de retiro: solo se apaga el legacy cuando NADIE lo llama. ---
print("-" * 62)
can_retire = (last["legacy"] == 0)
print(f"\nAl 100%: llamadas al legacy = {last['legacy']}")
print(f"Gate de retiro (llamadas_al_legacy == 0): "
f"{'RETIRAR el legacy' if can_retire else 'NO retirar todavia'}")
if can_retire:
print("\n -> Se borra el modulo legacy del catalog (no se comenta: se BORRA).")
print(" -> El facade se simplifica: ya no decide, siempre llama a modern.")
print(" -> La migracion del catalog TERMINO. Una rebanada menos de monolito.")
print("\n Dejar el legacy 'prendido por si acaso' con 0 llamadas es migracion")
print(" eterna: pagas dos sistemas para siempre. Si el burn-down llego a 0, se apaga.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Burn-down de llamadas al legacy durante el ramp (1000 req/dia)
dia traffic -> modern -> legacy llamadas al legacy
--------------------------------------------------------------
1 0% 0 1000 |############################|
2 10% 109 891 |######################## |
3 25% 271 729 |#################### |
4 50% 520 480 |############# |
5 75% 765 235 |###### |
6 100% 1000 0 | |
--------------------------------------------------------------
Al 100%: llamadas al legacy = 0
Gate de retiro (llamadas_al_legacy == 0): RETIRAR el legacy
-> Se borra el modulo legacy del catalog (no se comenta: se BORRA).
-> El facade se simplifica: ya no decide, siempre llama a modern.
-> La migracion del catalog TERMINO. Una rebanada menos de monolito.
Dejar el legacy 'prendido por si acaso' con 0 llamadas es migracion
eterna: pagas dos sistemas para siempre. Si el burn-down llego a 0, se apaga.
Mira la columna -> legacy de arriba hacia abajo, y mira la barra encogerse: 1000, 891, 729, 480, 235, 0. Eso es un burn-down —la métrica más satisfactoria de una migración, la evidencia visual de que el legacy va muriendo—. Cada escalón del traffic_percent es un mordisco a las llamadas del legacy: al 50% el legacy atiende ~480 (menos de la mitad), al 75% solo ~235, y al 100% ninguna. La barra, que empezó llena, termina vacía. El árbol viejo dejó de recibir savia.
Cuando el burn-down llega a 0, el gate de retiro se activa: llamadas_al_legacy == 0 → RETIRAR el legacy. Y fíjate en lo que "retirar" significa, en las tres líneas del output:
- Se borra el módulo legacy —el código de
legacy_catalogse elimina del repositorio—. No se comenta, no se deja en unif False, no se archiva "por si acaso". Se borra. El código muerto que se deja "por si acaso" es deuda: nadie lo mantiene, se pudre, y el día que alguien lo reactiva por error, reintroduce los bugs que tanto trabajo costó dejar atrás. - El facade se simplifica —ya no tiene dos rutas que decidir; siempre llama al modern—. Con el legacy fuera, el
if bucket < traffic_percentsobra: el facade se vuelve un pasamanos directo al modern, o desaparece por completo si ya no aporta nada. El andamio de decisión también se retira. - La migración del catálogo terminó —una rebanada menos de monolito—. Este es el pago del strangler: el
catalogya no es parte del monolito legacy; es un servicio propio, limpio, entendido y probado. El monolito de Mercado es una rebanada más pequeño.
La última línea es la advertencia central de la lección: dejar el legacy prendido con 0 llamadas es migración eterna. Es el peor de los estados —hiciste todo el trabajo de desviar el tráfico, llegaste al 100%, y no cobras el premio— porque sigues pagando la operación, el mantenimiento y la complejidad de dos sistemas, uno de los cuales no atiende a nadie. El burn-down llegó a cero: apágalo.
Profundización: por qué retirar es tan difícil, y cómo hacerlo seguro
Si retirar el legacy es tan claramente lo correcto, ¿por qué tantos equipos no lo hacen? Por miedo, y el miedo tiene una lógica: "¿y si hay un caso que no vimos, un cliente que llama al legacy por una ruta rara, un proceso nocturno que depende de él?". Ese miedo no es irracional —es el conocimiento tácito del módulo 1—, y la respuesta no es "apágalo y reza", es retirar con evidencia:
Ramp del traffic_percent Retiro con evidencia
───────────────────────── ──────────────────────────────────────
100% + fallback = 0 sostenido el modern atiende TODO, sin huecos
burn-down de llamadas = 0 NADIE llama al legacy, ni por fallback
observado durante N dias no fue un dia tranquilo: incluye picos,
cierres de mes, procesos nocturnos
La clave es el tiempo: el burn-down tiene que estar en cero durante un periodo que cubra los casos raros. Un catálogo puede tener tráfico distinto un lunes que un domingo, o un pico de cierre de mes, o un proceso nocturno que solo corre a las 3 a.m. Si observas el burn-down en cero durante un ciclo completo —días, semanas si hace falta, incluyendo esos momentos raros— la evidencia de que nadie necesita el legacy es sólida. El miedo se combate con datos, no con andamios permanentes.
Y hay una red para el miedo residual: el retiro reversible por etapas. En vez de borrar el legacy de golpe, primero lo dejas en el repositorio pero desconectado del facade durante un periodo corto (el facade ya no lo llama, pero el código existe). Si en ese periodo nada se rompe, entonces lo borras. Es la diferencia entre "apagar" (reversible, un periodo de gracia) y "retirar" (borrar, definitivo). El periodo de gracia da confianza; pero tiene fecha de caducidad —una semana, dos— para no convertirse en la migración eterna que combatimos. "Desconectado dos semanas, y si nada se rompe, se borra" es un plan; "desconectado para siempre por si acaso" es el mismo problema con otro nombre.
Vale la pena ver el ciclo completo del strangler cerrado, ahora que tienes todas las piezas:
flowchart LR
A["1. facade<br/>a 0%"] --> B["2. modern<br/>al lado"]
B --> C["3. desviar<br/>0->100 con gate"]
C --> D["4. burn-down<br/>= 0"]
D --> E["retirar legacy<br/>(borrar + simplificar facade)"]
E --> F["catalog<br/>modernizado"]
Errores comunes
Declarar la migración terminada al llegar a 100% sin retirar el legacy. Qué pasa: el traffic_percent llega a 100, el equipo celebra, y el legacy se queda encendido indefinidamente. Por qué pasa: llegar a 100% se siente como el final, y retirar el legacy es trabajo extra sin recompensa visible inmediata (ningún usuario nota que borraste código muerto). Cómo detectarlo: el legacy sigue desplegado, consumiendo recursos y saliendo en los dashboards, meses después de llegar a 100%. Cómo corregirlo: la migración no termina en 100% de tráfico, termina cuando el legacy está retirado. El pago del strangler —dejar de mantener dos sistemas, encoger el monolito— solo se cobra al retirar. Pon el retiro como el criterio de "terminado" desde el día uno, y no cierres el proyecto de migración hasta que el legacy esté borrado. 100% de tráfico es la penúltima estación, no la última.
Dejar el legacy "apagado pero presente" para siempre. Qué pasa: el equipo desconecta el legacy del facade pero deja el código en el repositorio "por si acaso", indefinidamente. Por qué pasa: borrar código da miedo (¿y si lo necesitamos?), y "apagado pero presente" parece un punto medio prudente. Cómo detectarlo: hay módulos legacy que llevan meses o años sin recibir una llamada pero siguen en el código, sin dueño, sin mantenimiento, saliendo en cada búsqueda y confundiendo a cada desarrollador nuevo. Cómo corregirlo: "apagado pero presente" es válido solo como periodo de gracia con fecha de caducidad (una o dos semanas para ganar confianza). Pasado ese periodo sin incidentes, se borra. El código muerto permanente es deuda: se pudre, se vuelve una trampa (alguien lo reactiva y reintroduce bugs viejos), y contradice la razón de la migración —simplificar—. Un git con historial guarda el código para siempre si de verdad lo necesitas; no hace falta dejarlo vivo en main.
El facade que sobrevive al legacy y se vuelve otro monolito. Qué pasa: retirado el legacy, el facade —que ya no tiene nada que decidir— se queda, y con el tiempo se le va agregando lógica (validación, transformación, más rutas) hasta convertirse en un nuevo punto central enredado. Por qué pasa: el facade ya está ahí, tocando todo el tráfico, y es un lugar tentador para meter lógica transversal. Cómo detectarlo: el facade, que debía ser un pasamanos, acumula responsabilidades y crece; empieza a parecerse al monolito que reemplazaste. Cómo corregirlo: cuando el legacy se retira, el facade también se simplifica o se retira. Si su única razón de existir era decidir entre viejo y nuevo, y ya no hay viejo, el facade sobra: los clientes pueden llamar al modern directo. Si se conserva por otra razón (autenticación, enrutamiento a varios servicios), se mantiene flaco, con una responsabilidad clara. Un facade que se vuelve monolito convierte la migración en un círculo: reemplazaste un monolito por otro con nombre nuevo.
Ejercicios
Ejercicio 1 — Lee el burn-down. En la salida, el burn-down de llamadas al legacy fue 1000, 891, 729, 480, 235, 0 en los escalones 0, 10, 25, 50, 75, 100. (a) ¿Por qué a 50% el legacy atiende 480 y no exactamente 500? (b) ¿Qué significa que la última cifra sea exactamente 0 y no, digamos, 100? (c) Si a 100% el legacy hubiera atendido 100 (por fallbacks), ¿podrías retirarlo? ¿Qué harías?
Ver solución
(a) Porque el bucket reparte por hash: a 50%, aproximadamente la mitad de los ids cae por debajo de 50, pero no exactamente la mitad. Salieron 520 al modern y 480 al legacy —cerca del 50/50, con la variación normal de un hash sobre 1000 ids—. Lo importante es la tendencia descendente, no el número exacto.
(b) Que la última cifra sea 0 significa que el modern está completo: a 100% de tráfico enrutado, no hubo ni una sola llamada al legacy, ni siquiera por fallback. El modern atendió con éxito todas las requests, incluidas las de webcam (que en este ejemplo ya está arreglado). Cero es el criterio de retiro: nadie necesita el legacy. Si hubiera salido 100, significaría que 100 requests aún dependían del legacy —el modern tiene un hueco—.
(c) No podrías retirarlo. Si a 100% el legacy atiende 100 requests por fallback, esas 100 requests son casos que el modern no maneja; apagar el legacy las dejaría sin quién las atienda (se volverían errores 500). Lo que harías es: identificar qué casos son esos 100 fallbacks (con la observabilidad de la lección 6), completar el modern para que los maneje, verificar que el fallback baje a 0, y entonces retirar el legacy. El burn-down a cero es innegociable para el retiro: mientras alguien llame al legacy —aunque sea por fallback—, el legacy es necesario.
Ejercicio 2 — Retirar con evidencia. Un equipo llegó a 100% de tráfico en el modern, con fallback en 0, y quiere retirar el legacy hoy mismo. (a) ¿Qué le falta verificar antes de borrar? (b) ¿Por qué el tiempo de observación importa tanto para el retiro? (c) Describe un plan de retiro reversible por etapas.
Ver solución
(a) Le falta verificar que el burn-down en cero se sostiene en el tiempo, cubriendo los casos raros. Un solo día a 100% con fallback 0 no basta: puede que ese día no haya corrido el proceso nocturno que llama al catálogo, o que no haya sido cierre de mes, o que un cliente que usa una ruta rara no haya entrado. Retirar el mismo día que llegas a 100% es retirar con evidencia insuficiente.
(b) Porque el tráfico de un sistema tiene estacionalidad: un lunes no es un domingo, un día normal no es un cierre de mes, el día no es la madrugada. El legacy podría tener un caso que solo se ejercita en esos momentos raros (un reporte mensual, un batch nocturno, un flujo de un socio que llama una vez por semana). Observar el burn-down en cero durante un ciclo completo —que incluya esos momentos— es lo que da evidencia sólida de que nadie necesita el legacy, no solo "nadie lo necesitó hoy".
(c) Un plan reversible por etapas: (1) Desconectar el legacy del facade —el facade deja de poder enrutar al legacy, pero el código sigue en el repositorio—. (2) Observar durante un periodo con fecha de caducidad (una o dos semanas, cubriendo un cierre de mes y los procesos nocturnos). Si algo se rompe, reconectar es trivial (el código está ahí). (3) Si nada se rompe en el periodo, borrar el código del legacy y simplificar el facade. La etapa (1)-(2) da un periodo de gracia reversible que combate el miedo; la fecha de caducidad de (2) evita que ese periodo se vuelva la migración eterna. El historial de git conserva el código borrado por si algún día se necesita de verdad.
Ejercicio 3 — Diagnostica la migración eterna. En Mercado, el catalog legacy lleva ocho meses desconectado del facade —cero llamadas— pero sigue desplegado y en el código. (a) ¿En qué estado está esta migración? (b) Nombra tres costos concretos de dejarlo así. (c) ¿Qué argumento darías para retirarlo, y cómo responderías al "¿y si lo necesitamos?"?
Ver solución
(a) Está en migración eterna: técnicamente el tráfico se migró (cero llamadas al legacy), pero el legacy nunca se retiró, así que la migración nunca se cerró. Es el peor estado —todo el trabajo hecho, el premio sin cobrar—. Ocho meses de cero llamadas es evidencia más que suficiente de que nadie lo necesita; dejarlo es puro miedo, no prudencia.
(b) Tres costos concretos:
- Costo de operación: el legacy sigue desplegado, consumiendo servidores, memoria, licencias —recursos que se pagan cada mes por un sistema que no atiende a nadie—.
- Costo cognitivo: cada desarrollador nuevo que explora el código encuentra el
cataloglegacy, no sabe que está muerto, y pierde tiempo entendiéndolo o —peor— lo modifica pensando que está vivo. El código muerto confunde y desperdicia atención. - Costo de riesgo: el código muerto sin mantenimiento se pudre (dependencias sin actualizar, vulnerabilidades sin parchar) y es una trampa: alguien podría reactivarlo por error y reintroducir los bugs y comportamientos que la migración dejó atrás.
(c) El argumento: ocho meses de cero llamadas es evidencia abrumadora de que el legacy no se necesita; los tres costos de arriba se pagan cada día que sigue ahí; y el pago de la migración —encoger el monolito, dejar de mantener dos sistemas— no se cobra hasta retirarlo. Al "¿y si lo necesitamos?", la respuesta es doble: primero, ocho meses de datos dicen que no; segundo, el código no se pierde —el historial de git lo guarda—, así que en el caso improbable de necesitarlo, se recupera. Retirarlo no es tirar el código a la basura: es sacarlo del sistema vivo. La reversibilidad existe (git); la migración eterna no tiene por qué.
Resumen y siguiente paso
En esta lección hiciste la última fase del strangler fig: el corte final y retirar el legacy. Viste, con los andamios que se quitan cuando el edificio se sostiene solo, que dejar el legacy encendido "por si acaso" no es prudencia sino migración eterna —el trabajo hecho sin cobrar el premio—. Y lo ejecutaste: el burn-down de llamadas al legacy bajando 1000→891→729→480→235→0 a lo largo del ramp, y el gate de retiro activándose al llegar a cero. Retirar significó tres cosas concretas: borrar el código del legacy (no comentarlo), simplificar el facade que ya no decide, y declarar el catalog una rebanada menos de monolito. Y aprendiste a retirar con evidencia —burn-down en cero sostenido durante un ciclo que cubra los casos raros— y de forma reversible por etapas, para combatir el miedo sin caer en la permanencia.
Antes de avanzar deberías poder: leer un burn-down y entender por qué el cero es el criterio de retiro; explicar por qué 100% de tráfico no es el final, sino retirar el legacy; argumentar por qué "retirar" es borrar y no "apagar por si acaso"; y diagnosticar una migración eterna y defender su cierre frente al "¿y si lo necesitamos?".
La lección 8 es el capstone: modernizar el catalog de Mercado de punta a punta, integrando todo el módulo en una sola simulación ejecutada. Vas a montar un StranglerFacade que combina las cuatro fases —facade + servicio nuevo al lado + desvío por porcentaje con fallback + observabilidad + gate + burn-down + retiro— y a correrlo como un diario de migración día por día: el gate sube el porcentaje solo cuando la ruta nueva está sana, el equipo despliega el arreglo a mitad del ramp, y al llegar a 100% con burn-down en cero se retira el legacy. La entrega será el plan de migración por rebanadas, el código ejecutado, y la justificación de por qué incremental venció al rewrite —el cierre del módulo y el puente a M4 y M5—.
Recursos
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. Fowler enfatiza que la meta del strangler es reemplazar, no coexistir: el sistema viejo debe morir. La base conceptual de por qué retirar es la fase que cierra el patrón. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — la última milla de la migración: retirar la funcionalidad del monolito una vez que el servicio nuevo la atiende por completo, y por qué dejarla "por si acaso" perpetúa el problema. En inglés.
- Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. El resultado del patrón: el monolito se encoge hasta desaparecer (o quedar mínimo) a medida que su funcionalidad se retira. En inglés.
- Paul Hammant, "Legacy Application Strangulation: Case Studies" (2013) — paulhammant.com/2013/07/14/legacy-application-strangulation-case-studies. Casos reales donde el estrangulamiento se completa —el legacy se apaga— frente a los que se quedan a medias. La diferencia entre estrangular y coexistir. En inglés.