Módulo 6: Migrar datos sin downtime

Proyecto: migrar los datos del catalog de Mercado sin downtime

Descripción

Llegaste al capstone del módulo. En las siete lecciones anteriores construiste la migración de datos pieza por pieza: el marco expand-contract (L2), el dual-write que escribe a ambos (L3), el backfill que carga lo histórico sin pisar lo fresco (L4), el parallel-run que compara y detecta discrepancias (L5), la reconciliación que las lleva a cero (L6), y el read-switch con el apagado del dual-write (L7). En este proyecto juntas todo en una sola simulación ejecutada: migras los datos del catalog de Mercado de old_db a new_db de punta a punta, con una clase DataMigration que integra las seis técnicas, corrida como un diario de migración día por día.

El diario es la forma más honesta de ver una migración de datos completa, porque muestra lo que las lecciones aisladas no: cómo las piezas trabajan juntas a lo largo del tiempo, con el sistema vivo en cada paso. Vas a ver el seed histórico solo en el viejo, el dual-write encenderse y capturar una escritura viva, el backfill correr con un bug real (salta los productos sin stock), el parallel-run atrapar ese hueco antes de que haga daño, la reconciliación arreglar la causa y llevar las discrepancias a cero, el read-switch mover las lecturas, el apagado del dual-write, y —el premio— el viejo retirado. Es la película entera, no las fotos sueltas.

Y este proyecto tiene una entrega, como todo capstone: (1) el plan de migración de datos —el orden de las fases y por qué ese orden—, (2) el código ejecutado —la clase DataMigration completa corrida con su salida literal—, y (3) la justificación de por qué esta migración incremental y verificada venció al "copiar todo en una ventana de mantenimiento" que apagaría Mercado. Al terminar, tendrás en las manos una migración de datos real, de principio a fin, que puedes defender con números.

Conexión con el módulo. Este proyecto integra las lecciones 2 a 7 en una ejecución continua. Cierra el módulo 6 y prepara los que siguen: el módulo 7 (medir el progreso) toma este corte y lo convierte en una métrica —el burn-down de llamadas al viejo hasta cero, la fitness function que evita la migración eterna—. Fíjate en la frontera del capstone: aquí migramos los datos del catalog con la técnica simulada en memoria; ejecutar esto a escala en producción (CDC, backfill por lotes reanudable, reconciliación automatizada de billones de filas) es del ecosistema Data Engineering. Este capstone es la migración de una rebanada de datos, ejecutada entera con el patrón completo.

El plan de migración de datos

Antes del código, el plan —porque una migración de datos sin plan es una forma elegante de perder datos—. Migrar el catalog de Mercado de old_db (modelo viejo: sku, name, price_cents, stock) a new_db (modelo nuevo: id, title, price_usd, in_stock) tiene un orden de fases que no es negociable, porque cada fase depende de la anterior:

Fase                  Depende de              Por que en este orden
────────────────────  ──────────────────────  ─────────────────────────────────
1. dual_write ON       expand-contract (L2)    capturar el frente ANTES de copiar
                                                el pasado (evita el hueco de L4)
2. backfill            dual_write ya ON         cargar lo historico; el solape con
                                                dual_write es seguro (insert-if-absent)
3. parallel_run        dual_write + backfill    validar que el new cuadra con el old;
                                                sin esto, el read_switch es un salto de fe
4. reconciliar         parallel_run detecto     arreglar la CAUSA hasta discrepancias=0
5. read_switch         parallel_run VERDE        mover lecturas al new (old caliente)
6. dual_write OFF      confianza en el new       dejar de escribir al old (red retirada)
7. retirar old         nadie lee ni escribe      borrar el old: cobrar el premio

