Módulo 6: Migrar datos sin downtime
Expand-contract: nunca un cambio destructivo de golpe
Descripción
La lección 1 te dio el mapa de las cinco piezas de la migración de datos. Esta lección fija la ley bajo la cual todas operan, la que hace posible cambiar datos sin apagar el sistema: expand-contract. Dicho en una frase: nunca quitas lo viejo y pones lo nuevo en el mismo paso; primero expandes (agregas lo nuevo, dejando lo viejo intacto), luego migras (mueves a los que usaban lo viejo hacia lo nuevo), y solo al final contraes (quitas lo viejo, cuando ya nadie lo usa). Un cambio destructivo hace las dos cosas de golpe —quita y pone en un instante— y por eso rompe a todo el que todavía dependía de lo viejo. Expand-contract separa esas dos cosas en el tiempo, y esa separación es lo que mantiene el sistema vivo.
El nombre lo dice: el esquema crece (expand) antes de encoger (contract), y nunca al revés. Entre esos dos momentos hay una fase de coexistencia —lo viejo y lo nuevo conviven— que es incómoda (tienes dos formas de la misma cosa a la vez) pero es exactamente el precio de no apagar nada. Martin Fowler lo llama también parallel change: el cambio ocurre en paralelo, con las dos versiones vivas, en vez de en un salto atómico.
Esta idea no es solo para columnas de una base de datos. Es la misma para renombrar un campo, cambiar un tipo, partir una tabla en dos, o cambiar el formato de un valor. Y es la misma que rige el dual-write, el backfill y el read-switch de las lecciones siguientes: cada uno de ellos es una aplicación de "agregar antes de quitar". El dual-write agrega el nuevo almacén sin quitar el viejo; el read-switch mueve las lecturas antes de retirar el viejo. Todo el módulo es expand-contract aplicado, así que entenderlo aquí, en su forma pura, es entender el resto.
Conexión con el módulo. Esta lección da el marco; las que siguen lo instancian. La lección 3 (dual-write) es el "expand" de las escrituras: agregar el segundo destino sin quitar el primero. La lección 4 (backfill) llena lo que el expand dejó vacío. La lección 7 (read-switch) es el "migrate" y el "contract" de las lecturas: mover los lectores al nuevo y solo después retirar el viejo. Fíjate en la frontera: aquí trabajamos el marco a nivel de esquema/campo (migrar price_cents a price_usd), con lecturas en memoria; la migración del contenido de las filas entre dos almacenes distintos es lo que arman las lecciones 3 a 7. Expand-contract es el principio; ellas son la mecánica.
Una analogía: cambiar el cableado de la casa con la luz siempre prendida
Vas a renovar la instalación eléctrica de tu casa —el cableado viejo ya no da abasto—. Pero pones una condición: la casa no se queda sin luz ni un minuto. Hay gente viviendo ahí, un refrigerador funcionando, alguien trabajando desde casa. Cortar toda la corriente el fin de semana para rehacer todo de golpe no es opción; eso sería el big-bang.
La forma destructiva sería: cortas el cable viejo de una habitación y, en ese hueco de tiempo, tiendes el nuevo. Durante esos minutos —o esas horas— la habitación (o media casa, si el cable viejo alimentaba varias) se queda sin luz. Y si el cable nuevo resulta estar mal y no funciona, ya cortaste el viejo: te quedaste a oscuras sin forma rápida de volver atrás. Quitaste antes de que lo nuevo estuviera probado.
La forma expand-contract es como lo hace un buen electricista que no puede cortar la luz:
- Expand. Tiende el cable nuevo al lado del viejo, sin tocar el viejo. Los dos cables corren en paralelo por la pared. La casa sigue alimentada por el viejo; el nuevo está puesto pero aún no conectado a los enchufes. Nadie notó nada; solo hay más cable.
- Migrate. Habitación por habitación, mueve los enchufes del cable viejo al nuevo. Cuando termina una habitación, esa habitación ya toma su corriente del cable nuevo —y sigue teniendo luz todo el tiempo, porque el nuevo ya estaba tendido y probado antes de conectarlo—. Pasa a la siguiente habitación. En ningún momento una habitación se queda a oscuras.
- Contract. Solo cuando todas las habitaciones toman su corriente del cable nuevo, y lo confirmó revisando que todo funciona, retira el cable viejo de la pared. Ya nadie lo usa, así que quitarlo no apaga nada.
Fíjate en el orden y en lo que garantiza. El cable nuevo se agrega antes de que nada dependa de él (expand). Los enchufes se mueven de a poco, con las dos opciones disponibles (migrate). Y el cable viejo se quita solo cuando ya nadie lo usa (contract). La luz nunca se corta porque nunca hay un instante en que lo viejo ya no está pero lo nuevo todavía no. Un cambio destructivo crea justo ese instante —el hueco a oscuras—; expand-contract lo elimina poniendo lo nuevo primero y quitando lo viejo al final.
En términos de datos: el cable viejo es el campo price_cents, el nuevo es price_usd. Tenderlo al lado es agregar price_usd sin quitar price_cents. Mover los enchufes es cambiar los lectores para que usen price_usd. Y retirar el cable viejo es borrar price_cents cuando ya nadie lo lee. Vamos a ejecutar exactamente eso.
Ejemplo trabajado: migrar el campo de precio en un sistema vivo
Vamos a comparar las dos formas —destructiva y expand-contract— midiendo lo único que importa: cuántas lecturas se rompen mientras el sistema está vivo. El escenario: una fila del catálogo guarda el precio en price_cents (el campo viejo) y queremos migrarlo a price_usd (el nuevo). Mientras hacemos el cambio, el sistema sigue sirviendo lecturas —100 de ellas—. Un lector viejo espera price_cents; un lector nuevo espera price_usd. La clave, y lo que hace realista al ejemplo: el código y el esquema no cambian en el mismo instante. Cuando cambias el esquema, el lector desplegado sigue siendo el que estaba; se actualiza después, en otro despliegue. Esa brecha es donde un cambio destructivo hace daño.
# --- Una fila del catalogo. Empieza con el campo viejo: price_cents. ---
def seed_row():
return {"sku": "ssd-1tb", "name": "SSD 1TB", "price_cents": 8999}
# --- Lectores desplegados. El viejo espera price_cents; el nuevo, price_usd. ---
def old_reader(row):
return row["price_cents"] # KeyError si el campo ya no existe
def new_reader(row):
return round(row["price_usd"] * 100)
# --- Un turno de lecturas: sirve N reads y cuenta cuantas se ROMPEN. ---
def serve_reads(n, row, reader):
broken = 0
for _ in range(n):
try:
reader(row)
except KeyError:
broken += 1
return broken
# ---------------------------------------------------------------------------
# ESCENARIO A: cambio destructivo de golpe.
# En un solo paso quitamos price_cents y ponemos price_usd. Pero el lector
# desplegado sigue siendo el viejo (codigo y esquema no cambian atomicos).
# ---------------------------------------------------------------------------
row = seed_row()
broken_a = 0
broken_a += serve_reads(50, row, old_reader) # reads 1..50: todo bien
# --- El cambio destructivo: quita lo viejo Y pone lo nuevo, de golpe. ---
row["price_usd"] = row.pop("price_cents") / 100 # price_cents DESAPARECE
broken_a += serve_reads(50, row, old_reader) # reads 51..100: KeyError
# ---------------------------------------------------------------------------
# ESCENARIO B: expand-contract (aditivo, en tres pasos).
# EXPAND: agrega price_usd SIN quitar price_cents (ambos coexisten).
# MIGRATE: cambia el lector a new_reader (price_usd ya existe).
# CONTRACT: recien ahora quita price_cents (ya nadie lo lee).
# ---------------------------------------------------------------------------
row = seed_row()
broken_b = 0
broken_b += serve_reads(25, row, old_reader) # reads 1..25: lector viejo
# EXPAND: agrega el campo nuevo, deja el viejo. Nada se rompe.
row["price_usd"] = row["price_cents"] / 100
broken_b += serve_reads(25, row, old_reader) # reads 26..50: viejo sigue OK
# MIGRATE: ahora cambia el lector al nuevo. price_usd ya esta, no falla.
broken_b += serve_reads(25, row, new_reader) # reads 51..75: lector nuevo
# CONTRACT: ya nadie lee price_cents -> se puede quitar sin romper nada.
del row["price_cents"]
broken_b += serve_reads(25, row, new_reader) # reads 76..100: sin el viejo
print("Migrar el campo de precio en un sistema VIVO (100 lecturas c/u)\n")
print(f"{'estrategia':<22}{'lecturas rotas':>16}")
print("-" * 38)
print(f"{'cambio destructivo':<22}{broken_a:>16}")
print(f"{'expand-contract':<22}{broken_b:>16}")
print("-" * 38)
print("\n Destructivo: quitar y poner de golpe rompio a todo lector que aun")
print(" esperaba el campo viejo (50 lecturas caidas).")
print(" Expand-contract: aditivo primero, quitar al final -> 0 lecturas rotas.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Migrar el campo de precio en un sistema VIVO (100 lecturas c/u)
estrategia lecturas rotas
--------------------------------------
cambio destructivo 50
expand-contract 0
--------------------------------------
Destructivo: quitar y poner de golpe rompio a todo lector que aun
esperaba el campo viejo (50 lecturas caidas).
Expand-contract: aditivo primero, quitar al final -> 0 lecturas rotas.
Los dos números lo dicen todo: 50 lecturas rotas con el cambio destructivo, 0 con expand-contract.
En el escenario destructivo, las primeras 50 lecturas van bien: el lector viejo lee price_cents, que existe. Luego ocurre el cambio destructivo —row.pop("price_cents") quita el campo viejo y crea price_usd en el mismo paso—. Pero el lector desplegado sigue siendo old_reader: en la vida real no puedes actualizar el esquema y todo el código lector en el mismo microsegundo. Así que las lecturas 51 a 100 hacen row["price_cents"] sobre una fila que ya no tiene ese campo: KeyError, 50 veces. Cada una de esas 50 lecturas es un error servido a un usuario. Ese es el hueco a oscuras: lo viejo ya no está, pero el lector todavía lo pide.
En el escenario expand-contract, el cambio se parte en tres momentos y nunca se crea ese hueco:
- Expand (después de 50 lecturas sanas con el lector viejo): agregamos
price_usdsin quitarprice_cents. Ahora la fila tiene los dos campos. El lector viejo sigue leyendoprice_cents, que sigue ahí: 0 lecturas rotas. El esquema creció, nada se rompió. - Migrate: cambiamos al
new_reader, que leeprice_usd. Ese campo ya existe (lo pusimos en el expand), así que el lector nuevo funciona de inmediato: 0 rotas. Los lectores se mueven al campo nuevo mientras el viejo sigue disponible. - Contract: ahora que nadie lee
price_cents(todos los lectores son nuevos), lo borramos. El lector nuevo usaprice_usd, que sigue ahí: 0 rotas. El esquema encogió, y como ya nadie dependía de lo que quitamos, no se rompió nada.
Total: 0 lecturas rotas. La diferencia entera entre 50 y 0 es el orden: destructivo quita y pone a la vez (creando el hueco); expand-contract agrega primero, migra en medio, y quita al final (nunca hay hueco).
Profundización: por qué las tres fases y en ese orden
El núcleo de expand-contract es una regla de dependencias. Un lector depende del campo que lee. Si quitas un campo del que alguien depende, lo rompes. La única forma segura de quitar algo es asegurarte primero de que nadie depende de ello —y eso toma tiempo, porque hay que mover a todos los que dependían al nuevo—. Las tres fases son exactamente ese proceso:
expand migrate contract
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ agrega lo │ │ mueve a los │ │ quita lo │
│ nuevo; deja │──▶│ que usaban lo │──▶│ viejo (ya │
│ lo viejo │ │ viejo hacia lo │ │ nadie │
│ intacto │ │ nuevo │ │ depende) │
└──────────────┘ └──────────────────┘ └──────────────┘
viejo y nuevo coexistencia: solo el nuevo
coexisten ambos disponibles queda
Por qué expand primero. Si vas a mover lectores al campo nuevo, el campo nuevo tiene que existir antes de que el primer lector lo pida. Agregar es una operación segura: nadie se rompe porque aparezca un campo de más (los lectores viejos lo ignoran). Por eso el crecimiento del esquema siempre puede ir primero, sin riesgo.
Por qué migrate en medio. No puedes mover a todos los lectores en un instante. En un sistema real hay varias instancias del servicio, despliegues escalonados, quizás clientes que actualizan a su ritmo. La fase de coexistencia —los dos campos vivos— es lo que da tiempo a que todos los lectores pasen al nuevo sin que ninguno se quede sin su campo. Cuanto más distribuido el sistema, más larga y más necesaria esta fase.
Por qué contract al final. Quitar es la operación peligrosa: rompe a todo el que aún dependa. Por eso es lo último, y solo se hace cuando puedes demostrar que nadie depende de lo viejo. Esa demostración —"nadie lee ya price_cents"— es la que en las lecciones siguientes se vuelve el parallel-run verde y el burn-down de llamadas al viejo: no quitas por corazonada, quitas por evidencia.
Hay una simetría útil con el módulo 3. El strangler fig también agrega lo nuevo (el modern) al lado sin quitar lo viejo (el legacy), desvía el tráfico de a poco (coexistencia), y retira el viejo solo cuando el burn-down llega a cero. Expand-contract es el mismo esqueleto —agregar, coexistir, quitar— aplicado a los datos y al esquema en vez de al tráfico. Si dominaste el strangler, ya tienes la forma mental; aquí solo cambia el objeto sobre el que actúa.
Un matiz importante: expand-contract tiene un costo temporal, la fase de coexistencia. Durante ella mantienes dos campos (o dos almacenes) sincronizados, lo que es más trabajo y más complejidad que tener uno solo. Ese costo es real, pero es temporal (desaparece cuando contraes) y acotado (sabes que termina cuando nadie usa lo viejo). El error no es pagar ese costo; es no pagarlo (hacer el cambio destructivo) o pagarlo para siempre (nunca contraer, quedándote con los dos campos indefinidamente —la migración eterna que el módulo 7 combate—).
Errores comunes
El cambio destructivo de golpe: quitar y poner en el mismo paso. Qué pasa: el equipo renombra una columna, cambia un tipo, o reemplaza un campo en una sola migración de esquema —ALTER TABLE ... RENAME, o quitar y agregar a la vez—. Por qué pasa: es lo que parece "el cambio", una sola operación limpia; separarlo en tres fases se siente burocrático. Cómo detectarlo: la migración de esquema quita algo (un DROP COLUMN, un RENAME) en el mismo paso en que agrega o cambia; el código lector y el esquema se despliegan como si fueran atómicos. Cómo corregirlo: parte todo cambio de esquema en expand (agregar, aditivo, seguro) y contract (quitar, en otro despliegue posterior, cuando nadie use lo viejo), con la migración del código en medio. Nunca RENAME en un sistema vivo: es ADD la nueva, migrar, y DROP la vieja en tres pasos separados. La regla mecánica: si una migración de esquema quita algo que el código desplegado todavía usa, es destructiva —y va a romper lecturas en la brecha entre desplegar el esquema y desplegar el código—.
Contraer antes de que todos hayan migrado. Qué pasa: el equipo hace el expand y el migrate, ve que "el sistema ya usa el campo nuevo", y borra el viejo enseguida —pero quedaba un lector rezagado (un job nocturno, una instancia vieja, un cliente que no actualizó) que todavía usaba el campo viejo—. Por qué pasa: es fácil creer que "ya migramos" cuando migró lo que ves, olvidando los lectores menos visibles. Cómo detectarlo: después de borrar el campo viejo aparecen errores en componentes periféricos —reportes, jobs batch, integraciones— que nadie tocó en la migración. Cómo corregirlo: el contract solo es seguro cuando puedes demostrar que nadie lee lo viejo, no cuando crees que nadie lee lo viejo. Esa demostración es medible: instrumenta el campo viejo para contar sus lecturas y espera a que el conteo llegue y se mantenga en cero (es el burn-down del módulo 7). Contraer es la operación peligrosa; se hace por evidencia de cero uso, no por la sensación de que ya se migró.
Quedarse en la coexistencia para siempre (nunca contraer). Qué pasa: el equipo hace el expand y el migrate, todo funciona con el campo nuevo, y... deja el campo viejo ahí "por si acaso". Meses después la tabla tiene los dos campos, el código escribe a los dos por costumbre, y nadie recuerda cuál es la verdad. Por qué pasa: contraer no da valor visible (el sistema ya anda con lo nuevo) y da un poco de miedo (¿y si algo aún lo usa?). Cómo detectarlo: hay campos "viejos" que llevan meses sin que nadie los lea pero siguen ahí, y código que mantiene los dos sincronizados sin razón. Cómo corregirlo: expand-contract termina en contract. La fase de coexistencia es un medio, no un destino: es cara (dos cosas que mantener sincronizadas) y confusa (dos fuentes de verdad). Ponle desde el inicio una condición de salida —"cuando el conteo de lecturas del campo viejo sea cero por N días, se borra"— y cúmplela. Un expand sin su contract es una deuda que crece; el módulo 7 trata a fondo esta trampa de la migración que nunca termina.
Ejercicios
Ejercicio 1 — El hueco a oscuras. En el escenario destructivo se rompieron exactamente 50 lecturas. (a) ¿Por qué 50 y no 100, ni 0? (b) ¿Qué evento concreto, en el código, creó el "hueco a oscuras"? (c) Con la analogía del cableado, ¿qué representa ese hueco?
Ver solución
(a) Porque el cambio destructivo ocurrió justo a la mitad, después de 50 lecturas y antes de las otras 50. Las primeras 50 corrieron con price_cents aún presente (lector viejo, campo viejo: bien). Las últimas 50 corrieron después de que pop quitó price_cents, pero con el lector todavía viejo (aún pide price_cents): KeyError, 50 veces. Si el cambio hubiera ocurrido en la lectura 30, se habrían roto 70; en la 90, solo 10. El número depende de cuánto tiempo el lector viejo sobrevive al cambio de esquema —y en la realidad esa brecha es el tiempo entre desplegar el esquema y desplegar el código nuevo, que nunca es cero—.
(b) El evento fue row["price_usd"] = row.pop("price_cents") / 100. El pop quita price_cents en el mismo instante en que crea price_usd. Ese "quitar" es lo destructivo: en el microsegundo siguiente, cualquier lector que aún espere price_cents se rompe. La operación mezcló agregar (seguro) y quitar (peligroso) en un solo paso, que es la definición del cambio destructivo.
(c) Ese hueco representa el instante en que cortaste el cable viejo antes de que la habitación tomara corriente del nuevo. La habitación (el lector viejo) se quedó a oscuras porque su fuente de corriente (el campo price_cents) desapareció mientras todavía dependía de ella. Expand-contract elimina ese hueco tendiendo el cable nuevo primero y cortando el viejo solo cuando ya nadie lo usa.
Ejercicio 2 — Ordena las fases. Un equipo quiere partir el campo name en dos: first_name y last_name. Tienen estas cinco acciones. Ordénalas en una secuencia expand-contract segura, y marca cuál es expand, cuál migrate y cuál contract: (i) borrar la columna name; (ii) desplegar el código que lee first_name/last_name; (iii) agregar las columnas first_name y last_name; (iv) rellenar first_name/last_name a partir de name para las filas existentes; (v) confirmar (con métricas) que ya nadie lee name.
Ver solución
El orden seguro es iii → iv → ii → v → i:
- (iii) agregar las columnas
first_nameylast_name— EXPAND. Aditivo y seguro: los lectores viejos que usannamelo ignoran. El esquema crece primero. - (iv) rellenar
first_name/last_namedesdename— parte del expand/migrate de los datos: las columnas nuevas existen pero están vacías para las filas viejas; hay que llenarlas (esto es un backfill, lección 4). Se hace después de agregar las columnas y sin tocarname. - (ii) desplegar el código que lee
first_name/last_name— MIGRATE. Mueve a los lectores del campo viejo al nuevo, que ya existe y ya está lleno. La coexistencia (name + los dos nuevos) da tiempo a que todas las instancias se actualicen. - (v) confirmar que nadie lee
name— la evidencia requerida antes de contraer. No se borra por creencia, sino por medición de cero uso. - (i) borrar la columna
name— CONTRACT. Lo último, y solo tras confirmar que nadie depende de ella.
Puntos clave: rellenar (iv) va después de agregar (iii) —no puedes llenar columnas que no existen—; desplegar el código nuevo (ii) va después de que las columnas existan y estén llenas —si no, el código nuevo leería columnas vacías—; y borrar name (i) va al final, tras la evidencia (v). Invertir cualquiera de esos órdenes crea un hueco a oscuras.
Ejercicio 3 — Cambios seguros y peligrosos. Clasifica cada cambio de esquema como seguro (aditivo, se puede hacer en un paso sin romper lectores) o peligroso (destructivo, requiere expand-contract), y explica por qué: (a) agregar una columna nueva con valor por defecto; (b) renombrar una columna existente; (c) agregar un índice; (d) cambiar el tipo de una columna de int a string; (e) hacer más estricta una restricción (p. ej., NOT NULL sobre una columna que tenía nulos).
Ver solución
- (a) Agregar una columna nueva → SEGURO. Es puro expand: los lectores viejos ignoran la columna nueva; ninguno se rompe. Se puede hacer en un paso. (Con valor por defecto, además, las filas viejas quedan consistentes de inmediato.)
- (b) Renombrar una columna → PELIGROSO. Un
RENAMEes quitar el nombre viejo y poner el nuevo a la vez: todo lector que use el nombre viejo se rompe en el instante del rename. Requiere expand-contract: agregar la columna nueva, copiar los datos, migrar los lectores, y borrar la vieja —nunca un rename directo en un sistema vivo—. - (c) Agregar un índice → SEGURO. No cambia el esquema que los lectores ven ni quita nada; solo acelera consultas. (En bases grandes puede requerir crearlo sin bloquear la tabla, pero no rompe lectores.)
- (d) Cambiar el tipo
int→string→ PELIGROSO. Los lectores que esperan unintpueden romperse al recibir unstring, y el cambio de tipo suele ser destructivo del valor viejo. Requiere expand-contract: agregar una columna nueva del tipo nuevo, migrar los datos y los lectores, y contraer la vieja. - (e) Hacer una restricción más estricta (
NOT NULL) → PELIGROSO. Si la columna tenía nulos, imponerNOT NULLde golpe falla o rompe filas existentes, y los escritores que insertaban nulos se rompen. Requiere el proceso inverso pero análogo: primero asegurar (por migración de datos) que ya no hay nulos y que nadie escribe nulos, y solo entonces imponer la restricción.
La regla mecánica: agregar es seguro; quitar, renombrar, cambiar tipo y restringir son peligrosos. Todo lo peligroso se descompone en un expand (agregar lo nuevo) y un contract (quitar lo viejo) separados en el tiempo, con la migración en medio.
Resumen y siguiente paso
En esta lección fijaste la ley que rige todo el módulo: expand-contract, nunca un cambio destructivo de golpe. Viste, con el cableado de la casa que se cambia habitación por habitación con la luz siempre prendida, que la seguridad viene del orden —agregar lo nuevo (expand), mover a los que usan lo viejo (migrate), y quitar lo viejo solo cuando nadie depende de él (contract)—, y que un cambio destructivo rompe porque hace el "quitar" y el "poner" en el mismo instante, creando un hueco donde lo viejo ya no está pero los lectores todavía lo piden. Y lo ejecutaste: migrar price_cents a price_usd en un sistema vivo rompió 50 lecturas de golpe y 0 con expand-contract. La diferencia entera fue el orden.
Antes de avanzar deberías poder: nombrar las tres fases (expand, migrate, contract) y por qué van en ese orden; distinguir un cambio de esquema seguro (aditivo) de uno peligroso (destructivo); explicar qué es el "hueco a oscuras" y cómo expand-contract lo elimina; y reconocer las dos formas de fallar con el marco (contraer antes de tiempo, o no contraer nunca).
La lección 3 instancia el "expand" de las escrituras: el dual-write. Vas a agregar un segundo destino a cada escritura —el almacén nuevo— sin quitar el primero —el viejo—, y vas a ver que eso mantiene el nuevo al día con todo lo que se escribe de ahora en adelante. Pero también vas a ver, ejecutado, el límite exacto del dual-write: no toca lo que se escribió antes de encenderlo. Ese hueco histórico —lo que el expand dejó vacío en las filas viejas— es lo que la lección 4 llenará con el backfill. Expand-contract acaba de darte el marco; el dual-write es su primera pieza en movimiento.
Recursos
- Martin Fowler, "ParallelChange" — martinfowler.com/bliki/ParallelChange.html. La entrada canónica del patrón expand-contract (también llamado parallel change): las tres fases (expand, migrate, contract) para cambiar una interfaz o un esquema sin romper a sus clientes. La lectura fundacional de esta lección. En inglés.
- Pramod Sadalage y Martin Fowler, Refactoring Databases: Evolutionary Database Design (Addison-Wesley, 2006) — el catálogo de refactorings de bases de datos, cada uno diseñado como un cambio pequeño y reversible que preserva el comportamiento. El "Rename Column" del libro es exactamente el expand-contract de esta lección aplicado a una columna. En inglés.
- Pramod Sadalage, "Evolutionary Database Design" — martinfowler.com/articles/evodb.html. El artículo que resume cómo evolucionar el esquema de una base de datos de forma continua e incremental, con migraciones versionadas y transiciones sin downtime. El marco de todo el módulo. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, "Decomposing the Database" — los patrones para cambiar y separar el esquema de un monolito por pasos, incluyendo cómo agregar antes de quitar al partir tablas y mover datos entre servicios. En inglés.