Módulo 7: Medir el progreso de la migración
Impedir que el legacy vuelva a crecer
Descripción
La fitness function de la lección 4 tiene una debilidad sutil, y esta lección la cierra. Tal como la construimos, la fitness function compara contra un baseline fijo: mientras el baseline sea 4, cualquier estado con 4 o menos referencias pasa. Pero eso permite algo que no queremos. Imagina que un sprint reduce las referencias de 4 a 2 —progreso real—. El baseline sigue en 4, así que en el sprint siguiente alguien podría volver a subir de 2 a 4 (agregar dos dependencias nuevas al legacy) y la fitness function pasaría, porque 4 sigue siendo menor o igual al baseline de 4. El progreso ganado se perdería sin que la alarma sonara. El legacy encogió y volvió a crecer, y nadie se enteró.
La solución es una idea vieja y poderosa: el trinquete (en inglés, ratchet). Un trinquete es un mecanismo que solo gira en una dirección —aprieta, pero no afloja—. Aplicado a la fitness function: cada vez que las referencias al legacy bajan, el baseline baja con ellas y se queda ahí; nunca vuelve a subir. Si este sprint llegaste a 2, el baseline pasa a 2, y de ahora en adelante 3 ya sería FAIL. El trinquete convierte cada avance en un piso irreversible: no solo mides que el legacy no crezca respecto al último estado sano, sino que fijas cada mejora para que no se pueda deshacer. La migración se vuelve monótona hacia cero —cada paso hacia adelante queda cerrado con llave—.
Conexión con el módulo. Esta lección perfecciona el instrumento de la lección 4: la fitness function medía "el legacy no creció"; el trinquete garantiza "el progreso no retrocede". Juntos hacen que el burn-down (lección 3) sea a prueba de reversas: no solo baja, sino que no puede volver a subir. Y esta lección conecta con la disciplina que sostiene toda migración sana: no se agregan features nuevas al legacy —toda funcionalidad nueva nace en el modern—, y el trinquete es cómo se hace cumplir esa disciplina de forma automática. La lección 6 usará el trinquete en cero (referencias al legacy que ya no pueden subir de cero) como una de las condiciones de done. Fíjate en la frontera: el trinquete es una técnica de cómo aplicar la fitness function de la lección 4; no es un concepto nuevo de arquitectura, sino la disciplina de mover el baseline en una sola dirección.
Una analogía: el cincho plástico que solo se aprieta
Piensa en un cincho plástico —esas abrazaderas que usas para amarrar cables—. Tienen un mecanismo de trinquete: metes la punta por la cabeza, jalas, y el cincho se aprieta. Y aquí está la magia: puedes seguir apretando cuanto quieras, pero no puedes aflojarlo. Los dientecitos internos solo dejan que la cinta avance en una dirección. Si lo aprietas al punto 5, se queda en 5; puedes apretarlo a 6, a 7, pero jamás vuelve a 4. Para aflojarlo tendrías que cortarlo —no hay marcha atrás por accidente—.
Ese mecanismo de "solo aprieta, nunca afloja" es exactamente lo que le falta a la fitness function con baseline fijo. Con un baseline fijo, es como un cordón con nudo corredizo: lo aprietas a 2, pero se puede volver a aflojar a 4 sin que nada lo impida. Con el trinquete, es un cincho: cada vez que aprietas (bajas las referencias al legacy), el baseline se queda en ese punto y no puede volver atrás. El progreso queda fijado mecánicamente, no por buena voluntad.
La belleza del cincho es que no requiere que nadie recuerde mantenerlo apretado —el mecanismo lo hace por ti—. Igual con el trinquete del baseline: no dependes de que el equipo se acuerde de "no dejar que el legacy vuelva a crecer más allá de nuestro mejor punto"; el número está guardado, versionado, y la fitness function lo hace cumplir en cada PR. Cada avance de la migración queda cerrado con llave. Y como el cincho, la única forma de "aflojar" el baseline sería un acto deliberado y visible —subir el número a mano en un commit, que todos verían y cuestionarían—, no un descuido silencioso.
Ejemplo trabajado: el baseline que solo baja, y el intento bloqueado
Vamos a ejecutar el trinquete. El baseline arranca en 4 (el conteo inicial de referencias al legacy). Sprint a sprint, el equipo reduce las referencias, y cada vez que bajan, el trinquete aprieta el baseline al nuevo mínimo. Al final, con el baseline ya en 0, alguien intenta agregar una referencia nueva al legacy —y el trinquete lo bloquea, porque 1 ya no es menor o igual que 0.
# El baseline como TRINQUETE (ratchet): solo baja, nunca sube. Cada sprint verde
# fija el nuevo maximo permitido; asi el legacy no puede volver a crecer y el
# progreso hacia 0 es monotono. Un intento de agregar codigo al legacy se bloquea.
def migration_fitness(legacy_refs, baseline):
return legacy_refs <= baseline
# Trinquete: arranca en el conteo inicial y solo se aprieta cuando bajamos.
baseline = 4
print(f"El trinquete del baseline (arranca en {baseline}, solo baja)\n")
print(f"{'sprint':>7}{'legacy_refs':>13}{'baseline':>10}{'fitness':>9} accion")
print("-" * 62)
# Cada sprint reporta cuantas referencias al legacy quedan en el codigo.
sprint_refs = [4, 3, 2, 1, 0]
for sprint, legacy_refs in enumerate(sprint_refs):
ok = migration_fitness(legacy_refs, baseline)
if ok and legacy_refs < baseline:
action = f"verde -> apretar baseline a {legacy_refs}"
baseline = legacy_refs # el trinquete baja, nunca vuelve a subir
elif ok:
action = "verde (sin cambio)"
else:
action = "ROJO -> bloquear el merge"
fit = "PASS" if ok else "FAIL"
print(f"{sprint:>7}{legacy_refs:>13}{baseline:>10}{fit:>9} {action}")
print("-" * 62)
# --- Ahora el baseline esta en 0. Alguien intenta agregar una feature que
# vuelve a importar del monolito: legacy_refs pasaria de 0 a 1. ---
print("\nIntento de agregar codigo nuevo al legacy con el baseline ya en 0:")
attempted_refs = 1
ok = migration_fitness(attempted_refs, baseline)
print(f" legacy_refs={attempted_refs} baseline={baseline} "
f"fitness={'PASS' if ok else 'FAIL'} -> "
f"{'entra' if ok else 'BLOQUEADO: nadie revive el legacy'}")
print("\n El trinquete convierte el progreso en irreversible: cada avance queda")
print(" fijado, y el legacy no puede volver a crecer ni un import.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
El trinquete del baseline (arranca en 4, solo baja)
sprint legacy_refs baseline fitness accion
--------------------------------------------------------------
0 4 4 PASS verde (sin cambio)
1 3 3 PASS verde -> apretar baseline a 3
2 2 2 PASS verde -> apretar baseline a 2
3 1 1 PASS verde -> apretar baseline a 1
4 0 0 PASS verde -> apretar baseline a 0
--------------------------------------------------------------
Intento de agregar codigo nuevo al legacy con el baseline ya en 0:
legacy_refs=1 baseline=0 fitness=FAIL -> BLOQUEADO: nadie revive el legacy
El trinquete convierte el progreso en irreversible: cada avance queda
fijado, y el legacy no puede volver a crecer ni un import.
Lee la columna baseline de arriba hacia abajo: 4, 3, 2, 1, 0. Ese descenso escalonado es el trinquete apretando. Cada sprint que reduce las referencias al legacy (columna legacy_refs) arrastra el baseline hacia abajo con él y lo deja ahí. Fíjate en el mecanismo, sprint por sprint:
- Sprint 0:
legacy_refs = 4, igual al baseline. No hubo reducción, así que el trinquete no aprieta: "verde (sin cambio)". El baseline se queda en 4. - Sprint 1:
legacy_refs = 3, menor que el baseline de 4. Progreso. El trinquete aprieta: el baseline baja a 3. A partir de ahora, 4 referencias sería FAIL —el estado que antes pasaba, ahora está prohibido—. - Sprints 2, 3, 4: lo mismo, escalón por escalón. El baseline sigue al
legacy_refshacia abajo —2, 1, 0—, fijando cada mejora. En el sprint 4, conlegacy_refs = 0, el baseline llega a 0: el código ya no depende del legacy en absoluto, y ese logro queda cerrado con llave.
Y ahora el momento que muestra el poder del trinquete. Con el baseline ya en 0, alguien intenta agregar una feature nueva que importa del monolito —legacy_refs pasaría de 0 a 1—. La fitness function evalúa 1 <= 0: FALSE, FAIL. El intento queda BLOQUEADO. Con un baseline fijo en 4, ese mismo intento habría pasado (1 es menor que 4); con el trinquete en 0, es imposible. Nadie puede revivir el legacy ni con un solo import, porque el mejor punto alcanzado (cero) quedó fijado como el nuevo techo.
Compara las dos versiones para ver lo que el trinquete agrega. Con el baseline fijo de la lección 4, la fitness function decía "no crezcas más allá del punto de partida (4)" —lo que permite que el legacy oscile entre 2 y 4 para siempre—. Con el trinquete, dice "no crezcas más allá de tu mejor punto hasta ahora" —lo que fuerza a la migración a ser monótona: cada mejora es permanente, y el único camino es hacia abajo—. El baseline fijo tolera el vaivén; el trinquete solo tolera el progreso.
Profundización: la disciplina que el trinquete hace cumplir
El trinquete no es solo un truco técnico; es la forma automática de imponer una disciplina que casi toda migración exitosa necesita y casi toda migración eterna viola: no se agregan features nuevas al legacy. Vale la pena entender por qué esa disciplina es tan importante y por qué es tan difícil de sostener sin ayuda mecánica.
Durante una migración, el equipo vive con dos sistemas: el legacy que muere y el modern que crece. Cuando llega un requerimiento nuevo —una feature, un cambio—, hay una decisión implícita en cada PR: ¿lo construyo sobre el legacy o sobre el modern? Construirlo sobre el legacy casi siempre es más rápido hoy (el código ya está ahí, la infraestructura ya existe) y más caro mañana (acabas de agregar algo que tendrás que migrar, alargando la migración). Construirlo sobre el modern es más lento hoy (a veces hay que crear la capacidad primero) y más barato mañana (nace ya migrado). La presión del corto plazo empuja hacia el legacy; la salud de la migración exige el modern.
Requerimiento nuevo llega durante la migracion
│
┌─────────┴──────────┐
sobre el LEGACY sobre el MODERN
(rapido hoy) (a veces lento hoy)
el legacy CRECE el legacy no crece
mas que migrar nace ya migrado
│ │
fitness FAIL fitness PASS
(el trinquete (el camino
lo bloquea) correcto)
Sin el trinquete, esa decisión se toma caso por caso, y bajo presión gana el corto plazo: "esta vez sí sobre el legacy, es urgente". Cada "esta vez" hace crecer el legacy y alarga la migración, y como el burn-down todavía baja por el frente (se sigue cortando tráfico), nadie nota que por atrás está creciendo. Es exactamente cómo una migración que iba bien se convierte en eterna: no por un gran error, sino por muchas features pequeñas coladas al legacy "solo esta vez". El trinquete quita esa decisión de las manos de la presión: si construir sobre el legacy sube las referencias por encima del baseline (que ahora es tu mejor marca), el CI se pone rojo y no hay "esta vez". La feature tiene que nacer en el modern. La disciplina deja de depender de la fuerza de voluntad y pasa a ser una propiedad del sistema.
Hay un detalle práctico sobre cómo se guarda el baseline. En un repositorio real, el baseline no vive en una variable de Python que se reinicia cada corrida; vive versionado en el repositorio —un archivo con el número actual permitido, commiteado junto al código—. Así, apretar el trinquete (bajar el baseline) es un cambio explícito y visible en el historial: cuando un sprint reduce las referencias, el mismo PR que hace la reducción actualiza el archivo del baseline al nuevo mínimo. Y "aflojar" el trinquete —subir el baseline a mano— sería un commit deliberado que cualquiera vería en la revisión y cuestionaría ("¿por qué estás subiendo el límite de referencias al legacy?"). El trinquete no es infalible contra la mala fe (alguien podría subir el número), pero convierte el retroceso silencioso en un acto visible y defendible —que es justo lo que hace que no ocurra por descuido—.
Una precisión sobre cuándo el trinquete no debe apretar demasiado rápido. Si mides las referencias sprint a sprint y el número oscila por razones legítimas y temporales (una refactorización intermedia que sube el conteo un día y lo baja al siguiente), un trinquete que aprieta en cada mínimo instantáneo podría bloquear trabajo en curso. En la práctica, el baseline se aprieta sobre estados estables (al cerrar un sprint, al mergear a la rama principal), no sobre cada commit intermedio de una rama de trabajo. La regla sigue siendo "solo baja", pero se aplica sobre los puntos de control donde el código está sano, no sobre cada instante. El espíritu es fijar el progreso real y consolidado, no castigar el vaivén natural del trabajo dentro de un sprint.
Errores comunes
Dejar el baseline fijo y permitir el vaivén. Qué pasa: el equipo pone la fitness function con un baseline que nunca se actualiza, así que el legacy puede encoger y volver a crecer dentro del rango del baseline sin que suene la alarma. Por qué pasa: un baseline fijo es más simple —un número y ya—, y actualizar el baseline en cada mejora parece un paso extra. Cómo detectarlo: las referencias al legacy suben y bajan sprint a sprint sin dirección clara, oscilando bajo el baseline; el burn-down de referencias no es monótono. Cómo corregirlo: convierte el baseline en un trinquete que solo baja. Cada vez que las referencias alcancen un nuevo mínimo estable, baja el baseline a ese mínimo y guárdalo versionado. Así cada avance queda fijado y el legacy no puede recuperar terreno. Un baseline fijo mide "no crezcas desde el inicio"; un trinquete mide "no crezcas desde tu mejor punto", que es lo que hace la migración irreversible.
Seguir construyendo features nuevas sobre el legacy "solo esta vez". Qué pasa: durante la migración, cada requerimiento urgente se cuela al legacy porque es más rápido, con la promesa de migrarlo después. Por qué pasa: construir sobre el legacy siempre es más barato hoy (el código ya existe), y la urgencia del corto plazo pesa más que la salud de la migración. Cómo detectarlo: las referencias al legacy suben en los sprints con features nuevas; el burn-down por el frente baja pero el legacy crece por atrás; la migración se alarga sin explicación clara. Cómo corregirlo: toda funcionalidad nueva nace en el modern, y el trinquete lo hace cumplir —si una feature sube las referencias por encima del baseline, el CI se pone rojo y no entra—. No es que las features urgentes no se hagan; es que se hacen sobre el sistema nuevo, no sobre el que estás matando. "Solo esta vez" repetido es cómo una migración sana se vuelve eterna; el trinquete elimina el "solo esta vez".
Aflojar el trinquete a mano para "desbloquear" un PR. Qué pasa: un PR necesario falla la fitness function, y en vez de quitar la dependencia al legacy, alguien sube el baseline a mano para que pase. Por qué pasa: subir el número es el camino de menor resistencia cuando hay prisa y el CI está rojo. Cómo detectarlo: en el historial aparecen commits que suben el baseline de referencias al legacy —el trinquete girando al revés—. Cómo corregirlo: subir el baseline debe ser un evento excepcional, visible y justificado, no una forma rutinaria de desbloquear PRs. Como el baseline está versionado, cada aumento es un commit que la revisión debe cuestionar: "¿por qué estamos permitiendo más referencias al legacy en plena migración?". La respuesta casi siempre es "no deberíamos —quita la dependencia en su lugar—". El trinquete solo protege si el equipo trata girarlo al revés como lo que es: revertir el progreso de la migración, no una molestia del CI que se calla subiendo un número.
Ejercicios
Ejercicio 1 — El trinquete en acción. El baseline arranca en 5. Los sprints reportan estas referencias al legacy: 5, 4, 4, 2, 3. (a) ¿Cómo evoluciona el baseline sprint a sprint? (b) ¿En qué sprint la fitness function daría FAIL, si en alguno? (c) ¿Qué habría pasado en el último sprint con un baseline fijo en 5 en vez de un trinquete?
Ver solución
(a) El baseline sigue al mínimo alcanzado, solo hacia abajo: sprint 0 (refs=5) → baseline 5; sprint 1 (refs=4) → aprieta a 4; sprint 2 (refs=4) → sin cambio, sigue en 4; sprint 3 (refs=2) → aprieta a 2; sprint 4 (refs=3) → aquí 3 > baseline 2.
(b) En el sprint 4. Después de que el trinquete apretó a 2 en el sprint 3, el sprint 4 sube a 3 referencias —alguien volvió a agregar una dependencia al legacy—. Como 3 > 2, la fitness function da FAIL y bloquea el merge. El progreso ganado en el sprint 3 (llegar a 2) quedó fijado, y el intento de retroceder a 3 se detiene.
(c) Con un baseline fijo en 5, el sprint 4 (refs=3) daría PASS, porque 3 <= 5. El retroceso de 2 a 3 pasaría sin alarma: el legacy recuperó una referencia y nadie se enteró. Esta es exactamente la debilidad del baseline fijo que el trinquete cierra —el baseline fijo tolera el vaivén bajo su techo; el trinquete no, porque su techo es el mejor punto alcanzado (2), no el punto de partida (5)—.
Ejercicio 2 — La feature urgente. Estás a mitad de la migración del catalog (baseline en 1) y llega una feature urgente cuya forma más rápida de implementarse importa una función del monolito legacy. (a) ¿Qué pasaría con la fitness function si la construyes sobre el legacy? (b) ¿Cuáles son tus dos opciones legítimas? (c) ¿Por qué es bueno que el CI te obligue a elegir una de ellas en vez de dejarte colar la dependencia?
Ver solución
(a) Si la construyes sobre el legacy, las referencias subirían de 1 a 2. Como 2 > baseline 1, la fitness function da FAIL y el CI se pone rojo: el PR de la feature no puede mergearse. El trinquete bloquea la feature tal como está construida (sobre el legacy).
(b) Las dos opciones legítimas son: (1) construir la feature sobre el modern —crear la capacidad en el servicio nuevo, aunque tome más tiempo hoy, para que la feature nazca ya migrada—; o (2) migrar primero la función del legacy que la feature necesita (mover discount_rules, o lo que sea, al modern) y luego construir la feature sobre ella —lo que además hace bajar las referencias al legacy—. Ambas mantienen o reducen el conteo; ninguna hace crecer el legacy.
(c) Porque sin esa obligación, bajo la presión de la urgencia, casi siempre ganaría el atajo ("sobre el legacy, es más rápido, lo migramos después"), y ese atajo repetido es cómo la migración se vuelve eterna. El CI rojo convierte la decisión implícita y presionada en una explícita y sana: no te deja colar la dependencia por descuido; te obliga a hacer el trabajo del lado correcto. No impide que la feature exista —impide que exista sobre el sistema que estás matando—. La restricción mecánica protege la migración de tu propia prisa.
Ejercicio 3 — Guardar el baseline. El texto dice que el baseline debe vivir "versionado en el repositorio". (a) ¿Por qué no basta con tenerlo en una variable que se calcula al vuelo cada corrida? (b) ¿Qué ventaja da que apretar el trinquete sea un commit explícito? (c) ¿Cómo hace esto visible un intento de aflojar el trinquete?
Ver solución
(a) Si el baseline se calcula al vuelo (por ejemplo, "el baseline es el conteo actual"), entonces nunca puede fallar: siempre estaría comparando el código contra sí mismo, y cualquier conteo pasaría porque siempre es igual a sí mismo. El baseline tiene que ser un valor guardado del pasado (el mejor punto alcanzado hasta ahora) contra el cual comparar el presente. Y para persistir entre corridas y entre desarrolladores, ese valor debe estar en un archivo versionado, no en memoria.
(b) Que apretar el trinquete —bajar el baseline— quede en el historial como un cambio deliberado. El mismo PR que reduce las referencias al legacy actualiza el archivo del baseline al nuevo mínimo, así que el progreso queda registrado y fijado en el repositorio, no en la memoria de alguien. Cualquiera puede ver, en el historial, cómo el baseline fue bajando de 4 a 3 a 2 a 1 a 0 —el registro del avance de la migración—.
(c) Como el baseline está versionado, subirlo (aflojar el trinquete) también sería un commit visible: aparecería en el diff como "baseline: 1 → 2", y cualquier revisor lo vería y preguntaría "¿por qué estás permitiendo más referencias al legacy?". Un retroceso deja de ser silencioso —una dependencia que se cuela sin que nadie note— y se vuelve un acto explícito que hay que justificar en la revisión. Esa visibilidad es lo que hace que el retroceso no ocurra por descuido: no puedes aflojar el trinquete sin que se vea.
Resumen y siguiente paso
En esta lección cerraste la debilidad del baseline fijo con el trinquete: un baseline que solo baja, nunca sube. Viste, con el cincho plástico que solo se aprieta, que cada avance de la migración queda fijado mecánicamente —no por buena voluntad, sino por un número versionado que la fitness function hace cumplir—. Y lo ejecutaste: el baseline apretando escalón por escalón (4→3→2→1→0) a medida que las referencias al legacy bajaban, y un intento de agregar una dependencia con el baseline ya en 0 quedando bloqueado (1 <= 0 es FAIL). Aprendiste la disciplina que el trinquete hace cumplir —toda funcionalidad nueva nace en el modern, nunca en el legacy que se está matando—, por qué esa disciplina es tan difícil de sostener a mano (la presión del corto plazo empuja al legacy), y cómo guardar el baseline versionado convierte cualquier retroceso en un acto visible y cuestionable.
Antes de avanzar deberías poder: explicar por qué un baseline fijo tolera el vaivén y un trinquete no; simular la evolución de un baseline-trinquete dado una secuencia de conteos; argumentar por qué toda feature nueva debe nacer en el modern; y explicar por qué el baseline se guarda versionado.
La lección 6 responde la pregunta que da sentido a todo el tablero: ¿cuándo se acabó?. Con el burn-down en cero y el trinquete en cero, parece que terminaste —pero "parece" no basta—. Vas a ver que el done de una migración no es una sola métrica sino una lista de condiciones que todas deben cumplirse (legacy_calls == 0, endpoints cortados, tablas cortadas, legacy_refs == 0), y que done significa algo concreto e incómodo: borrar el legacy —código, deploy y tablas—, no dejarlo "apagado por si acaso". Verás, ejecutado, un estado "ya casi" que parece terminado pero no lo está, y el estado de verdad terminado que autoriza el borrado.
Recursos
- Neal Ford, Rebecca Parsons y Patrick Kua, Building Evolutionary Architectures (O'Reilly, 2ª ed., 2022) — el libro que trata las fitness functions y la idea de fijar propiedades arquitectónicas para que no se degraden; el trinquete es la forma de hacer que la propiedad "el legacy no crece" sea irreversible. En inglés.
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. El patrón exige que el legacy solo se reduzca, nunca crezca; el trinquete es cómo se garantiza esa dirección única. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — sobre la regla de no seguir agregando al monolito durante su descomposición, y por qué violarla alarga (o eterniza) la migración. La disciplina que el trinquete automatiza. En inglés.
- Martin Fowler, martinfowler.com — el bliki con las entradas sobre arquitectura evolutiva que enmarcan la idea de restricciones que solo se aprietan (ratchets) para proteger propiedades del sistema. En inglés.