Módulo 8: Proyecto — modernizar una rebanada de Mercado

Migrar los datos de la rebanada

Descripción

En la lección anterior extrajiste el catalog a su propio servicio y viste que la extracción exige que el servicio duene sus datos —la fase 3, mover los datos de la BD compartida (shared_db) a la BD propia (owned_db)—. El cuarto paso del método —el módulo 6 hecho acción— es ejecutar esa mudanza sin downtime: mover los datos del catalog de un store al otro mientras Mercado sigue vendiendo, sin perder ni corromper una fila, y sin apagar el sistema ni un segundo.

La mudanza tiene un ciclo con un orden que no es negociable. Primero se enciende el dual-write: desde ese momento, cada escritura cae en los dos stores, así el frente queda cubierto y nada nuevo se pierde. Luego el backfill copia lo histórico del store viejo al nuevo, traduciendo el modelo viejo al Product limpio. Después —y esta es la pieza que separa una migración de datos seria de un salto de fe— el parallel-run compara el nuevo contra el viejo, fila por fila, y reporta las discrepancias: filas que faltan, campos mal traducidos. La reconciliación arregla la causa de esas discrepancias hasta llevarlas a cero. Y solo entonces, con el parallel-run en verde, se hace el read-switch: las lecturas pasan al store nuevo, con el viejo caliente como respaldo.

La regla dura de este paso, la que vas a ver ejecutada, es una sola: no se cambia una lectura al store nuevo mientras las discrepancias no sean cero. El backfill de este ejemplo tiene dos bugs reales —salta los productos inactivos (una fila queda faltante) y mistraduce el precio de un producto (un campo mal traducido)—, y el parallel-run los atrapa antes del switch, con las lecturas aún saliendo del viejo, así que ningún usuario ve el hueco. Reconcilias la causa, el parallel-run vuelve a cero, y recién ahí las lecturas se mueven. El parallel-run es la diferencia entre atrapar el bug en la migración o descubrirlo en producción por una queja.

Conexión con el módulo. Este es el cuarto paso del método (M6), y se apoya directamente en el paso 3: el ACL de vuelta que pusiste en la lección 4 es lo que mantiene el contrato del monolito intacto mientras los datos se mudan por debajo —el monolito sigue recibiendo su modelo viejo aunque la fuente del dato cambie de shared_db a owned_db—. Este paso produce el store nuevo verificado (discrepancias = 0) que la lección 6 medirá como parte del progreso. Fíjate en la frontera: aquí ejecutamos la idea de la migración de datos —dual-write, backfill, comparar, reconciliar, cambiar— simulada en memoria, sobre cinco filas. Hacer esto a escala real en producción —CDC, backfill por lotes reanudable, reconciliación automatizada de billones de filas— es del ecosistema Data Engineering; la lección 8 te dice hacia dónde seguir para eso.

Una analogía: mudar el archivo de expedientes de una clínica sin cerrar

Piensa en una clínica que cambia su archivo de expedientes de pacientes —del papel de toda la vida a un sistema digital nuevo— con una regla inviolable: 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.

Así lo hace una clínica sensata. Primero, empieza a escribir cada nota nueva en los dos sistemas (dual-write): desde ese día, cada consulta se registra en el papel y en el digital, así nada nuevo se pierde. Luego digitaliza el archivo histórico (backfill), sin pisar las notas frescas que ya cayeron en digital. Después audita: compara expedientes del papel contra el digital (parallel-run), y encuentra problemas reales —los expedientes de pacientes dados de alta no se digitalizaron (alguien filtró "solo activos" por error), y a un paciente le copiaron mal la dosis de un medicamento—. Corrige la causa y re-digitaliza (reconciliación): arregla el filtro y la copia, y la auditoría vuelve a correr: cero diferencias. Solo entonces los médicos empiezan a consultar el digital (read-switch), con el papel actualizado un tiempo por si acaso.

