Módulo 5: Extraer un servicio
Cortar la BD compartida
Descripción
La lección anterior fijó la meta: el servicio de catalog debe duenar sus datos, ser el único dueño de su store. Pero fijar la meta no es alcanzarla. En la vida real, un servicio recién extraído casi nunca nace con su base de datos propia el primer día —nace compartiendo la base de datos del monolito, porque cortar los datos de golpe es arriesgado—. Esta lección enseña cómo se corta esa dependencia: cómo se lleva el servicio de la BD compartida (shared_db) a la BD propia (owned_db) sin apagar el sistema, con el ACL en el borde manteniendo al monolito idéntico durante todo el corte.
La clave es entender que el corte es por fases, no un salto. La BD compartida es una escala transitoria, y se abandona en pasos, cada uno reversible. Primero el servicio existe pero sus datos siguen en la tabla compartida —lo lee todo a través del ACL, pero de la BD del viejo—. Luego el monolito deja de tocar la tabla directamente y empieza a pedirle los datos al servicio: la tabla sigue compartida físicamente, pero el monolito ya no la lee por su cuenta. Y al final, el servicio tiene su propia base de datos (owned_db), lee de ahí, y la tabla compartida se puede retirar. En cada fase, el ACL garantiza que el monolito recibe exactamente lo que recibía —el corte pasa por debajo, invisible para quien consume el servicio—.
Esta lección lo ejecuta con las tres fases corridas de verdad, midiendo en cada una: de dónde lee el servicio, si el monolito todavía toca la tabla, si la dependencia compartida sigue viva, y —lo más importante— si la respuesta que el monolito recibe es idéntica. Vas a ver la dependencia compartida pasar de SI a no, y la respuesta del monolito quedarse en SI (idéntica) en las tres fases. El corte de datos, invisible desde afuera.
Conexión con el módulo. La lección 5 definió qué es duenar los datos y por qué importa; esta ejecuta cómo se llega ahí, cortando la BD compartida por fases. Con esto se completan los dos cortes de la extracción: el de modelo (ACL, lecciones 3-4) y el de datos (propiedad + corte, lecciones 5-6). La lección 7 sube un nivel: el orden en que se extraen todos los contextos. Fíjate en la frontera, que aquí es especialmente fina: esta lección corta la propiedad de los datos (de quién es la tabla, quién la lee). Cómo se mueven los registros del store viejo al nuevo sin downtime —dual-write para escribir en ambos, backfill para llenar lo histórico, parallel-run para comparar antes de confiar— es el módulo 6. Aquí el corte es de topología (dónde viven los datos y quién manda); la mecánica de mudarlos sin apagar el sistema es la lección siguiente del temario.
Una analogía: mudar el archivo de la oficina sin cerrar la atención al público
Imagina una oficina de gobierno que atiende trámites. Todos los expedientes están en un archivero compartido en el sótano, y tanto el departamento viejo como el nuevo (que estás formando) sacan papeles de ahí. Quieres que el departamento nuevo tenga su propio archivo, en su propio cuarto, pero con una restricción: la oficina no puede cerrar —la gente sigue llegando a hacer trámites todos los días—.
No mudas el archivero de un jalón. Lo haces por fases. Fase A: el departamento nuevo ya existe y atiende, pero sigue bajando al sótano a sacar los expedientes del archivero compartido. Todo funciona; nada cambió para el ciudadano. Fase B: pones una regla —"el departamento viejo ya no baja al sótano; si necesita un expediente del área nueva, se lo pide al departamento nuevo por la ventanilla"—. El archivero sigue en el sótano, pero ahora solo el departamento nuevo lo consulta. Fase C: el departamento nuevo copia sus expedientes a su propio cuarto, empieza a trabajar desde ahí, y el archivero del sótano (para esa área) se puede vaciar. Ahora el departamento nuevo tiene su archivo propio, y nadie más lo toca.
En las tres fases, el ciudadano recibió el mismo servicio: entregó su trámite en la misma ventanilla y recibió la misma respuesta, sin enterarse de que los expedientes se estaban mudando por dentro. Esa ventanilla que mantiene la experiencia idéntica mientras el archivo se muda es el ACL: traduce entre lo que el ciudadano (el monolito) espera y donde sea que estén los papeles ahora. El archivero compartido del sótano es la shared_db; el cuarto propio del departamento nuevo es la owned_db; y mudar por fases sin cerrar la oficina es cortar la BD compartida sin downtime.
Ejemplo trabajado: las tres fases del corte, con el monolito idéntico
No vamos a prometer que el monolito no se entera: lo vamos a verificar, corriendo las tres fases y midiendo la respuesta del monolito en cada una. El servicio de catalog cambia su fuente de datos según la fase (A y B leen de shared_db a través del ACL; C lee de owned_db), y en cada fase el monolito pide el producto 1 y comparamos su respuesta contra la línea base —lo que recibía antes de empezar—.
from dataclasses import dataclass
@dataclass
class Product:
id: int
name: str
price_cents: int
active: bool
# --- shared_db: la tabla legacy que hoy comparten monolito y servicio. ---
shared_db = {"products": [
{"prod_id": 1, "desc": "SSD 1TB", "prc_cents": "8999", "act": "Y"},
{"prod_id": 2, "desc": "USB-C Hub", "prc_cents": "3200", "act": "Y"},
]}
def to_modern(row):
return Product(row["prod_id"], row["desc"], int(row["prc_cents"]), row["act"] == "Y")
def to_legacy(p):
return {"prod_id": p.id, "desc": p.name, "prc_cents": str(p.price_cents),
"act": "Y" if p.active else "N"}
# --- El servicio de catalogo. Su fuente de datos cambia entre fases. ---
class CatalogService:
def __init__(self, phase):
self.phase = phase
# owned_db se llena al cortar (fase C): copia de los datos, ya en modelo limpio.
self.owned_db = {p.id: p for p in (to_modern(r) for r in shared_db["products"])}
def get(self, prod_id):
if self.phase in ("A", "B"):
# Aun lee de shared_db a traves del ACL (fase transitoria).
row = next(r for r in shared_db["products"] if r["prod_id"] == prod_id)
return to_modern(row)
# Fase C: lee de su BD propia. Ya no toca shared_db.
return self.owned_db[prod_id]
# --- El monolito pide el catalogo. Su contrato legacy NO cambia en ninguna fase. ---
def monolith_gets(prod_id, service):
return to_legacy(service.get(prod_id)) # el ACL le devuelve la forma vieja
BASELINE = to_legacy(to_modern(shared_db["products"][0])) # respuesta esperada para id=1
print("Cortar la BD compartida: shared_db -> owned_db, con el monolito identico\n")
print(f"{'fase':<6}{'servicio lee de':<18}{'monolito toca tabla?':<22}"
f"{'dep. compartida?':<18}{'resp. monolito igual?':>10}")
print("-" * 78)
phases = [
("A", "shared_db", True, "el servicio existe pero comparte la tabla del monolito"),
("B", "shared_db", False, "el monolito ya pide al servicio; datos aun en shared_db"),
("C", "owned_db", False, "corte: el servicio duena owned_db; shared_db se retira"),
]
for phase, reads_from, monolith_touches_table, _desc in phases:
svc = CatalogService(phase)
resp = monolith_gets(1, svc)
shared_dep = (reads_from == "shared_db")
same = (resp == BASELINE)
print(f"{phase:<6}{reads_from:<18}"
f"{('si' if monolith_touches_table else 'no'):<22}"
f"{('SI' if shared_dep else 'no'):<18}{('SI' if same else 'NO'):>10}")
print("-" * 78)
print("\nDetalle de las fases:")
for phase, _r, _t, desc in phases:
print(f" Fase {phase}: {desc}")
print(f"\nRespuesta que el monolito recibio en TODAS las fases: {BASELINE}")
print("\n El corte mueve la fuente de datos de shared_db a owned_db, pero el ACL")
print(" mantiene el contrato legacy: el monolito recibe lo mismo en A, B y C.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Cortar la BD compartida: shared_db -> owned_db, con el monolito identico
fase servicio lee de monolito toca tabla? dep. compartida? resp. monolito igual?
------------------------------------------------------------------------------
A shared_db si SI SI
B shared_db no SI SI
C owned_db no no SI
------------------------------------------------------------------------------
Detalle de las fases:
Fase A: el servicio existe pero comparte la tabla del monolito
Fase B: el monolito ya pide al servicio; datos aun en shared_db
Fase C: corte: el servicio duena owned_db; shared_db se retira
Respuesta que el monolito recibio en TODAS las fases: {'prod_id': 1, 'desc': 'SSD 1TB', 'prc_cents': '8999', 'act': 'Y'}
El corte mueve la fuente de datos de shared_db a owned_db, pero el ACL
mantiene el contrato legacy: el monolito recibe lo mismo en A, B y C.
Lee la tabla fila por fila, porque cada una es una fase del corte, y las columnas cuentan qué se movió.
Fase A — servicio lee de: shared_db, monolito toca tabla: si, dep. compartida: SI. Es el punto de partida después de extraer el modelo: el servicio ya existe y ya tiene su ACL, pero todos leen de la tabla compartida. El servicio saca los datos de shared_db (traduciéndolos con el ACL), y el monolito todavía toca la tabla por su cuenta. La dependencia compartida está viva. Es el departamento nuevo que sigue bajando al sótano.
Fase B — servicio lee de: shared_db, monolito toca tabla: no, dep. compartida: SI. Aquí se da el primer corte, y es un corte de comportamiento, no de datos todavía: el monolito deja de tocar la tabla directamente y empieza a pedirle los datos al servicio. La tabla sigue siendo compartida físicamente (el servicio aún lee de shared_db), pero ya solo un consumidor la toca. Es la regla nueva: "el departamento viejo ya no baja al sótano; le pide los expedientes al departamento nuevo por la ventanilla". Este paso es importante porque prepara el corte de datos: una vez que el monolito ya no lee la tabla, moverla no lo afecta.
Fase C — servicio lee de: owned_db, monolito toca tabla: no, dep. compartida: no. El corte final: el servicio ahora lee de su propia base de datos (owned_db), y la dependencia compartida cae a no. Los datos se copiaron a la BD del servicio (esa copia sin downtime es la mecánica del módulo 6), el servicio lee de ahí, y la shared_db —para esta área— se puede retirar. El departamento nuevo trabaja desde su cuarto propio; el archivero del sótano se vació.
Y ahora la columna que lo sella, la de más a la derecha: resp. monolito igual? da SI en las tres fases. La respuesta que el monolito recibió —{'prod_id': 1, 'desc': 'SSD 1TB', 'prc_cents': '8999', 'act': 'Y'}— fue idéntica en A, B y C. Por dentro, la fuente de datos migró de la tabla compartida a la BD propia, la dependencia compartida se cortó, pero el monolito recibió exactamente lo mismo todo el tiempo. Ese es el trabajo del ACL en el borde: absorbe el cambio de topología por debajo, y mantiene el contrato del monolito intacto por arriba. El ciudadano recibió el mismo servicio mientras el archivo se mudaba.
Profundización: el orden de las fases y por qué la reversibilidad importa
El orden de las fases no es arbitrario; cada una habilita la siguiente y ninguna quema las naves. Vale la pena ver por qué van en ese orden:
Fase Que se corta Estado tras la fase Reversible?
───── ───────────────────────── ──────────────────────────── ──────────
A nada aun (punto de partida) servicio + ACL, datos en shared si
B el monolito deja de leer solo el servicio lee la tabla si (el
la tabla directamente monolito vuelve
a leerla)
C la tabla compartida servicio con owned_db, si (apuntar
(se copia a owned_db) shared_db retirable de vuelta a
shared_db)
Por qué B va antes que C. Podrías pensar en saltar directo a darle al servicio su owned_db. Pero si el monolito todavía lee la tabla compartida (fase A) y tú mueves los datos a owned_db, el monolito se queda leyendo una tabla que ya no es la fuente de verdad —los cambios ahora van a owned_db, y el monolito no los ve—. Por eso la fase B va primero: cortas la lectura del monolito antes de mover los datos. Una vez que el monolito ya no lee la tabla (pide todo al servicio), puedes mover los datos con libertad, porque el monolito ya no depende de dónde estén. B prepara el terreno para que C sea seguro.
Por qué cada fase es reversible. En A→B, si algo sale mal cuando el monolito empieza a pedirle al servicio, vuelves a que el monolito lea la tabla directamente. En B→C, si el owned_db da problemas, apuntas el servicio de vuelta a shared_db. Ninguna fase destruye la anterior de inmediato: la tabla compartida sigue ahí hasta que el owned_db esté probado, el camino viejo sigue disponible hasta que el nuevo demuestre que aguanta. Esa reversibilidad es la misma filosofía del strangler (módulo 3) y de branch by abstraction (módulo 4): cada paso de la migración se puede deshacer, así que ninguno es una apuesta total.
Y aquí está la frontera con el módulo 6, que conviene tener clara. La fase C dice "los datos se copian a owned_db". Esa copia, en este ejemplo, es una línea de código (llenamos owned_db desde shared_db). En un sistema real, con millones de registros y escrituras llegando mientras copias, esa "copia sin downtime" es un problema serio: ¿cómo copias lo histórico sin bloquear la tabla?, ¿cómo no pierdes las escrituras que llegan durante la copia?, ¿cómo verificas que el owned_db quedó igual al shared_db antes de confiar en él? La respuesta —dual-write (escribir en ambos durante la transición), backfill (llenar lo histórico en lotes), parallel-run (leer de ambos y comparar)— es exactamente el módulo 6. Esta lección corta la propiedad (quién manda sobre los datos, de dónde lee cada quién); el módulo 6 ejecuta la mudanza de los registros sin apagar el sistema. Los dos juntos completan el corte de datos.
Errores comunes
Mover los datos a owned_db antes de cortar la lectura del monolito. Qué pasa: el equipo, con prisa, le da al servicio su owned_db y empieza a escribir ahí, pero el monolito todavía lee la tabla compartida. Por qué pasa: parece el paso "importante" (¡el servicio ya tiene su BD!) y se salta el paso "aburrido" de cortar la lectura del monolito primero. Cómo detectarlo: el monolito muestra datos desactualizados —lee la tabla vieja mientras las escrituras nuevas van a owned_db—; hay dos fuentes de verdad divergiendo. Cómo corregirlo: respeta el orden de las fases —B antes que C—. Primero corta la lectura del monolito (que pida todo al servicio), y solo cuando el monolito ya no depende de la tabla, mueve los datos. Mover los datos mientras el monolito todavía los lee de la tabla vieja crea dos fuentes de verdad, que es una de las peores cosas que le pueden pasar a una migración de datos.
Tratar la fase compartida como permanente. Qué pasa: el servicio se queda en la fase A (o B) indefinidamente —comparte la tabla "por ahora" y nunca corta a owned_db—. Por qué pasa: las fases A y B funcionan lo suficiente; la fase C (copiar los datos, retirar la tabla) es trabajo real sin recompensa visible inmediata. Cómo detectarlo: la dependencia compartida sigue en SI meses después; no hay fecha para la fase C; el owned_db nunca se creó. Cómo corregirlo: el corte tiene un final (la fase C), igual que el strangler tiene el retiro del legacy. Compartir la BD es una escala con fecha de vencimiento; agenda la fase C desde que empiezas y trátala como parte de la extracción. Un servicio atascado en la fase compartida no logró la propiedad de sus datos —logró el acoplamiento de la lección 5, ahora con la etiqueta de "temporal" que nunca caduca—.
Cortar sin verificar que la respuesta del monolito no cambió. Qué pasa: el equipo hace el corte (mueve los datos, cambia la fuente del servicio) sin comparar la respuesta que el monolito recibe antes y después. Por qué pasa: "el ACL traduce, seguro está bien". Cómo detectarlo: aparece un bug sutil en producción —un precio que cambió de formato, un campo que se perdió en el corte— que nadie atrapó porque nadie comparó. Cómo corregirlo: cada fase del corte se verifica con la misma prueba del ejemplo: comparar la respuesta del monolito contra la línea base y confirmar que es idéntica (resp. monolito igual: SI). Es el equivalente de la prueba de transparencia del facade (módulo 3) y del round-trip del ACL (lección 1), aplicado al corte de datos. El ACL debería mantener el contrato, pero "debería" no es "se verificó". Comparar contra la línea base en cada fase es barato y atrapa el corte que salió mal antes de que llegue al usuario.
Ejercicios
Ejercicio 1 — Mudar el archivo sin cerrar. Con la analogía de la oficina de gobierno, empareja cada fase del corte con lo que ocurre en la oficina: (a) fase A, (b) fase B, (c) fase C. Luego di qué representa la ventanilla que mantiene la experiencia del ciudadano idéntica, y por qué la oficina "no puede cerrar" durante la mudanza.
Ver solución
- (a) Fase A → el departamento nuevo ya atiende, pero sigue bajando al sótano a sacar los expedientes del archivero compartido. Existe y funciona, pero comparte el archivo con el viejo.
- (b) Fase B → la regla nueva: el departamento viejo ya no baja al sótano, le pide los expedientes del área nueva al departamento nuevo por la ventanilla. El archivero sigue en el sótano, pero ya solo el departamento nuevo lo consulta.
- (c) Fase C → el departamento nuevo copia sus expedientes a su cuarto propio y trabaja desde ahí; el archivero del sótano (para esa área) se vacía. Archivo propio, nadie más lo toca.
La ventanilla que mantiene la experiencia del ciudadano idéntica representa el ACL: traduce entre lo que el ciudadano (el monolito) espera y donde sea que estén los papeles ahora, así que el ciudadano recibe la misma respuesta sin importar en qué fase de la mudanza esté el archivo. La oficina "no puede cerrar" porque representa un sistema en producción: el negocio no se detiene durante la migración (la convicción del módulo 1). Todo el arte del corte es mudar el archivo mientras se sigue atendiendo, por fases y sin apagón —justo lo que la columna "resp. monolito igual: SI" demuestra—.
Ejercicio 2 — Lee las tres fases. En la salida, la columna dep. compartida? fue SI, SI, no y la columna resp. monolito igual? fue SI, SI, SI. (a) ¿Por qué la dependencia compartida sigue en SI en la fase B si el monolito ya no toca la tabla? (b) ¿Qué cambió entre B y C para que caiga a no? (c) ¿Qué demuestra que la respuesta del monolito sea SI (idéntica) en las tres fases?
Ver solución
(a) Porque en la fase B el servicio todavía lee de shared_db. Que el monolito haya dejado de tocar la tabla (monolito toca tabla: no) corta una de las dos lecturas, pero la tabla sigue siendo compartida mientras el servicio la lea. La dependencia compartida existe si cualquiera de los dos depende de la tabla común; en B, el servicio aún depende de ella, así que sigue en SI. B corta el comportamiento del monolito, no todavía los datos.
(b) Entre B y C, el servicio pasó de leer de shared_db a leer de su owned_db propia. Los datos se copiaron a la BD del servicio, el servicio lee de ahí, y ya nadie —ni el monolito (cortado en B) ni el servicio (cortado en C)— depende de la tabla compartida. Con cero dependientes, la shared_db se puede retirar y la dependencia compartida cae a no. Ese es el corte de datos propiamente dicho.
(c) Demuestra que el corte es invisible desde afuera: por dentro, la fuente de datos migró de la tabla compartida a la BD propia y la dependencia se cortó, pero el monolito recibió exactamente la misma respuesta ({'prod_id': 1, 'desc': 'SSD 1TB', ...}) en las tres fases. El ACL en el borde absorbió todo el cambio de topología y mantuvo el contrato del monolito idéntico. Es la prueba de que la extracción de datos no rompió nada: el monolito no puede notar la diferencia entre leer de la tabla compartida o de la BD propia del servicio, porque el ACL le entrega lo mismo en ambos casos.
Ejercicio 3 — El corte prematuro. Un equipo, en la fase A (el monolito todavía lee la tabla), decide darle al servicio su owned_db y empezar a escribir ahí, saltándose la fase B. (a) ¿Qué problema concreto crea? (b) ¿Por qué la fase B lo habría evitado? (c) ¿Qué principio general sobre el orden del corte ilustra?
Ver solución
(a) Crea dos fuentes de verdad divergentes. El servicio escribe los cambios nuevos en owned_db, pero el monolito sigue leyendo la tabla compartida (shared_db), que ya no recibe esos cambios. Resultado: el monolito muestra datos desactualizados —un precio que se cambió en el servicio pero que el monolito no ve, porque lee de la tabla vieja—. Los dos stores dicen cosas distintas sobre el mismo producto.
(b) La fase B habría cortado la lectura del monolito antes de mover los datos: una vez que el monolito le pide todo al servicio (en vez de leer la tabla), ya no importa dónde estén físicamente los datos —el monolito recibe la verdad a través del servicio—. Con esa lectura cortada, mover los datos a owned_db es seguro, porque el monolito no depende de la tabla. Saltarse B significa mover los datos mientras el monolito todavía depende de la tabla vieja, que es justo lo que produce las dos fuentes de verdad.
(c) Ilustra que el corte de la lectura debe preceder al corte de los datos: primero haz que nadie dependa de la ubicación vieja de los datos, y solo entonces muévelos. En términos generales, antes de mover algo de lo que otros dependen, primero corta esa dependencia (redirígela a la nueva forma de acceso), y después mueve. Mover primero y cortar después deja a los que aún dependían de la ubicación vieja mirando datos obsoletos. Es la misma lógica del facade antes de desviar tráfico (módulo 3) y de la abstracción antes de cambiar la implementación (módulo 4): se instala el punto de indirección primero, se mueve lo de atrás después.
Resumen y siguiente paso
En esta lección ejecutaste el corte que la anterior pidió: llevar el servicio de la BD compartida a la BD propia sin apagar el sistema. Viste, con la oficina de gobierno que muda su archivo sin cerrar la atención al público, que el corte se hace por fases y que la ventanilla (el ACL) mantiene la experiencia idéntica mientras el archivo se muda por dentro. Y lo ejecutaste con las tres fases: A (todos leen de la tabla compartida), B (el monolito deja de tocar la tabla y le pide al servicio), C (el servicio duena owned_db y la tabla se retira), con la dependencia compartida pasando de SI a no y —el sello— la respuesta del monolito idéntica (SI) en las tres. Aprendiste por qué B va antes que C (cortar la lectura antes de mover los datos), por qué cada fase es reversible, y dónde está la frontera con el módulo 6 (aquí la propiedad; allá la mudanza de registros sin downtime).
Antes de avanzar deberías poder: describir las tres fases del corte y qué se corta en cada una; explicar por qué el monolito recibe lo mismo en las tres (el ACL en el borde); justificar por qué cortar la lectura del monolito precede a mover los datos; y ubicar la frontera entre el corte de propiedad (este módulo) y la migración de datos sin downtime (módulo 6).
Con las lecciones 2 a 6 ya sabes extraer un servicio de punta a punta: encontrar el bounded context, ponerle el ACL bidireccional, darle la propiedad de sus datos y cortar la BD compartida. La lección 7 sube la mirada del árbol al bosque: el orden de extracción. Un monolito no tiene un solo bounded context, tiene varios, y extraerlos en el orden equivocado —empezando por el más acoplado— arrastra medio sistema consigo. Vas a puntuar los cuatro módulos de Mercado por independencia y a producir el orden correcto de extracción: la hoja primero, el hub al final. Sabes extraer uno; ahora aprendes en qué orden extraerlos todos.
Recursos
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, "Decomposing the Database" — los patrones para separar una base de datos compartida por fases: la vista compartida, la tabla de sincronización, y el paso de la BD compartida a la BD por servicio. La referencia directa de esta lección. En inglés.
- Chris Richardson, "Pattern: Database per service" — microservices.io/patterns/data/database-per-service.html. La ficha del destino del corte: cada servicio con su propia base de datos, y las fuerzas que hacen difícil llegar ahí desde una BD compartida. En inglés.
- Martin Fowler, "Patterns of Legacy Displacement" — martinfowler.com/articles/patterns-legacy-displacement. La técnica de correr el sistema viejo y el nuevo en paralelo y comparar sus resultados antes de confiar en el nuevo; el parallel run como patrón nombrado es de Sam Newman (Monolith to Microservices). Se usa al verificar el corte de datos, y es central en el módulo 6. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, sección sobre migración de datos por fases (expand-contract) — la conexión con el módulo 6: cómo mover los datos del store viejo al nuevo sin downtime, que es la mecánica que esta lección deja apuntada. En inglés.