El principio del plan: cada fase se apoya en la anterior, y saltarse una rompe la siguiente. Encender el dual-write antes del backfill evita el hueco de escrituras perdidas (L4). El backfill antes del parallel-run deja el nuevo completo para poder compararlo. El parallel-run verde antes del read-switch garantiza que no muevas las lecturas a datos malos. Y el read-switch antes de apagar el dual-write mantiene el viejo caliente como red (L7). El orden no es una lista de tareas; es una cadena de dependencias donde cada eslabón sostiene al siguiente.

Una analogía: mudar el archivo de una clínica sin cerrar ni un día

Piensa en una clínica que va a cambiar su sistema de expedientes de pacientes —del archivo de papel de toda la vida a uno digital nuevo—, con una regla que no se puede violar: la clínica no cierra ni un día. Siguen llegando pacientes, se siguen escribiendo notas, se siguen actualizando historias. No hay un "fin de semana de mantenimiento" para mudar todo de golpe; la clínica, como Mercado, atiende sin parar.

Así lo hace una clínica sensata, y cada paso es una fase del plan:

  1. Empiezan a escribir cada nota nueva en los dos sistemas (dual-write). Desde el día que lo activan, cada consulta se registra en el papel y en el digital. El frente queda cubierto: nada nuevo se pierde.
  2. Digitalizan el archivo histórico (backfill), sin tocar las notas frescas que ya se registraron en digital ese mismo día. Copian lo viejo, respetan lo nuevo.
  3. Auditan: comparan expedientes del papel contra el digital (parallel-run). Y encuentran un problema real: los expedientes de pacientes dados de alta (que ya no están internados) no se digitalizaron —alguien filtró "solo pacientes activos" por error—.
  4. Corrigen la causa y re-digitalizan (reconciliación): arreglan el filtro y vuelven a pasar el archivo, que ahora incluye a los dados de alta. La auditoría vuelve a correr: cero diferencias.
  5. Los médicos empiezan a consultar el sistema digital (read-switch), pero el archivo de papel se mantiene actualizado un tiempo, por si el digital falla.
  6. Dejan de actualizar el papel (dual-write off), una vez que confían en el digital.
  7. Archivan (retiran) el papel viejo (retirar). La clínica opera solo con el sistema nuevo, y nunca cerró un día.

El catalog de Mercado es el archivo de la clínica; los productos son los expedientes; el bug del backfill que salta los productos sin stock es el filtro que se saltó a los pacientes dados de alta. Este proyecto muda ese archivo completo, de punta a punta, con la clínica siempre abierta.

Ejemplo trabajado: el diario de migración del catalog, ejecutado

Aquí está la clase DataMigration completa, integrando las seis técnicas del módulo, corrida como un diario día por día. El catalog arranca con 5 productos históricos solo en el viejo; el dual-write se enciende y captura una escritura viva; el backfill corre con un bug (salta los productos con stock 0, como webcam); el parallel-run atrapa el hueco; la reconciliación arregla el filtro y re-corre el backfill; y con las discrepancias en cero, el read-switch, el apagado del dual-write y el retiro cierran la migración.

def canon_old(r):
    return (r["sku"], r["name"], r["price_cents"], r["stock"] > 0)

def canon_new(r):
    return (r["id"], r["title"], round(r["price_usd"] * 100), r["in_stock"])

class DataMigration:
    def __init__(self):
        self.old_db = {}
        self.new_db = {}
        self.write_to_old = True
        self.write_to_new = False        # dual_write OFF al arrancar
        self.read_source = "old"
        self.backfill_skips_oos = True    # BUG: el backfill filtra stock 0
        self.retired = False

    def to_new_model(self, r):
        return {"id": r["sku"], "title": r["name"],
                "price_usd": r["price_cents"] / 100, "in_stock": r["stock"] > 0}

    def write(self, r):                   # la app sigue sirviendo escrituras
        if self.write_to_old and not self.retired:
            self.old_db[r["sku"]] = r
        if self.write_to_new:
            self.new_db[r["sku"]] = self.to_new_model(r)

    def backfill(self):                   # idempotente + insert-if-absent
        copied = 0
        for sku, r in self.old_db.items():
            if sku in self.new_db:
                continue                  # no pisa lo fresco
            if self.backfill_skips_oos and r["stock"] == 0:
                continue                  # el bug: salta los out-of-stock
            self.new_db[sku] = self.to_new_model(r)
            copied += 1
        return copied

    def parallel_run(self):
        disc = 0
        for sku in self.old_db:
            if sku not in self.new_db or canon_old(self.old_db[sku]) != canon_new(self.new_db[sku]):
                disc += 1
        return disc

    def dual(self):
        return "ON" if (self.write_to_old and self.write_to_new) else "OFF"


