Módulo 5: Extraer un servicio
La propiedad de los datos
Descripción
Con el ACL bidireccional listo, el servicio de catalog ya habla un modelo limpio y el monolito no se enteró. Pero hay una pregunta que el ACL no responde y que decide si la extracción es real o es teatro: ¿de dónde saca el servicio sus datos? Si el "servicio nuevo", a pesar de su modelo limpio, sigue leyendo la tabla products del monolito, no está extraído —está disfrazado—. Esta lección ataca el segundo corte de la extracción, el de los datos: un servicio extraído de verdad debe duenar sus datos.
Duenar los datos significa una cosa precisa: el servicio es el único que lee y escribe su propio store. Nadie más toca sus tablas directamente —ni el monolito, ni otro servicio, ni un script suelto—. Si alguien necesita datos de catalog, se los pide al servicio (a través de su API, con el ACL traduciendo), no los saca de las tablas por su cuenta. Esa exclusividad es lo que le da al servicio el control real sobre su dominio: puede cambiar su esquema, sus índices, su base de datos entera, sin romper a nadie —porque nadie más depende de la forma interna de sus datos, solo de su API—.
El anti-patrón es tentador y común: el "servicio" que comparte las tablas del monolito para siempre. Se ve como una extracción (hay un servicio, hay un modelo limpio, hay un ACL) pero por debajo sigue anclado a la base de datos del viejo. Esta lección lo hace visible con un detector: instrumentamos la tabla del monolito para que registre quién la toca, y ejecutamos dos "servicios" —uno falso que lee las tablas del monolito, uno real que tiene su propio store—. El detector atrapa al falso (2 accesos a la tabla del monolito) y absuelve al real (0 accesos). Mover el código no es extraer; extraer es cortar la dependencia de datos.
Conexión con el módulo. Las lecciones 3 y 4 cortaron el modelo (el ACL traduce viejo↔nuevo). Esta lección introduce el corte de los datos: por qué el servicio debe duenar su store. La lección 6 ejecuta ese corte por fases —de la BD compartida transitoria a la BD propia—. Fíjate en la frontera: aquí definimos de quién son los datos (la propiedad). Cómo se mueven los registros de un store a otro sin apagar el sistema —dual-write, backfill, parallel-run— es el módulo 6. La propiedad es "quién manda sobre las tablas"; la migración es "cómo se mudan los datos sin downtime". Son dos cosas distintas y este módulo hace la primera.
Una analogía: la cuenta bancaria propia
Vuelve al hijo que se independiza. Puede haberse mudado a su propio departamento (tiene su espacio, su modelo de vida), pero mientras siga usando tu tarjeta de crédito para todo, no es autosuficiente: cada compra pasa por tu cuenta, tú ves sus gastos, y si cancelas la tarjeta él se queda sin poder pagar nada. Comparten la cuenta. La independencia real llega cuando el hijo abre su propia cuenta bancaria: su dinero es suyo, sus movimientos son suyos, y lo que haga con su cuenta no toca la tuya ni al revés.
La cuenta compartida tiene un problema que va más allá de la dependencia: el enredo. Mientras compartan la cuenta, tú no puedes cambiar nada de tu cuenta sin arriesgarte a afectarlo a él —cambiar de banco, ajustar el límite, cerrar una tarjeta— porque sus gastos están mezclados con los tuyos. Y él no puede organizar sus finanzas a su manera porque están atadas a las tuyas. La cuenta compartida los encadena a los dos: ninguno puede evolucionar sin consultar al otro.
Cuando el hijo abre su cuenta propia, los dos se liberan. Tú reorganizas tu cuenta sin afectarlo; él maneja la suya como quiera. Si necesitan coordinar —él te pide prestado, tú le transfieres— lo hacen por una transferencia explícita entre cuentas, no metiendo la mano en la cuenta del otro. Esa transferencia explícita es la API del servicio (con el ACL): la forma acordada de pedir datos, en vez de leer las tablas ajenas directamente.
En la extracción: la tarjeta compartida es la tabla del monolito que el "servicio" sigue leyendo; la cuenta propia es el owned_store del servicio; y la transferencia explícita entre cuentas es pedirle los datos al servicio por su API en vez de sacarlos de sus tablas. Un servicio que comparte las tablas del monolito es el hijo que sigue con tu tarjeta: parece independiente, pero cualquier cambio en la cuenta compartida los enreda a los dos.
Ejemplo trabajado: el detector de servicios que no duenan sus datos
No vamos a confiar en que un servicio esté bien extraído: lo vamos a medir. Instrumentamos la tabla del monolito (MonolithDB) para que registre cada acceso y quién lo hizo. Luego construimos dos "servicios" de catálogo: uno falso (FakeCatalogService) que, a pesar de devolver Product limpio, saca los datos de la tabla del monolito; y uno real (CatalogService) que tiene su propio owned_store. Corremos los dos y le preguntamos al detector cuántas veces cada uno tocó la tabla del monolito. Un servicio extraído de verdad la toca cero veces.
from dataclasses import dataclass
@dataclass
class Product:
id: int
name: str
price_cents: int
active: bool
# --- La tabla del monolito. Se instrumenta: registra quien la toca. ---
class MonolithDB:
def __init__(self):
self.products = [
{"prod_id": 1, "desc": "SSD 1TB", "prc_cents": "8999", "act": "Y"},
{"prod_id": 2, "desc": "USB-C Hub", "prc_cents": "3200", "act": "Y"},
]
self.accessed_by = [] # log de acceso: quien leyo las tablas
def read_products(self, who):
self.accessed_by.append(who) # queda registrado que 'who' toco la tabla
return self.products
monolith_db = MonolithDB()
# --- Servicio FALSO: "extraido" de nombre, pero sigue leyendo la tabla del viejo. ---
class FakeCatalogService:
def get(self, prod_id):
rows = monolith_db.read_products(who="FakeCatalogService") # <- acopla
row = next(r for r in rows if r["prod_id"] == prod_id)
return Product(row["prod_id"], row["desc"], int(row["prc_cents"]), row["act"] == "Y")
# --- Servicio REAL: duena su propio store. El monolito NO le presta tablas. ---
class CatalogService:
def __init__(self):
self.owned_store = { # datos propios, en el modelo limpio
1: Product(1, "SSD 1TB", 8999, True),
2: Product(2, "USB-C Hub", 3200, True),
}
def get(self, prod_id):
return self.owned_store[prod_id]
def is_truly_extracted(service_name, db):
touched = db.accessed_by.count(service_name)
return touched == 0, touched
print("Un servicio extraido de verdad NO lee las tablas del monolito\n")
fake = FakeCatalogService()
fake.get(1); fake.get(2)
ok_fake, touched_fake = is_truly_extracted("FakeCatalogService", monolith_db)
real = CatalogService()
real.get(1); real.get(2)
ok_real, touched_real = is_truly_extracted("CatalogService", monolith_db)
print(f"{'servicio':<22}{'lee de':<20}{'toco monolith_db':>18}{'extraido?':>12}")
print("-" * 72)
print(f"{'FakeCatalogService':<22}{'monolith_db.products':<20}{touched_fake:>18}{('SI' if ok_fake else 'NO'):>12}")
print(f"{'CatalogService':<22}{'owned_store (propio)':<20}{touched_real:>18}{('SI' if ok_real else 'NO'):>12}")
print("-" * 72)
print(f"\nLog de acceso a la tabla del monolito: {monolith_db.accessed_by}")
print("\n El 'servicio' falso comparte la tabla del monolito: no esta extraido,")
print(" solo movio el codigo. El real duena su store: cero accesos a la tabla vieja.")
print(" Duenar los datos = ser el unico que lee y escribe su propio store.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Un servicio extraido de verdad NO lee las tablas del monolito
servicio lee de toco monolith_db extraido?
------------------------------------------------------------------------
FakeCatalogService monolith_db.products 2 NO
CatalogService owned_store (propio) 0 SI
------------------------------------------------------------------------
Log de acceso a la tabla del monolito: ['FakeCatalogService', 'FakeCatalogService']
El 'servicio' falso comparte la tabla del monolito: no esta extraido,
solo movio el codigo. El real duena su store: cero accesos a la tabla vieja.
Duenar los datos = ser el unico que lee y escribe su propio store.
Lee las dos filas de la tabla, porque el contraste es toda la lección.
FakeCatalogService tocó la tabla del monolito 2 veces (una por cada get), y el detector lo marca NO extraído. Fíjate en lo engañoso que es este servicio: devuelve Product limpio, usa el ACL para traducir (int(row["prc_cents"]), row["act"] == "Y"), tiene todo el aspecto de un servicio nuevo. Pero su fuente de datos es monolith_db.products —la tabla del viejo—. Por fuera parece extraído; por dentro sigue amarrado a la base de datos del monolito. Es el hijo con la tarjeta compartida: departamento propio, pero cada gasto pasa por tu cuenta.
CatalogService tocó la tabla del monolito 0 veces, y el detector lo marca SI. Sus datos viven en owned_store —su propio diccionario de Product—. Cuando le pides un producto, lo saca de su store, no del monolito. Por eso el Log de acceso a la tabla del monolito de abajo solo lista ['FakeCatalogService', 'FakeCatalogService']: el servicio real nunca aparece, porque nunca la tocó. Ese log es la evidencia dura de la propiedad: quién accedió a las tablas del viejo, con nombre y apellido.
El punto de la lección está en que el ACL no basta. Los dos servicios usan el modelo limpio; los dos traducen. La diferencia no está en el modelo, está en los datos: quién es el dueño del store del que salen. El falso comparte; el real duena. Y esa diferencia es la que decide si extrajiste algo o solo moviste el código a otra clase. Un detector tan simple como "¿cuántas veces tocaste las tablas del monolito?" separa una extracción real de un disfraz.
Profundización: por qué la propiedad exclusiva, y qué se rompe sin ella
Duenar los datos no es un capricho de pureza; es lo que hace que un servicio pueda evolucionar por su cuenta. Cuando el servicio es el único dueño de su store, tiene una libertad que un módulo del monolito nunca tuvo: puede cambiar su esquema (renombrar columnas, dividir tablas, agregar índices), cambiar de motor de base de datos (de la BD relacional del monolito a una especializada en catálogo), o cambiar su estrategia de almacenamiento entera —y nadie se entera, porque nadie más lee sus tablas—. Lo único que el servicio le promete al mundo es su API (con el ACL); lo de adentro es asunto suyo.
Compara eso con el servicio que comparte tablas. Si el monolito también lee la tabla products, entonces el "servicio" no puede tocar esa tabla sin arriesgarse a romper al monolito. ¿Quieres renombrar prc_cents a algo sensato? No puedes: el monolito lee esa columna. ¿Quieres agregar un índice o cambiar el tipo? Tienes que coordinar con el monolito. La tabla compartida encadena la evolución de los dos: ninguno puede cambiar sus datos sin consultar al otro. Es el peor de los mundos —extrajiste el código pero no la libertad—.
COMPARTIR LA TABLA (falso): DUENAR EL STORE (real):
monolito ──> products <── servicio monolito ──API──> servicio ──> owned_store
(tabla (nadie mas
compartida: lee esta:
nadie puede el servicio
cambiarla evoluciona
sin romper solo)
al otro)
Hay una regla dura que resume la propiedad: cada dato tiene un solo dueño, y solo el dueño escribe. Los demás pueden leer una copia (a través de la API del dueño), pero no escriben en el store ajeno ni dependen de su forma interna. En Mercado, catalog duena los datos de producto; si orders necesita el precio de un producto, se lo pregunta al servicio de catálogo, no lo saca de las tablas de catálogo. Esta regla es la que evita el enredo de "dos sistemas escribiendo la misma tabla", que es una fuente inagotable de bugs de consistencia.
Y una honestidad sobre la fase transitoria. En la práctica, un servicio recién extraído casi siempre pasa un rato compartiendo la base de datos del monolito —porque cortar los datos el primer día es arriesgado—. Eso está bien como paso transitorio, con una fecha de corte. El error no es compartir la BD un tiempo; el error es compartirla para siempre, dando la fase transitoria por permanente. La lección 6 trata exactamente cómo se ejecuta ese corte —de la BD compartida a la propia— sin apagar el sistema. Aquí lo importante es la meta: el servicio terminará duenando sus datos; compartir es una escala, no el destino.
Errores comunes
El "servicio" que sigue leyendo las tablas del monolito. Qué pasa: el equipo crea el servicio con su modelo limpio y su ACL, pero por debajo sus consultas van a las tablas del monolito —SELECT * FROM products contra la BD del viejo—. Por qué pasa: es lo más rápido; los datos ya están ahí, ¿para qué copiarlos? Cómo detectarlo: el detector de esta lección —¿cuántas veces el servicio toca las tablas del monolito?— da más que cero; o, en un sistema real, los logs de la BD muestran al servicio consultando tablas que "no son suyas". Cómo corregirlo: un servicio extraído duena sus datos. Si todavía lee las tablas del monolito, la extracción está a medias —tiene el corte de modelo (ACL) pero no el de datos—. El plan tiene que incluir darle al servicio su propio store y cortar el acceso a las tablas del viejo (lección 6). Un servicio que comparte tablas no es un servicio, es un módulo del monolito con una fachada.
Compartir la base de datos para siempre. Qué pasa: el servicio se extrae "por ahora" sobre la BD compartida, y ese "por ahora" se vuelve permanente —dos años después, el servicio y el monolito siguen escribiendo las mismas tablas—. Por qué pasa: la fase transitoria funciona lo suficiente para no doler, y cortar la BD es trabajo sin recompensa visible. Cómo detectarlo: no hay fecha de corte; la BD compartida está en la lista de "cosas que algún día arreglaremos" desde hace mucho; cambiar cualquier tabla requiere coordinar dos equipos. Cómo corregirlo: la BD compartida es una escala con fecha de vencimiento, no el destino. Cuando extraes un servicio sobre la BD compartida, agenda el corte desde el día uno y trátalo como parte de la extracción, no como un extra opcional. Un servicio que comparte la BD para siempre no logró la independencia; logró un acoplamiento más caro de mantener, ahora repartido en dos procesos.
Dejar que otros escriban en el store del servicio. Qué pasa: el servicio tiene su owned_store, pero para "ir rápido" un script de migración, o el monolito, escriben directamente en él. Por qué pasa: escribir directo en la tabla es más rápido que llamar a la API del servicio. Cómo detectarlo: hay más de un escritor tocando el store del servicio —el servicio y "alguien más"—. Cómo corregirlo: la regla dura es solo el dueño escribe. Si alguien necesita cambiar los datos de catalog, lo hace a través de la API del servicio (que valida, traduce y mantiene las invariantes), no metiendo la mano en el store. Dos escritores sobre el mismo store reintroducen los bugs de consistencia que la propiedad exclusiva evita —y hacen que el "dueño" no controle de verdad sus datos—. Todos leen por la API; solo el dueño escribe.
Ejercicios
Ejercicio 1 — La tarjeta compartida. Con la analogía del hijo y la cuenta bancaria, explica: (a) qué representa que el hijo siga usando tu tarjeta de crédito; (b) por qué compartir la cuenta encadena a los dos, no solo al hijo; (c) qué representa la transferencia explícita entre cuentas propias.
Ver solución
(a) Que el hijo siga usando tu tarjeta representa el servicio que comparte las tablas del monolito: tiene su espacio propio (su departamento / su modelo limpio), pero sus datos siguen pasando por la cuenta del monolito. No es autosuficiente; depende de la cuenta ajena para funcionar.
(b) Porque la cuenta compartida enreda las finanzas de los dos: tú no puedes cambiar nada de tu cuenta (cambiar de banco, ajustar límites) sin arriesgarte a afectar los gastos del hijo mezclados ahí, y él no puede organizar sus finanzas atadas a las tuyas. Igual con la tabla compartida: el monolito no puede cambiar el esquema sin arriesgarse a romper al servicio, y el servicio no puede evolucionar sus datos sin coordinar con el monolito. La tabla compartida encadena la evolución de ambos, no solo la del servicio.
(c) La transferencia explícita entre cuentas propias representa pedir los datos por la API del servicio (con el ACL) en vez de leer sus tablas directamente. Cuando cada uno tiene su cuenta, si necesitan coordinar dinero lo hacen por una transferencia acordada, no metiendo la mano en la cuenta del otro. En la extracción: si orders necesita el precio de un producto, se lo pide al servicio de catálogo por su API, no lo saca de las tablas de catálogo. La transferencia explícita es la frontera respetada; meter la mano en la cuenta ajena es compartir tablas.
Ejercicio 2 — Lee el detector. En la salida, FakeCatalogService tocó la tabla del monolito 2 veces (NO extraído) y CatalogService la tocó 0 veces (SI). (a) ¿Por qué el FakeCatalogService es engañoso a pesar de devolver Product limpio? (b) ¿Qué evidencia dura del Log de acceso confirma que el servicio real está extraído? (c) ¿Por qué "usar el ACL" no basta para estar extraído?
Ver solución
(a) Porque FakeCatalogService tiene todo el aspecto de un servicio extraído —devuelve Product, usa el ACL para traducir tipos y dominio— pero su fuente de datos es monolith_db.products, la tabla del viejo. La limpieza está en el modelo de salida, no en la propiedad de los datos. Es un disfraz: modelo nuevo por fuera, datos del monolito por dentro. El aspecto engaña; el detector no.
(b) El Log de acceso a la tabla del monolito: ['FakeCatalogService', 'FakeCatalogService'] — el servicio real (CatalogService) no aparece en la lista. La tabla registró a todos los que la tocaron, y el servicio real nunca está ahí porque nunca la tocó: sacó sus datos de owned_store. Ese log, con nombre y apellido de cada accesor, es la prueba dura de quién duena los datos y quién los comparte.
(c) Porque el ACL corta el modelo (traduce viejo↔limpio), pero no corta los datos (de dónde salen). Un servicio puede usar el ACL perfectamente y aun así leer las tablas del monolito —como el falso—. La extracción tiene dos cortes: el de modelo (ACL, lecciones 3-4) y el de datos (propiedad, esta lección; corte de BD, lección 6). "Usar el ACL" resuelve el primero; duenar el store resuelve el segundo. Faltar el segundo deja la extracción a medias, con el servicio todavía amarrado a la BD del viejo.
Ejercicio 3 — Evolucionar el esquema. El servicio de catalog quiere renombrar internamente prc_cents a price_cents en su almacenamiento y agregar un índice por categoría. (a) ¿Puede hacerlo si duena sus datos (owned_store)? (b) ¿Puede hacerlo si comparte la tabla products con el monolito? (c) ¿Qué principio general ilustra esto?
Ver solución
(a) Sí, sin problema. Si el servicio duena su store, nadie más lee ni depende de su forma interna: puede renombrar columnas, agregar índices, cambiar de motor de BD, lo que quiera. Lo único que le promete al mundo es su API (con el ACL); mientras la API no cambie, lo de adentro es asunto suyo. El monolito ni se entera, porque le pide los datos por la API, no por la tabla.
(b) No, o no sin dolor. Si el monolito también lee la tabla products, renombrar prc_cents rompería todas las consultas del monolito que leen esa columna, y agregar un índice o cambiar un tipo tendría que coordinarse con el equipo del monolito para no romper nada. La tabla compartida convierte un cambio interno del servicio en un cambio coordinado entre dos sistemas. La evolución queda encadenada.
(c) Ilustra el principio de que la propiedad exclusiva de los datos es lo que compra la libertad de evolucionar. Un servicio que duena su store puede cambiar por dentro sin romper a nadie; uno que comparte tablas no puede tocar sus datos sin negociar. La independencia de un servicio no se mide por tener su propio proceso o su propio modelo, sino por controlar sus propios datos: mientras alguien más lea o escriba su store, no es dueño, es co-inquilino —y un co-inquilino no puede remodelar la casa a su gusto—.
Resumen y siguiente paso
En esta lección atacaste el segundo corte de la extracción, el de los datos: la propiedad de los datos. Viste, con el hijo que abre su propia cuenta bancaria, que la independencia real no llega con el departamento propio (el modelo limpio) sino con la cuenta propia (el store propio), y que la cuenta compartida encadena la evolución de los dos. Y lo mediste: un detector instrumentó la tabla del monolito y atrapó al FakeCatalogService (2 accesos, NO extraído) frente al CatalogService real (0 accesos, SI), demostrando que el ACL no basta —la diferencia está en quién duena los datos—. Aprendiste la regla dura (cada dato tiene un solo dueño, y solo el dueño escribe), por qué la propiedad exclusiva compra la libertad de evolucionar, y por qué la BD compartida es una escala con fecha de vencimiento, no el destino.
Antes de avanzar deberías poder: definir qué significa duenar los datos; explicar por qué la tabla compartida encadena la evolución de ambos lados; usar un detector para distinguir un servicio extraído de uno disfrazado; y enunciar la regla de "un solo dueño, solo el dueño escribe".
La lección 6 ejecuta el corte que esta lección pidió: llevar el servicio de la BD compartida a la BD propia sin apagar el sistema. Vas a ver las fases del corte —primero el servicio lee de la tabla compartida a través del ACL (transitorio), luego el monolito deja de tocar la tabla y le pide al servicio, y al final el servicio duena su owned_db y la shared_db se retira— y vas a verificar que, en las tres fases, el ACL mantiene el contrato del monolito idéntico. La propiedad de los datos es la meta; cortar la BD compartida es cómo se llega a ella.
Recursos
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, "Decomposing the Database" — el capítulo central sobre la propiedad de los datos: por qué cada servicio debe duenar sus datos, los problemas de la base de datos compartida, y los patrones para separarla. La referencia de cabecera de esta lección y la siguiente. En inglés.
- Chris Richardson, "Pattern: Database per service" — microservices.io/patterns/data/database-per-service.html. La ficha del patrón: cada servicio duena su propia base de datos, y nadie accede a las tablas de otro directamente. El principio que esta lección hace medible. En inglés.
- Chris Richardson, "Pattern: Shared database" — microservices.io/patterns/data/shared-database.html. El anti-patrón contrario, con sus fuerzas y por qué se usa a veces como paso transitorio; útil para entender qué se está evitando. En inglés.
- Martin Fowler, "BoundedContext" — martinfowler.com/bliki/BoundedContext.html. El fundamento: un bounded context incluye su propio modelo y sus datos; la frontera del contexto es también la frontera de la propiedad de los datos. En inglés.