El catalog de Mercado es el archivo de la clínica; los productos son los expedientes. El bug que salta los inactivos es el filtro que se saltó a los dados de alta; el precio mistraducido es la dosis mal copiada. Y la regla es la misma: no se cambia a leer del digital mientras la auditoría no dé cero. Un médico que consulte el digital antes de que la auditoría cuadre podría leer una dosis mal copiada; una lectura del catalog cambiada antes de que el parallel-run dé cero serviría un precio mal traducido. La auditoría antes del switch es lo que hace segura la mudanza.

Ejemplo trabajado: el ciclo completo con dos discrepancias atrapadas

Vamos a ejecutar la migración de datos del catalog de shared_db (modelo viejo) a owned_db (modelo Product). La clase DataMigration integra las piezas del módulo 6. El backfill tiene dos bugs: salta los productos inactivos (el webcam-hd, prod 3, queda faltante) y mistraduce el precio del prod 4 (le pierde un dígito). El parallel-run compara fila por fila y los atrapa; la reconciliación arregla la causa y re-corre el backfill; el parallel-run vuelve a cero; y solo entonces se hace el read-switch.

# Paso 4 del metodo: migrar los datos del catalog de shared_db (modelo viejo) a
# owned_db (Product) SIN downtime. dual_write + backfill llenan el nuevo, el
# parallel_run compara fila por fila y encuentra 2 discrepancias, la reconciliacion
# las arregla, el parallel_run vuelve a 0, y SOLO entonces se hace el read_switch.

from dataclasses import dataclass


@dataclass
class Product:
    id: int
    name: str
    price_cents: int
    active: bool


# --- Canonicalizacion: para comparar el modelo viejo con el nuevo se llevan a una
#     misma forma. Si el nuevo cuadra con el viejo, canon_old == canon_new. ---
def canon_old(row):
    return (row["prod_id"], row["desc"], int(row["prc_cents"]), row["act"] == "Y")


def canon_new(p):
    return (p.id, p.name, p.price_cents, p.active)


class DataMigration:
    def __init__(self):
        # El store viejo del catalog, en el modelo legacy (5 productos).
        self.old_store = {
            1: {"prod_id": 1, "desc": "SSD 1TB",    "prc_cents": "8999", "act": "Y"},
            2: {"prod_id": 2, "desc": "USB-C Hub",  "prc_cents": "3499", "act": "Y"},
            3: {"prod_id": 3, "desc": "Webcam HD",  "prc_cents": "5999", "act": "N"},
            4: {"prod_id": 4, "desc": "Mech Kbd",   "prc_cents": "7999", "act": "Y"},
            5: {"prod_id": 5, "desc": "Mouse Pro",  "prc_cents": "2499", "act": "Y"},
        }
        self.new_store = {}                 # owned_db, en modelo Product
        self.write_to_new = False           # dual_write OFF al arrancar
        self.read_source = "old"
        self.backfill_skips_inactive = True # BUG 1: el backfill salta inactivos
        self.backfill_drops_digit = True    # BUG 2: mistraduce el precio de prod 4

    def to_modern(self, row):
        price = int(row["prc_cents"])
        if self.backfill_drops_digit and row["prod_id"] == 4:
            price = price // 10             # BUG 2: pierde un digito (7999 -> 799)
        return Product(row["prod_id"], row["desc"], price, row["act"] == "Y")

    def backfill(self):                     # idempotente + insert/fix-if-needed
        copied = 0
        for pid, row in self.old_store.items():
            if self.backfill_skips_inactive and row["act"] == "N":
                continue                    # BUG 1: salta los inactivos (prod 3)
            candidate = self.to_modern(row)
            if pid not in self.new_store or self.new_store[pid] != candidate:
                self.new_store[pid] = candidate
                copied += 1
        return copied

    def parallel_run(self):                 # compara fila por fila, reporta el tipo
        discrepancies = []
        for pid, row in self.old_store.items():
            if pid not in self.new_store:
                discrepancies.append((pid, "MISSING_IN_NEW"))
            elif canon_old(row) != canon_new(self.new_store[pid]):
                discrepancies.append((pid, "FIELD_MISMATCH"))
        return discrepancies