def prod(sku, name, cents, stock):
    return {"sku": sku, "name": name, "price_cents": cents, "stock": stock}

m = DataMigration()

def diary(day, phase, disc, decision):
    old_n = "-" if m.retired else len(m.old_db)
    dv = "-" if disc is None else str(disc)
    print(f"{day:>3}  {phase:<22}{str(old_n):>4}{len(m.new_db):>5}{dv:>6}   "
          f"{m.read_source:<5}{m.dual():<5}{decision}")

print("Diario de migracion de datos del catalog de Mercado (sin downtime)\n")
print(f"{'dia':>3}  {'fase':<22}{'old':>4}{'new':>5}{'disc':>6}   "
      f"{'read':<5}{'dual':<5}decision")
print("-" * 82)

# DIA 1 -- Seed historico: 5 productos con dual_write OFF (solo en old).
for r in [prod("ssd-1tb", "SSD 1TB", 8999, 12), prod("usb-c-hub", "USB-C Hub", 3499, 40),
          prod("webcam", "Webcam HD", 5999, 0), prod("kbd-mech", "Mech Kbd", 7999, 5),
          prod("mouse-pro", "Mouse Pro", 2499, 8)]:
    m.write(r)
diary(1, "seed historico", None, "dual_write OFF -> encender")

# DIA 2 -- Encender dual_write. Llega una escritura viva (nuevo producto).
m.write_to_new = True
m.write(prod("hdmi-cable", "HDMI 2m", 1299, 30))     # cae en old Y new
diary(2, "dual_write ON + write", None, "el write vivo cae en ambos; falta backfill")

# DIA 3 -- Backfill (con el bug: salta webcam por stock 0). parallel_run.
copied = m.backfill()
disc = m.parallel_run()
diary(3, "backfill", disc, f"copio {copied}; parallel_run={disc} -> HOLD (reconciliar)")

# DIA 4 -- Reconciliar: arreglar el filtro del backfill y re-correr (idempotente).
m.backfill_skips_oos = False
copied = m.backfill()
disc = m.parallel_run()
diary(4, "reconciliar", disc, f"fix backfill +{copied}; parallel_run={disc} -> autoriza switch")

# DIA 5 -- read_switch: parallel_run verde -> las lecturas pasan al new.
m.read_source = "new"
disc = m.parallel_run()
diary(5, "read_switch", disc, "lecturas movidas al new; old de respaldo")

# DIA 6 -- Apagar dual_write: se deja de escribir al old (queda congelado).
m.write_to_old = False
disc = m.parallel_run()
diary(6, "dual_write OFF", disc, "el old deja de recibir escrituras")

# DIA 7 -- Retirar el old: nadie lo lee ni lo escribe. Fin de la migracion.
frozen = len(m.old_db)
m.retired = True
m.old_db = {}
diary(7, "retirar old", 0, f"old_db retirado ({frozen} filas liberadas). FIN")
print("-" * 82)
print("\nEntregable: el catalog de Mercado migro de old_db a new_db SIN apagarse.")
print("dual_write mantuvo el frente sincronizado, el backfill cubrio lo historico,")
print("parallel_run atrapo el hueco (webcam, stock 0) ANTES del switch, y solo con")
print("discrepancias=0 se movieron las lecturas y se retiro el old. Cada paso reversible.")

