Módulo 7: Medir el progreso de la migración
El burn-down de llamadas al legacy
Descripción
Ya decidiste medir el resultado (lección 2). Ahora construyes con él el instrumento central del módulo: el burn-down de las llamadas al legacy. Un burn-down es, sencillamente, una gráfica que va hacia abajo —de la cantidad de trabajo que queda hasta cero—, y es la métrica de migración más satisfactoria que existe, porque es la evidencia visual de que el legacy va muriendo. Sprint a sprint, cuentas cuántas llamadas al monolito legacy quedan (legacy_calls), y las ves bajar: 1000, 820, 610, 400, 210, 60, 0. Cuando toca cero, el legacy no atiende a nadie, y la migración de esa rebanada llegó a su meta.
Pero el burn-down tiene una sutileza que separa a quien lo lee bien de quien lo lee mal, y es el corazón de esta lección: lo que importa es la tendencia, no el valor absoluto de un sprint. Un sprint que reporta legacy_calls = 400 no te dice, por sí solo, si vas a terminar. ¿400 es bueno o malo? Depende de dónde venías y hacia dónde vas. Si el sprint pasado estabas en 610 y este en 400, quemaste 210 llamadas —vas rápido, terminarás pronto—. Si el sprint pasado estabas en 410 y este en 400, quemaste 10 —te atoraste, esto no converge—. El mismo número, 400, significa cosas opuestas según la pendiente. Por eso el burn-down se lee como una película (la tendencia entre sprints), no como una foto (el valor de hoy), y por eso de él se puede proyectar el sprint de cierre.
Conexión con el módulo. Esta lección construye la métrica de avance que la lección 1 mostró en miniatura y que la lección 2 justificó como métrica de resultado. Es el traffic_percent del strangler (M3) visto a lo largo del tiempo, convertido en una curva descendente con velocity y proyección. La lección 4 agrega la fitness function que vigila que el legacy no crezca mientras el burn-down baja; la lección 6 usará el legacy_calls == 0 del final del burn-down como una de las condiciones de done; y la lección 7 mostrará qué pasa cuando el burn-down se atora cerca de cero (la migración eterna). Fíjate en la frontera: el traffic_percent de una request individual lo decide el strangler router del M3; aquí lo agregamos en el tiempo y lo leemos como tendencia. Y proyectar una fecha de fin no es planeación de proyecto en abstracto —es extrapolar la pendiente de una métrica de resultado concreta—.
Una analogía: pagar una deuda y ver la fecha de liquidación
Imagina que debes 10 000 pesos en una tarjeta y decides pagarla. Cada mes abonas y el saldo baja. La pregunta que de verdad te importa no es "¿cuánto debo hoy?" —eso es una foto— sino "¿cuándo voy a terminar de pagar?" —eso es la tendencia—. Y para responderla no basta el saldo de hoy: necesitas ver cuánto bajó el saldo cada mes.
Piensa en dos situaciones con el mismo saldo actual. En la primera, debías 10 000, luego 8 000, luego 6 000, luego 4 000: bajas 2 000 al mes. Con 4 000 de saldo, te faltan dos meses —vas a liquidar—. En la segunda, debías 10 000, luego 9 500, luego 9 000, luego 4 000 este mes porque hiciste un abono grande de una sola vez que no se repetirá; el mes que viene vuelves a abonar 500. Con el mismo saldo de 4 000, a 500 al mes te faltan ocho meses. El saldo de hoy es idéntico (4 000), pero la fecha de liquidación es completamente distinta, y solo la conoces mirando la tendencia —cuánto abonas de forma sostenida—, no el saldo de un mes.
El burn-down de una migración es exactamente ese estado de cuenta. Las legacy_calls son el saldo de la deuda: lo que todavía debes al legacy. La velocity —cuántas llamadas cortas por sprint— es tu abono mensual. Y la fecha de liquidación es el sprint en que legacy_calls llega a cero, que proyectas dividiendo el saldo entre el abono. Quien mira solo "debo 400" (el saldo de hoy) no sabe cuándo terminará; quien mira "vengo bajando 200 por sprint" (la tendencia) sabe que en dos sprints liquida. Medir el progreso de una migración es leer su estado de cuenta como quien planea salir de deudas: por la pendiente, no por el saldo del día.
Ejemplo trabajado: el burn-down con velocity y proyección
Vamos a ejecutar el burn-down completo del catalog de Mercado, sprint a sprint, y en cada uno calcular dos cosas más allá del valor: la velocity (cuántas legacy_calls quemamos respecto al sprint anterior) y la ETA (el sprint proyectado de cierre, estimado al ritmo del último sprint). Los datos del burn-down están fijos; la velocity y la proyección se derivan de ellos.
# El burn-down completo de llamadas de catalog al monolito, sprint a sprint.
# La tendencia (velocity) importa mas que el valor absoluto: proyecta el final.
import math
SAMPLE = 1000
burn_down = [1000, 820, 610, 400, 210, 60, 0] # legacy_calls por sprint
def traffic_percent(legacy_calls):
return (SAMPLE - legacy_calls) / SAMPLE * 100
print(f"Burn-down de llamadas de catalog al monolito ({SAMPLE} req/sprint)\n")
print(f"{'sprint':>7}{'legacy_calls':>13}{'quemadas':>10}{'traffic%':>10}"
f" ETA (a 0) burn-down")
print("-" * 78)
for sprint, legacy_calls in enumerate(burn_down):
tp = traffic_percent(legacy_calls)
# velocity = cuantas llamadas quemamos este sprint (delta vs el anterior)
if sprint == 0:
burned = 0
eta = "-"
else:
burned = burn_down[sprint - 1] - legacy_calls
# proyeccion simple: al ritmo del ultimo sprint, cuantos faltan a 0
if legacy_calls == 0:
eta = "LLEGO"
elif burned > 0:
eta = f"~{sprint + math.ceil(legacy_calls / burned)}"
else:
eta = "nunca"
bar = "#" * (legacy_calls * 24 // SAMPLE)
print(f"{sprint:>7}{legacy_calls:>13}{burned:>10}{tp:>9.0f}%"
f"{eta:>11} |{bar:<24}|")
print("-" * 78)
total_burned = burn_down[0] - burn_down[-1]
avg_velocity = total_burned / (len(burn_down) - 1)
print(f"\n Velocity promedio: {avg_velocity:.0f} llamadas/sprint. Tendencia: BAJA sostenida.")
print(" El valor de un sprint aislado (400) no dice si terminaras; la pendiente si.")
print(" ETA proyecta el sprint de cierre desde la velocity: la migracion converge a 0.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Burn-down de llamadas de catalog al monolito (1000 req/sprint)
sprint legacy_calls quemadas traffic% ETA (a 0) burn-down
------------------------------------------------------------------------------
0 1000 0 0% - |########################|
1 820 180 18% ~6 |################### |
2 610 210 39% ~5 |############## |
3 400 210 60% ~5 |######### |
4 210 190 79% ~6 |##### |
5 60 150 94% ~6 |# |
6 0 60 100% LLEGO | |
------------------------------------------------------------------------------
Velocity promedio: 167 llamadas/sprint. Tendencia: BAJA sostenida.
El valor de un sprint aislado (400) no dice si terminaras; la pendiente si.
ETA proyecta el sprint de cierre desde la velocity: la migracion converge a 0.
Lee la columna legacy_calls de arriba hacia abajo y mira la barra encogerse: 1000, 820, 610, 400, 210, 60, 0. Eso es un burn-down, y es la imagen más satisfactoria de una migración —la evidencia, sprint a sprint, de que el legacy va muriendo—. En paralelo, el traffic_percent sube de 0% a 100%: cada llamada que dejas de mandar al legacy es una que atiende la ruta nueva.
Ahora mira la columna quemadas, que es la velocity —cuántas legacy_calls cortaste respecto al sprint anterior—: 180, 210, 210, 190, 150, 60. Esta columna es la que de verdad te dice si terminarás, y por qué. Fíjate en el sprint 3: legacy_calls = 400. Si solo miraras ese número, no sabrías nada —¿400 es ir bien o mal?—. Pero la columna quemadas dice 210: vienes quemando alrededor de 200 llamadas por sprint, sostenido. Con 400 de saldo y 200 de velocity, te faltan dos sprints. Ese es el poder de la tendencia: convierte un número mudo (400) en una predicción (terminas cerca del sprint 5-6).
La columna ETA hace esa proyección explícita: en cada sprint, estima el sprint de cierre suponiendo que sigues al ritmo del último. En el sprint 1 proyecta ~6; en los sprints 2 y 3, ~5; y al final, en el sprint 6, marca LLEGO —legacy_calls tocó cero—. La ETA se mueve un poco sprint a sprint (porque la velocity no es perfectamente constante), pero se mantiene en un rango estrecho de 5 a 6: la migración converge. Una migración sana tiene una ETA estable o que se acerca; una migración en problemas tiene una ETA que se aleja sprint a sprint (cada vez proyecta un final más lejano) o que dice nunca (velocity cero: te atoraste).
El renglón final resume la velocity promedio: 167 llamadas por sprint, con tendencia a la baja sostenida. Esa pendiente pareja es la firma de una migración que va a terminar. La lección la repite dos veces porque es el punto entero: el valor de un sprint aislado (400) no dice si terminarás; la pendiente sí.
Profundización: leer la forma del burn-down
El valor de hoy es una foto; la forma de la curva es la película, y cada forma cuenta una historia distinta sobre si la migración va a terminar. Vale la pena reconocer las cuatro formas típicas, porque diagnosticar el burn-down por su forma es la habilidad central de esta lección.
El que converge El que se atora El que nunca arranca El de escalon
(sano, llega a 0) (migracion eterna) (todo esfuerzo, 0 corte) (avanza a saltos)
# # # # # # # # # #
## ## # # # # # # #
#### #### # # # # # # # #
###### ######___ # # # # # # #
########____ ###### # # # # # #____
0 (se queda ahi) # (no baja) 0
velocity sostenida velocity -> 0 cerca velocity = 0 siempre velocity a rachas,
ETA estable del final; ETA -> nunca el legacy no muere pero converge
- El que converge (el del ejemplo): la velocity se mantiene, la curva baja pareja hasta cero, la ETA es estable. Es la migración sana. No tienes que hacer nada más que seguir.
- El que se atora: baja bien al principio y luego la velocity cae a casi cero cerca del final —típicamente en el último 5-10%—. La curva se aplana antes de llegar a cero, la ETA empieza a decir "nunca". Es el patrón de la migración eterna (lección 7): el 90% se hace rápido, el último 10% no se hace nunca porque el equipo perdió interés o los casos que faltan son los raros y difíciles. La señal temprana es la velocity cayendo mientras
legacy_callstodavía es mayor que cero. - El que nunca arranca:
legacy_callsno baja aunque el equipo trabaje. Es la migración que confunde esfuerzo con resultado (lección 2): mucha actividad, cero corte. La velocity es cero desde el principio. La señal es un burn-down plano mientras los story points suben. - El de escalón: avanza a saltos (un sprint corta mucho, otro nada) porque el trabajo viene en bloques —por ejemplo, migrar un endpoint entero de golpe—. Puede ser perfectamente sano si la tendencia general converge; solo hay que promediar la velocity sobre varios sprints en vez de mirar sprint a sprint.
La proyección (ETA) merece un matiz honesto: es una extrapolación, y como toda extrapolación, supone que el ritmo se mantiene. En la realidad, el último tramo de una migración suele ser el más lento —los casos que quedan al final son los raros, los difíciles, los que nadie quería tocar—, así que la ETA tiende a ser optimista cerca del cierre. Fíjate en el ejemplo: la velocity cae de 210 a 150 a 60 en los últimos sprints. Eso no es un problema si la curva sigue bajando, pero es la razón por la que una migración "al 95%" puede tardar tanto como el 95% anterior: el 5% final es de otra naturaleza. Leer eso en el burn-down —la velocity cayendo cerca del final— es lo que te permite anticipar el atoramiento antes de que se vuelva eterno.
Un detalle sobre qué contar en legacy_calls. En el modelo de este módulo contamos requests que tocan el legacy sobre una muestra fija de 1000, que es lo más directo. Pero "llamadas al legacy" puede medirse de varias formas complementarias, y en una migración real conviene mirar más de una: requests servidas por la ruta vieja (el traffic_percent del strangler), llamadas que el código nuevo hace de vuelta al monolito (dependencias internas que aún no cortaste), y consultas a las tablas todavía compartidas. Cada una es un burn-down distinto, y la migración no termina hasta que todos llegan a cero —un punto que la lección 6 desarrolla al definir el done con varias condiciones—. Aquí usamos el burn-down agregado como la métrica principal; recuerda que debajo puede haber varias curvas que también deben tocar el suelo.
Errores comunes
Leer el valor de un sprint en vez de la tendencia. Qué pasa: el equipo mira legacy_calls = 400 y discute si eso es bueno o malo, sin mirar de dónde venía ni a qué ritmo baja. Por qué pasa: el valor de hoy es el número más visible y el más fácil de reportar; la tendencia requiere comparar sprints. Cómo detectarlo: las conversaciones de avance giran en torno al número actual ("estamos en 400") y no en torno a la pendiente ("venimos bajando 200 por sprint"); nadie puede decir la ETA. Cómo corregirlo: mide y reporta la velocity (cuántas legacy_calls se cortaron respecto al sprint anterior) y la ETA derivada de ella. El valor de 400 es idéntico en una migración que converge (venía de 610) y en una que se atoró (venía de 410); solo la tendencia los distingue. El burn-down es una película, no una foto.
Proyectar la fecha de fin con la velocity de los primeros sprints. Qué pasa: el equipo ve que en los primeros sprints quema 200 llamadas cada uno, extrapola en línea recta, y promete un final que llega antes de tiempo. Por qué pasa: la extrapolación lineal es la más simple, y los primeros sprints suelen ser los más rápidos (los casos fáciles se migran primero). Cómo detectarlo: la ETA prometida al principio resulta ser demasiado optimista; el último tramo tarda mucho más de lo proyectado. Cómo corregirlo: recuerda que el último tramo es más lento —los casos que quedan al final son los raros y difíciles, y la velocity cae (en el ejemplo, de 210 a 60)—. Proyecta con la velocity reciente, no con la de los primeros sprints, y trata la ETA como un rango que se ajusta, no como una promesa fija. Y vigila la señal de atoramiento: si la velocity cae mientras legacy_calls sigue lejos de cero, la ETA se está alejando y hay que actuar antes de que se vuelva "nunca".
Celebrar el 90% como si fuera el final. Qué pasa: el burn-down llega rápido al 90% (legacy_calls bajó de 1000 a 100), el equipo lo lee como "ya casi", y afloja. Por qué pasa: el 90% se siente como terminado, y la curva bajó tan bien que parece que el resto será igual de rápido. Cómo detectarlo: la velocity cae justo después del 90%, la curva se aplana, y el legacy_calls restante se queda semanas en el mismo rango. Cómo corregirlo: el último 10% suele ser de otra naturaleza que el primer 90% —son los casos raros, los flujos que nadie documentó, las integraciones olvidadas—, y puede costar tanto como todo lo anterior. No es 90% del trabajo hecho; es 90% del tráfico cortado, que es distinto. La migración termina en cero, no en 90%, y el burn-down te lo recuerda mostrando que la barra todavía no está vacía. La lección 7 es entera sobre lo que pasa cuando se cede a esta tentación: la migración eterna atorada en el último tramo.
Ejercicios
Ejercicio 1 — Foto vs película. Dos migraciones reportan hoy legacy_calls = 300. La migración A venía de 900, 600, 300 (últimos tres sprints); la B venía de 340, 320, 300. (a) ¿Cuál es la velocity de cada una? (b) Proyecta la ETA de cada una (sprints para llegar a 0 al ritmo actual). (c) ¿Por qué el mismo valor de 300 significa cosas opuestas?
Ver solución
(a) Migración A: bajó de 600 a 300 en el último sprint → velocity 300 llamadas/sprint. Migración B: bajó de 320 a 300 → velocity 20 llamadas/sprint.
(b) A: con 300 de saldo y velocity 300, 300 / 300 = 1 → termina en ~1 sprint más. B: con 300 de saldo y velocity 20, 300 / 20 = 15 → termina en ~15 sprints más (si es que la velocity no cae aún más). A converge; B está prácticamente atorada.
(c) Porque el valor de hoy (300) es una foto que no contiene la información que importa: la pendiente. La migración A viene bajando 300 por sprint —está a un sprint de terminar—; la B viene bajando 20 por sprint —está a 15 sprints, y probablemente en camino a la migración eterna—. El mismo saldo con abonos distintos da fechas de liquidación opuestas. Solo la tendencia (velocity) distingue una migración que converge de una que se atoró, y por eso el burn-down se lee como película, no como foto.
Ejercicio 2 — Diagnostica la forma. Para cada burn-down (secuencia de legacy_calls por sprint), di qué forma tiene y si la migración va a terminar: (a) 1000, 700, 450, 250, 100, 0; (b) 1000, 850, 750, 720, 715, 713; (c) 1000, 1000, 1000, 990, 1000, 995; (d) 1000, 1000, 400, 400, 100, 100, 0.
Ver solución
(a) 1000→0 con velocity sostenida (300, 250, 200, 150, 100) que baja pero nunca se apaga, y llega a cero. Es el que converge: migración sana, termina. (Nota que la velocity decrece hacia el final —el último tramo es más lento—, pero la curva sí toca el suelo.)
(b) 1000→713 y ahí se aplana: velocity 150, 100, 30, 5, 2 —cae a casi cero cerca del 70%—. Es el que se atora: la curva se detuvo lejos de cero, la ETA tiende a "nunca". Es el patrón de la migración eterna; hay que intervenir antes de que se quede ahí para siempre.
(c) Se queda pegado en ~1000, sin bajar (las pequeñas variaciones son ruido). Es el que nunca arranca: velocity cero, el legacy no muere. Probablemente el equipo trabaja (esfuerzo) pero no corta llamadas (resultado); la lección 2 en acción.
(d) Baja a saltos: dos sprints planos, luego corta 600 de golpe, dos planos, corta 300, plano, corta 100 hasta cero. Es el de escalón: avanza en bloques (migrar endpoints enteros de una vez). Sí termina —la tendencia general converge a cero—; solo hay que promediar la velocity sobre varios sprints en vez de asustarse por los sprints planos.
Ejercicio 3 — La ETA que se aleja. Una migración proyecta estas ETAs sprint a sprint: sprint 1 → ~8, sprint 2 → ~9, sprint 3 → ~11, sprint 4 → ~15. (a) ¿Qué está pasando con la velocity? (b) ¿Es esta migración sana? (c) ¿Qué harías al ver este patrón, y por qué es mejor detectarlo ahora que en el sprint 15?
Ver solución
(a) La velocity está cayendo sprint a sprint. Una ETA que se aleja (8 → 9 → 11 → 15) solo puede pasar si cada sprint corta menos llamadas que el anterior: el saldo baja más lento, así que dividirlo entre una velocity menor da un plazo mayor. La proyección se aleja porque el ritmo se desinfla.
(b) No. Una migración sana tiene una ETA estable o que se acerca (como el ejemplo del texto, que se mantuvo en 5-6). Una ETA que se aleja es la firma temprana de una migración en camino a atorarse —la forma "el que se atora"—: si la velocity sigue cayendo, la ETA tenderá a "nunca" y la migración quedará eterna en algún punto antes de cero.
(c) Al ver este patrón, investigaría por qué la velocity cae y actuaría ahora: ¿los casos que quedan son los raros y difíciles (esperado, pero hay que planear tiempo para ellos)?, ¿el equipo se distrajo con otro trabajo (recuperar foco)?, ¿hay un bloqueo técnico en el último tramo (resolverlo)? Es mejor detectarlo en el sprint 4 que en el 15 porque en el sprint 4 la migración todavía tiene inercia y presupuesto y atención; en el sprint 15, con la ETA disparada y el equipo cansado, es exactamente cuando las migraciones se abandonan "al 90%" y se vuelven eternas. La ETA que se aleja es una alarma temprana; la lección 7 trata qué hacer para no dejar que la migración muera de vieja en ese último tramo.
Resumen y siguiente paso
En esta lección construiste el instrumento central del módulo: el burn-down de las llamadas al legacy. Viste, con la deuda que pagas mes a mes y la fecha de liquidación que solo conoces por la tendencia, que lo que importa no es el saldo de hoy (legacy_calls = 400) sino la pendiente con que baja (la velocity) —el mismo valor significa "terminas en dos sprints" o "te atoraste" según de dónde vengas—. Y lo ejecutaste: la curva 1000→820→...→0 con su traffic_percent subiendo a 100%, la columna de llamadas quemadas como velocity, y la ETA proyectando un cierre estable en el sprint 5-6. Aprendiste a leer las cuatro formas del burn-down (converge, se atora, nunca arranca, escalón), a proyectar con la velocity reciente y no la de los primeros sprints, y a reconocer la señal temprana del atoramiento: la velocity cayendo o la ETA alejándose mientras legacy_calls sigue lejos de cero.
Antes de avanzar deberías poder: calcular la velocity y la ETA de un burn-down; distinguir una migración que converge de una que se atora por la forma de su curva; explicar por qué el valor de un sprint aislado no dice si terminarás; y anticipar que el último tramo es más lento que el primero.
La lección 4 agrega el segundo instrumento del tablero, uno que vigila un peligro que el burn-down no ve: mientras reduces el legacy por el frente (cortando llamadas), alguien podría estar agregándole código por atrás (una feature nueva construida sobre el monolito que estás matando). El burn-down bajaría igual, pero el legacy estaría creciendo. La fitness function de la migración es la alarma que atrapa eso: una prueba que cuenta las referencias al legacy y falla si crecieron. Vas a escribirla, verla dar PASS cuando el legacy encoge y FAIL —romper el CI— cuando alguien mete código nuevo al monolito.
Recursos
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — Newman describe medir el avance de la descomposición del monolito por la funcionalidad y las llamadas que se retiran de él; el burn-down de este módulo es esa idea hecha gráfica. En inglés.
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. El patrón cuya señal de éxito es que el sistema viejo atienda cada vez menos hasta atender nada: exactamente lo que el burn-down mide sprint a sprint. En inglés.
- Neal Ford, Rebecca Parsons y Patrick Kua, Building Evolutionary Architectures (O'Reilly, 2ª ed., 2022) — sobre medir propiedades de la arquitectura de forma objetiva y seguirlas en el tiempo; el espíritu de tratar el avance como una métrica que se grafica y se proyecta, no como una percepción. En inglés.
- Martin Fowler, martinfowler.com — el bliki con las entradas sobre strangler fig y arquitectura evolutiva que fundamentan por qué el progreso de una migración se mide como una curva que baja hacia cero. En inglés.