Módulo 6: Migrar datos sin downtime
Presentación del módulo: migrar datos sin downtime
Por qué este módulo existe aquí
Los módulos anteriores movieron el tráfico y el código. En el módulo 3 pusiste un strangler facade delante del catalog y desviaste las requests del viejo al nuevo, un porcentaje a la vez. En el módulo 5 extrajiste el servicio del catalog del monolito, con un anti-corruption layer que traduce entre el modelo viejo y el modelo nuevo. Tienes el tráfico fluyendo al servicio nuevo y el servicio nuevo corriendo en su propio proceso. Pero hay una pieza que ninguno de esos módulos tocó, y sin ella el servicio nuevo está vacío: los datos.
El servicio nuevo del catálogo necesita sus productos —sus precios, su stock, sus nombres— y hoy esos datos viven en la base de datos del monolito, en el modelo viejo. Migrarlos suena a un problema de tarde: exportar de una base, importar en la otra, listo. Y lo sería, si no fuera por la restricción que define toda esta guía: el sistema no se puede apagar. Mercado vende las 24 horas. No hay una ventana de mantenimiento de un fin de semana para "copiar todo y ya", porque mientras copias, la base vieja sigue recibiendo escrituras: alguien actualiza un precio, otro cambia el stock, entra un producto nuevo. Si haces una copia de las 3 de la mañana y la restauras a las 6, perdiste tres horas de cambios. Y si algo sale mal a medio camino, no puedes simplemente "volver atrás": el sistema estuvo vivo todo el tiempo.
Este módulo enseña cómo mover datos de un almacén a otro sin apagar el sistema y sin perder ni corromper una sola fila. La técnica tiene cinco piezas que encajan en un orden preciso, y el módulo entero es aprenderlas una por una:
- Expand-contract: nunca un cambio destructivo de golpe. Primero agregas lo nuevo (dejando lo viejo intacto), luego migras, y solo al final quitas lo viejo.
- Dual-write: durante la transición, cada escritura de la app va a ambos almacenes, para que el nuevo se mantenga al día con lo que pasa ahora.
- Backfill: llenas el almacén nuevo con los datos históricos —los que ya existían antes de encender el dual-write—.
- Parallel-run: lees de ambos almacenes y comparas, reportando cada discrepancia, para detectar los errores antes de confiar en el nuevo.
- Read-switch: solo cuando las discrepancias son cero, mueves las lecturas al nuevo; después apagas el dual-write y retiras el viejo.
Conexión con el módulo. Esta es la lección-mapa. No entramos todavía al detalle de cada pieza; instalamos la metáfora (cambiar de banco sin quedarte sin acceso al dinero), el ciclo completo (expand-contract → dual-write → backfill → parallel-run → read-switch) y el vocabulario (old_db, new_db, dual_write, backfill, parallel_run, discrepancy, read_switch). Y corremos un ejemplo-mapa: el pipeline entero en miniatura, ejecutado, para ver los datos moverse del viejo al nuevo con una discrepancia detectada y resuelta antes de entrar al detalle. La lección 2 fija el marco expand-contract; la 3 enciende el dual-write; la 4 hace el backfill; la 5 corre el parallel-run que compara; la 6 reconcilia las discrepancias; la 7 hace el corte final; y la 8 lo integra todo en los datos del catalog de Mercado. Fíjate en la frontera: aquí enseñamos la técnica de migración de datos, simulada en memoria. Los pipelines de datos a escala en producción —CDC leyendo el log de transacciones, colas de replicación, streaming de terabytes— son del ecosistema Data Engineering. Aquí aprendes el patrón y por qué su orden es sagrado; allá aprendes las herramientas que lo ejecutan a escala.
Y la promesa de siempre: nada se afirma "de memoria", todo se ejecuta. Cada simulación corre con Python 3.14 y solo la biblioteca estándar, con datos fijos, así que la salida que ves en cada bloque "Qué esperar" es la salida literal de correr el código. Puedes copiarlo y reproducirlo idéntico.
Una analogía: cambiar de banco sin quedarte sin acceso al dinero
Tienes tu dinero en un banco viejo. Cobras ahí tu sueldo, pagas ahí tus recibos, tienes ahí tus ahorros. Y decides cambiarte a un banco nuevo —mejores condiciones, mejor app—. La pregunta no es si te cambias, sino cómo, porque hay una condición que no puedes violar: en ningún momento puedes quedarte sin acceso a tu dinero. No puedes pasar ni un día sin poder pagar la renta o sacar efectivo. Tu vida financiera, como Mercado, no se apaga.
La forma ingenua y peligrosa sería: un lunes cierras la cuenta vieja, retiras todo, y el martes lo depositas en la nueva. ¿Qué pasa si el martes el depósito se traba? ¿Qué pasa con el recibo de luz que estaba domiciliado en la cuenta vieja y se cobra el lunes por la noche, cuando ya la cerraste? Te quedaste sin banco en el peor momento, con recibos rebotando y sin poder volver atrás, porque la cuenta vieja ya no existe. Es el "copiar todo en una ventana de mantenimiento" —y su versión rota, el big-bang—.
La forma sensata es la que de verdad usa la gente:
- Abres la cuenta nueva sin cerrar la vieja. Ahora tienes las dos. La nueva empieza vacía; la vieja sigue con todo tu dinero y todos tus movimientos.
- Empiezas a usar las dos en paralelo. Domicilias tus nuevos recibos en la cuenta nueva, pero dejas los viejos donde están por ahora. Cada quincena que entra, la mandas a las dos, o al menos te aseguras de que ambas reflejen tu situación. Lo que pasa de ahora en adelante queda registrado en las dos.
- Mueves el histórico. Transfieres tus ahorros y tu saldo viejo de la cuenta vieja a la nueva —eso que ya estaba ahí antes de empezar—, con cuidado de no pisar un movimiento reciente.
- Verificas que cuadren. Durante unas semanas revisas los dos estados de cuenta lado a lado: ¿el saldo de la nueva coincide con el de la vieja más lo que moviste? ¿Aparecen todos tus movimientos en las dos? Si hay una diferencia, la investigas y la corriges antes de confiar del todo en la nueva.
- Solo cuando confías, cierras la vieja. Cuando llevas semanas viendo que las dos cuentas cuadran al centavo, mueves todo tu uso a la nueva —domicilias los últimos recibos, cambias tu depósito de nómina— y recién entonces cierras la cuenta vieja.
Cada paso de esta analogía es una pieza del módulo, con su nombre técnico:
- Abrir la cuenta nueva sin cerrar la vieja es expand-contract: agregas lo nuevo sin quitar lo viejo (lección 2).
- Mandar los movimientos nuevos a las dos cuentas es el dual-write (lección 3).
- Mover el saldo y los ahorros viejos es el backfill (lección 4).
- Revisar los dos estados de cuenta lado a lado buscando diferencias es el parallel-run (lección 5), y corregir las diferencias que encuentras es la reconciliación (lección 6).
- Mover tu uso a la nueva y cerrar la vieja es el read-switch y el retiro (lección 7).
Y el orden importa. Nadie cierra la cuenta vieja antes de verificar que la nueva cuadra; nadie mueve el histórico antes de tener las dos cuentas abiertas. Todo el módulo es aprender ese orden y por qué cada paso va donde va. Hay una segunda imagen que ayuda cuando pienses en el esquema de los datos: cambiar el cableado de una casa habitación por habitación con la luz siempre prendida —jamás cortas toda la corriente de golpe; agregas el cable nuevo al lado del viejo, mueves los enchufes de una habitación, y solo cuando esa habitación funciona con el cable nuevo, quitas el viejo—. Esa es la lección 2.
Ejemplo trabajado: el pipeline en miniatura
No vamos a describir la migración de datos: la vamos a ejecutar, aunque sea en su versión más pequeña. La idea es tener los dos almacenes —old_db y new_db— como diccionarios en memoria, y correr el pipeline completo de una vez: encender el dual_write, hacer el backfill, correr un parallel_run que compare y reporte discrepancias, reconciliar, y hacer el read_switch solo cuando las discrepancias sean cero.
Un detalle que verás repetido en todo el módulo: old_db y new_db usan modelos distintos. El viejo guarda un producto como {"sku", "name", "price_cents", "stock"}; el nuevo como {"id", "title", "price_usd", "in_stock"}. Entre ellos hay un traductor —el mismo anti-corruption layer del módulo 5—, la función to_new_model. Y para comparar dos filas que tienen forma distinta, las normalizamos a una forma canónica común (una tupla) con canon_old y canon_new: si el ACL tradujo bien, las dos canónicas son iguales; si tradujo mal, difieren, y ahí está la discrepancia.
# --- Los dos almacenes de Mercado, en memoria. Modelos DISTINTOS. ---
# old (legacy): sku, name, price_cents, stock
# new (modern): id, title, price_usd, in_stock
old_db = {}
new_db = {}
# --- El anti-corruption layer: traduce una fila del modelo viejo al nuevo. ---
def to_new_model(old_row):
return {
"id": old_row["sku"],
"title": old_row["name"],
"price_usd": old_row["price_cents"] / 100,
"in_stock": old_row["stock"] > 0,
}
# --- Forma canonica para COMPARAR: normaliza ambos modelos a lo mismo. ---
def canon_old(row):
return (row["sku"], row["name"], row["price_cents"], row["stock"] > 0)
def canon_new(row):
return (row["id"], row["title"], round(row["price_usd"] * 100), row["in_stock"])
# --- Flag global: mientras dual_write este encendido, escribir a AMBOS. ---
dual_write_on = False
def write_product(row):
old_db[row["sku"]] = row # el almacen viejo, siempre
if dual_write_on:
new_db[row["sku"]] = to_new_model(row) # y el nuevo, si dual_write ON
# --- backfill: copia al new_db las filas historicas que ya estaban en old_db.
# Idempotente: no pisa una fila que ya exista en new_db (la fresca gana). ---
def backfill():
copied = 0
for sku, old_row in old_db.items():
if sku not in new_db: # solo lo que falta: no pisa lo fresco
new_db[sku] = to_new_model(old_row)
copied += 1
return copied
# --- parallel_run: lee de AMBOS, compara canonicos, reporta discrepancias. ---
def parallel_run():
discrepancies = []
for sku in sorted(old_db):
if sku not in new_db:
discrepancies.append((sku, "MISSING_IN_NEW"))
elif canon_old(old_db[sku]) != canon_new(new_db[sku]):
discrepancies.append((sku, "VALUE_MISMATCH"))
return discrepancies
print("Migracion de datos sin downtime: el pipeline en miniatura\n")
# FASE 0 -- Estado inicial: 4 productos historicos, solo en el viejo.
for row in [
{"sku": "ssd-1tb", "name": "SSD 1TB", "price_cents": 8999, "stock": 12},
{"sku": "usb-c-hub", "name": "USB-C Hub", "price_cents": 3499, "stock": 40},
{"sku": "webcam", "name": "Webcam HD", "price_cents": 5999, "stock": 0},
{"sku": "kbd-mech", "name": "Mech Kbd", "price_cents": 7999, "stock": 5},
]:
write_product(row)
print(f" FASE 0 seed historico old={len(old_db):>2} new={len(new_db):>2} (new vacio: dual_write OFF)")
# FASE 1 -- Encender dual_write. A partir de aqui, toda escritura va a AMBOS.
dual_write_on = True
write_product({"sku": "hdmi-cable", "name": "HDMI 2m", "price_cents": 1299, "stock": 30})
print(f" FASE 1 dual_write ON + 1 write old={len(old_db):>2} new={len(new_db):>2} (el write nuevo cae en ambos)")
# FASE 2 -- Backfill: llenar el new con lo historico que quedo solo en old.
copied = backfill()
print(f" FASE 2 backfill old={len(old_db):>2} new={len(new_db):>2} ({copied} filas historicas copiadas)")
# FASE 3 -- parallel_run: comparar. Simulamos una discrepancia de formato:
# el ACL guardo mal el title de 'webcam' (quedo en minusculas).
new_db["webcam"]["title"] = "webcam hd" # <- bug del ACL, a proposito
disc = parallel_run()
print(f" FASE 3 parallel_run discrepancias = {len(disc)} -> {disc}")
print(" NO se hace read_switch mientras haya discrepancias > 0.")
# FASE 4 -- Reconciliar: arreglar el ACL y re-migrar la fila afectada.
new_db["webcam"] = to_new_model(old_db["webcam"]) # el ACL corregido re-traduce
disc = parallel_run()
print(f" FASE 4 reconciliar + re-run discrepancias = {len(disc)} -> {disc if disc else 'ninguna'}")
# FASE 5 -- read_switch: SOLO ahora que discrepancias == 0, leer del nuevo.
read_source = "new_db" if len(disc) == 0 else "old_db"
print(f" FASE 5 read_switch las lecturas ahora van a: {read_source}")
print("\n El sistema nunca se apago. old y new coexistieron; se comparo antes de confiar.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Migracion de datos sin downtime: el pipeline en miniatura
FASE 0 seed historico old= 4 new= 0 (new vacio: dual_write OFF)
FASE 1 dual_write ON + 1 write old= 5 new= 1 (el write nuevo cae en ambos)
FASE 2 backfill old= 5 new= 5 (4 filas historicas copiadas)
FASE 3 parallel_run discrepancias = 1 -> [('webcam', 'VALUE_MISMATCH')]
NO se hace read_switch mientras haya discrepancias > 0.
FASE 4 reconciliar + re-run discrepancias = 0 -> ninguna
FASE 5 read_switch las lecturas ahora van a: new_db
El sistema nunca se apago. old y new coexistieron; se comparo antes de confiar.
Lee las fases de arriba hacia abajo, porque ahí está el módulo entero en seis renglones.
En la FASE 0, hay 4 productos históricos y todos viven solo en el viejo (old=4, new=0). Es el punto de partida: el monolito lleva años acumulando datos en old_db, y new_db está vacío. El dual_write está apagado, así que estas escrituras no tocan el nuevo.
En la FASE 1, encendemos el dual_write y llega una escritura (un producto nuevo, hdmi-cable). Fíjate: old=5, new=1. La escritura nueva cayó en ambos almacenes (por eso new subió a 1), pero los 4 productos históricos siguen solo en el viejo. Esto es exactamente lo que el dual-write garantiza y lo que no: sincroniza lo de ahora en adelante, no lo histórico. Es la lección 3.
En la FASE 2, el backfill copia al nuevo las 4 filas históricas que quedaron solo en el viejo. Ahora old=5, new=5: los dos almacenes tienen las mismas 5 filas. El backfill cerró el hueco que el dual-write no cubría. Es la lección 4. (Fíjate que copió 4, no 5: la fila hdmi-cable ya estaba en el nuevo por el dual-write, y el backfill no la volvió a tocar —esa es su idempotencia—.)
En la FASE 3, corremos el parallel_run. Antes, inyectamos a propósito un error: el ACL guardó mal el title de webcam (lo dejó en minúsculas). El parallel-run lo atrapa: discrepancias = 1 -> [('webcam', 'VALUE_MISMATCH')]. Y aquí está la regla de oro del módulo, en el renglón siguiente: no se hace read_switch mientras haya discrepancias > 0. El nuevo todavía miente en una fila; confiar en él ahora sería servir datos malos. Es la lección 5.
En la FASE 4, reconciliamos: arreglamos el ACL y re-migramos la fila de webcam. El parallel_run vuelve a correr y ahora discrepancias = 0. El nuevo ya cuadra con el viejo en todas las filas. Es la lección 6.
En la FASE 5, y solo porque las discrepancias son cero, hacemos el read_switch: las lecturas ahora salen de new_db. Es la lección 7. Y el renglón final resume la promesa del módulo: el sistema nunca se apagó, los dos almacenes coexistieron, y se comparó antes de confiar.
El ciclo completo de la migración de datos
Ese ejemplo tocó las cinco piezas sin desarrollarlas. Vale la pena verlas explícitas, porque son la columna vertebral de las siete lecciones que siguen:
Pieza Leccion Que hace Estado del new
──────────────── ──────── ──────────────────────────────────── ──────────────
1. expand-contract L2 agregar lo nuevo sin quitar lo viejo esquema listo
2. dual_write L3 escribir a AMBOS (sincroniza el frente) recibe lo de ahora
3. backfill L4 cargar lo historico (sin pisar lo nuevo) recibe el pasado
4. parallel_run L5-L6 leer de ambos y COMPARAR + reconciliar validado
5. read_switch L7 mover lecturas al new, apagar dual_write confiable
Y así encajan las piezas en el flujo del módulo:
flowchart LR
A["expand-contract<br/>(agregar lo nuevo)"] --> B["dual_write ON<br/>(escribir a ambos)"]
B --> C["backfill<br/>(cargar lo historico)"]
C --> D["parallel_run<br/>(comparar)"]
D -->|discrepancias > 0| E["reconciliar<br/>(arreglar)"]
E --> D
D -->|discrepancias = 0| F["read_switch<br/>(+ apagar dual_write)"]
Léelo así: primero expand-contract deja el esquema del nuevo listo sin romper el viejo. Luego el dual_write se enciende para que toda escritura caiga en los dos —el nuevo empieza a recibir lo de ahora—. El backfill carga lo histórico que quedó atrás. El parallel_run compara los dos almacenes: mientras haya discrepancias, se reconcilian y se vuelve a comparar, en un ciclo, hasta que las discrepancias son cero. Y solo entonces el read_switch mueve las lecturas al nuevo y apaga el dual-write. El ciclo parallel_run ↔ reconciliar es el corazón de la seguridad: no avanzas al read-switch hasta que la comparación esté verde.
El mapa: dónde está este módulo en la guía
Este módulo es la pieza que le faltaba a los anteriores: moviste el tráfico (M3), moviste el código (M4, M5), y ahora mueves los datos que ambos usan. Así se conecta con el resto:
flowchart TD
M3["M3 · Strangler fig<br/>(mover el trafico)"]
M4["M4 · Branch by abstraction<br/>(migrar por dentro)"]
M5["M5 · Extraer un servicio<br/>(anti-corruption layer)"]
M6["M6 · Migrar datos sin downtime<br/>(dual-write, backfill, parallel-run)"]
M7["M7 · Medir el progreso"]
M3 --> M4 --> M5 --> M6 --> M7
Léelo así: en M3 aprendiste a desviar el tráfico con un facade; en M4 y M5 a migrar el código, primero por dentro y luego extrayendo el servicio con un ACL; aquí, en M6, migras los datos de ese servicio sin apagar el sistema; y en M7 medirás el progreso de la migración completa hasta apagar la vieja ruta. El anti-corruption layer que construiste en M5 reaparece aquí como el traductor entre old_db y new_db: la misma pieza, ahora al servicio de mover datos.
Y la frontera con las guías hermanas del ecosistema, que es importante en este módulo: enseñamos la técnica de migración de datos —dual-write, backfill, parallel-run, read-switch— simulada en memoria, con diccionarios de Python. Los pipelines de datos a escala en producción —CDC (change data capture) leyendo el log de transacciones de una base real, colas de replicación, herramientas de streaming que mueven terabytes con garantías de consistencia y checkpoints— son del ecosistema Data Engineering. Aquí aprendes el patrón y por qué su orden es sagrado; allá aprendes las herramientas que lo ejecutan a escala. Piensa en este módulo como el plano conceptual: cuando después uses una herramienta de CDC real, reconocerás debajo el mismo dual-write, el mismo backfill y el mismo parallel-run que ejecutaste aquí a mano.
Errores comunes
Creer que "migrar datos" es exportar de una base e importar en otra. Qué pasa: el equipo trata la migración como una tarea de una tarde —un dump de la base vieja, un restore en la nueva, y a otra cosa—. Por qué pasa: para una base estática (una que nadie está escribiendo), eso sería suficiente; el error es olvidar que la base de un sistema vivo cambia mientras la copias. Cómo detectarlo: si tu plan de migración incluye la frase "ventana de mantenimiento" o "congelar las escrituras", estás asumiendo que puedes apagar el sistema —y este módulo existe porque no puedes—. Cómo corregirlo: la migración sin downtime no es una copia, es un proceso de cinco piezas (expand-contract, dual-write, backfill, parallel-run, read-switch) que mantiene los dos almacenes vivos y sincronizados mientras cambias, y que solo confía en el nuevo tras compararlo. La copia-e-importa es el big-bang de los datos; este módulo es su alternativa incremental.
Confiar en el almacén nuevo sin compararlo contra el viejo. Qué pasa: el equipo hace el dual-write y el backfill, ve que "el nuevo tiene datos", y mueve las lecturas al nuevo directamente. Por qué pasa: si el dual-write y el backfill corrieron sin lanzar errores, es tentador asumir que los datos están bien. Cómo detectarlo: no hay un paso de parallel-run en el plan; el criterio para el read-switch es "el backfill terminó", no "las discrepancias son cero". Cómo corregirlo: un backfill que "termina sin error" puede aun así haber saltado filas, traducido mal un campo, o pisado una escritura fresca —todos errores silenciosos, que no lanzan excepción pero corrompen datos—. El parallel-run es lo que los atrapa: lee de ambos, compara, y reporta la diferencia. Es el equivalente exacto, sobre datos, del characterization test del módulo 2: fija la verdad del viejo y atrapa cuando el nuevo difiere. Sin parallel-run, el read-switch es un salto de fe; con él, es una decisión respaldada por una comparación verde.
Ejercicios
Ejercicio 1 — Las cinco piezas en el cambio de banco. Con la analogía del cambio de banco, empareja cada pieza del módulo con lo que ocurre al cambiarte de banco: (a) expand-contract, (b) dual-write, (c) backfill, (d) parallel-run, (e) read-switch. Luego di por qué cerrar la cuenta vieja antes de verificar que las dos cuadran sería el error más grave.
Ver solución
- (a) expand-contract → abrir la cuenta nueva sin cerrar la vieja. Agregas lo nuevo (la cuenta nueva) sin quitar lo viejo (la cuenta vieja sigue abierta con todo tu dinero). Nada destructivo todavía.
- (b) dual-write → mandar tus movimientos nuevos a las dos cuentas. Lo que pasa de ahora en adelante queda registrado en las dos, así el saldo de la nueva se mantiene al día.
- (c) backfill → mover tus ahorros y tu saldo viejo a la nueva. Lo que ya estaba antes de empezar —el histórico— se transfiere a la cuenta nueva.
- (d) parallel-run → revisar los dos estados de cuenta lado a lado buscando diferencias. Comparas: ¿el saldo de la nueva cuadra con el de la vieja? ¿Aparecen todos tus movimientos en las dos?
- (e) read-switch → mover todo tu uso a la nueva y cerrar la vieja. Domicilias los últimos recibos, cambias tu nómina, y recién entonces cierras la cuenta vieja.
Cerrar la cuenta vieja antes de verificar que las dos cuadran es el error más grave porque destruye tu red de respaldo justo cuando más la necesitas. Mientras las dos cuentas están abiertas y no has verificado que cuadran, la vieja es tu seguro: si la nueva tiene un error (un movimiento que no se transfirió, un saldo mal), lo detectas comparando y lo corriges, con la vieja aún ahí. Si cierras la vieja primero y después descubres que la nueva está mal, ya no tienes contra qué comparar ni a qué volver —perdiste la verdad de referencia—. Por eso el read-switch (y el retiro del viejo) va al final, después del parallel-run verde, nunca antes.
Ejercicio 2 — Lee el pipeline. En el ejemplo trabajado, después de la FASE 1 el estado era old=5, new=1, y después de la FASE 2 era old=5, new=5. (a) ¿Por qué en la FASE 1 el nuevo solo tenía 1 fila si el viejo tenía 5? (b) ¿Cuántas filas copió el backfill en la FASE 2, y por qué no copió las 5? (c) Si el orden se hubiera invertido —backfill antes de encender el dual-write—, ¿qué problema podría aparecer?
Ver solución
(a) Porque el dual_write solo sincroniza las escrituras que ocurren mientras está encendido. Las 4 filas históricas (ssd-1tb, usb-c-hub, webcam, kbd-mech) se escribieron en la FASE 0, con el dual-write apagado, así que cayeron solo en el viejo. La única fila que cayó en el nuevo fue hdmi-cable, escrita en la FASE 1 con el dual-write ya encendido. Por eso new=1: el dual-write no mira hacia atrás, solo hacia adelante.
(b) Copió 4 filas. El viejo tenía 5, pero hdmi-cable ya estaba en el nuevo (la puso el dual-write en la FASE 1), y el backfill usa insert-if-absent: solo copia lo que falta en el nuevo. Las 4 históricas faltaban, así que las copió; hdmi-cable ya estaba, así que la saltó. Ese "saltar lo que ya está" es lo que hace al backfill idempotente y lo que evita que pise escrituras frescas (lección 4).
(c) Si hicieras el backfill antes de encender el dual-write, se abriría un hueco de escrituras perdidas. El backfill copia una foto del viejo en el momento T; si entre T y el momento en que enciendes el dual-write llega una escritura, esa escritura cae solo en el viejo (el dual-write aún no está) y el backfill ya pasó (tomó su foto antes) —así que nunca llega al nuevo—. Encender el dual-write primero garantiza que todo lo escrito desde ese instante caiga en el nuevo, y el backfill después cubre lo anterior; entre los dos no queda ningún hueco. Es la razón profunda del orden, y la lección 4 la desarrolla.
Ejercicio 3 — ¿Técnica o herramienta? Para cada situación, di si es algo que este módulo enseña (la técnica, simulada) o algo que pertenece al ecosistema Data Engineering (la herramienta a escala), y por qué: (a) entender por qué el backfill va después del dual-write; (b) configurar Debezium para leer el log de transacciones de PostgreSQL y publicar los cambios a Kafka; (c) escribir un parallel-run que compare dos almacenes y reporte discrepancias; (d) mover 3 terabytes de una base a otra con checkpoints para reanudar si el proceso se cae a mitad.
Ver solución
- (a) Por qué el backfill va después del dual-write → este módulo (la técnica). Es un principio del patrón, independiente de la herramienta: aplica igual si migras 4 filas a mano o 4 billones con una herramienta de CDC. Lo enseña la lección 4.
- (b) Configurar Debezium + Kafka → Data Engineering (la herramienta a escala). Debezium es una herramienta concreta de change data capture que lee el log de transacciones; Kafka es una cola de streaming. Son las herramientas que ejecutan el dual-write/replicación a escala en producción. Cómo operarlas es del ecosistema Data Engineering; aquí solo simulamos el patrón que hay debajo.
- (c) Escribir un parallel-run que compare y reporte → este módulo (la técnica). El parallel-run es una idea del patrón —leer de ambos, comparar, reportar diferencias— y la ejecutamos aquí en memoria. La lección 5 la desarrolla. (A escala usarías herramientas de reconciliación de datos, pero la idea es la de este módulo.)
- (d) Mover 3 TB con checkpoints → Data Engineering (la herramienta a escala). El volumen (terabytes), la reanudabilidad (checkpoints para no empezar de cero si se cae), y las garantías de consistencia a esa escala son problemas de ingeniería de datos en producción. El patrón (backfill idempotente) es el mismo que aquí; la maquinaria para ejecutarlo con 3 TB es del otro ecosistema.
La lección de fondo: este módulo te da el plano —las cinco piezas y su orden—. Reconocerlo debajo de cualquier herramienta de CDC o de migración a escala es exactamente lo que te vuelve capaz de usarlas con criterio, en vez de configurarlas a ciegas.
Resumen y siguiente paso
En esta lección instalaste la última técnica de migración de la guía: mover datos de un almacén a otro sin apagar el sistema. Viste su idea completa —expand-contract, dual-write, backfill, parallel-run, read-switch— y su metáfora: cambiar de banco sin quedarte sin acceso al dinero, abriendo la cuenta nueva sin cerrar la vieja, mandando los movimientos nuevos a las dos, moviendo el histórico, verificando que cuadren, y cerrando la vieja solo cuando confías. Y lo ejecutaste en su versión mínima: old_db y new_db reales, el dual_write encendiéndose, el backfill copiando 4 filas históricas, un parallel_run reportando 1 discrepancia, la reconciliación llevándola a 0, y el read_switch cambiando la fuente de las lecturas —todo con el sistema vivo—.
Antes de avanzar deberías poder: nombrar las cinco piezas del módulo y en qué lección se desarrolla cada una; explicar por qué el dual-write sincroniza el frente pero no el histórico (y por qué el backfill cierra ese hueco); leer el estado old=N, new=M en cada fase y entender por qué difieren; y argumentar por qué no se hace read-switch mientras haya discrepancias > 0.
La lección 2 fija el marco que rige todo el módulo: expand-contract, nunca un cambio destructivo de golpe. Vas a ver, con un sistema vivo sirviendo lecturas mientras cambia el campo de precio, por qué quitar lo viejo y poner lo nuevo en un solo paso rompe a todo lector que aún espera lo viejo —y por qué el camino aditivo (expandir, migrar, contraer) no rompe una sola lectura—. Es la ley bajo la cual todas las demás piezas operan: agregar antes de quitar, siempre.
Recursos
- Martin Fowler, "ParallelChange" (también llamado expand-contract) — martinfowler.com/bliki/ParallelChange.html. La entrada que nombra el patrón de cambiar en tres fases (expand, migrate, contract) para nunca romper a los clientes de una interfaz o un esquema. El fundamento del módulo entero. En inglés.
- Pramod Sadalage y Martin Fowler, Refactoring Databases: Evolutionary Database Design (Addison-Wesley, 2006) — el libro que llevó la idea del refactoring a las bases de datos: cómo evolucionar un esquema en pasos pequeños y reversibles sin downtime. La referencia de cabecera para todo este módulo. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, "Decomposing the Database" — el capítulo que trata cómo separar y migrar los datos de un monolito hacia servicios, con los patrones de dual-write, sincronización y corte. La referencia para conectar este módulo con la extracción del módulo 5. En inglés.
- Martin Fowler, "Patterns of Legacy Displacement" — martinfowler.com/articles/patterns-legacy-displacement. El artículo vivo sobre correr la implementación vieja y la nueva en paralelo y comparar sus resultados para ganar confianza antes de cambiar; el parallel run como patrón nombrado es de Sam Newman (Monolith to Microservices). La base de la lección 5. En inglés.