Qué esperar. Al correr el archivo, la salida es exactamente esta:

Diario de migracion de datos del catalog de Mercado (sin downtime)

dia  fase                   old  new  disc   read dual decision
----------------------------------------------------------------------------------
  1  seed historico           5    0     -   old  OFF  dual_write OFF -> encender
  2  dual_write ON + write    6    1     -   old  ON   el write vivo cae en ambos; falta backfill
  3  backfill                 6    5     1   old  ON   copio 4; parallel_run=1 -> HOLD (reconciliar)
  4  reconciliar              6    6     0   old  ON   fix backfill +1; parallel_run=0 -> autoriza switch
  5  read_switch              6    6     0   new  ON   lecturas movidas al new; old de respaldo
  6  dual_write OFF           6    6     0   new  OFF  el old deja de recibir escrituras
  7  retirar old              -    6     0   new  OFF  old_db retirado (6 filas liberadas). FIN
----------------------------------------------------------------------------------

Entregable: el catalog de Mercado migro de old_db a new_db SIN apagarse.
dual_write mantuvo el frente sincronizado, el backfill cubrio lo historico,
parallel_run atrapo el hueco (webcam, stock 0) ANTES del switch, y solo con
discrepancias=0 se movieron las lecturas y se retiro el old. Cada paso reversible.

Lee el diario día por día, porque cada renglón es una decisión de migración real y cada columna una de las piezas del módulo trabajando junta.

Día 1 — Seed histórico: 5 productos escritos con el dual_write en OFF. Caen solo en el viejo (old=5, new=0). Es el punto de partida: el catálogo del monolito lleva tiempo acumulando datos, el almacén nuevo está vacío. Decisión: encender el dual-write.

Día 2 — Se enciende el dual_write (columna dual pasa a ON) y llega una escritura viva: un producto nuevo, hdmi-cable. Cae en ambos (old=6, new=1). El dual-write ya captura el frente: la escritura nueva llegó a los dos almacenes. Pero el nuevo solo tiene 1 fila —le falta todo el histórico—; el backfill está pendiente.

Día 3 — Corre el backfill, con el bug: salta los productos con stock 0 (como webcam). Copió 4 filas (las históricas con stock), dejando new=5. El parallel_run compara los 6 productos del viejo contra el nuevo y encuentra 1 discrepancia: webcam falta (MISSING_IN_NEW, porque el backfill la saltó). La decisión es HOLD (reconciliar): aquí está el corazón de la migración verificada —el parallel-run no dejó avanzar al read-switch porque el nuevo estaba incompleto—. Y fíjate en la protección: webcam aún se lee bien, porque las lecturas siguen saliendo del viejo (read=old); el hueco se detectó antes de que hiciera daño.

Día 4 — Reconciliación: se arregla la causa (el filtro del backfill, backfill_skips_oos = False) y se re-corre el backfill. Por ser idempotente e insert-if-absent, copió solo la fila que faltaba (+1: webcam), sin tocar las 5 que ya estaban. El parallel_run vuelve a correr y ahora marca 0 discrepancias. Decisión: autoriza switch. Arreglar la causa (no copiar webcam a mano) significa que cualquier otro producto sin stock también quedó cubierto.

Día 5 — El read_switch: con el parallel-run verde, las lecturas pasan al nuevo (read cambia a new). El dual_write sigue en ON: el viejo se mantiene caliente como red de respaldo. Las discrepancias siguen en 0. Este es el momento de máximo riesgo (primera vez leyendo del nuevo en vivo), por eso el viejo no se apaga aún.

Día 6 — Se apaga el dual_write (columna dual pasa a OFF): ya se confía en el nuevo, así que se deja de escribir al viejo. El viejo queda congelado, pero es inofensivo —nadie lo lee (read=new desde el día 5)—. La red de respaldo se retira.

