Módulo 3: El patrón strangler fig
Formas de desviar: por ruta, por porcentaje, por flag
Descripción
En la lección anterior desviaste tráfico por porcentaje: el bucket de cada request decidía si iba al legacy o al modern, y subías un solo número —el traffic_percent— para mover el tráfico del viejo al nuevo. El porcentaje es la forma más conocida de desviar, pero no es la única. Esta lección abre las tres formas de desviar tráfico en un strangler fig, y —más importante— cuándo conviene cada una:
- Por ruta/endpoint. Decides por el camino de la request:
GET /products(el listado) va al modern,GET /products/{id}(el detalle) sigue en el legacy. Grano grueso, un endpoint entero a la vez, todo o nada por camino. - Por porcentaje/canary. Decides por una fracción del tráfico, como en la lección 4. Gradual, una muestra del tráfico, ideal para subir de a poco. Con un matiz que esta lección introduce: el bucket se calcula sobre el usuario, no sobre la request, para que un mismo usuario no salte de ruta a media sesión.
- Por feature flag/usuario. Decides por quién hace la request: solo los usuarios internos (staff) y los beta ven el modern; el resto sigue en el legacy. Dirigido, fuera del azar del porcentaje, perfecto para probar con gente que sabe que está probando.
Las tres son legítimas y a menudo se combinan. La lección las ejecuta sobre la misma cola de requests para ver cómo cada una reparte el tráfico distinto, y se detiene en una propiedad crítica que solo tienen si se diseñan bien: la estabilidad 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 —sería confuso y haría los bugs imposibles de reproducir—. Vas a ver, medido, cómo el bucket pegado al usuario garantiza que cada persona caiga siempre en la misma ruta.
Conexión con el módulo. La lección 4 desvió por porcentaje sobre la request; esta muestra las tres estrategias y cuándo usar cada una, con el porcentaje ya pegado al usuario. La lección 6 usa la observabilidad de las dos rutas para decidir cuándo subir el porcentaje (independiente de qué estrategia elijas); la 7 lleva cualquiera de las tres a su final y retira el legacy. Fíjate en la frontera: aquí decidimos cómo elegir la ruta (el criterio del desvío). Cuándo es seguro subir ese desvío —con qué métricas— es la lección 6. Y todas estas estrategias son variantes de la misma fase 3 del strangler; el mecanismo del router (facade + fallback) es el de la lección 4.
Una analogía: tres formas de repartir a los clientes entre dos filas
Imagina un banco con dos cajas: la caja vieja (lenta pero probada) y la caja nueva (que estás estrenando). Un empleado en la entrada reparte a los clientes entre las dos filas. Tiene tres criterios posibles para repartir, y cada uno tiene sentido en un momento distinto.
Por tipo de trámite (por ruta). "Los depósitos, a la caja nueva; los retiros, a la vieja." Reparte por el tipo de operación. Es grano grueso: todos los depósitos van al nuevo, todos los retiros al viejo. Útil cuando la caja nueva ya sabe hacer un tipo de trámite completo y bien, pero aún no los otros.
Por conteo (por porcentaje). "Uno de cada diez clientes, a la caja nueva; el resto, a la vieja." Reparte por fracción, sin importar el trámite ni la persona. Útil para probar la caja nueva con poca gente primero y subir de a poco. Pero ojo: si María va, deposita, sale, y vuelve a entrar, ¿debería ir a la misma caja? Sí —para no confundirla—, así que el reparto por conteo conviene atarlo a la identidad del cliente, no a un dado que se tira en cada visita.
Por lista (por feature flag). "Los empleados del banco y los clientes que se anotaron al programa piloto, a la caja nueva; el resto, a la vieja." Reparte por quién es el cliente. Dirigido: le das el nuevo a gente que sabe que está probando y que te va a reportar los problemas. Fuera del azar: no depende de la suerte del conteo, depende de una lista.
El empleado de la entrada es el router. El tipo de trámite es la ruta; el conteo es el porcentaje; la lista es el feature flag. Y "que María vaya siempre a la misma caja" es la estabilidad por usuario que esta lección mide.
Ejemplo trabajado: las tres estrategias sobre la misma cola
Vamos a ejecutar las tres estrategias sobre la misma cola de requests —cuatro usuarios, cada uno pegando el listado (GET /products) o el detalle (GET /products/42)— y a ver cómo cada una decide distinto. La estrategia por porcentaje calcula el bucket sobre el usuario (no la request), de modo que un usuario cae siempre en la misma ruta. Al final, verificamos esa estabilidad: mandamos a un mismo usuario tres veces y comprobamos que no salta de ruta.
import zlib
def bucket(key):
return zlib.crc32(str(key).encode()) % 100
# --- Tres estrategias para decidir legacy vs modern. Todas deterministas. ---
# (A) Por RUTA/endpoint: el listado ya va al nuevo; el detalle, aun al viejo.
def route_by_endpoint(request):
return "modern" if request["path"] == "GET /products" else "legacy"
# (B) Por PORCENTAJE/canary, pegado al USUARIO: el mismo user cae siempre igual
# (no salta entre viejo y nuevo a media sesion).
def route_by_percentage(request, traffic_percent):
return "modern" if bucket(request["user"]) < traffic_percent else "legacy"
# (C) Por FEATURE FLAG/usuario: solo los usuarios beta ven el nuevo servicio.
BETA_USERS = {"u-staff", "u-beta"}
def route_by_feature_flag(request):
return "modern" if request["user"] in BETA_USERS else "legacy"
# --- Un lote fijo: 4 usuarios, cada uno pega listado y detalle. ---
requests = [
{"user": "u-alice", "path": "GET /products"},
{"user": "u-alice", "path": "GET /products/42"},
{"user": "u-beta", "path": "GET /products"},
{"user": "u-beta", "path": "GET /products/42"},
{"user": "u-bob", "path": "GET /products"},
{"user": "u-staff", "path": "GET /products/42"},
]
print("Misma cola de requests, tres estrategias de desvio (traffic_percent=50)\n")
print(f"{'user':<9}{'path':<20}{'por ruta':>10}{'por %':>8}{'por flag':>10}")
print("-" * 57)
for req in requests:
a = route_by_endpoint(req)
b = route_by_percentage(req, traffic_percent=50)
c = route_by_feature_flag(req)
print(f"{req['user']:<9}{req['path']:<20}{a:>10}{b:>8}{c:>10}")
# --- Estabilidad: el mismo usuario, 3 requests, no debe saltar de ruta. ---
print("\nEstabilidad por usuario (por %): u-alice pega 3 veces seguidas")
routes = [route_by_percentage({"user": "u-alice"}, 50) for _ in range(3)]
print(f" u-alice -> {routes} (bucket={bucket('u-alice')}, estable: {len(set(routes))==1})")
print("\n Por ruta: grano grueso, un endpoint entero a la vez.")
print(" Por %: canary gradual; pegado al user, sin saltos a media sesion.")
print(" Por flag: dirigido (staff/beta primero), fuera del azar del porcentaje.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Misma cola de requests, tres estrategias de desvio (traffic_percent=50)
user path por ruta por % por flag
---------------------------------------------------------
u-alice GET /products modern modern legacy
u-alice GET /products/42 legacy modern legacy
u-beta GET /products modern modern modern
u-beta GET /products/42 legacy modern modern
u-bob GET /products modern legacy legacy
u-staff GET /products/42 legacy legacy modern
Estabilidad por usuario (por %): u-alice pega 3 veces seguidas
u-alice -> ['modern', 'modern', 'modern'] (bucket=32, estable: True)
Por ruta: grano grueso, un endpoint entero a la vez.
Por %: canary gradual; pegado al user, sin saltos a media sesion.
Por flag: dirigido (staff/beta primero), fuera del azar del porcentaje.
Lee la tabla por columnas, porque cada columna es una estrategia con su propia lógica.
La columna por ruta decide solo por el path: todas las requests a GET /products (el listado) van al modern, todas las de GET /products/42 (el detalle) al legacy. Fíjate que no le importa quién hace la request ni el azar: u-alice, u-beta, u-bob —todos— van al modern cuando piden el listado, y al legacy cuando piden el detalle. Es grano grueso: migraste el endpoint del listado por completo, y el del detalle aún no. Ideal cuando el modern ya sabe hacer un endpoint entero bien pero otros no.
La columna por % decide por el bucket del usuario con traffic_percent=50. u-alice tiene bucket 32 (menor que 50) → siempre modern; u-beta tiene bucket menor que 50 → modern; u-bob tiene bucket mayor o igual a 50 → legacy; u-staff idem → legacy. Y aquí está lo crucial: mira las dos filas de u-alice. En la columna "por ruta", u-alice va a modern en una fila y a legacy en la otra (porque cambió el endpoint). Pero en la columna "por %", u-alice va a modern en las dos filas —porque el bucket se calcula sobre el usuario, no sobre la request—. u-alice cae siempre en el mismo lado, sin importar qué pida. Esa es la estabilidad por usuario.
La columna por flag decide por la lista BETA_USERS = {"u-staff", "u-beta"}. Solo u-beta y u-staff ven el modern; u-alice y u-bob —por más buckets que tengan— van al legacy, porque no están en la lista. Fíjate que es ortogonal al porcentaje: u-staff va al legacy en la columna "por %" (su bucket es alto) pero al modern en la columna "por flag" (está en la lista beta). El feature flag ignora el azar y elige por identidad.
Y abajo, la prueba de estabilidad: mandamos a u-alice tres veces por la estrategia de porcentaje y obtenemos ['modern', 'modern', 'modern'] —estable: True—. Su bucket (32) no cambia entre requests, así que cae en modern las tres veces. Si el bucket se calculara sobre algo que cambia en cada request (un id de request aleatorio, la hora), u-alice podría caer en modern, luego legacy, luego modern —saltando de ruta a media sesión—, que es justo lo que la estabilidad por usuario evita.
Profundización: cuándo cada estrategia, y por qué la estabilidad importa
Las tres estrategias no compiten: se usan en momentos distintos de la migración, y a menudo juntas.
Estrategia Decide por Granularidad Mejor para
────────────── ───────────── ───────────── ───────────────────────────────
por ruta el endpoint gruesa migrar un endpoint completo a
la vez; frontera clara
por porcentaje fraccion del fina canary: subir 0->100 de a poco,
trafico (user) muestra aleatoria del trafico
por flag identidad del dirigida probar con staff/beta primero;
usuario dogfooding antes del canary
Una secuencia típica combina las tres: primero enciendes el modern por feature flag solo para el staff (dogfooding: el equipo usa su propio producto y encuentra los bugs obvios). Cuando el staff está contento, abres por porcentaje un canary del 1% de usuarios reales, y subes 1→10→50→100. Y si el modern solo cubre un endpoint, usas por ruta para mandar solo ese endpoint al modern mientras los demás siguen en el legacy. Cada estrategia resuelve una pregunta distinta: qué (ruta), cuánto (porcentaje), quién (flag).
La estabilidad por usuario merece su propio párrafo porque es donde más strangler mal diseñados fallan. Si el desvío por porcentaje calcula el bucket sobre algo que cambia entre requests del mismo usuario, la persona salta de ruta:
Bucket sobre la REQUEST (inestable): Bucket sobre el USUARIO (estable):
u-alice, req#1 -> bucket 12 -> modern u-alice, req#1 -> bucket 32 -> modern
u-alice, req#2 -> bucket 88 -> legacy u-alice, req#2 -> bucket 32 -> modern
u-alice, req#3 -> bucket 45 -> modern u-alice, req#3 -> bucket 32 -> modern
(salta viejo/nuevo a media sesion) (siempre la misma ruta)
¿Por qué importa tanto? Tres razones. Primero, coherencia de experiencia: si el modern y el legacy tienen diferencias sutiles (un orden distinto, un campo extra), un usuario que salta entre los dos ve el catálogo "parpadear" entre dos versiones. Segundo, reproducibilidad de bugs: si un usuario reporta un problema, necesitas saber qué ruta le tocó; si salta en cada request, el bug es imposible de reproducir. Tercero, estado de sesión: si el modern y el legacy manejan sesión o caché distinto, saltar entre ellos puede corromper el estado del usuario. La regla es dura: el bucket del desvío por porcentaje se calcula sobre un identificador estable del usuario (su id, su sesión), nunca sobre algo que cambia por request. Un usuario, una ruta.
Errores comunes
Calcular el bucket del porcentaje sobre la request en vez del usuario. Qué pasa: el router calcula el bucket con un id aleatorio de request, o con la hora, así que un mismo usuario cae unas veces en modern y otras en legacy. Por qué pasa: es lo más fácil de escribir —cada request trae su propio id— y a 10% "reparte bien" en agregado, así que el problema no se ve en las métricas. Cómo detectarlo: un usuario reporta que "a veces el catálogo se ve distinto" o que un bug "aparece y desaparece"; al investigar, descubres que salta de ruta entre requests. Cómo corregirlo: calcula el bucket sobre un identificador estable del usuario (bucket(user_id)), no sobre la request. Así, aunque el usuario haga mil requests, todas caen en la misma ruta mientras el traffic_percent no cambie. La estabilidad por usuario no es un lujo: es lo que hace la experiencia coherente y los bugs reproducibles.
Elegir la estrategia equivocada para el estado del modern. Qué pasa: el equipo desvía por porcentaje (una muestra de todos los endpoints) cuando el modern solo implementa bien un endpoint. Resultado: las requests a los endpoints que el modern no cubre caen en fallback constantemente. Por qué pasa: el porcentaje es la estrategia "por defecto" y se aplica sin pensar en qué cubre el modern. Cómo detectarlo: el fallback está alto para ciertos endpoints y bajo para otros —señal de que el modern cubre unos sí y otros no—. Cómo corregirlo: si el modern cubre un endpoint completo pero no otros, desvía por ruta: manda solo ese endpoint al modern y deja los demás en el legacy hasta que el modern los implemente. La estrategia debe coincidir con lo que el modern sabe hacer: por ruta cuando la cobertura es por endpoint, por porcentaje cuando el modern cubre todo y quieres un canary gradual.
Quedarse para siempre en feature flag "solo para beta". Qué pasa: el modern lleva meses encendido solo para los usuarios beta, y nunca se abre al tráfico general. Por qué pasa: el feature flag para beta es cómodo y seguro —nadie se queja porque los beta saben que están probando— y quita la presión de completar la migración. Cómo detectarlo: el conjunto BETA_USERS no crece hacia "todos" con el tiempo; la migración está congelada en "piloto permanente". Cómo corregirlo: el feature flag para staff/beta es la primera fase (dogfooding), no la última. Su propósito es encontrar los bugs obvios con gente tolerante antes de abrir el canary por porcentaje a usuarios reales. Ponle una fecha: "dos semanas de beta, y luego abrimos el canary al 1%". Un flag de beta que nunca se abre es otra forma de migración eterna —el legacy sigue sirviendo al 99% para siempre—, que la lección 7 combate.
Ejercicios
Ejercicio 1 — Elige la estrategia. Para cada situación, di qué estrategia de desvío usarías (por ruta, por porcentaje, por feature flag) y por qué: (a) el modern implementa GET /products completo pero aún no GET /products/{id}; (b) quieres que el equipo de desarrollo pruebe el modern en producción antes que nadie; (c) el modern cubre todos los endpoints y quieres subir el tráfico del 1% al 100% de forma gradual y medida.
Ver solución
- (a) Por ruta. El modern cubre un endpoint (
GET /products) pero no otro (GET /products/{id}). Desvía por ruta: mandaGET /productsal modern yGET /products/{id}al legacy. Así el modern solo recibe el tráfico que sabe atender, sin fallbacks constantes en el endpoint que no cubre. Cuando el modern implemente el detalle, agregas esa ruta. - (b) Por feature flag. Quieres que un grupo específico —el equipo de desarrollo— vea el modern antes que el público. Un feature flag con los usuarios internos en la lista les da el modern a ellos y deja a todos los demás en el legacy. Es el dogfooding: el equipo encuentra los bugs obvios con su propio uso antes de exponer a usuarios reales.
- (c) Por porcentaje (canary), pegado al usuario. El modern cubre todo y quieres un canary gradual: subir 1→10→50→100 midiendo en cada escalón. El desvío por porcentaje sobre el bucket del usuario te da exactamente eso: una fracción creciente del tráfico real, con cada usuario estable en su ruta.
En una migración madura, las usarías en secuencia: (b) feature flag para staff → (c) canary por porcentaje para el endpoint que el modern cubre, restringido (a) por ruta a ese endpoint hasta que el modern cubra los demás.
Ejercicio 2 — Diagnostica el salto de ruta. Un usuario reporta: "el catálogo a veces me muestra los productos en un orden y a veces en otro, en la misma sesión". El equipo usa desvío por porcentaje al 50%. (a) ¿Cuál es la causa más probable? (b) ¿Sobre qué está calculando el bucket el router, casi seguro? (c) ¿Cómo lo corriges, y por qué eso resuelve el reporte?
Ver solución
(a) La causa más probable es que el usuario está saltando entre el legacy y el modern entre requests de la misma sesión. El legacy y el modern devuelven los productos en órdenes distintos (una diferencia sutil de implementación), así que cuando el usuario cae en modern ve un orden y cuando cae en legacy ve otro. El "a veces uno, a veces otro" es la firma del salto de ruta.
(b) Casi seguro el router calcula el bucket sobre algo que cambia en cada request —un id de request aleatorio, un timestamp— en vez de sobre la identidad del usuario. Con traffic_percent=50, cada request del usuario tiene ~50% de caer en cada lado, independientemente, así que a lo largo de una sesión el usuario rebota entre las dos rutas.
(c) Se corrige calculando el bucket sobre un identificador estable del usuario (bucket(user_id) o bucket(session_id)), no sobre la request. Eso resuelve el reporte porque el bucket del usuario es constante: mientras el traffic_percent sea 50, ese usuario cae siempre en el mismo lado (modern si su bucket < 50, legacy si no), toda la sesión. Deja de ver el catálogo "parpadear" porque deja de saltar de ruta. Un usuario, una ruta.
Ejercicio 3 — Combina las estrategias. Diseña, en prosa, un plan de desvío de tres fases para migrar el catalog de Mercado, usando las tres estrategias en el orden correcto. Para cada fase, di qué estrategia usas, a quién expone el modern, y qué señal te dice que puedes pasar a la siguiente fase.
Ver solución
Un plan razonable de tres fases:
Fase 1 — Dogfooding (por feature flag). Enciendes el modern solo para BETA_USERS = {staff}: el equipo de desarrollo y algunos internos. Expones el modern a gente que sabe que está probando y que reporta los bugs directo. Señal para avanzar: el staff usa el modern durante, digamos, una semana sin encontrar bugs bloqueantes; el conteo de fallbacks para los usuarios beta es cero o casi.
Fase 2 — Canary gradual (por porcentaje, pegado al usuario). Abres el modern a usuarios reales por porcentaje, empezando en 1% y subiendo 1→10→50→100, con el bucket sobre el id del usuario (estabilidad). Cada usuario cae siempre en la misma ruta; una fracción creciente ve el modern. Señal para avanzar entre escalones: en cada nivel, el error_rate del modern está bajo el umbral y los fallbacks no crecen (la métrica que la lección 6 formaliza con un gate). Solo entonces subes al siguiente escalón.
Fase 3 — Restricción por ruta (si hace falta). Si a mitad del canary descubres que el modern cubre bien GET /products pero aún no GET /products/{id}, combinas con desvío por ruta: mandas GET /products al canary por porcentaje y dejas GET /products/{id} 100% en el legacy hasta que el modern lo implemente. Señal para avanzar: el modern implementa el detalle y sus fallbacks para ese endpoint caen a cero; entonces incorporas esa ruta al canary.
El resultado: expusiste el modern primero a quien lo tolera (staff), luego a una muestra creciente de usuarios reales (canary), respetando siempre lo que el modern sabe hacer (por ruta). Cuando el canary llega a 100% y los fallbacks son cero, estás listo para el corte final de la lección 7.
Resumen y siguiente paso
En esta lección abriste las tres formas de desviar tráfico en un strangler fig: por ruta (por el endpoint, grano grueso, un camino completo a la vez), por porcentaje (por una fracción del tráfico, canary gradual, pegado al usuario) y por feature flag (por la identidad, dirigido a staff y beta primero). Las ejecutaste sobre la misma cola de requests y viste cómo cada una reparte distinto: la ruta ignora quién hace la request, el porcentaje respeta al usuario, y el flag ignora el azar. Y mediste la estabilidad por usuario: con el bucket calculado sobre el usuario, u-alice cayó en modern sus tres requests, sin saltar de ruta —la propiedad que hace la experiencia coherente y los bugs reproducibles—.
Antes de avanzar deberías poder: nombrar las tres estrategias y qué pregunta responde cada una (qué, cuánto, quién); elegir la estrategia adecuada según lo que el modern cubre y a quién quieres exponer; explicar por qué el bucket del porcentaje se calcula sobre el usuario y no sobre la request; y diagnosticar un salto de ruta a partir del síntoma de "el catálogo parpadea".
La lección 6 responde la pregunta que estas estrategias dejan abierta: elegiste cómo desviar, pero ¿cuándo es seguro subir el porcentaje? Vas a montar la observabilidad de las dos rutas —conteos, error_rate y latencia por ruta— y a construir un gate de promoción que solo sube el traffic_percent cuando la ruta nueva está sana. Vas a ver, medido, cómo con el modern con bug el gate no promueve, y cómo tras arreglarlo el gate sí promueve —convirtiendo la decisión de subir de una corazonada a un criterio con números—.
Recursos
- Pete Hodgson, "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. El artículo de referencia sobre feature flags: tipos de toggle, cómo se implementan, y por qué un "release toggle" (como nuestro desvío por usuario) debe ser de vida corta. En inglés.
- Martin Fowler, "CanaryRelease" — martinfowler.com/bliki/CanaryRelease.html. El desvío por porcentaje como canary, y por qué el enrutamiento debe ser estable por usuario para una experiencia coherente. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — las distintas formas de dividir el tráfico entre el monolito y los servicios nuevos (por request, por usuario, por tipo de operación). En inglés.
- Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. El enrutamiento en el strangler facade y cómo se decide qué requests van al servicio nuevo. En inglés.