m = DataMigration()

print("Migracion de datos del catalog: shared_db (viejo) -> owned_db (Product)\n")

# Paso 1: encender dual_write (el frente ya cae en ambos) y correr el backfill BUGGY.
m.write_to_new = True
copied = m.backfill()
disc = m.parallel_run()
print(f"1. dual_write ON + backfill (con bugs): copio {copied} filas al new.")
print(f"   parallel_run -> {len(disc)} discrepancias:")
for pid, kind in disc:
    print(f"     prod {pid}: {kind}")
print("   regla dura: discrepancias != 0  ->  NO se hace read_switch todavia.\n")

# Paso 2: reconciliar arreglando la CAUSA (los dos bugs) y re-correr el backfill.
m.backfill_skips_inactive = False           # fix BUG 1
m.backfill_drops_digit = False              # fix BUG 2
copied = m.backfill()
disc = m.parallel_run()
print(f"2. reconciliar (fix de la causa) + re-backfill: corrigio/copio {copied} filas.")
print(f"   parallel_run -> {len(disc)} discrepancias.\n")

# Paso 3: read_switch, permitido SOLO porque discrepancias == 0.
if not disc:
    m.read_source = "new"
    print("3. read_switch: parallel_run en 0  ->  las lecturas pasan al owned_db (new).")
    print(f"   read_source = {m.read_source}. El viejo queda de respaldo.\n")

print(f"  old_store: {len(m.old_store)} filas   new_store: {len(m.new_store)} filas   "
      f"read={m.read_source}")
print("  Los datos del catalog se movieron SIN downtime: el parallel_run atrapo la")
print("  fila faltante (inactivo) y el campo mal traducido ANTES del switch; solo con")
print("  discrepancias=0 se cambiaron las lecturas. No se cambia una lectura con hueco.")

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

Migracion de datos del catalog: shared_db (viejo) -> owned_db (Product)

1. dual_write ON + backfill (con bugs): copio 4 filas al new.
   parallel_run -> 2 discrepancias:
     prod 3: MISSING_IN_NEW
     prod 4: FIELD_MISMATCH
   regla dura: discrepancias != 0  ->  NO se hace read_switch todavia.

2. reconciliar (fix de la causa) + re-backfill: corrigio/copio 2 filas.
   parallel_run -> 0 discrepancias.

3. read_switch: parallel_run en 0  ->  las lecturas pasan al owned_db (new).
   read_source = new. El viejo queda de respaldo.

  old_store: 5 filas   new_store: 5 filas   read=new
  Los datos del catalog se movieron SIN downtime: el parallel_run atrapo la
  fila faltante (inactivo) y el campo mal traducido ANTES del switch; solo con
  discrepancias=0 se cambiaron las lecturas. No se cambia una lectura con hueco.

Lee los tres pasos, porque juntos son el ciclo completo de una migración de datos verificada.

Paso 1 — dual-write y el backfill con bugs. Se enciende el dual-write (el frente ya cae en ambos stores) y se corre el backfill, que copió 4 filas al nuevo —no las 5 del viejo—. El parallel-run compara los 5 productos del viejo contra el nuevo y encuentra 2 discrepancias, cada una con su tipo:

  • prod 3: MISSING_IN_NEW. El webcam-hd está inactivo (act: "N"), y el backfill lo saltó (el bug de saltar inactivos). Está en el viejo, ausente en el nuevo.
  • prod 4: FIELD_MISMATCH. El mech-kbd sí se copió, pero con el precio mal traducido: el backfill le perdió un dígito (7999 → 799). Está en los dos stores, pero el campo del precio no cuadra.

Y aquí actúa la regla dura: discrepancias != 0 → NO se hace read_switch. La migración se detiene. Fíjate en la protección: las dos filas malas aún se leen bien, porque las lecturas siguen saliendo del viejo (read=old). El hueco se detectó antes de que hiciera daño.