Día 7 — Se retira el viejo (old_db eliminado, columna old pasa a -). Nadie lo lee ni lo escribe, así que borrarlo no afecta nada. El catalog de Mercado ahora vive solo en new_db, con las 6 filas correctas. La migración terminó, y el sistema nunca se apagó.

El diario cuenta la historia completa de una migración de datos en siete renglones: un almacén nuevo que arrancó vacío, un dual-write que capturó el frente, un backfill con un bug que el parallel-run atrapó antes de hacer daño, una reconciliación que arregló la causa y llegó a cero, un read-switch con el viejo como red, y un viejo retirado al final. Cada paso fue sin downtime (el sistema sirvió lecturas y escrituras los siete días), verificado (el read-switch solo pasó con discrepancias=0), y reversible (hasta el día 6, podías volver las lecturas al viejo). Eso es lo que este módulo prometió: mover datos sin apagar el negocio y sin perder ni corromper una fila.

La entrega del capstone

Un capstone entrega artefactos, no solo comprensión. Aquí están los tres:

1. El plan de migración de datos. El catalog de Mercado se migra de old_db a new_db en siete fases encadenadas: encender el dual-write (capturar el frente), backfill (cargar lo histórico sin pisar lo fresco), parallel-run (validar), reconciliar (arreglar la causa hasta cero), read-switch (mover las lecturas con el viejo caliente), apagar el dual-write (retirar la red), y retirar el viejo (cobrar el premio). El orden no es negociable: cada fase depende de la anterior. (La tabla del plan está arriba.)

2. El código ejecutado. La clase DataMigration completa —dual-write + backfill idempotente + parallel-run + reconciliación + read-switch + apagado del dual-write + retiro— corrida con su salida literal, el diario de siete días. No es pseudocódigo ni una descripción: es la migración simulada, reproducible, que puedes correr y modificar (cambia el bug del backfill, el día del arreglo, el orden de las fases, y observa cómo cambia el diario —o cómo se rompe si inviertes el orden—).

3. La justificación: por qué la migración incremental venció al "copiar todo en una ventana de mantenimiento". La alternativa ingenua sería apagar Mercado un fin de semana, copiar old_db a new_db, y encender de nuevo. Ese plan tiene tres defectos que este proyecto evita: apaga el negocio (Mercado no vende durante la ventana, y las ventanas siempre se alargan), no valida (copias y rezas; si el ACL tradujo mal, lo descubres con usuarios reales, no con un parallel-run), y no es reversible (si algo sale mal a mitad, no hay vuelta atrás limpia). Este proyecto muestra la alternativa ejecutada: el catalog se migró sin apagar Mercado (sirvió tráfico los siete días), verificado (el parallel-run atrapó el bug del backfill antes de exponerlo), y reversible (hasta el día 6 se podía volver al viejo). Donde el big-bang pedía apagar el negocio y apostar todo a una copia sin verificar, la migración incremental entregó los datos movidos con el riesgo acotado en cada paso. Esa es la justificación, y ahora la puedes respaldar con un diario ejecutado.

Errores comunes

Saltarse el parallel-run "porque el backfill terminó sin error". Qué pasa: el equipo, con prisa, hace el dual-write y el backfill, y como ninguno lanzó excepción, pasa directo al read-switch —sin correr el parallel-run—. Por qué pasa: "terminó sin error" se confunde con "está bien", y el parallel-run se ve como un paso opcional de doble verificación. Cómo detectarlo: en el diario del proyecto, esto sería saltar del día 3 (backfill) al día 5 (read-switch), omitiendo el día 4. Cómo corregirlo: el bug del backfill de este proyecto (saltar webcam) es exactamente la clase de error que no lanza excepción y que solo el parallel-run atrapa. Si el equipo hubiera saltado el parallel-run, habría movido las lecturas al nuevo con webcam ausente, y el producto habría desaparecido del catálogo para todos los usuarios —descubierto por una queja, no por una comparación—. El parallel-run no es opcional: es la diferencia entre atrapar el bug antes (día 3, sin daño) o después (en producción, con usuarios afectados).

