Módulo 7: Medir el progreso de la migración
Definir el "done": cuándo se borra el legacy
Descripción
Tienes el burn-down bajando (lección 3), la fitness function vigilando que el legacy no crezca (lección 4) y el trinquete fijando cada avance (lección 5). Todos los instrumentos apuntan hacia abajo, hacia cero. Y aquí llega la pregunta que da sentido a todo el tablero, la más importante y la más descuidada de cualquier migración: ¿cuándo se acabó? Parece obvia —"cuando llegue a cero"—, pero esconde dos trampas. La primera: ¿cero de qué? El tráfico puede estar en cero mientras todavía queda una tabla compartida y una referencia suelta en el código. La segunda, más profunda: ¿qué significa "terminado"? Para demasiados equipos significa "apagar el legacy". Y esta lección sostiene que terminado significa borrar —eliminar el código, el deploy y las tablas del legacy—, no dejarlo apagado "por si acaso".
Definir el "done" de una migración es escribir, por adelantado y de forma verificable, la lista de condiciones que todas deben cumplirse para declarar la migración completa. No es una sola métrica; es una checklist. legacy_calls == 0 (nadie llama al legacy), sí, pero también: todos los endpoints cortados del monolito, todas las tablas migradas, legacy_refs == 0 (el código nuevo ya no depende del legacy). Y solo cuando cada casilla está marcada, la migración está done, y la acción que done autoriza es borrar el legacy entero. Una checklist con casillas objetivas, y una acción concreta que dispara al completarse. Sin esa definición escrita de antemano, "terminado" es una sensación, y las sensaciones se quedan en 99% para siempre.
Conexión con el módulo. Esta lección da el criterio de terminación que usan todos los instrumentos anteriores: el burn-down llega a legacy_calls == 0 (una condición del done), la fitness function con trinquete llega a legacy_refs == 0 (otra condición), y aquí las juntamos en una sola definición verificable. La lección 7 muestra qué pasa cuando no hay criterio de done —la migración eterna que se atora sin nunca declararse terminada—, y por qué tener este criterio es lo que la evita. La lección 8 lo integra en el MigrationTracker del capstone. Fíjate en la frontera: el mecanismo de retirar una rebanada —borrar el código legacy, simplificar el facade— se ejecutó en el M3 (lección 7 de ese módulo) para una rebanada individual. Aquí definimos el criterio que declara la migración completa como un conjunto de condiciones medibles, el punto que dispara ese retiro. M3 hizo el corte; aquí definimos cuándo se autoriza.
Una analogía: la recepción de obra con acta y sin andamios
Cuando contratas una remodelación, hay un momento formal que se llama recepción de obra: el día en que declaras el trabajo terminado, firmas un acta, y haces el último pago. Y ese momento no depende de que "se vea terminado" ni de que el maestro diga "ya quedó". Depende de una lista de verificación: ¿las instalaciones eléctricas funcionan?, ¿no hay filtraciones?, ¿los acabados están completos?, ¿retiraron todos los andamios, la basura y las herramientas?, ¿te entregaron las llaves? Solo cuando todas las casillas están marcadas firmas el acta. Si falta una —quedó un cuarto sin pintar, hay una fuga, dejaron los andamios "por si vuelven"— la obra no está recibida, por más que el 95% se vea impecable.
Fíjate en dos detalles de la recepción de obra que son exactamente el punto de esta lección. Primero: es una lista, no una sola cosa. No firmas porque "la cocina quedó bonita"; firmas porque todo lo de la lista está hecho. Una obra puede tener la cocina perfecta y una fuga en el baño, y no está terminada. Segundo, y más importante: la recepción exige que se lleven los andamios y las herramientas. Una obra donde todo funciona pero dejaron los andamios puestos "por si hay que volver" no está terminada —está a medias, con la casa ocupada por estructuras que ya no sirven, estorbando y costando la renta del andamio—. La obra termina cuando puedes vivir en la casa y los andamios se fueron.
El "done" de una migración es esa recepción de obra. La lista son las condiciones (legacy_calls == 0, endpoints cortados, tablas cortadas, legacy_refs == 0): todas marcadas, o no está terminado. Y llevarse los andamios es borrar el legacy —el código, el deploy, las tablas—. Una migración donde el tráfico está en cero pero el legacy sigue desplegado "por si acaso" es la obra con los andamios puestos: parece terminada, pero no lo está, y el andamio cuesta. Done es firmar el acta y ver el camión de los andamios alejarse.
Ejemplo trabajado: "ya casi" contra "de verdad terminado"
Vamos a ejecutar el criterio de done como una checklist de cuatro condiciones sobre el catalog de Mercado. La función done_report evalúa cada condición y solo declara done = True si todas se cumplen. Corremos dos estados: el estado A, "ya casi" —el tráfico ya no toca el legacy, pero quedan una tabla compartida y una referencia en el código—, y el estado B, de verdad terminado —todo en cero—. Solo el segundo autoriza borrar.
# Definir el "done" de la migracion. No es una sola metrica: es una lista de
# condiciones que TODAS deben cumplirse. Y done significa BORRAR el legacy,
# no dejarlo "apagado por si acaso".
ENDPOINTS_TOTAL = 6
TABLES_TOTAL = 3
def done_report(state):
checks = {
"legacy_calls == 0": state["legacy_calls"] == 0,
"endpoints cortados == total": state["endpoints_cut"] == ENDPOINTS_TOTAL,
"tables cortadas == total": state["tables_cut"] == TABLES_TOTAL,
"legacy_refs == 0": state["legacy_refs"] == 0,
}
return checks, all(checks.values())
def show(title, state):
checks, done = done_report(state)
print(f"{title}")
for name, ok in checks.items():
mark = "[x]" if ok else "[ ]"
print(f" {mark} {name}")
if done:
print(" => done = True -> BORRAR el legacy (codigo + deploy + tablas)\n")
else:
pend = [n for n, ok in checks.items() if not ok]
print(f" => done = False -> faltan: {', '.join(pend)}\n")
print("El criterio de done: TODAS las condiciones, no una sola\n")
# Estado A: el trafico ya no toca el legacy, pero queda 1 tabla compartida y
# 1 referencia en el codigo. Parece terminado; NO lo esta.
show("Estado A - 'ya casi' (trafico en 0, pero quedan restos):", {
"legacy_calls": 0,
"endpoints_cut": 6,
"tables_cut": 2,
"legacy_refs": 1,
})
# Estado B: todo cortado. Ahora si: done -> borrar.
show("Estado B - de verdad terminado:", {
"legacy_calls": 0,
"endpoints_cut": 6,
"tables_cut": 3,
"legacy_refs": 0,
})
print(" 'Trafico en 0' no es done: la rebanada esta migrada cuando el legacy")
print(" se puede BORRAR entero, no cuando dejo de recibir requests.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
El criterio de done: TODAS las condiciones, no una sola
Estado A - 'ya casi' (trafico en 0, pero quedan restos):
[x] legacy_calls == 0
[x] endpoints cortados == total
[ ] tables cortadas == total
[ ] legacy_refs == 0
=> done = False -> faltan: tables cortadas == total, legacy_refs == 0
Estado B - de verdad terminado:
[x] legacy_calls == 0
[x] endpoints cortados == total
[x] tables cortadas == total
[x] legacy_refs == 0
=> done = True -> BORRAR el legacy (codigo + deploy + tablas)
'Trafico en 0' no es done: la rebanada esta migrada cuando el legacy
se puede BORRAR entero, no cuando dejo de recibir requests.
Lee las dos checklists, porque la diferencia entre ellas es toda la lección.
El estado A es el más peligroso de una migración, porque es el que se siente terminado sin estarlo. Mira sus casillas: legacy_calls == 0 marcada (el tráfico ya no toca el legacy —el burn-down llegó a cero—), endpoints cortados marcada (los seis endpoints ya viven en el modern). Dos de cuatro en verde, y las dos más visibles. Un equipo que solo mirara el burn-down y los endpoints diría "terminamos": el tráfico está en cero, los endpoints migrados. Pero dos casillas siguen sin marcar: tables cortadas == total (2 de 3 —queda una tabla que el modern y el legacy todavía comparten—) y legacy_refs == 0 (queda 1 referencia en el código nuevo que aún llama al legacy). El veredicto: done = False, faltan las tablas y las referencias. La migración parece terminada por donde la mires primero (tráfico, endpoints), pero no lo está: si borraras el legacy ahora, romperías el modern, que todavía depende de esa tabla compartida y esa referencia. Es la obra con la cocina perfecta y una fuga en el baño.
El estado B es la recepción de obra completa: las cuatro casillas marcadas. legacy_calls == 0, los seis endpoints cortados, las tres tablas cortadas, cero referencias en el código. done = True. Y fíjate en la acción que dispara: no "apagar el legacy", sino BORRAR el legacy (código + deploy + tablas). Ahora sí se puede eliminar el módulo legacy entero —el código del repositorio, el servicio desplegado, y las tablas que ya nadie usa— sin romper nada, porque nada depende ya de él. Es firmar el acta y ver el camión de los andamios irse.
El renglón final es el corazón de la lección: "tráfico en 0" no es done. La rebanada está migrada cuando el legacy se puede borrar entero, no cuando dejó de recibir requests. El burn-down en cero es una condición necesaria del done, pero no suficiente —es la casilla más visible, no la única—. Un done honesto exige que todas las dependencias del legacy estén cortadas, porque el legacy no está muerto mientras algo, por pequeño que sea, todavía dependa de él.
Profundización: por qué done es una lista, y por qué done es borrar
Dos ideas de esta lección merecen desarrollarse, porque son las que más se resisten en la práctica: que done sea una lista y no una métrica, y que done sea borrar y no apagar.
Por qué una lista. Una migración corta el legacy por varios frentes a la vez, y esos frentes no terminan al mismo tiempo. El tráfico se desvía (frente del strangler, M3); los endpoints se extraen (frente del servicio, M5); las tablas se migran (frente de los datos, M6); las referencias en el código se eliminan (frente del código). Cada frente tiene su propio burn-down, y llegan a cero en momentos distintos. Es perfectamente posible —y común— que el tráfico llegue a cero primero (es lo más visible y lo que el negocio empuja) mientras una tabla compartida o una dependencia de código se quedan atrás, porque son menos visibles y "no urgen". Si defines done por el frente más visible (el tráfico), declaras terminado con frentes abiertos, y esos frentes abiertos son lo que impide borrar el legacy. La lista te obliga a verificar todos los frentes, no solo el que ves primero. Done es la conjunción (all(...)), no la casilla más llamativa.
Frente Metrica Llega a 0 tipicamente...
───────────────── ────────────────── ────────────────────────
trafico (M3) legacy_calls == 0 primero (visible, lo empuja el negocio)
endpoints (M5) endpoints_cut pronto (se ven en el codigo)
tablas (M6) tables_cut tarde (menos visible, "no urge")
codigo (fitness) legacy_refs == 0 al final (la ultima dependencia)
┌──────────────────────────┐
done = AND de los cuatro ───────────────┤ solo entonces: BORRAR │
└──────────────────────────┘
Por qué borrar y no apagar. "Apagar por si acaso" suena prudente y es, en realidad, la puerta de entrada a la migración eterna (lección 7). Dejar el legacy desplegado pero desconectado tiene tres costos reales que no desaparecen por estar "apagado": el costo de operación (sigue consumiendo servidores, licencias, mantenimiento —pagas por un sistema que no atiende a nadie—), el costo cognitivo (cada desarrollador nuevo lo encuentra en el código, no sabe que está muerto, y pierde tiempo entendiéndolo o lo modifica pensando que está vivo), y el costo de riesgo (el código muerto sin mantenimiento se pudre —dependencias sin actualizar, vulnerabilidades sin parchar— y es una trampa: alguien podría reactivarlo por error y reintroducir los bugs que la migración dejó atrás). Borrar elimina los tres costos; apagar no elimina ninguno. Y el miedo que motiva el "por si acaso" —"¿y si lo necesitamos?"— tiene una respuesta técnica: el código no se pierde al borrarlo, el historial de git lo conserva. Borrar el legacy del sistema vivo no es tirarlo a la basura; es sacarlo de donde estorba, con una copia guardada en el historial por si el caso improbable ocurre. Done es borrar porque solo borrar cobra el premio de la migración —encoger el sistema, dejar de mantener dos—.
Hay una versión intermedia y sana, que ya viste en el M3: el retiro reversible por etapas. En vez de borrar de golpe, primero desconectas el legacy (el modern ya no lo llama, pero el código sigue) durante un periodo corto con fecha de caducidad —una o dos semanas que cubran los casos raros (un cierre de mes, un proceso nocturno)—, y si nada se rompe, borras. Eso da un margen de confianza sin caer en la permanencia. La clave es la fecha de caducidad: "desconectado dos semanas y luego se borra" es un plan; "desconectado para siempre por si acaso" es la migración eterna con otro nombre. El done se declara cuando el borrado ocurre, no cuando el periodo de gracia empieza.
Una última precisión: el done se define al principio, no al final. La checklist de condiciones debe escribirse el día uno de la migración, en el plan, para que todos sepan hacia qué línea de meta corren. Definir el done al final —cuando ya estás "casi"— es demasiado tarde: para entonces cada quien tiene su idea de "terminado", y la tentación de declarar victoria en el frente más visible es máxima. El done escrito por adelantado es un contrato con el futuro: "no diremos que terminamos hasta que estas cuatro casillas estén marcadas y el legacy borrado". Ese contrato es lo que resiste la presión de dar por terminada una migración que solo va por el 99%.
Errores comunes
Declarar "migrado" con la vieja ruta aún prendida. Qué pasa: el tráfico llega a cero, el equipo declara la migración terminada, y el legacy se queda desplegado "por si acaso". Por qué pasa: legacy_calls == 0 es la casilla más visible y la que el negocio empuja, así que se toma como el final; y borrar el legacy es trabajo extra sin recompensa visible inmediata. Cómo detectarlo: la migración se declaró "hecha" pero el legacy sigue en producción, sale en los dashboards, y consume recursos meses después. Cómo corregirlo: done no es "tráfico en cero", es todas las condiciones cumplidas y el legacy borrado. El tráfico en cero es necesario pero no suficiente —quedan las tablas, las referencias, el deploy—. La migración termina cuando el legacy está borrado, no cuando dejó de recibir requests. Poner "borrado" en la definición de done desde el día uno evita esta declaración prematura.
Definir done por la métrica más visible en vez de por la lista completa. Qué pasa: el equipo mira solo el burn-down de tráfico y, al verlo en cero, da por terminada la migración, sin verificar las tablas compartidas ni las referencias de código. Por qué pasa: el tráfico es lo más visible y lo más fácil de medir; las tablas compartidas y las dependencias de código son menos visibles y "no urgen". Cómo detectarlo: la migración se declara done con frentes abiertos —una tabla que el modern y el legacy todavía comparten, un import al legacy que quedó—; al intentar borrar el legacy, algo se rompe. Cómo corregirlo: done es la conjunción de todas las condiciones (all(...)), no la casilla más llamativa. Cada frente de la migración (tráfico, endpoints, tablas, código) tiene su propia condición, y llegan a cero en momentos distintos; la lista te obliga a verificar todos. Si al intentar borrar el legacy algo se rompe, es la prueba de que done estaba mal definido —faltaba una casilla—.
Dejar el legacy "apagado pero presente" indefinidamente. Qué pasa: el equipo desconecta el legacy pero deja el código y el deploy "por si acaso", sin fecha para borrarlo. Por qué pasa: borrar da miedo ("¿y si lo necesitamos?"), y "apagado pero presente" parece un punto medio prudente. Cómo detectarlo: hay módulos legacy desconectados hace meses o años, sin dueño, sin mantenimiento, saliendo en cada búsqueda del código y confundiendo a cada desarrollador nuevo. Cómo corregirlo: "apagado pero presente" es válido solo como periodo de gracia con fecha de caducidad (una o dos semanas para ganar confianza, cubriendo los casos raros). Pasado ese periodo sin incidentes, se borra. El código muerto permanente es deuda: se pudre, se vuelve una trampa, y contradice la razón de la migración (simplificar). El historial de git conserva el código si de verdad se necesita, así que borrar no es perder —es sacar del sistema vivo lo que ya no sirve—. Done se declara con el borrado, no con la desconexión.
Ejercicios
Ejercicio 1 — ¿Done o no? Para cada estado del catalog (endpoints total 6, tablas total 3), di si está done y qué falta: (a) legacy_calls=0, endpoints_cut=6, tables_cut=3, legacy_refs=0; (b) legacy_calls=0, endpoints_cut=6, tables_cut=3, legacy_refs=2; (c) legacy_calls=5, endpoints_cut=6, tables_cut=3, legacy_refs=0; (d) legacy_calls=0, endpoints_cut=4, tables_cut=1, legacy_refs=3.
Ver solución
- (a) DONE. Las cuatro condiciones en verde: tráfico en 0, los 6 endpoints cortados, las 3 tablas cortadas, cero referencias. Se puede borrar el legacy. Es el estado B del ejemplo.
- (b) NO done. Falta
legacy_refs == 0: quedan 2 referencias en el código nuevo que aún llaman al legacy. Aunque el tráfico, los endpoints y las tablas estén en cero, esas 2 referencias significan que el modern todavía depende del legacy —borrarlo rompería el modern—. - (c) NO done. Falta
legacy_calls == 0: el legacy todavía atiende 5 requests. Aunque todo lo demás esté cortado, hay 5 casos que dependen del legacy; apagarlo los dejaría sin atender. - (d) NO done. Faltan tres condiciones: solo 4 de 6 endpoints cortados, solo 1 de 3 tablas, y 3 referencias en el código. Solo el tráfico está en 0 —es el estado más engañoso, el que "parece" avanzado por la casilla más visible pero tiene tres frentes abiertos—.
La regla es siempre all(...): done solo si las cuatro están marcadas. Una sola casilla sin marcar significa que algo todavía depende del legacy, y mientras algo dependa, no se puede borrar.
Ejercicio 2 — El costo de "apagado por si acaso". Un equipo declaró la migración del catalog terminada con el tráfico en cero, pero dejó el legacy desplegado "por si acaso". (a) Nombra los tres costos concretos de dejarlo así. (b) ¿Cuál es la respuesta técnica al miedo "¿y si lo necesitamos?"? (c) ¿Cómo se combina un periodo de gracia con la exigencia de borrar?
Ver solución
(a) Los tres costos: operación (el legacy sigue desplegado, consumiendo servidores, licencias y mantenimiento —se paga cada mes por un sistema que no atiende a nadie—); cognitivo (cada desarrollador nuevo encuentra el legacy en el código, no sabe que está muerto, y pierde tiempo entendiéndolo o lo modifica creyéndolo vivo); y riesgo (el código muerto sin mantenimiento se pudre —dependencias y vulnerabilidades sin parchar— y es una trampa: alguien podría reactivarlo por error y reintroducir bugs viejos).
(b) Que el código no se pierde al borrarlo: el historial de git lo conserva. Borrar el legacy del sistema vivo no es tirarlo a la basura; es sacarlo de donde estorba, con una copia guardada en el historial por si el caso improbable de necesitarlo ocurre. El miedo asume que borrar es irreversible, y no lo es —el control de versiones es exactamente la red que hace seguro borrar—.
(c) El periodo de gracia es un retiro reversible por etapas con fecha de caducidad: primero desconectas el legacy (el modern ya no lo llama, pero el código sigue) durante una o dos semanas que cubran los casos raros (un cierre de mes, un proceso nocturno); si nada se rompe en ese periodo, borras. La combinación es "desconectado con fecha de caducidad, y luego borrado". Lo que no es válido es "desconectado para siempre por si acaso" —esa es la migración eterna—. El done se declara cuando el borrado ocurre, no cuando la desconexión empieza; el periodo de gracia solo da confianza, no reemplaza el borrado.
Ejercicio 3 — Escribe el done. Vas a arrancar la migración del módulo de orders de Mercado y te piden definir su "done" el día uno. (a) Escribe la checklist de condiciones. (b) ¿Por qué es mejor definirla ahora que cuando estés "casi terminando"? (c) ¿Qué acción concreta dispara marcar todas las casillas?
Ver solución
(a) La checklist de done para orders: (1) legacy_calls == 0 —ninguna request de orders toca el monolito legacy—; (2) todos los endpoints de orders cortados del monolito y sirviéndose desde el modern; (3) todas las tablas de orders migradas y ninguna compartida con el legacy; (4) legacy_refs == 0 —el código nuevo de orders no tiene ninguna referencia al monolito—. (Podrían agregarse condiciones específicas de orders, como "todos los procesos nocturnos que tocaban orders reapuntados al modern", porque orders tiene más escrituras y flujos batch que el catálogo.) Done solo si todas se cumplen.
(b) Porque definir el done al principio es un contrato con el futuro: fija la línea de meta antes de que la presión y las opiniones divergentes aparezcan. Cuando estés "casi terminando", cada quien tendrá su idea de "terminado", y la tentación de declarar victoria en el frente más visible (el tráfico) será máxima —justo el momento más débil para definir el criterio—. Un done escrito el día uno resiste esa presión: "acordamos que no terminamos hasta que estas casillas estén marcadas y el legacy borrado". Definirlo tarde es dejar que la migración se declare terminada por cansancio, no por criterio.
(c) Marcar todas las casillas dispara borrar el legacy de orders entero: el código del módulo en el repositorio, el servicio/deploy si tenía uno propio, y las tablas del legacy que ya nadie usa —con un posible periodo de gracia corto y con fecha de caducidad antes del borrado definitivo—. La acción no es "declarar en una reunión que terminamos" ni "apagar por si acaso"; es el borrado concreto que cobra el premio de la migración: orders deja de ser parte del monolito legacy, y el monolito es una rebanada más pequeño.
Resumen y siguiente paso
En esta lección definiste el done de una migración: no una sola métrica, sino una lista de condiciones que todas deben cumplirse (legacy_calls == 0, endpoints cortados, tablas cortadas, legacy_refs == 0), y una acción concreta que dispara al completarse: borrar el legacy. Viste, con la recepción de obra —el acta que solo se firma con toda la lista marcada y los andamios retirados—, que done es una checklist (no la casilla más visible) y que done es borrar (no apagar por si acaso). Y lo ejecutaste: el estado A "ya casi" (tráfico y endpoints en cero, pero una tabla y una referencia pendientes) dando done = False, y el estado B de verdad terminado (todo en cero) dando done = True → borrar. Aprendiste por qué done es una lista (la migración corta el legacy por varios frentes que terminan en momentos distintos), por qué done es borrar (los tres costos de dejarlo apagado, y que el historial de git hace seguro borrar), y por qué el done se define al principio y no al final.
Antes de avanzar deberías poder: escribir la checklist de done de una migración; evaluar un estado y decir si está done y qué falta; explicar por qué "tráfico en cero" no es done; y defender por qué done es borrar y no apagar frente al "¿y si lo necesitamos?".
La lección 7 muestra, con toda su crudeza, qué pasa cuando no hay criterio de done: la migración eterna, el peor resultado posible —dos sistemas mantenidos para siempre—. Vas a ver, ejecutadas lado a lado, dos migraciones con la misma velocidad los primeros sprints: una sin criterio de done que se atora en el último tramo (98.5%) y sigue pagando el costo de dos sistemas indefinidamente, y otra con una forcing function (un deadline de done) que llega a cero y borra el legacy. El costo acumulado que se dispara en una y se detiene en la otra es la prueba, en números, de por qué la definición de done de esta lección no es burocracia: es lo que hace que una migración termine.
Recursos
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. Fowler insiste en que la meta del strangler es que el sistema viejo muera —se retire, no coexista—: el done de esta lección es el criterio que declara esa muerte y autoriza el borrado. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — la última milla de la migración: retirar (no solo desconectar) la funcionalidad del monolito, y por qué dejarla "por si acaso" perpetúa el problema. La fuente de por qué done es borrar. En inglés.
- Neal Ford, Rebecca Parsons y Patrick Kua, Building Evolutionary Architectures (O'Reilly, 2ª ed., 2022) — sobre definir criterios objetivos y verificables para el estado de una arquitectura; el done como checklist de condiciones medibles es esa idea aplicada al final de una migración. En inglés.
- Martin Fowler, martinfowler.com — el bliki con las entradas sobre strangler fig y retiro de sistemas legacy que fundamentan por qué "terminado" significa borrado y no apagado. En inglés.