Paso 2 — reconciliar la causa. Se arreglan los dos bugs en su causa (backfill_skips_inactive = False y backfill_drops_digit = False) y se re-corre el backfill. Por ser idempotente y arreglar por diferencia, corrigió/copió exactamente las 2 filas que estaban mal: insertó el webcam-hd que faltaba y sobrescribió el mech-kbd con el precio correcto, sin tocar las 3 filas que ya estaban bien. El parallel-run vuelve a correr y ahora marca 0 discrepancias. Fíjate en que se arregló la causa, no el síntoma: arreglar el filtro cubre a cualquier inactivo (no solo al webcam-hd), y arreglar la traducción cubre a cualquier precio (no solo al del prod 4).

Paso 3 — read-switch. Con el parallel-run en 0, y solo por eso, las lecturas pasan al owned_db (read_source = new). El viejo queda caliente como respaldo. Los datos del catalog ahora viven en el store propio del servicio, verificados fila por fila, y el sistema nunca se apagó: al final, old_store: 5 filas, new_store: 5 filas, read=new.

El ciclo cuenta la historia completa de una migración de datos en tres pasos: un backfill con dos bugs reales, un parallel-run que los atrapó antes de exponerlos, una reconciliación que arregló la causa hasta cero, y un read-switch que solo pasó con las discrepancias en cero. La regla dura —no cambiar una lectura al nuevo con un hueco abierto— es lo que separa mover los datos con seguridad de moverlos con los dedos cruzados.

Profundización: por qué el parallel-run no es opcional, y por qué se arregla la causa

Dos ideas de este paso merecen desarrollarse, porque son las que la prisa suele saltarse.

El parallel-run es la diferencia entre atrapar el bug antes o después. Los dos bugs del backfill —saltar inactivos, perder un dígito— tienen algo en común que los hace peligrosos: no lanzan ninguna excepción. El backfill "terminó sin error"; copió filas, no reventó. Un equipo que confunda "terminó sin error" con "está bien" pasaría directo del backfill al read-switch, y ahí los bugs se vuelven visibles para los usuarios: el webcam-hd desaparecería del catálogo (una queja), y el mech-kbd se vendería a $7.99 en vez de $79.99 (una pérdida). El parallel-run es lo único que atrapa esta clase de error, porque no confía en que el backfill "no reventó": compara el resultado contra la fuente, fila por fila.

   old_store (fuente)        new_store (backfill buggy)     parallel_run
   ───────────────────       ──────────────────────────     ─────────────
   1 SSD 1TB   8999 Y   -->   1 SSD 1TB   8999 True          OK
   2 USB-C Hub 3499 Y   -->   2 USB-C Hub 3499 True          OK
   3 Webcam HD 5999 N   -->   (saltado por el bug)           MISSING_IN_NEW
   4 Mech Kbd  7999 Y   -->   4 Mech Kbd   799 True          FIELD_MISMATCH (799!=7999)
   5 Mouse Pro 2499 Y   -->   5 Mouse Pro 2499 True          OK
                                          el parallel_run compara y NO deja
                                          pasar al read_switch con 2 huecos

Fíjate en que el parallel-run distingue dos tipos de discrepancia, y esa distinción importa: MISSING_IN_NEW (una fila que no llegó) apunta a un problema de cobertura del backfill (saltó algo); FIELD_MISMATCH (una fila que llegó mal) apunta a un problema de traducción (un campo se copió incorrecto). Saber el tipo te lleva a la causa: la fila faltante te manda a revisar qué filtra el backfill; el campo que no cuadra te manda a revisar la traducción de ese campo. El parallel-run no solo dice "hay un problema"; dice qué clase de problema y en qué fila, que es lo que hace la reconciliación dirigida.