Reconciliar el síntoma y no la causa (copiar webcam a mano). Qué pasa: el parallel-run reporta webcam faltante, y el equipo la copia al nuevo directamente en vez de arreglar el filtro del backfill que la excluyó. Por qué pasa: copiar una fila baja el contador de inmediato; investigar el filtro es más lento. Cómo detectarlo: la discrepancia de webcam desaparece, pero si hay otros productos sin stock (o si se re-corre el backfill), vuelven a faltar —el filtro sigue roto—. Cómo corregirlo: en el proyecto, el día 4 arregla la causa (backfill_skips_oos = False) y re-corre el backfill, que ahora cubre webcam y cualquier otro producto sin stock de un golpe. Un catálogo real puede tener cientos de productos agotados; copiar webcam a mano dejaría los otros faltando. Arreglar la causa —el filtro— los cubre a todos y deja el backfill correcto para el futuro.

Declarar terminado el proyecto en el día 5 (read-switch) sin llegar al día 7 (retirado). Qué pasa: al ver las lecturas ya en el nuevo y todo funcionando, el equipo da la migración por hecha y no ejecuta el apagado del dual-write ni el retiro del viejo. Por qué pasa: el read-switch es el momento visible y satisfactorio; apagar el dual-write y borrar el viejo son trabajo sin recompensa visible. Cómo detectarlo: el diario se queda en el día 5; meses después el sistema sigue escribiendo a los dos almacenes y el viejo sigue desplegado. Cómo corregirlo: el capstone —como la migración— termina en el día 7, con el viejo retirado, no en el día 5. El premio de la migración (un solo almacén, el fin del doble costo de escritura, la mitad de la complejidad) solo se cobra cuando el viejo desaparece. Un proyecto que llega al read-switch pero no retira el viejo pagó todo el costo de migrar sin cobrar el beneficio —lo peor de los dos mundos, como vimos en la lección 7—. El módulo 7 mide justamente este cierre.

Ejercicios

Ejercicio 1 — Explica el día 3. En el diario, el día 3 el parallel_run marcó 1 discrepancia y la decisión fue HOLD (reconciliar) en vez de avanzar al read-switch. (a) ¿Qué producto causó la discrepancia y por qué? (b) ¿Qué habría pasado si el equipo hubiera hecho el read-switch de todos modos ese día? (c) ¿Por qué es una buena señal que la migración se detuviera ahí?

Ver solución

(a) La causó webcam. El backfill del día 3 corrió con el bug backfill_skips_oos = True, que salta los productos con stock 0 —y webcam tiene stock 0—. Así que webcam nunca se copió al nuevo, y el parallel-run la reportó como MISSING_IN_NEW: presente en el viejo, ausente en el nuevo.

(b) Si el equipo hubiera hecho el read-switch el día 3, las lecturas habrían pasado al nuevo con webcam ausente. Cualquier usuario que consultara webcam en el catálogo no la encontraría —el producto habría desaparecido de la tienda—, aunque en el viejo seguía existiendo perfectamente. El bug del backfill, invisible mientras se leía del viejo, se habría vuelto visible para los usuarios en el peor momento: en producción, servido como si fuera correcto.

(c) Porque significa que el sistema de verificación funcionó: el parallel-run detectó el hueco del backfill antes de que el read-switch lo expusiera, con las lecturas aún saliendo del viejo (así que ningún usuario se vio afectado). Una migración que se detiene ante una discrepancia real es una migración sana —está usando su red de seguridad—. Lo peligroso sería lo contrario: avanzar al read-switch ignorando el parallel-run. El HOLD del día 3 es el patrón haciendo justo lo que promete: atrapar el error antes de confiar en el nuevo.

