Módulo 5: Extraer un servicio
El anti-corruption layer
Descripción
Ya elegiste la pieza: catalog, la hoja limpia del monolito. Ahora viene el paso que le da nombre al módulo entero y que decide si el servicio nuevo nace sano o nace enfermo: construir el anti-corruption layer. Es la capa que se pone en el borde del servicio y traduce entre el modelo viejo del monolito y el modelo nuevo y limpio del servicio, para que el idioma sucio del legacy no se cuele en el diseño nuevo.
El problema que resuelve es concreto y traicionero. El monolito guarda cada producto como un diccionario con nombres crípticos y tipos sucios —prod_id, desc (que en realidad es el nombre), prc_cents guardado como texto, act como 'Y'/'N'—. Ese modelo cargó años de decisiones apresuradas y compromisos olvidados. Si el servicio nuevo hablara ese mismo modelo, heredaría toda esa deuda: su código estaría lleno de int(row["prc_cents"]) y comparaciones con 'Y', y en unos meses sería tan enredado como el monolito que querías dejar atrás. El ACL evita eso: absorbe la suciedad del viejo en el borde, y del ACL hacia adentro el servicio solo conoce un modelo limpio.
Esta lección construye ese traductor y lo ejecuta campo por campo. Vas a ver cómo el ACL toma {'prod_id': 1, 'desc': 'SSD 1TB', 'prc_cents': '8999', 'act': 'Y'} y lo convierte en Product(id=1, name='SSD 1TB', price_cents=8999, active=True): renombrando campos (desc → name), normalizando tipos ('8999' string → 8999 int) y normalizando dominio ('Y' → True). Y vas a ver, con una función de negocio mínima, por qué el modelo limpio importa: price_cents == 0 se lee solo, mientras que int(row["prc_cents"]) == 0 es la jerga del viejo colada en el nuevo.
Conexión con el módulo. La lección 2 eligió el bounded context; esta construye el ACL para ese contexto, en la dirección de entrada (legacy → modern). La lección 4 completa el ACL con la dirección de vuelta (modern → legacy), para que el monolito reciba lo que espera. Las lecciones 5 y 6 usan este mismo ACL en el borde mientras cortan los datos. Fíjate en la frontera: el ACL traduce entre dos modelos. Cómo se diseña el modelo limpio del servicio a fondo —las reglas del dominio, los agregados, los value objects— es materia de las guías de diseño; aquí el ACL es la herramienta de extracción que evita que el modelo viejo contamine al nuevo.
Una analogía: el intérprete que convierte unidades y modismos
Imagina una negociación entre dos socios: uno habla inglés y mide en millas, libras y dólares; el otro habla español y mide en kilómetros, kilos y pesos. Entre ellos hay un intérprete profesional. Su trabajo no es solo cambiar palabras de un idioma a otro; es más fino que eso.
Cuando el socio inglés dice "the package weighs 5 pounds and costs 20 dollars", el intérprete no dice "el paquete pesa 5 libras y cuesta 20 dólares" —eso obligaría al socio español a hacer las conversiones en su cabeza—. Dice "el paquete pesa 2.3 kilos y cuesta 380 pesos": traduce el idioma, convierte las unidades, y adapta los modismos para que el socio español reciba todo en su mundo, listo para trabajar, sin tener que descifrar nada del mundo del otro. Y hace lo mismo de vuelta.
El intérprete tiene una regla de oro: la suciedad de un lado no cruza al otro. Si el socio inglés usa una jerga interna rara —"we'll handle it FOB"—, el intérprete no repite "FOB" y deja al español confundido; lo traduce a algo que el español entiende en sus propios términos. Toda la rareza de cada lado se queda en el borde, en el intérprete, y cada socio conversa como si el otro hablara su idioma perfectamente.
El anti-corruption layer es ese intérprete. El monolito habla el idioma viejo (desc, prc_cents como texto, act como 'Y'). El servicio habla el idioma limpio (name, price_cents como número, active como booleano). El ACL, en el borde, traduce el idioma, convierte los tipos (el texto '8999' al número 8999, como las libras a kilos) y adapta el dominio ('Y' al booleano True, como el modismo a algo entendible). Y su regla de oro es la misma: la jerga del viejo se queda en el ACL, y del ACL hacia adentro el servicio conversa en su propio idioma limpio.
Ejemplo trabajado: la traducción campo por campo
No vamos a describir la traducción: la vamos a ejecutar, campo por campo, para ver exactamente qué le hace el ACL a cada pieza del modelo viejo. Tomamos una fila legacy del monolito, la pasamos por el ACL (translate), y desglosamos cada campo: qué valor tenía en el viejo, en qué campo del nuevo cae, con qué valor, y qué transformación aplicó el ACL.
from dataclasses import dataclass
@dataclass
class Product:
id: int
name: str
price_cents: int
active: bool
# --- El ACL: una sola funcion translate() que absorbe la jerga del legacy. ---
def translate(row):
return Product(
id=row["prod_id"], # copiar tal cual
name=row["desc"], # renombrar: 'desc' -> 'name'
price_cents=int(row["prc_cents"]), # normalizar tipo: str -> int
active=(row["act"] == "Y"), # normalizar dominio: 'Y'/'N' -> bool
)
# --- El monolito guarda el catalogo en su modelo viejo. ---
legacy_row = {"prod_id": 1, "desc": "SSD 1TB", "prc_cents": "8999", "act": "Y"}
print("El ACL traduce el modelo viejo -> el modelo limpio, campo por campo\n")
prod = translate(legacy_row)
mapping = [
("prod_id", legacy_row["prod_id"], "id", prod.id, "copiar"),
("desc", legacy_row["desc"], "name", prod.name, "renombrar campo"),
("prc_cents", legacy_row["prc_cents"], "price_cents", prod.price_cents, "str -> int"),
("act", legacy_row["act"], "active", prod.active, "'Y'/'N' -> bool"),
]
print(f"{'campo legacy':<12}{'valor':>10} -> {'campo modern':<13}{'valor':>8} transformacion")
print("-" * 74)
for lf, lv, mf, mv, tr in mapping:
print(f"{lf:<12}{repr(lv):>10} -> {mf:<13}{repr(mv):>8} {tr}")
print("-" * 74)
print(f"\nProduct limpio: {prod}")
# --- Por que el modelo limpio importa: la logica de negocio se lee sola. ---
# Sobre el modelo VIEJO habria que escribir: int(row['prc_cents']) == 0
# Sobre el modelo LIMPIO se escribe lo obvio:
def is_free(product):
return product.price_cents == 0
print("\nLa logica del servicio habla el modelo limpio, no la jerga del legacy:")
print(f" is_free(product) -> {is_free(prod)} (price_cents == 0, sin parsear strings)")
print("\n El ACL absorbe la suciedad del viejo en el borde. Del ACL hacia adentro,")
print(" el servicio solo conoce Product: nombres claros y tipos correctos.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
El ACL traduce el modelo viejo -> el modelo limpio, campo por campo
campo legacy valor -> campo modern valor transformacion
--------------------------------------------------------------------------
prod_id 1 -> id 1 copiar
desc 'SSD 1TB' -> name 'SSD 1TB' renombrar campo
prc_cents '8999' -> price_cents 8999 str -> int
act 'Y' -> active True 'Y'/'N' -> bool
--------------------------------------------------------------------------
Product limpio: Product(id=1, name='SSD 1TB', price_cents=8999, active=True)
La logica del servicio habla el modelo limpio, no la jerga del legacy:
is_free(product) -> False (price_cents == 0, sin parsear strings)
El ACL absorbe la suciedad del viejo en el borde. Del ACL hacia adentro,
el servicio solo conoce Product: nombres claros y tipos correctos.
Lee la tabla renglón por renglón, porque cada uno es un tipo distinto de traducción que un ACL hace.
prod_id → id (copiar). El caso más simple: el valor no cambia (1 sigue siendo 1), solo el nombre del campo. Aún así el ACL lo pasa por sus manos, porque el nombre importa: prod_id es la convención críptica del monolito (el prefijo prod_ porque las tablas legacy repetían el nombre de la entidad en cada columna), y id es la convención limpia del servicio. Renombrar es la traducción más barata, pero cuenta.
desc → name (renombrar campo). Aquí el ACL corrige una mentira del modelo viejo: el campo se llama desc (de "descripción"), pero lo que guarda es el nombre del producto. Es un nombre heredado que nadie se atrevió a cambiar en el monolito porque mil lugares dependen de él. El servicio nuevo no arrastra esa mentira: el ACL la traduce a name, que dice la verdad. Del ACL hacia adentro, nadie tiene que recordar que "desc en realidad es el nombre".
prc_cents → price_cents (str → int). La traducción más importante, porque cambia el tipo. El monolito guarda el precio como texto ('8999') —una decisión vieja, quizás porque venía de un CSV, quizás por un ORM mal configurado—. Trabajar con precios-como-texto es una fuente infinita de bugs: no puedes sumarlos, compararlos ni operarlos sin convertirlos primero. El ACL convierte una sola vez, en el borde (int(row["prc_cents"])), y del ACL hacia adentro el precio es un número entero de verdad. Como el intérprete que convierte libras a kilos: el socio español nunca ve una libra.
act → active ('Y'/'N' → bool). El ACL normaliza el dominio: el flag legacy usa las cadenas 'Y' y 'N' (herencia de bases de datos viejas que no tenían tipo booleano), y el servicio quiere un bool de verdad —True/False—. Comparar con 'Y' es frágil (¿y si viene 'y' minúscula, o '1', o 'YES'?); un bool es inequívoco. El ACL absorbe esa fragilidad en el borde.
Y abajo está el pago de todo esto: is_free(product) -> False, calculado como price_cents == 0. Ese es el punto de la lección. Sobre el modelo viejo, la misma pregunta se escribiría int(row["prc_cents"]) == 0 —con el int() recordándote en cada línea que el precio es texto sucio—. Sobre el modelo limpio, se escribe price_cents == 0, que se lee solo. Multiplica esa diferencia por los cientos de lugares donde el servicio toca el precio, el nombre y el estado, y verás por qué el ACL no es burocracia: es lo que mantiene limpio el código del servicio nuevo durante toda su vida.
Profundización: dónde vive el ACL y qué NO debe hacer
El ACL vive en el borde del servicio —en la capa que recibe lo que viene del monolito y entrega lo que el servicio consume—, nunca en el corazón del dominio. Esta ubicación es deliberada y tiene una consecuencia arquitectónica importante:
borde del servicio
│
monolito ─────────────>│ ACL ─────────────> dominio del servicio
(modelo viejo) │ translate() (Product, logica limpia)
│
la suciedad del viejo se queda AQUI, en el borde;
del ACL hacia adentro, todo es Product limpio
La regla de oro del ACL es de disciplina, igual que la del facade en el módulo 3: el ACL solo traduce. No aplica reglas de negocio, no valida invariantes del dominio, no toma decisiones. Su única responsabilidad es convertir un modelo en otro, fielmente. Si el ACL empieza a decidir —"si el precio viene vacío, pon 0"; "si el producto está inactivo, no lo devuelvas"—, deja de ser un traductor y se vuelve lógica de negocio escondida en la capa equivocada, donde nadie la busca cuando algo falla. Las decisiones de negocio viven en el dominio del servicio (el is_free del ejemplo, y todo lo demás); la traducción vive en el ACL. Uno no invade al otro.
Hay un matiz honesto sobre el costo del ACL. Escribir y mantener el traductor es trabajo real: cada campo del modelo viejo hay que mapearlo, y cuando el modelo viejo cambia, el ACL cambia. No es gratis. Pero es un costo acotado y localizado —vive en un solo lugar, el borde— y compra algo caro: que el modelo del servicio no herede la deuda del monolito. La alternativa (que el servicio hable el modelo viejo directamente) parece más barata al principio y se vuelve carísima después, cuando la deuda del viejo se ha propagado por todo el servicio nuevo. El ACL es una inversión: pagas el traductor hoy para no pagar la contaminación mañana.
Y una nota sobre el final del ACL. Mientras el monolito exista y hable el modelo viejo, el ACL hace falta. Pero el ACL no es necesariamente para siempre: cuando el monolito termine de migrarse —cuando ya nadie hable el modelo viejo— el ACL de entrada puede retirarse, porque ya no hay un idioma sucio del cual protegerse. En una migración larga, el ACL es un andamio: sostiene la traducción mientras los dos modelos coexisten, y se puede desmontar cuando solo queda el modelo limpio.
Errores comunes
Que el servicio nuevo hable el modelo viejo "para ir rápido". Qué pasa: el equipo hace que el servicio de catalog reciba y opere directamente los diccionarios legacy —row["prc_cents"], row["act"] == "Y"— sin ACL. Por qué pasa: escribir el traductor se siente como trabajo extra; el modelo viejo "ya existe" y parece más rápido reusarlo. Cómo detectarlo: el código del servicio "nuevo" está salpicado de int(row["prc_cents"]), comparaciones con 'Y', y accesos a claves crípticas como desc. La jerga del viejo está por todos lados. Cómo corregirlo: el ACL existe justo para esto. Si el servicio hereda el modelo sucio, nace con la deuda que querías dejar atrás, y el "servicio nuevo" es solo el monolito con otro nombre. Escribe el traductor en el borde, y protege el dominio del servicio con un modelo limpio (Product). El costo del ACL es acotado; el costo de la contaminación es infinito.
Meter lógica de negocio en el ACL. Qué pasa: el ACL, además de traducir, empieza a decidir —"si prc_cents viene vacío, ponlo en 0"; "no traduzcas los productos inactivos"—. Por qué pasa: el ACL toca todos los datos que entran, parece el lugar cómodo para "arreglar de paso" o filtrar. Cómo detectarlo: el traductor tiene ifs que no son sobre formato sino sobre reglas del negocio; y cuando una regla falla, nadie la busca en la capa de traducción. Cómo corregirlo: el ACL solo traduce, fielmente, un modelo en otro. Las decisiones —qué es un precio válido, qué productos se muestran— viven en el dominio del servicio, donde se buscan y se prueban. Mezclar traducción con negocio en el ACL esconde la lógica en la capa equivocada y hace el traductor infiel (deja de ser un round-trip limpio). Traduce en el ACL; decide en el dominio.
Un ACL infiel que pierde información en la traducción. Qué pasa: el ACL de entrada convierte el modelo viejo al nuevo, pero olvida algún campo o lo convierte mal, y cuando hay que traducir de vuelta (lección 4) el dato ya no está o quedó deformado. Por qué pasa: al construir el ACL uno se enfoca en los campos "importantes" y descuida los borde (un flag raro, un campo que casi nadie usa). Cómo detectarlo: el round-trip legacy → modern → legacy no vuelve idéntico (lo viste en la lección 1). Cómo corregirlo: el ACL debe ser fiel —preservar toda la información que el otro lado necesita—. Verifica el round-trip sobre casos que cubran los borde (precio cero, producto inactivo, campos opcionales) y arregla cualquier fila que no vuelva idéntica antes de confiar en la traducción. Un traductor que pierde palabras no es un traductor, es un filtro.
Ejercicios
Ejercicio 1 — El intérprete que convierte. En la analogía del intérprete entre dos socios, empareja cada tarea del intérprete con la transformación correspondiente del ACL: (a) cambiar el idioma de las palabras, (b) convertir libras a kilos, (c) traducir un modismo interno a algo entendible. Luego explica cuál es la "regla de oro" que comparten el intérprete y el ACL.
Ver solución
- (a) Cambiar el idioma de las palabras → renombrar campos (
desc→name,prod_id→id). Es la traducción más básica: la misma información, dicha en el vocabulario del otro lado. - (b) Convertir libras a kilos → normalizar tipos (
'8999'string →8999int). No es solo cambiar la palabra; es convertir la unidad/el tipo para que el otro lado lo reciba en su propio sistema, listo para operar. - (c) Traducir un modismo interno a algo entendible → normalizar el dominio (
'Y'/'N'→True/False). El'Y'es un modismo del viejo (bases de datos sin booleanos); el ACL lo traduce a unbool, el "idioma" que el servicio entiende sin ambigüedad.
La regla de oro que comparten: la suciedad de un lado se queda en el borde y no cruza al otro. El intérprete no repite la jerga interna del socio inglés y deja confundido al español; la traduce. El ACL no deja que 'Y' ni los precios-como-texto entren al dominio del servicio; los absorbe en el borde. Cada lado conversa en su propio idioma limpio, y toda la rareza vive en el traductor.
Ejercicio 2 — Lee la traducción. En la tabla del ejemplo, prc_cents='8999' se tradujo a price_cents=8999 con "str -> int", y act='Y' a active=True con "'Y'/'N' -> bool". (a) ¿Por qué la traducción de prc_cents es más valiosa que la de prod_id? (b) ¿Qué bug concreto evita convertir el precio a int en el borde? (c) ¿Por qué is_free se escribe mejor sobre el modelo limpio?
Ver solución
(a) Porque la de prod_id solo renombra (el valor 1 no cambia), mientras que la de prc_cents cambia el tipo ('8999' texto → 8999 número). Un renombre es cosmético; un cambio de tipo cambia lo que puedes hacer con el dato. Después de la traducción de prc_cents, el servicio puede sumar, comparar y operar el precio como número; antes, cualquier operación exigía convertir primero. La traducción de tipo compra capacidad, no solo claridad.
(b) Evita el clásico bug de operar texto como si fuera número. Si el precio queda como string '8999', entonces '8999' + '100' da '8999100' (concatenación de texto, no suma), y '8999' < '900' compara alfabéticamente ('8' vs '9') en vez de numéricamente —dando resultados absurdos—. Convertir a int una sola vez en el borde elimina toda esa clase de bugs del código del servicio: del ACL hacia adentro, el precio es un número y se comporta como tal.
(c) Porque sobre el modelo limpio, is_free se escribe price_cents == 0 —directo, legible, sin recordatorios de suciedad—. Sobre el modelo viejo se escribiría int(row["prc_cents"]) == 0: el int() está ahí solo para deshacer la suciedad del texto, y la clave "prc_cents" obliga a recordar la jerga críptica. Cada función de negocio del servicio pagaría ese impuesto de conversión y descifrado. El ACL lo paga una vez en el borde, y todas las funciones del dominio se escriben limpias. Multiplicado por cientos de funciones, esa es la diferencia entre un servicio legible y uno tan enredado como el monolito.
Ejercicio 3 — Diagnostica el ACL. Un equipo escribe un ACL que, además de traducir, incluye esta línea: if row["act"] != "Y": return None (no traduce los productos inactivos, devuelve None). (a) ¿Qué regla del ACL rompe esto? (b) ¿Qué problema causa en el round-trip? (c) ¿Dónde debería vivir esa decisión?
Ver solución
(a) Rompe la regla de oro de que el ACL solo traduce, no decide. "No devolver los productos inactivos" es una regla de negocio (qué productos se muestran), no una traducción de formato. El ACL se metió a decidir, cuando su único trabajo es convertir un modelo en otro fielmente.
(b) Rompe el round-trip: un producto inactivo (act='N') entra al ACL y sale como None, así que ya no puedes traducirlo de vuelta —perdiste el dato—. El legacy → modern → legacy no vuelve idéntico para los inactivos; vuelve None. El ACL dejó de ser un traductor fiel: filtra en vez de traducir, y la información se pierde en el borde. El monolito, que espera recibir todos sus productos (activos e inactivos), recibiría menos de los que mandó.
(c) Esa decisión —"no mostrar productos inactivos"— vive en el dominio del servicio, no en el ACL. El ACL traduce todos los productos fielmente (activos e inactivos, preservando active=True/False), y luego una función del dominio del servicio (por ejemplo, list_visible_products) decide cuáles se muestran según la regla de negocio. Así la traducción queda limpia y reversible, la regla queda donde se busca y se prueba, y cada capa hace una sola cosa: el ACL convierte, el dominio decide.
Resumen y siguiente paso
En esta lección construiste el corazón del módulo: el anti-corruption layer, el traductor que absorbe la jerga del modelo viejo en el borde del servicio. Viste, con el intérprete que convierte idioma, unidades y modismos, que un buen ACL no solo renombra: normaliza tipos y dominio para que cada lado converse en su propio idioma limpio. Y lo ejecutaste campo por campo: prod_id → id (copiar), desc → name (renombrar, corrigiendo una mentira del viejo), prc_cents → price_cents (str → int), act → active ('Y'/'N' → bool), y viste el pago —is_free escrito como price_cents == 0, no como int(row["prc_cents"]) == 0—. Aprendiste dónde vive el ACL (en el borde, no en el dominio), su regla de oro (solo traduce, no decide), y su costo acotado frente a la contaminación infinita de la alternativa.
Antes de avanzar deberías poder: explicar qué contamina el ACL y por qué; leer una traducción campo por campo y clasificar cada transformación (renombrar, cambiar tipo, normalizar dominio); enunciar la regla de oro del ACL y por qué meter lógica de negocio la rompe; y decir por qué el modelo limpio hace que la lógica del servicio se lea sola.
La lección 4 completa el ACL con su otra dirección. Hasta ahora tradujiste solo de ida (legacy → modern), para que el servicio reciba el modelo limpio. Pero cuando el servicio responde, el monolito espera su modelo viejo de vuelta —si le devuelves un Product, se rompe—. Vas a construir la traducción de retorno (modern → legacy) y a ejecutar una request cruzando el borde dos veces: monolito → ACL → servicio → ACL → monolito. Y vas a verificar que el servicio nunca vio una clave legacy y el monolito nunca vio un Product —cada lado protegido del otro, en ambos sentidos—.
Recursos
- Eric Evans, Domain-Driven Design (Addison-Wesley, 2003), capítulo 14, sección Anti-Corruption Layer — la fuente original del patrón. Evans lo describe como la capa que aísla el modelo de un sistema del modelo de otro con el que debe integrarse, traduciendo entre ambos para que uno no corrompa al otro. La lectura fundacional de esta lección. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — la sección sobre cómo un servicio extraído se comunica con el monolito a través de una capa de traducción, y por qué el modelo del servicio nuevo no debe heredar el esquema del viejo. En inglés.
- Martin Fowler, "BoundedContext" — martinfowler.com/bliki/BoundedContext.html. El contexto conceptual del ACL: cada bounded context tiene su propio modelo, y en las fronteras entre contextos hace falta una traducción explícita para que los modelos no se mezclen. En inglés.
- Microsoft, "Anti-corruption Layer pattern" — learn.microsoft.com/azure/architecture/patterns/anti-corruption-layer. La ficha del patrón en el catálogo de arquitectura de Azure: contexto, solución y cuándo aplicarlo al integrar un sistema nuevo con uno legacy. Corta y directa. En inglés.