Se arregla la causa, no el síntoma. Cuando el parallel-run reporta las 2 discrepancias, la tentación es arreglarlas a mano: insertar el webcam-hd faltante, corregir el precio del mech-kbd. Baja el contador de inmediato, sí, pero deja los bugs vivos: si el backfill se re-corre, o si hay otros inactivos y otros precios, vuelven a fallar. Por eso la reconciliación de este ejemplo arregla la causa —el filtro que salta inactivos y la traducción que pierde el dígito— y re-corre el backfill, que ahora cubre a todos los inactivos y traduce bien todos los precios de un golpe. Un catálogo real puede tener cientos de productos inactivos; copiar el webcam-hd a mano dejaría los otros faltando. Arreglar la causa los cubre a todos y deja el backfill correcto para el futuro. Esta es la disciplina que hace que el parallel-run vuelva a cero de verdad, no que el contador baje mientras el bug sigue ahí.

Un matiz sobre la reversibilidad: hasta el read-switch, y aun un tiempo después, la migración es reversible. Con el dual-write encendido, el viejo se mantiene sincronizado, así que si algo sale mal tras el switch, un cambio de read_source de vuelta a old te devuelve al store viejo, que estuvo caliente todo el tiempo. Por eso el read-switch no apaga el dual-write de inmediato: el viejo es la red de respaldo del momento de máximo riesgo (la primera vez leyendo del nuevo en vivo). Apagar el dual-write y retirar el viejo viene después, cuando la confianza en el nuevo es total —parte del criterio de done de la lección 7—.

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. 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 ciclo de este proyecto, esto sería saltar del paso 1 (backfill) al paso 3 (read-switch) sin el paso 2. Cómo corregirlo: los dos bugs del backfill (saltar inactivos, perder un dígito) son exactamente la clase de error que no lanza excepción y que solo el parallel-run atrapa. Sin él, el read-switch habría movido las lecturas al nuevo con el webcam-hd ausente y el mech-kbd a $7.99 —descubierto por una queja y una pérdida, no por una comparación—. El parallel-run no es opcional: es la diferencia entre atrapar el bug en el paso 1 (sin daño) o en producción (con usuarios afectados).

Reconciliar el síntoma y no la causa. Qué pasa: el parallel-run reporta las 2 discrepancias, y el equipo inserta el webcam-hd y corrige el precio del mech-kbd a mano, en vez de arreglar los bugs del backfill. Por qué pasa: copiar/corregir dos filas baja el contador de inmediato; investigar los bugs es más lento. Cómo detectarlo: las 2 discrepancias desaparecen, pero si hay otros inactivos u otros precios afectados (o si se re-corre el backfill), vuelven a fallar —los bugs siguen vivos—. Cómo corregirlo: arregla la causa (el filtro de inactivos, la traducción del precio) y re-corre el backfill, que ahora cubre a todos los inactivos y todos los precios de un golpe. Un catálogo real con cientos de inactivos dejaría la mayoría faltando si solo copiaras el webcam-hd a mano. La causa arreglada los cubre a todos y deja el backfill correcto.

Cambiar la lectura al nuevo con discrepancias abiertas. Qué pasa: el equipo ve que "casi todo cuadra" (3 de 5 filas bien) y hace el read-switch de todos modos, pensando que arreglará las 2 restantes después. Por qué pasa: el read-switch es el paso visible y satisfactorio; esperar a que el parallel-run dé cero se siente como una demora. Cómo detectarlo: las lecturas ya salen del nuevo mientras el parallel-run todavía reporta discrepancias; los usuarios empiezan a ver las filas malas. Cómo corregirlo: la regla dura es una sola —no se cambia una lectura al nuevo mientras las discrepancias no sean cero—. "Casi cuadra" no es cuadra: mover las lecturas con 2 huecos abiertos significa servir el webcam-hd ausente y el precio malo a usuarios reales. El read-switch es la recompensa que se cobra después de la reconciliación, no antes. Solo el parallel-run en cero autoriza el switch.

Ejercicios

Ejercicio 1 — Los dos tipos de discrepancia. El parallel-run reportó prod 3: MISSING_IN_NEW y prod 4: FIELD_MISMATCH. (a) ¿Qué bug del backfill causó cada una? (b) ¿A qué causa distinta apunta cada tipo? (c) ¿Qué habría visto un usuario de cada una si se hubiera hecho el read-switch en el paso 1?