Ejercicio 2 — Cambia el orden y predice. Sin correr el código, predice qué pasaría si el equipo invirtiera dos fases del plan. (a) Si hicieran el backfill (día 3) antes de encender el dual-write (día 2), ¿qué problema aparecería con hdmi-cable? (b) Si apagaran el dual-write (día 6) antes del read-switch (día 5), ¿qué leería un usuario? (c) ¿Qué te dice esto sobre el plan?

Ver solución

(a) Si el backfill corriera antes de encender el dual-write, se abriría el hueco de escrituras perdidas (lección 4). El backfill tomaría su foto del viejo antes de que hdmi-cable se escribiera; luego hdmi-cable se escribiría solo en el viejo (el dual-write aún no está); y cuando el dual-write se encendiera, ya no capturaría esa escritura pasada. Resultado: hdmi-cable quedaría en el viejo pero ausente del nuevo, y el parallel-run lo reportaría como faltante. El orden correcto (dual-write primero) evita ese hueco: hdmi-cable, escrito con el dual-write ya encendido, cae en ambos.

(b) Si apagaran el dual-write antes del read-switch, las lecturas seguirían saliendo del viejo, pero el viejo dejaría de recibir escrituras —se congelaría—. Un usuario que consultara un producto actualizado después del apagado vería el valor rancio del viejo (el que tenía antes de congelarse), no el valor real que solo cayó en el nuevo. Es el peligro del orden invertido de la lección 7: dejar de alimentar el almacén que todavía se lee sirve datos congelados como si fueran frescos.

(c) Que el plan no es una lista de tareas intercambiables, sino una cadena de dependencias. Cada fase se apoya en la anterior: el dual-write antes del backfill evita el hueco; el parallel-run antes del read-switch evita mover lecturas a datos malos; el read-switch antes de apagar el dual-write evita las lecturas rancias. Invertir cualquier par rompe una garantía. El orden es la migración; cambiarlo la corrompe.

Ejercicio 3 — Big-bang vs. incremental. El jefe propone: "es más simple apagar Mercado el domingo a las 3 a.m., copiar old_db a new_db, y encender a las 5 a.m. ¿Para qué todo este proceso de siete días?". Responde con tres argumentos concretos. (a) ¿Qué riesgo del negocio corre el plan del domingo? (b) ¿Qué error de datos no detectaría? (c) ¿Qué pasa si algo sale mal a las 4:30 a.m.?

Ver solución

(a) El plan del domingo apaga el negocio: durante la ventana (que "son dos horas" pero siempre se alargan), Mercado no vende, no procesa pedidos, no atiende a nadie. Cada minuto apagado es dinero perdido y clientes frustrados, y si la copia tarda más de lo previsto —lo normal con datos reales—, la tienda sigue cerrada mientras el reloj corre. El plan de siete días no apaga Mercado ni un segundo: sirve tráfico todos los días.

(b) No detectaría los errores silenciosos de la migración: si el ACL traduce mal un campo (como el título en minúsculas de las lecciones anteriores) o si la copia salta filas (como webcam), el plan del domingo lo copia mal y enciende, sin comparar nada. Descubrirías el error con usuarios reales quejándose el lunes, no con un parallel-run el sábado. El plan de siete días valida con el parallel-run antes de confiar en el nuevo: atrapa el bug del backfill el día 3, sin exponerlo.

(c) Si algo sale mal a las 4:30 a.m. —la copia falló, los datos se ven raros, el nuevo no arranca—, el plan del domingo no tiene vuelta atrás limpia: ya apagaste el viejo, ya empezaste a escribir en el nuevo, y a las 5 a.m. tienes que encender algo, funcione o no. Estás atrapado entre un viejo a medio migrar y un nuevo a medio probar, con la tienda por abrir. El plan de siete días es reversible en cada paso: hasta el día 6, un cambio de read_source te devuelve al viejo, que estuvo caliente todo el tiempo. El riesgo está acotado en cada escalón, no concentrado en una madrugada sin salida.

