Módulo 8: Proyecto — modernizar una rebanada de Mercado
Poner la rebanada tras el facade
Descripción
Con la rebanada elegida y su golden master grabado (lección 2), el segundo paso del método es el módulo 3 hecho acción: poner el catalog detrás de un strangler facade y empezar a desviarle el tráfico. Interpones un router delante del catalog —un proxy que decide, request por request, si la atiende la ruta vieja (el catalog en el monolito) o la nueva (el servicio modern que construyes al lado)— y subes el traffic_percent por escalones: 0, 10, 50, 100. Cada request que desvías al modern es una que dejas de mandar al legacy. Es el momento en que el tráfico real empieza a moverse del viejo al nuevo, controlado por un solo número, con la vieja ruta siempre lista como fallback por si el modern falla.
La seguridad de este paso descansa en dos piezas. La primera es subir por escalones (un canary): mandas primero el 10% al modern y observas; si va bien, subes a 50, luego a 100. Si el modern tiene un problema, solo una fracción de los usuarios lo ve y te enteras a tiempo. La segunda es el fallback: si el modern lanza un error al procesar una request, el router no le devuelve el error al usuario —lo manda al legacy, que sigue ahí y sigue funcionando—. El usuario recibe una respuesta correcta (la del legacy), y el fallo del modern queda registrado solo en tus contadores.
Y aquí está la revelación de esta lección, la que conecta el paso 2 con el paso 1 y con el resto del método: a traffic_percent = 100 —"todo el tráfico al nuevo"— el fallback no llega a cero. Sigue en 100, porque el modern todavía no sabe calcular el descuento por volumen —justo la rareza cuyo golden master congelaste en la lección 2—. Las compras por volumen se desvían al modern, el modern no las sabe atender, y el fallback las manda de vuelta al legacy. El traffic_percent mide lo que intentaste (desviar el 100%); el fallback mide lo que el modern no pudo (las 100 compras de volumen). "El tráfico está en el nuevo" no es "terminado", y el fallback te lo dice con números.
Conexión con el módulo. Este es el segundo paso del método (M3), y produce dos cosas para los pasos siguientes: el tráfico desviado (que la lección 6 medirá como burn-down) y el fallback como detector del hueco del modern (las compras por volumen que la lección 6 cerrará implementando el bulk pricing guiado por el golden master de la lección 2). Fíjate en la frontera: aquí desviamos por porcentaje del tráfico con un facade externo (un proxy delante del catalog). Cuando no puedes poner un proxy externo —porque el código a migrar está enterrado dentro del monolito y no tiene una frontera de red— la técnica es branch by abstraction (módulo 4): una capa de abstracción interna con dos implementaciones tras un flag. El paso 2 del método usa el facade externo porque el catalog sí tiene una frontera clara; branch by abstraction es la alternativa cuando no la hay.
Una analogía: el desvío de una carretera con retorno señalizado
Imagina que construyen una carretera nueva para reemplazar una vieja que pasa por el pueblo. No cierran la vieja de golpe y mandan todo el tránsito a la nueva —si la nueva tiene un tramo sin terminar, se arma un caos—. Ponen un desvío gradual: al principio, solo los coches (no los camiones) toman la nueva; si funciona, agregan más tipos de vehículos; hasta que, eventualmente, todo el tránsito va por la nueva. Y en el punto donde se bifurcan, ponen un retorno señalizado: si un vehículo entra a la nueva y se encuentra con que su tramo aún no está pavimentado, un señalamiento lo regresa a la vieja, que sigue abierta. Nadie se queda varado; en el peor caso, da la vuelta y toma el camino de siempre.
Ahora fíjate en lo que pasa cuando abren el desvío "para todos". El señalamiento del retorno registra cuántos vehículos tuvieron que regresar a la vieja. Si todos los coches pasan bien por la nueva pero todos los camiones dan la vuelta —porque el puente de la carretera nueva todavía no soporta su peso—, ese conteo de retornos te dice algo preciso: la carretera nueva atiende a los coches pero no a los camiones. Aunque hayas "abierto el desvío al 100%", no puedes cerrar la vieja: si lo hicieras, los camiones se quedarían sin por dónde pasar. El conteo del retorno es la evidencia de qué le falta a la nueva.
El strangler facade es ese desvío. El traffic_percent es cuánto tránsito mandas por la carretera nueva (0, 10, 50, 100). El canary es abrir primero solo a los coches, con poco riesgo. Y el fallback es el retorno señalizado: si el modern no sabe atender una request (el camión que el puente no aguanta), el router la regresa al legacy, y nadie se queda varado. Cuando abras el desvío al 100% y veas que el conteo de retornos sigue en 100 —todas las compras por volumen dando la vuelta—, sabrás exactamente qué le falta al modern: el puente para los camiones, es decir, el descuento por volumen.
Ejemplo trabajado: el ramp 0→100 y el fallback que no cierra
Vamos a ejecutar el desvío completo. Tenemos el legacy_catalog —sólido, maneja todos los casos, incluido el descuento por volumen con su rareza— y el modern_catalog —nuevo y limpio, pero con un hueco: aún no implementa el bulk pricing y lanza un error cuando la cantidad es 10 o más—. El strangler_router enruta por porcentaje usando el bucket estable de la request, y envuelve la llamada al modern en un try/except: si el modern falla, cae al legacy y lo cuenta como fallback. Corremos un lote fijo de 1000 requests donde 1 de cada 10 es una compra por volumen (cantidad 12), y subimos el traffic_percent por los escalones 0, 10, 50, 100.
# Paso 2 del metodo: poner el catalog tras un strangler facade y desviar el
# trafico por porcentaje, con la vieja ruta como fallback. Revelacion: a 100%
# el fallback sigue en 100 porque el modern aun no cubre el descuento por volumen.
import math
import zlib
TAX_RATE = 0.16
BULK_MIN_QTY = 10
BULK_DISCOUNT = 0.05
# === Legacy: solido, maneja TODOS los casos (normal y bulk con su rareza). ===
def legacy_catalog(request):
p, q = request["unit_price"], request["quantity"]
subtotal = p * q
if q >= BULK_MIN_QTY:
subtotal = math.floor(subtotal * (1 - BULK_DISCOUNT) / 10) * 10
return {"status": 200, "source": "legacy", "price": math.floor(subtotal * (1 + TAX_RATE))}
# === Modern: nuevo, limpio... pero AUN no implementa el descuento por volumen. ===
def modern_catalog(request):
if request["quantity"] >= BULK_MIN_QTY:
raise ValueError("modern: bulk pricing no implementado todavia")
p, q = request["unit_price"], request["quantity"]
return {"status": 200, "source": "modern", "price": math.floor(p * q * (1 + TAX_RATE))}
# === El strangler router: enruta por porcentaje, con FALLBACK al legacy. ===
def bucket(request_id):
return zlib.crc32(str(request_id).encode()) % 100
def strangler_router(request, traffic_percent, counters):
if bucket(request["id"]) < traffic_percent:
try:
resp = modern_catalog(request) # intenta la ruta nueva
counters["modern_ok"] += 1
return resp
except Exception:
counters["fallback"] += 1 # el modern no supo: cae al viejo
return legacy_catalog(request)
counters["legacy"] += 1
return legacy_catalog(request)
# --- Un lote fijo: 1 de cada 10 requests es una compra por volumen (qty>=10). ---
N = 1000
requests = [{"id": i, "unit_price": 3499,
"quantity": 12 if i % 10 == 0 else 1} for i in range(1, N + 1)]
print(f"Subiendo traffic_percent 0 -> 10 -> 50 -> 100 ({N} requests por nivel)\n")
print(f"{'traffic':>8}{'modern_ok':>11}{'fallback':>10}{'legacy':>8}"
f"{'servidas x legacy':>19}")
print("-" * 56)
for pct in (0, 10, 50, 100):
counters = {"modern_ok": 0, "fallback": 0, "legacy": 0}
for req in requests:
strangler_router(req, pct, counters)
served_by_legacy = counters["legacy"] + counters["fallback"]
print(f"{pct:>7}%{counters['modern_ok']:>11}{counters['fallback']:>10}"
f"{counters['legacy']:>8}{served_by_legacy:>19}")
print("-" * 56)
print("\n A 100%: modern sirve la mayoria, pero las ~100 compras por volumen")
print(" siguen cayendo al legacy por el FALLBACK -> el modern aun NO cubre el bulk.")
print(" El traffic_percent mide lo que intentaste (100%); el fallback mide lo que")
print(" el modern no pudo (100). No se puede retirar el legacy mientras el bulk caiga.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Subiendo traffic_percent 0 -> 10 -> 50 -> 100 (1000 requests por nivel)
traffic modern_ok fallback legacy servidas x legacy
--------------------------------------------------------
0% 0 0 1000 1000
10% 99 10 891 901
50% 471 49 480 529
100% 900 100 0 100
--------------------------------------------------------
A 100%: modern sirve la mayoria, pero las ~100 compras por volumen
siguen cayendo al legacy por el FALLBACK -> el modern aun NO cubre el bulk.
El traffic_percent mide lo que intentaste (100%); el fallback mide lo que
el modern no pudo (100). No se puede retirar el legacy mientras el bulk caiga.
Lee la tabla escalón por escalón, porque cada renglón es una parte del desvío.
En 0%, las 1000 requests van al legacy (legacy=1000): la llave está cerrada, todo el tráfico sigue por la ruta vieja. En 10%, el bucket manda 109 requests a la ruta del modern: 99 tienen éxito (modern_ok=99) y 10 caen en fallback —las compras por volumen que el modern no supo atender, desviadas al legacy por el try/except—. Fíjate en que esas 10 requests no fueron un error para el usuario: el modern falló, pero el fallback las atendió con el legacy, así que el cliente recibió su precio correcto. El fallo quedó invisible para él y visible solo en tu contador.
En 50%, el bucket selecciona ~520 requests para el modern: 471 tienen éxito y 49 caen en fallback (las compras por volumen de la mitad seleccionada). Y en 100%, el bucket selecciona todas las 1000: 900 tienen éxito en el modern y 100 caen en fallback —las 100 compras por volumen de todo el lote—. La columna legacy (requests elegidas directamente para el viejo) llega a 0: ninguna request se enrutó al legacy a propósito.
Aquí está la revelación, en la columna servidas x legacy (que suma legacy + fallback). A 100%, esa columna marca 100, no 0. Aunque desviaste "todo el tráfico al nuevo", 100 requests siguieron siendo atendidas por el legacy —vía fallback, porque el modern no supo calcular su precio por volumen—. Y esto no es un accidente: es exactamente el hueco que la lección 2 anticipó. El descuento por volumen —cuyo golden master congelaste, con su truncado raro incluido— es el caso que el modern todavía no implementa. El fallback hizo dos cosas a la vez: te protegió (ningún usuario vio un error de precio) y te avisó (el modern tiene un hueco exactamente en el bulk). No puedes retirar el legacy: si lo apagaras ahora, esas 100 compras por volumen no tendrían quién las atienda.
El punto que este paso graba en tu cabeza para el resto del método: el traffic_percent no es la métrica de terminación. Llegó a 100 y la migración está lejos de terminar. La métrica que importa es el fallback (o servidas x legacy) a 100%: mientras sea mayor que cero, el modern tiene huecos que el legacy está tapando. El burn-down de la lección 6 va a seguir ese número hacia cero, y solo cuando llegue —cuando el modern implemente el bulk pricing guiado por el golden master— se podrá apagar el legacy.
Profundización: el fallback como detector, y cuándo conviene branch by abstraction
Dos ideas de este paso merecen desarrollarse: qué te está diciendo el fallback, y qué haces cuando no puedes poner un facade externo.
El fallback es una red y un detector. La función obvia del fallback es la red de seguridad: si el modern falla, el usuario no se queda sin respuesta. La menos obvia —y más valiosa para el método— es que el fallback es un detector de completitud alimentado por el tráfico real. Cada request que cae en fallback es una prueba, tomada de la producción, de que el modern tiene un caso que aún no maneja. No adivinas qué le falta al modern con tu imaginación; el tráfico real te lo ilumina. En este ejemplo, el fallback a 100% señala con precisión que lo que falta es el bulk pricing —y cuántas requests lo necesitan (100 de 1000, el 10%)—.
request al catalog
│
bucket < traffic_percent?
│ si │ no
▼ ▼
modern_catalog legacy_catalog (elegida para el viejo)
│
┌────┴─────┐
ok error (bulk no implementado)
│ │
respuesta FALLBACK ──> legacy_catalog
del modern (+cuenta: "al modern le falta el bulk")
Hay una regla de higiene sobre el fallback: es para errores inesperados del modern, no para casos que ya sabes que el modern no maneja. Si sabes que el modern no implementa el bulk, dejarlo fallar y caer en fallback en cada compra por volumen funciona, pero paga el costo de intentar el modern y luego el legacy en cada una. Una vez que el hueco está identificado (como aquí), lo más limpio es no enrutar las compras por volumen al modern hasta que las implemente —una decisión por ruta— y reservar el fallback para lo que de verdad no viste venir. El fallback es la red para lo imprevisto; una vez que el hueco es conocido, se cierra enrutando alrededor de él o —mejor— implementándolo.
Cuándo conviene branch by abstraction en vez del facade externo. El strangler facade de este paso es un proxy externo: se para delante del catalog y decide por request. Eso funciona porque el catalog tiene una frontera clara —se le puede poner algo enfrente—. Pero no todo el código a migrar la tiene. A veces lo que quieres reemplazar está enterrado dentro del monolito —una función que se llama desde cientos de lugares internos, sin una frontera de red donde interponer un proxy—. Ahí el facade externo no aplica, y la técnica es branch by abstraction (módulo 4): insertas una capa de abstracción en el código, pones dos implementaciones detrás de ella (la legacy y la modern) intercambiables por un flag, y migras la implementación por dentro sin tocar a los cientos de llamadores. La diferencia es dónde vive el desvío: el facade lo pone afuera (por tráfico, en la red); branch by abstraction lo pone adentro (por flag, en el código). El catalog de este capstone usa el facade porque tiene frontera; si estuvieras migrando una función de cálculo interna sin frontera, usarías branch by abstraction. Las dos son el mismo principio —desviar incrementalmente con la vieja implementación disponible—, aplicado según haya o no una frontera donde interponer el proxy.
Errores comunes
Saltar del 0% al 100% sin canary. Qué pasa: confiado en que el modern "ya está probado", el equipo pone el traffic_percent en 100 de un solo cambio. Por qué pasa: los escalones intermedios se sienten lentos —"si funciona, funciona"—. Cómo detectarlo: no hay un periodo de 10% ni de 50% en el historial; el tráfico pasó de todo-viejo a todo-nuevo en un despliegue. Cómo corregirlo: el punto del canary es descubrir los problemas del modern con poco tráfico expuesto. A 10%, un bug del modern afecta al 10% de los usuarios y te da tiempo de reaccionar; a 100%, afecta a todos a la vez —el big-bang que el strangler existe para evitar—. Sube por escalones, observa en cada uno, y solo sube al siguiente cuando el actual esté sano. La velocidad de subir no es una virtud; la seguridad de subir sí.
Desviar tráfico sin fallback. Qué pasa: el router manda el porcentaje al modern y, si el modern falla, le devuelve el error al usuario. Por qué pasa: el fallback es código extra (el try/except, la segunda llamada) y se omite "para simplificar". Cómo detectarlo: cuando el modern falla en producción, aparecen errores 500 visibles para los usuarios en la fracción de tráfico desviada. Cómo corregirlo: el fallback no es opcional en un strangler —es lo que hace que desviar tráfico sea seguro—. Sin él, cada request al modern es una apuesta sin red: si el modern tiene un caso no manejado (como el bulk), el usuario ve el error. Con fallback, el peor caso es una respuesta del legacy (correcta, quizás más lenta) y un contador que se incrementa. En este ejemplo, sin fallback las 100 compras por volumen habrían sido 100 errores de precio en producción.
Confundir "100% de tráfico enrutado" con "el modern está completo". Qué pasa: el equipo ve el traffic_percent en 100 y declara la migración lista para retirar el legacy. Por qué pasa: "100%" suena a final. Cómo detectarlo: aunque el traffic_percent sea 100, la columna de fallback (o servidas x legacy) no es 0 —como en el ejemplo, donde a 100% aún caían 100 requests al legacy—. Cómo corregirlo: el criterio para retirar el legacy no es "el traffic_percent llegó a 100", es "el legacy ya no atiende una sola request, ni siquiera por fallback". Mientras el fallback a 100% sea mayor que 0, el modern tiene huecos que el legacy está tapando, y apagar el legacy dejaría esas requests sin atender. El traffic_percent mide lo que intentaste; el fallback mide lo que el modern no pudo. Este es, exactamente, el error que la lección 7 (declarar el done) pone como criterio: fallback en 0 es una de las condiciones de terminación.
Ejercicios
Ejercicio 1 — Lee el reparto con fallback. En la salida, a traffic_percent=10 hubo modern_ok=99, fallback=10, legacy=891. (a) ¿Cuántas requests fueron enrutadas al modern en total? (b) ¿Por qué 10 de ellas terminaron en el legacy? (c) Si el modern implementara el bulk pricing, ¿cuánto valdría fallback a 10%?
Ver solución
(a) Fueron enrutadas al modern 109 requests: las que tuvieron éxito (modern_ok=99) más las que cayeron en fallback tras intentar el modern (fallback=10). El bucket seleccionó 109 para la ruta nueva; de esas, 99 el modern las resolvió y 10 fallaron. 99 + 10 = 109, aproximadamente el 10% de 1000.
(b) Porque esas 10 requests eran compras por volumen (cantidad 12 ≥ 10), el caso que el modern no implementa (lanza ValueError). Fueron elegidas para el modern por su bucket, el modern intentó atenderlas y falló, y el try/except las desvió al legacy —el fallback—. El usuario recibió el precio del legacy; el fallo quedó solo en el contador.
(c) Si el modern implementara el bulk pricing, el fallback a 10% valdría 0 (ya no falla ninguna compra por volumen) y modern_ok valdría 109 (las 109 requests enrutadas al modern, todas exitosas). Ese es el estado sano que permite subir el porcentaje con confianza y, a 100%, retirar el legacy —el objetivo del burn-down de la lección 6—.
Ejercicio 2 — El fallback como detector. El texto dice que el fallback "te protege Y te avisa". (a) Explica qué significa cada parte con las 100 requests que cayeron en fallback a 100%. (b) ¿Cómo se conecta este fallback con el golden master de la lección 2? (c) ¿Qué métrica seguirá el burn-down de la lección 6 hacia cero?
Ver solución
(a) Te protege: esas 100 compras por volumen habrían sido errores de precio para los usuarios si el modern las hubiera atendido sin red; en cambio, el fallback las mandó al legacy y los usuarios recibieron su precio correcto. Nadie se vio afectado. Te avisa: el contador fallback=100 a 100% de tráfico es la evidencia, tomada del tráfico real, de que el modern tiene un caso —el bulk pricing— que aún no maneja, y de que son 100 de cada 1000 requests las que lo necesitan.
(b) El caso que cae en fallback es exactamente la rareza cuyo golden master congelaste en la lección 2: el descuento por volumen con su truncado raro. El fallback te dice que el modern no lo cubre; el golden master te dice cómo debe calcularlo (el número exacto, rareza incluida) cuando llegue el momento de implementarlo. Los dos artefactos trabajan juntos: el fallback detecta el hueco, el golden master es la especificación para cerrarlo bien.
(c) El burn-down de la lección 6 seguirá el servidas x legacy (o el fallback a 100%) hacia cero. Ese número —las requests que el legacy todavía atiende, sea por ruta directa o por fallback— es lo que mide cuánto le falta al modern. Cuando llegue a 0, el modern atiende todo y el legacy se puede retirar (lección 7). El traffic_percent ya está en 100; lo que falta bajar es el fallback.
Ejercicio 3 — Facade externo o branch by abstraction. Para cada situación, di qué técnica del método usarías —el strangler facade externo (M3) o branch by abstraction (M4)— y por qué: (a) el catalog de Mercado, al que se accede por un endpoint HTTP claro; (b) una función interna calculate_price() que se llama desde 200 lugares dentro del monolito, sin frontera de red; (c) un módulo de reportes que otros equipos consultan por una API.
Ver solución
(a) Strangler facade externo (M3). El catalog tiene una frontera clara —un endpoint HTTP— donde se puede interponer un proxy que decida viejo vs nuevo por request. Es el caso de este capstone: el facade externo desvía el tráfico por porcentaje, con fallback.
(b) Branch by abstraction (M4). calculate_price() está enterrada dentro del monolito, llamada desde 200 lugares internos, sin una frontera de red donde poner un proxy externo. Aquí insertas una capa de abstracción en el código, pones dos implementaciones (legacy y modern) detrás de ella tras un flag, y migras la implementación por dentro sin tocar a los 200 llamadores. El desvío vive adentro (por flag), no afuera (por tráfico).
(c) Strangler facade externo (M3). Un módulo de reportes consultado por una API tiene frontera de red (la API), así que se le puede poner un facade delante que enrute a la implementación vieja o a la nueva. Como el catalog, tiene un punto claro donde interponer el proxy.
La regla: si hay una frontera donde interponer un proxy (un endpoint, una API), usas el facade externo; si el código está enterrado sin frontera, usas branch by abstraction. Las dos desvían incrementalmente con la vieja implementación disponible; cambia dónde vive el desvío.
Resumen y siguiente paso
En esta lección diste el segundo paso del método: poner el catalog tras un strangler facade (módulo 3). Interpusiste un router delante de la rebanada y desviaste el tráfico por porcentaje de 0 a 100, subiendo por escalones (canary) con la vieja ruta como fallback. Viste, con el desvío de carretera y su retorno señalizado, que subir de a poco y mantener el fallback conectado hace seguro el desvío: si el modern falla, el legacy atiende y nadie se queda varado. Y ejecutaste el ramp sobre 1000 requests, descubriendo la revelación central: a traffic_percent = 100, el fallback sigue en 100 porque el modern aún no cubre el descuento por volumen —la misma rareza cuyo golden master congelaste en la lección 2—. El traffic_percent mide lo que intentaste; el fallback mide lo que el modern no pudo, y mientras el fallback sea mayor que cero, el legacy no se puede retirar.
Antes de avanzar deberías poder: explicar por qué se sube por escalones y no se salta al 100%; describir la doble función del fallback (red y detector); argumentar por qué "100% de tráfico enrutado" no es "el modern está completo"; y decidir cuándo usar el facade externo y cuándo branch by abstraction.
La lección 4 da el tercer paso: extraer el servicio con un anti-corruption layer (módulo 5). Hasta ahora el modern corre "al lado" del legacy detrás del facade; ahora vas a sacar el catalog a su propio servicio, con un ACL que traduce el modelo viejo del monolito (prod_id, desc, prc_cents como string) al modelo limpio (Product) y de vuelta. Vas a ejecutar el diario de extracción por fases —función interna → ACL sobre la BD compartida → BD propia— y verificar que el monolito responde idéntico en las cuatro fases (el round-trip del ACL en verde) mientras sus tripas cambian por completo. El facade desvió el tráfico; el ACL extrae el servicio sin que el monolito se entere.
Recursos
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. El patrón que da nombre a la guía: poner un facade delante del legacy y desviar el tráfico incrementalmente hasta que el viejo atienda nada. El segundo paso del método es esta entrada ejecutada. En inglés.
- Martin Fowler, "BranchByAbstraction" — martinfowler.com/bliki/BranchByAbstraction.html. La alternativa al facade externo cuando el código no tiene frontera de red: una capa de abstracción interna con dos implementaciones tras un flag. En inglés.
- Martin Fowler, "CanaryRelease" — martinfowler.com/bliki/CanaryRelease.html. Exponer la versión nueva a una fracción pequeña del tráfico antes de ir a todos: la razón de subir el
traffic_percentpor escalones. En inglés. - Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. El desvío gradual de funcionalidad del monolito a los servicios nuevos, con la vieja implementación como respaldo. En inglés.