Ver solución

(a) prod 3: MISSING_IN_NEW la causó el bug de saltar los inactivos (backfill_skips_inactive): el webcam-hd tiene act: "N", así que el backfill nunca lo copió al nuevo. prod 4: FIELD_MISMATCH la causó el bug de perder un dígito al traducir el precio (backfill_drops_digit): el mech-kbd sí se copió, pero con price_cents = 799 en vez de 7999.

(b) MISSING_IN_NEW apunta a un problema de cobertura: el backfill dejó fuera una fila (un filtro que excluye algo que no debía). FIELD_MISMATCH apunta a un problema de traducción: la fila llegó, pero un campo se copió incorrecto. Saber el tipo dirige la reconciliación: la faltante te manda a revisar qué filtra el backfill; el campo que no cuadra te manda a revisar cómo se traduce ese campo.

(c) Con el read-switch hecho en el paso 1, un usuario que buscara el webcam-hd (MISSING_IN_NEW) no lo encontraría —el producto habría desaparecido del catálogo, aunque en el viejo seguía existiendo—. Y un usuario que comprara el mech-kbd (FIELD_MISMATCH) lo vería a $7.99 en vez de $79.99 —una pérdida directa, un producto vendido a la décima parte de su precio—. Los dos bugs, invisibles mientras se leía del viejo, se habrían vuelto daños reales en el instante del switch prematuro.

Ejercicio 2 — Causa vs síntoma. El equipo debate cómo reconciliar las 2 discrepancias. Ana propone copiar el webcam-hd a mano y corregir el precio del mech-kbd; Beto propone arreglar los bugs del backfill y re-correrlo. (a) ¿Qué logra cada enfoque a corto plazo? (b) ¿Por qué el de Beto es el correcto? (c) ¿Qué pasaría con el de Ana si el catálogo tuviera 300 productos inactivos?

Ver solución

(a) A corto plazo, los dos bajan el contador a 0: Ana inserta la fila faltante y corrige el precio malo; Beto arregla los filtros y re-corre el backfill, que hace lo mismo pero por la causa. En la salida inmediata, ambos llegan a "0 discrepancias".

(b) El de Beto es el correcto porque arregla la causa: el filtro que salta inactivos y la traducción que pierde el dígito. Eso significa que cualquier inactivo y cualquier precio quedan cubiertos, no solo los dos que el parallel-run reportó esta vez. Además, deja el backfill correcto para el futuro: si se re-corre (por un re-sync, por más datos), ya no reintroduce los bugs. El de Ana arregla el síntoma: las dos filas que hoy fallaron, dejando los bugs vivos para volver a fallar.

(c) Con 300 productos inactivos, el enfoque de Ana dejaría 299 faltando: solo copió el webcam-hd a mano, pero el bug (backfill_skips_inactive) saltó a los 300. El parallel-run, si se re-corriera de verdad sobre todo el catálogo, seguiría reportando 299 MISSING_IN_NEW. Arreglar la causa (el filtro) los cubre a los 300 de un golpe. Este es justo el peligro de reconciliar el síntoma: funciona en la demo de dos filas y falla en el catálogo real.

Ejercicio 3 — La regla dura y la reversibilidad. (a) Enuncia la regla dura del read-switch y por qué existe. (b) ¿Por qué el read-switch no apaga el dual-write de inmediato? (c) Si tras el read-switch el nuevo empezara a dar problemas, ¿cómo volverías atrás, y por qué es posible?

Ver solución

(a) La regla dura: no se cambia una lectura al store nuevo mientras las discrepancias no sean cero. Existe porque mover las lecturas al nuevo con discrepancias abiertas significa servir datos malos (filas faltantes, campos mal traducidos) a usuarios reales. El parallel-run en cero es la única autorización del switch; "casi cuadra" no basta —cada discrepancia abierta es un usuario que verá un dato incorrecto en el instante del switch—.

