Módulo 5: Extraer un servicio
Encontrar el bounded context
Descripción
En la lección anterior viste el ciclo completo de la extracción y lo corriste en miniatura. Ahora empezamos por el principio de todo, el paso que decide si la extracción será limpia o un infierno: encontrar el bounded context. Antes de escribir una línea del anti-corruption layer, antes de pensar en datos propios, tienes que decidir qué vas a extraer —y no cualquier pedazo del monolito sirve—.
Un bounded context es una pieza del sistema con un modelo propio y coherente por dentro y una frontera clara por fuera. Dentro del contexto, las palabras significan una sola cosa: en catalog, un "producto" es un producto con su nombre, su precio y su disponibilidad. La frontera es el punto donde ese contexto se conecta con el resto —el seam—: por dónde le hablan los demás y a quiénes les habla él. La calidad de un candidato a extraer se lee en su seam: un contexto con un seam estrecho y cohesivo (pocas conexiones, todas del mismo tipo) se saca limpio; uno con un seam ancho y enredado (mil conexiones en todas direcciones) arrastra medio monolito consigo.
Esta lección lo hace medible. En vez de discutir "qué debería ser un servicio" con las manos, vamos a contar el seam de los cuatro módulos de Mercado —catalog, orders, payments, shipping—: cuántas llamadas entran a cada uno (otros lo leen), cuántas salen (él depende de otros), y cuántas de las que entran son escrituras. Con esos números vas a ver, sin opinar, que catalog es una hoja: todos lo leen, él no depende de nadie, nadie le escribe desde afuera. Es el candidato limpio a extraer primero.
Conexión con el módulo. La lección 1 te dio el ciclo completo; esta hace su primer paso: elegir la pieza. Las lecciones 3 y 4 construyen el ACL para el contexto que aquí elijas; la 5 le da la propiedad de sus datos; la 6 corta su BD; y la 7 usa esta misma medición de seam, extendida, para decidir el orden de extraer todos los contextos, no solo el primero. Fíjate en la frontera: aquí encontramos la pieza y su seam. Cómo debe modelarse el dominio de esa pieza por dentro —las reglas del diseño orientado al dominio a fondo— es materia de las guías de diseño; aquí usamos el bounded context como herramienta de extracción: la unidad que se saca del monolito de una pieza.
Una analogía: el departamento de contabilidad
Piensa en una empresa con varios departamentos: ventas, contabilidad, recursos humanos, logística. Quieres convertir uno de ellos en una empresa independiente —tercerizarlo—. ¿Cuál eliges primero?
Mira cómo se conecta cada uno con el resto. Contabilidad tiene un lenguaje propio (débitos, créditos, balances), sus propios archivos (los libros contables), y una forma estrecha de interactuar con los demás: le mandas una factura por una ventanilla y te devuelve un pago o un reporte. Nadie de ventas entra al archivo de contabilidad a hurgar en los libros; le pasan un documento por la ventanilla y esperan la respuesta. Esa ventanilla es el seam: una interfaz angosta, con pocas operaciones, todas del mismo tipo ("registra esto", "dame ese reporte").
Ahora mira logística, que coordina con ventas (qué se vendió), con contabilidad (cuánto costó enviarlo), con recursos humanos (quién maneja el camión) y con proveedores. Su seam es ancho: toca a todos, en todas direcciones, todo el tiempo. Si intentaras tercerizar logística primero, tendrías que desenredar sus mil conexiones con el resto de la empresa antes de poder sacarlo —un nudo—.
El departamento fácil de independizar es contabilidad: lenguaje propio, archivos propios, ventanilla estrecha. Le mandas trabajo por un canal claro y te responde; no anda metido en el trabajo de los demás. En Mercado, ese departamento es catalog: tiene su lenguaje (productos, precios, disponibilidad), sus datos (el catálogo), y una ventanilla estrecha —los demás le preguntan "¿cuál es el precio de este producto?" y ya—. Encontrar el bounded context es encontrar al departamento con la ventanilla más estrecha y limpia.
Ejemplo trabajado: medir el seam de los módulos de Mercado
No vamos a opinar cuál módulo es el mejor candidato: lo vamos a medir. Representamos todas las llamadas que cruzan fronteras de módulo dentro del monolito —quién llama a quién, con qué operación, y si esa operación lee o escribe—. Con eso calculamos, para cada módulo, su seam de entrada (llamadas de otros hacia él), su seam de salida (llamadas de él hacia otros), y cuántas de las que entran son escrituras. Un candidato limpio a extraer tiene seam de entrada cohesivo, cero escrituras desde afuera, y seam de salida mínimo.
# --- Todas las llamadas que cruzan fronteras de modulo dentro del monolito. ---
# (caller, callee, operation, kind) kind: "read" | "write"
crossings = [
("orders", "catalog", "get_product", "read"),
("orders", "catalog", "get_price", "read"),
("shipping", "catalog", "get_weight", "read"),
("orders", "payments", "charge", "write"),
("payments", "orders", "mark_paid", "write"),
("orders", "shipping", "quote", "read"),
("shipping", "orders", "get_address", "read"),
]
MODULES = ["catalog", "orders", "payments", "shipping"]
def inbound(module): # llamadas de OTROS hacia este modulo
return [c for c in crossings if c[1] == module]
def outbound(module): # llamadas de este modulo hacia OTROS
return [c for c in crossings if c[0] == module]
print("Seam de cada modulo: cuantas llamadas cruzan su frontera y de que tipo\n")
print(f"{'modulo':<10}{'entradas':>9}{'salidas':>9}{'writes-in':>11} candidato a extraer?")
print("-" * 66)
for m in MODULES:
ins = inbound(m)
outs = outbound(m)
writes_in = sum(1 for c in ins if c[3] == "write")
# Buen candidato: cohesivo (todas las entradas leen el MISMO concepto) y
# con seam de salida chico (no depende de muchos otros para funcionar).
clean = (writes_in == 0 and len(outs) == 0)
verdict = "SI (leaf, seam limpio)" if clean else "no (aun acoplado)"
print(f"{m:<10}{len(ins):>9}{len(outs):>9}{writes_in:>11} {verdict}")
print("-" * 66)
print("\nSeam de entrada de 'catalog' (lo que el resto le pide):")
for c in inbound("catalog"):
print(f" {c[0]:<9} -> catalog.{c[2]}() [{c[3]}]")
print("Seam de salida de 'catalog' (lo que catalog le pide a otros):")
outs = outbound("catalog")
print(" (ninguna)" if not outs else "\n".join(f" {c}" for c in outs))
print("\n catalog es un bounded context extraible: 3 lecturas cohesivas entran")
print(" (todas piden datos de producto), 0 escrituras entran, 0 llamadas salen.")
print(" Es una hoja: el resto lo LEE, el no depende de nadie. Se extrae primero.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
Seam de cada modulo: cuantas llamadas cruzan su frontera y de que tipo
modulo entradas salidas writes-in candidato a extraer?
------------------------------------------------------------------
catalog 3 0 0 SI (leaf, seam limpio)
orders 2 4 1 no (aun acoplado)
payments 1 1 1 no (aun acoplado)
shipping 1 2 0 no (aun acoplado)
------------------------------------------------------------------
Seam de entrada de 'catalog' (lo que el resto le pide):
orders -> catalog.get_product() [read]
orders -> catalog.get_price() [read]
shipping -> catalog.get_weight() [read]
Seam de salida de 'catalog' (lo que catalog le pide a otros):
(ninguna)
catalog es un bounded context extraible: 3 lecturas cohesivas entran
(todas piden datos de producto), 0 escrituras entran, 0 llamadas salen.
Es una hoja: el resto lo LEE, el no depende de nadie. Se extrae primero.
Lee la tabla columna por columna, porque cada una dice algo sobre qué tan fácil es extraer cada módulo.
La columna salidas es la más importante para extraer, y por eso catalog gana: tiene 0 salidas. Un módulo con cero salidas no llama a nadie más para funcionar; es autosuficiente. Cuando lo saques a su propio servicio, no tendrá que reachear de vuelta al monolito a cada rato para pedir datos —lo cual sería una pesadilla de latencia y acoplamiento—. orders, en cambio, tiene 4 salidas: llama a catalog, a payments, a shipping. Es el hub del que todo cuelga; extraerlo primero significaría arrastrar a los otros tres consigo.
La columna entradas cuenta cuántos módulos leen de este. catalog tiene 3 entradas (lo leen orders y shipping), y eso es bueno: un módulo muy leído pero que no lee a nadie es una hoja popular, exactamente el candidato ideal a extraer —muchos se benefician de tenerlo como servicio limpio, y él no depende de nadie—.
La columna writes-in cuenta las escrituras que entran desde afuera. catalog tiene 0: nadie le escribe desde otro módulo, solo lo leen. Eso importa muchísimo para la propiedad de los datos (lección 5): si nadie más escribe en catalog, puede duenar sus datos sin pelearse con nadie por quién manda. orders y payments tienen escrituras cruzadas (payments marca pedidos como pagados, orders cobra) —un enredo de quién escribe qué que hace su extracción mucho más delicada—.
Abajo, el detalle del seam de catalog confirma la cohesión: las tres entradas (get_product, get_price, get_weight) son todas lecturas del mismo concepto —datos de producto—. No es un módulo al que unos le piden productos y otros le piden, digamos, reportes de ventas; es un módulo con una sola responsabilidad, leído por una ventanilla estrecha. Y su seam de salida es "(ninguna)": no le pide nada a nadie. Esa combinación —muchas lecturas cohesivas que entran, cero que salen, cero escrituras desde afuera— es la firma de un bounded context listo para extraer.
Profundización: qué hace a un bounded context un buen candidato
Los números del ejemplo apuntan a cuatro propiedades que, juntas, hacen a una pieza fácil de extraer. Vale la pena nombrarlas, porque son la lista de verificación que aplicarás a cualquier monolito:
Propiedad Que mide catalog cumple?
────────────────────────── ──────────────────────────────── ──────────────
1. Seam de salida chico cuantos otros necesita para vivir SI (0 salidas)
2. Seam de entrada cohesivo si lo que le piden es UN concepto SI (solo producto)
3. Sin escrituras cruzadas si otros escriben en sus datos SI (0 writes-in)
4. Alta cohesion interna si sus conceptos van juntos SI (product/price/stock)
El seam de salida chico es la propiedad número uno para extraer (distinta de la número uno para diseñar). Un módulo que no depende de otros se puede sacar y correr solo; uno que depende de tres tendría que, extraído, llamar de vuelta al monolito constantemente —cada llamada ahora cruzando la red, con su latencia y su posibilidad de fallar—. Por eso se extrae la hoja primero: es la que menos ataduras tiene hacia adentro del monolito.
El seam de entrada cohesivo distingue un bounded context real de un "cajón de sastre". Si a un módulo unos le piden productos, otros le piden cálculos de impuestos y otros le piden logs, no es un contexto: es un basurero de funciones sin relación. catalog pasa la prueba: todo lo que le piden es sobre productos.
Sin escrituras cruzadas es la condición que hace posible la propiedad de los datos. Si orders escribiera directamente en las tablas de catalog, extraer catalog con datos propios obligaría a resolver antes ese enredo. Que a catalog solo lo lean —nadie le escribe desde afuera— significa que puede duenar sus datos sin negociar con nadie.
Y una advertencia sobre cómo se obtiene el seam en un sistema real. En el ejemplo, las crossings están escritas a mano porque el monolito es de juguete. En un monolito de verdad, este mapa se levanta con herramientas: análisis estático de las llamadas entre módulos, grep de los imports, trazas de ejecución que registran qué función llama a cuál. El principio es el mismo —contar las conexiones que cruzan cada frontera candidata— pero la fuente de los datos es el código real, no una lista escrita a mano. Lo importante es que la decisión de qué extraer se toma con evidencia del acoplamiento, no con intuición.
Errores comunes
Elegir el contexto por "lo que más nos duele" en vez de por su seam. Qué pasa: el equipo decide extraer primero payments o orders porque son los módulos que más problemas dan —los que más cambian, los que más fallan—. Por qué pasa: es tentador atacar el dolor primero. Cómo detectarlo: el módulo elegido tiene un seam ancho (muchas salidas, escrituras cruzadas) y la extracción se atasca en desenredar dependencias antes de avanzar. Cómo corregirlo: la primera extracción se elige por facilidad de corte, no por urgencia. Extraer la hoja (catalog) primero le enseña al equipo el proceso completo —ACL, datos propios, corte de BD— sobre la pieza más simple, donde un error se perdona. Con esa experiencia, atacar después el módulo doloroso y enredado es mucho más seguro. El dolor se atiende, pero no con la primera extracción.
Confundir "un archivo grande" con "un bounded context". Qué pasa: el equipo ve un archivo de 3000 líneas llamado catalog.py y asume que ahí está el bounded context, listo para extraer. Por qué pasa: la cercanía en el código (un archivo, un paquete) se confunde con la cohesión del dominio. Cómo detectarlo: al medir el seam, ese "módulo" resulta tener salidas por todos lados —llama a orders, escribe en tablas de payments— porque en realidad varias responsabilidades viven revueltas en el mismo archivo. Cómo corregirlo: el bounded context se define por su modelo y su frontera, no por dónde vive el código. Mide el seam de verdad; puede que el contexto extraíble sea parte del archivo grande, o que abarque varios archivos. La organización del código es una pista, no la respuesta.
Saltarse la medición y decidir por intuición. Qué pasa: alguien "sabe" cuál es el mejor candidato y el equipo extrae sin medir el seam. Por qué pasa: medir se siente como burocracia cuando "ya sabemos" la respuesta. Cómo detectarlo: a mitad de la extracción aparecen dependencias que nadie esperaba —resulta que ese módulo sí escribía en tablas de otro, o sí llamaba a un tercero—. Cómo corregirlo: la medición del seam es barata y evita sorpresas caras. Contar las llamadas que cruzan cada frontera candidata toma una tarde y te da la evidencia para elegir con confianza —y a veces contradice la intuición, revelando que el candidato "obvio" tenía un seam más ancho de lo que se creía—. Se mide antes de extraer, no se descubre a mitad del corte.
Ejercicios
Ejercicio 1 — El departamento tercerizable. Con la analogía de los departamentos de una empresa, di cuál tercerizarías primero y por qué, usando el lenguaje del seam: (a) contabilidad, con su ventanilla estrecha; (b) logística, que coordina con todos. Luego traduce cada uno a los números del ejemplo (entradas, salidas, escrituras).
Ver solución
Tercerizarías contabilidad primero. En el lenguaje del seam: tiene un seam de salida chico (no anda metido en el trabajo de los demás; le mandas facturas y responde), un seam de entrada cohesivo (todo lo que le piden es del mismo tipo: registrar movimientos, dar reportes) y sin escrituras cruzadas de otros en sus libros. Es la hoja: la sacas por su ventanilla estrecha sin desenredar nada.
Logística es lo contrario: coordina con ventas, contabilidad, recursos humanos y proveedores —un seam de salida ancho, llamando a todos en todas direcciones—. Tercerizarla primero obligaría a desenredar sus mil conexiones antes de poder sacarla.
Traducido a los números del ejemplo: contabilidad se parece a catalog (muchas entradas cohesivas, 0 salidas, 0 escrituras cruzadas: SI, leaf, seam limpio). Logística se parece a orders (muchas salidas, dependencias en todas direcciones: no, aun acoplado). La analogía y los números dicen lo mismo: se extrae primero el de la ventanilla estrecha, no el que coordina con todos.
Ejercicio 2 — Lee la tabla del seam. En la salida, catalog tuvo entradas=3, salidas=0, writes-in=0 y orders tuvo entradas=2, salidas=4, writes-in=1. (a) ¿Por qué la columna salidas es la más importante para decidir qué extraer primero? (b) ¿Por qué que catalog tenga muchas entradas es bueno y no malo? (c) ¿Qué problema anticipa el writes-in=1 de orders para su futura propiedad de datos?
Ver solución
(a) Porque las salidas cuentan de cuántos otros módulos depende este para funcionar. Un módulo con 0 salidas (catalog) es autosuficiente: extraído a su propio servicio, corre solo, sin llamar de vuelta al monolito. Un módulo con 4 salidas (orders) extraído tendría que llamar constantemente a catalog, payments y shipping —ahora cada llamada cruzando la red—. La cantidad de salidas es, directamente, la cantidad de ataduras que hay que arrastrar al extraer.
(b) Porque muchas entradas significa que muchos módulos leen de catalog —es una pieza muy usada—, pero eso no lo ata a nada: leer desde otros no crea dependencias hacia otros. Un módulo muy leído pero que no lee a nadie es una hoja popular: extraerlo a un servicio limpio beneficia a todos los que lo consumen, y él no arrastra dependencias. Las entradas miden qué tan útil es extraerlo; las salidas miden qué tan difícil es. catalog tiene entradas altas y salidas cero: muy útil y muy fácil.
(c) El writes-in=1 de orders significa que otro módulo (payments, con mark_paid) escribe en el estado de orders desde afuera. Eso anticipa un problema de propiedad de datos: si payments escribe directamente en las tablas de orders, entonces cuando quieras que orders duene sus datos vas a tener que resolver antes ese acceso cruzado —¿quién manda sobre ese dato, orders o payments?—. catalog, con writes-in=0, no tiene ese enredo: nadie escribe en él desde afuera, así que puede duenar sus datos sin negociar con nadie. Las escrituras cruzadas son las que más complican la propiedad de los datos, que es el tema de la lección 5.
Ejercicio 3 — Un candidato con seam ancho. Supón que al medir el seam de un módulo promotions obtienes entradas=2, salidas=3, writes-in=1: lee precios de catalog, escribe descuentos en orders, y consulta a payments. (a) ¿Es un buen candidato para la primera extracción? (b) ¿Qué complicaciones concretas tendría extraerlo? (c) ¿Qué harías antes de intentar extraerlo?
Ver solución
(a) No, no para la primera extracción. Con salidas=3 depende de tres módulos para funcionar (lee de catalog, consulta a payments, y necesita a orders), y con writes-in=1 alguien escribe en su estado desde afuera. Su seam es ancho y enredado —lo contrario de la hoja limpia que conviene extraer primero—.
(b) Complicaciones concretas: (1) extraído, tendría que llamar de vuelta al monolito a catalog, payments y orders en cada operación, cada llamada ahora cruzando la red con su latencia y su posibilidad de fallar; (2) el writes-in significa que otro módulo escribe en sus datos, así que darle la propiedad de sus datos exige resolver antes ese acceso cruzado; (3) al escribir descuentos en orders, promotions y orders comparten un enredo de escritura que hay que desenmarañar. Es un nudo, no una hoja.
(c) Antes de intentar extraerlo, primero extraería los contextos de los que promotions depende —empezando por catalog, la hoja—. Con catalog ya extraído y estable, una de las salidas de promotions deja de apuntar al monolito y apunta a un servicio limpio, lo que reduce su enredo. La estrategia es extraer de las hojas hacia el hub: cada extracción de un contexto independiente simplifica el seam de los que dependían de él, hasta que módulos antes enredados como promotions u orders se vuelven extraíbles. Esto es exactamente lo que la lección 7 sistematiza como el orden de extracción.
Resumen y siguiente paso
En esta lección hiciste el primer paso de la extracción: encontrar el bounded context. Viste, con el departamento de contabilidad y su ventanilla estrecha, que el mejor candidato a independizar es la pieza con un modelo propio y una frontera limpia hacia el resto. Y lo mediste: contaste el seam de los cuatro módulos de Mercado —entradas, salidas, escrituras cruzadas— y viste, con números y no con intuición, que catalog es la hoja (3 lecturas cohesivas entran, 0 escrituras, 0 salidas): el candidato limpio a extraer primero. Nombraste las cuatro propiedades de un buen candidato —seam de salida chico, seam de entrada cohesivo, sin escrituras cruzadas, alta cohesión interna— y viste cómo se levanta el mapa del seam en un sistema real.
Antes de avanzar deberías poder: definir qué es un bounded context y qué es su seam; explicar por qué el seam de salida es la propiedad número uno para extraer; leer una tabla de seam y decir qué módulo se extrae primero y por qué; y reconocer un candidato de seam ancho que conviene dejar para después.
La lección 3 hace el segundo paso, ahora que ya elegiste catalog: construir el anti-corruption layer. Vas a escribir el traductor que absorbe la jerga del modelo viejo del monolito —prod_id, desc, prc_cents como string, act como 'Y'/'N'— y entrega el modelo limpio del servicio —Product con id, name, price_cents como int, active como bool—. Vas a ver, campo por campo, cómo el ACL normaliza nombres, tipos y dominio, y por qué ese modelo limpio hace que la lógica del servicio se lea sola. Elegiste la pieza; ahora le pones el traductor en el borde.
Recursos
- Eric Evans, Domain-Driven Design (Addison-Wesley, 2003), capítulo sobre Bounded Context — la definición original: el límite dentro del cual un modelo del dominio es único y consistente. Evans explica por qué un modelo grande se vuelve inmanejable y cómo dividirlo en contextos con fronteras explícitas lo hace tratable. La lectura fundacional de esta lección. En inglés.
- Martin Fowler, "BoundedContext" — martinfowler.com/bliki/BoundedContext.html. La ficha corta y clara del concepto, con el ejemplo de cómo un mismo término ("cliente") significa cosas distintas en contextos distintos (ventas vs soporte), y por qué eso justifica fronteras. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 2, "Planning a Migration" — la sección sobre cómo elegir qué extraer primero, priorizando por facilidad de descomposición y beneficio, exactamente el criterio que esta lección hace medible con el seam. En inglés.
- Chris Richardson, "Pattern: Decompose by subdomain" — microservices.io/patterns/decomposition/decompose-by-subdomain.html. La ficha del patrón de descomponer un monolito por subdominio, con la idea de alinear los servicios con los bounded contexts del dominio. En inglés.