La lección de fondo: el big-bang concentra todo el riesgo en un instante irreversible con el negocio apagado; la migración incremental reparte el riesgo en pasos pequeños, verificados y reversibles, con el negocio vivo. Por eso los siete días —que parecen "más trabajo"— son en realidad menos riesgo.

Resumen y siguiente paso

En este capstone integraste todo el módulo en una sola migración de datos ejecutada: moviste el catalog de Mercado de old_db a new_db de punta a punta con una clase DataMigration que combinó las seis técnicas —dual-write, backfill idempotente, parallel-run, reconciliación, read-switch y apagado del dual-write—, corrida como un diario de siete días. Viste el sistema completo trabajar junto: un dual-write que capturó el frente, un backfill con un bug real que el parallel-run atrapó antes de hacer daño, una reconciliación que arregló la causa y llegó a cero, un read-switch con el viejo como red, y un viejo retirado al final —con Mercado vivo cada día—. Y produjiste la entrega del capstone: el plan de fases encadenadas, el código ejecutado, y la justificación —respaldada por el diario— de por qué la migración incremental y verificada venció al big-bang que apagaría el negocio.

Con esto cierras el módulo 6. Dominas la migración de datos sin downtime: sabes encender el dual-write para capturar el frente, hacer el backfill idempotente para el histórico, correr el parallel-run que compara y detecta discrepancias, reconciliar por la causa hasta cero, y ejecutar el corte final en su orden exacto —read-switch, apagar dual-write, retirar—, todo sin apagar el sistema y sin perder ni corromper una fila.

Lo que sigue cierra la guía. El módulo 7 (medir el progreso de la migración) toma todo lo que construiste —el strangler del M3, la extracción del M5, la migración de datos de este módulo— y responde la pregunta que las técnicas dejan abierta: ¿cómo sé si la migración avanza, y cuándo terminó? Vas a ver el burn-down de llamadas al legacy hasta cero (el mismo que el retiro del viejo de este módulo insinuó), las fitness functions que fallan si el legacy vuelve a crecer, y cómo evitar la migración eterna —esa que hace el read-switch pero nunca apaga el dual-write, o que estrangula el tráfico pero nunca retira el legacy—. Las técnicas te dieron cómo migrar; el módulo 7 te da cómo saber que migraste, con números.

Recursos

  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, "Decomposing the Database" — el capítulo completo como mapa de este capstone: encender la sincronización, cargar los datos, verificar, cortar las lecturas y retirar el almacén viejo, todo mientras el sistema sigue vivo. La referencia integral de la migración de datos de un monolito. En inglés.
  • Stripe Engineering, "Online migrations at scale" — stripe.com/blog/online-migrations. El relato de producción de las cuatro fases (dual-write, backfill, comparación, corte) sobre una tabla real y en uso —el equivalente en el mundo real del diario de siete días de este proyecto—. En inglés.
  • Pramod Sadalage y Martin Fowler, Refactoring Databases: Evolutionary Database Design (Addison-Wesley, 2006) — el marco completo de evolucionar y migrar una base de datos por pasos pequeños, reversibles y verificados, sin downtime. El libro de cabecera para llevar este capstone a un esquema real. En inglés.
  • Martin Fowler, "Patterns of Legacy Displacement" y "ParallelChange" — martinfowler.com/articles/patterns-legacy-displacement y martinfowler.com/bliki/ParallelChange.html. Las dos ideas que sostienen el módulo entero: comparar el viejo y el nuevo antes de confiar (el parallel run, patrón de Sam Newman en Monolith to Microservices, que Fowler trata en el artículo vivo) y cambiar agregando antes de quitar (expand-contract). Vuelve a leerlas ahora que tienes la mecánica completa ejecutada. En inglés.