(b) Porque el read-switch es el momento de máximo riesgo (la primera vez leyendo del nuevo en vivo), y el viejo, mantenido caliente por el dual-write, es la red de respaldo de ese momento. Si algo sale mal tras el switch, tener el viejo sincronizado permite volver. Apagar el dual-write de inmediato quitaría esa red justo cuando más se necesita. El dual-write se apaga después, cuando la confianza en el nuevo es total (parte del done de la lección 7).

(c) Volvería atrás cambiando read_source de vuelta a old: las lecturas regresarían al store viejo. Es posible porque el dual-write siguió encendido tras el switch, así que el viejo se mantuvo sincronizado (recibiendo cada escritura) todo el tiempo —está caliente, con los datos al día, listo para volver a atender—. La reversibilidad es una propiedad del método: hasta que se apaga el dual-write y se retira el viejo, un cambio de read_source deshace el switch sin perder nada. El riesgo está acotado en cada paso, no concentrado en un salto sin retorno.

Resumen y siguiente paso

En esta lección diste el cuarto paso del método: migrar los datos del catalog de shared_db a owned_db sin downtime (módulo 6). Ejecutaste el ciclo completo: el dual-write encendido (el frente cubierto), el backfill llenando el nuevo —con dos bugs reales: saltar inactivos y perder un dígito—, el parallel-run comparando fila por fila y atrapando las 2 discrepancias (MISSING_IN_NEW, FIELD_MISMATCH) antes del switch, la reconciliación arreglando la causa hasta cero, y el read-switch pasando las lecturas al nuevo solo con el parallel-run en verde. Viste, con el archivo de la clínica que se muda sin cerrar, que la auditoría antes del switch es lo que hace segura la mudanza, y aprendiste la regla dura —no cambiar una lectura al nuevo con discrepancias abiertas—, por qué el parallel-run no es opcional (atrapa los errores que no lanzan excepción) y por qué se arregla la causa y no el síntoma (para cubrir todos los casos, no solo los reportados).

Antes de avanzar deberías poder: ordenar las fases de una migración de datos (dual-write → backfill → parallel-run → reconciliar → read-switch); explicar por qué el parallel-run atrapa bugs que el backfill "sin error" esconde; distinguir MISSING_IN_NEW de FIELD_MISMATCH y a qué causa apunta cada uno; y defender la regla dura del read-switch.

La lección 6 da el quinto paso: medir el progreso de la rebanada (módulo 7). Con la rebanada caracterizada, tras el facade, extraída y con sus datos migrados, la pregunta es cómo saber si la migración avanza y a qué ritmo. Vas a ejecutar el burn-down de llamadas al legacy bajando hasta cero —con el tramo terco final (el descuento por volumen que viste en la lección 3) que solo cierra cuando el modern lo implementa, guiado por el golden master de la lección 2— y la fitness function que da FAIL si alguien agrega código nuevo al legacy que estás matando. El burn-down dice cuánto falta; la fitness impide que el legacy crezca por detrás.

Recursos

  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, "Decomposing the Database" — el mapa de esta lección: encender la sincronización, cargar los datos, verificar, cortar las lecturas y retirar el store viejo, todo con el sistema vivo. La referencia integral de migrar los datos de un monolito. En inglés.
  • Martin Fowler, "Patterns of Legacy Displacement" — martinfowler.com/articles/patterns-legacy-displacement. Correr el viejo y el nuevo sobre los mismos datos y comparar antes de confiar; el parallel run como patrón nombrado es de Sam Newman (Monolith to Microservices). El mecanismo exacto que atrapa las 2 discrepancias de esta lección. 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 a escala del ciclo de este proyecto. En inglés.
  • Pramod Sadalage y Martin Fowler, Refactoring Databases (Addison-Wesley, 2006) — el marco de evolucionar y migrar una base de datos por pasos pequeños, reversibles y verificados, sin downtime. El libro de cabecera para llevar este paso a un esquema real. En inglés.