Integrar sistemas antiguos y ajenos
En resumen: Un anti-corruption layer traduce el modelo de un sistema antiguo o ajeno a tu dominio, y un hub con formato canónico reduce las conexiones entre n sistemas a un adaptador por sistema.
Problema
Un almacén de 2009 y contabilidad en XML
La tienda tiene un almacén antiguo: un programa de 2009 que nadie se atreve a tocar. Habla CSV, con ; y códigos: A es disponible, B es bloqueado, y la cantidad -1 significa «contando ahora mismo». Contabilidad quiere XML, con el total escrito con punto. Entrega quiere JSON.
Así avisan los pedidos a los tres:
class Orders: def __init__(self, warehouse, accounting, delivery): self.warehouse = warehouse self.accounting = accounting self.delivery = delivery
def place(self, order, sku, qty, cents): # el almacén de 2009: CSV y el código OUT self.warehouse.send(f"{order};{sku};{qty};OUT") # contabilidad: XML, el total con punto total = euros(cents) self.accounting.send( f'<invoice no="{order}" total="{total}"/>') # entrega: JSON self.delivery.send( json.dumps({"order": order, "sku": sku}, separators=(",", ":")))orders = Orders(Endpoint("almacén"), Endpoint("contabilidad"), Endpoint("entrega"))orders.place(1042, "HP-01", 1, 5900)almacén ← 1042;HP-01;1;OUTcontabilidad ← <invoice no="1042" total="59.00"/>entrega ← {"order":1042,"sku":"HP-01"}Funciona. Pero ahora los pedidos conocen tres formatos ajenos, y los códigos del almacén se han extendido hasta el cobro: en algún sitio hay un if (status == "B"). El catálogo también habla con el almacén, y los vendedores con contabilidad. Cuando el almacén añade un estado nuevo, nadie sabe en cuántos lugares hay que buscar.
La idea
Un traductor en la frontera
Un sistema ajeno tiene su propio modelo: sus propios códigos, sus propias unidades, sus propios valores raros. Si ese modelo entra en el dominio, el dominio empieza a pensar en el idioma del almacén de 2009 y cambia cada vez que cambia el almacén.
Por eso en la frontera hay un traductor. Lee el formato ajeno y deja pasar solo los conceptos del dominio: Stock(sku, available). Solo el traductor sabe qué significan A, B y -1. A esto se le llama anti-corruption layer.
Por dentro es un adaptador, pero con una tarea mayor: un adaptador cambia la forma de una interfaz, mientras que un anti-corruption layer impide que se filtre un modelo entero. Decide qué significa cada valor ajeno y qué hacer con lo desconocido. La regla: lo desconocido se rechaza en voz alta, nunca se adivina.
Míralo
El traductor en código
@dataclass(frozen=True)class Stock: sku: str available: int
# la frontera: solo aquí se sabe qué significan A, B y -1class WarehouseAcl: def __init__(self): self.rejected = []
def read(self, lines): stock = [] for line in lines: sku, qty, status = line.split(";") qty = int(qty) if status == "A" and qty >= 0: stock.append(Stock(sku, qty)) elif status == "A": self.rejected.append(f"{sku}: cantidad desconocida") elif status == "B": stock.append(Stock(sku, 0)) else: self.rejected.append(f"{sku}: estado desconocido {status}") return stockStock es una dataclass: el dominio recibe un objeto con nombres, no una línea del archivo.El archivo de esta mañana:
# el archivo que el almacén antiguo sube por FTP cada horafile = ["HP-01;3;A", "CH-07;0;A", "KB-12;5;B", "MS-03;-1;A", "SP-02;2;R"]acl = WarehouseAcl()for s in acl.read(file): print(f"{s.sku}: {s.available}")for r in acl.rejected: print("✗", r)HP-01: 3CH-07: 0KB-12: 0✗ MS-03: cantidad desconocida✗ SP-02: estado desconocido RKB-12 está bloqueado, así que para el dominio hay 0 disponibles. MS-03 se está contando: el traductor no sabe cuántos hay y no inventa un número. SP-02 tiene el estado R, que nadie ha descrito. Se rechaza, y alguien preguntará al dueño del almacén qué significa.
Pruébalo
Encuentra el error
Una colega escribe un traductor más corto: «todo lo que no está bloqueado se puede vender». Un mes después el almacén empieza a enviar el estado R (reservado para un vendedor externo), y la tienda vende el mismo altavoz a dos clientes. Además, MS-03 aparece en el sitio con -1 en stock. ¿En qué línea está el error?
Elige la línea con el error.
El traductor adivina en lugar de traducir: todo estado que no es B pasa a ser «disponible», y toda cantidad, incluso -1, entra en el dominio. Lo desconocido debe rechazarse en voz alta, como hace WarehouseAcl más arriba: los casos conocidos enumerados y un rechazo para todo lo demás.
La idea
La telaraña de conexiones
Seis sistemas: pedidos, catálogo, almacén, contabilidad, entrega y vendedores externos. Si cada uno habla con todos los demás, las conexiones son n(n-1)/2 = 15, cada una con su propia traducción. Un cambio del formato del almacén toca 5 de ellas, en 5 sistemas que hay que publicar juntos.
La otra vía es un hub con una forma canónica: una forma común del mensaje (OrderPlaced) que todos entienden. Cada sistema se conecta una vez, con un adaptador entre su propio formato y el canónico. Seis sistemas, seis adaptadores. Un cambio del formato del almacén toca solo su adaptador: ese adaptador es el anti-corruption layer del almacén.
@dataclass(frozen=True)class OrderPlaced: id: int sku: str qty: int cents: int
class Hub: def __init__(self): self.adapters = {}
def connect(self, system, adapter): self.adapters[system] = adapter
def publish(self, event): for system, adapter in self.adapters.items(): system.send(adapter(event))
def size(self): return len(self.adapters)class Orders: def __init__(self, warehouse, accounting, delivery): self.warehouse = warehouse self.accounting = accounting self.delivery = delivery
def place(self, order, sku, qty, cents): # el almacén de 2009: CSV y el código OUT self.warehouse.send(f"{order};{sku};{qty};OUT") # contabilidad: XML, el total con punto total = euros(cents) self.accounting.send( f'<invoice no="{order}" total="{total}"/>') # entrega: JSON self.delivery.send( json.dumps({"order": order, "sku": sku}, separators=(",", ":")))Paso a paso, en palabras
- Los pedidos envían a tres sistemas, cada uno en su propio idioma: CSV al almacén de 2009, XML a contabilidad, JSON a entrega. Los pedidos conocen los tres formatos.
- Así hablan también los demás: el catálogo con el almacén, los vendedores con contabilidad. 6 sistemas que hablan todos con todos son 15 conexiones, cada una con su propia traducción.
- El almacén cambia su formato. Cambia cada conexión hacia él: 5 lugares en 5 sistemas, publicados juntos.
- Un hub: cada sistema se conecta una vez, con un adaptador entre su formato y una forma canónica. 6 sistemas, 6 adaptadores.
- Los pedidos publican OrderPlaced una vez y no conocen ni CSV ni XML. El hub da a cada sistema su propia traducción.
- El almacén cambia otra vez su formato: solo cambia su adaptador. Mantiene el modelo antiguo fuera del dominio: un anti-corruption layer.
- Punto a punto: 15 conexiones, 5 cambios con un formato nuevo. Hub: 6 adaptadores, 1 cambio. El precio: el hub es un sistema más que no debe caer.
Míralo
El hub en código
El adaptador es una función: Python no pide una interfaz, siempre que se llame con un OrderPlaced y devuelva texto. Los pedidos publican una vez, y los adaptadores están junto a cada sistema:
hub = Hub()hub.connect(Endpoint("almacén"), lambda e: f"{e.id};{e.sku};{e.qty};OUT")hub.connect(Endpoint("contabilidad"), lambda e: f'<invoice no="{e.id}" total="{euros(e.cents)}"/>')hub.connect(Endpoint("entrega"), lambda e: json.dumps({"order": e.id, "sku": e.sku}, separators=(",", ":")))# los pedidos publican una vez, en la forma canónicahub.publish(OrderPlaced(1042, "HP-01", 1, 5900))print(f"adaptadores: {hub.size()}")almacén ← 1042;HP-01;1;OUTcontabilidad ← <invoice no="1042" total="59.00"/>entrega ← {"order":1042,"sku":"HP-01"}adaptadores: 3Los mensajes son los mismos que al principio. La diferencia es quién conoce el formato: los pedidos ya no conocen CSV ni XML, y para un sistema nuevo se añade un connect más, sin tocar los pedidos. euros convierte los centavos en texto con punto: 5900 pasa a ser 59.00.
Pruébalo
Predice la salida
for n in [3, 6, 10]: links = n * (n - 1) // 2 print(f"{n} sistemas: {links} conexiones o {n} adaptadores")Las conexiones son los pares de sistemas: cada uno de n con cada uno de los otros n - 1, dividido entre 2, porque la conexión A–B es la misma que B–A. Crecen casi como n², mientras que los adaptadores crecen como n. Este es el peor caso: en una tienda real no todos los sistemas hablan con todos. Pero incluso 8 conexiones con 8 traducciones distintas son muchas.
La idea
Síncrono o asíncrono, y el contrato
No todas las conexiones son iguales:
| Síncrona (solicitud y respuesta) | Asíncrona (cola, archivo) | |
|---|---|---|
| Cuándo | la respuesta se necesita ahora: «¿hay en stock?» antes de la venta | puede esperar: las facturas para la mañana, el archivo del almacén cada hora |
| Si el otro lado es lento | esperas tú también, de ahí timeout y retry | el mensaje espera en la cola |
| Si el otro lado está caído | rechazas o tienes una respuesta de reserva | se pone al día cuando vuelve |
La forma canónica es un contrato: todos dependen de ella. Cambia como un contrato de API: un campo nuevo se añade sin romper los adaptadores antiguos, y quitar un campo es una versión nueva. Si el contrato recibe campos para el almacén y campos solo para contabilidad, es de todos y de nadie.
La idea
Cuándo el hub perjudica
- Con dos o tres sistemas. Un hub es un sistema más: se despliega, se monitorea y falla. Con pocas conexiones, una conexión directa con un traductor es más barata.
- Cuando se le mete lógica. Un hub que decide descuentos o enruta según reglas de negocio se convierte en un lugar que ningún equipo posee y que todos los equipos esperan. El hub transporta y traduce; las reglas se quedan en los sistemas.
- Cuando la forma canónica lo quiere todo. Un modelo que describe el pedido para todos los sistemas a la vez tarda meses en acordarse, y luego nadie se atreve a tocarlo. Varios mensajes pequeños (
OrderPlaced,StockChanged), cada uno con un dueño claro, funcionan mejor. - Un anti-corruption layer no siempre hace falta. Si el modelo ajeno es bueno y estable (por ejemplo, el contrato del proveedor de pagos), o es un módulo de tu propio equipo, el traductor es solo una capa más que mantener.
Más precisamente, para curiosos
Hubs y brokers reales: Apache Kafka, RabbitMQ, las colas de la nube (Amazon SQS, Azure Service Bus); para la integración: Apache Camel, Spring Integration, MuleSoft, Kafka Connect. El nombre más antiguo es ESB (enterprise service bus). Su mala fama viene de los buses que acabaron cargando con toda la lógica de negocio.
Los patrones tienen nombre. El libro «Enterprise Integration Patterns» (Gregor Hohpe y Bobby Woolf, 2003) describe el message translator, el canonical data model, el router y decenas más. El anti-corruption layer viene de «Domain-Driven Design» de Eric Evans (2003).
Sistemas antiguos en la práctica: el traductor guarda también el archivo original, para poder reproducir una línea cuando alguien averigüe qué significa R. Un archivo por FTP se lee solo cuando se ha subido completo (por ejemplo, señalado con un archivo .done vacío a su lado). Los servidores FTP, los brokers y la red entre ellos son infraestructura y se enseñan en el curso de DevOps ZeroToOps.
Síncrono a través de un hub también se hace (un API gateway delante de varios sistemas), pero entonces el hub es parte de cada solicitud: si es lento, todo es lento.
Ejercicio
Mercancía devuelta
Resuelve la tarea en tu editor.
El almacén antiguo sube un segundo archivo: la mercancía devuelta. Una línea es RET;pedido;artículo;cantidad;fecha, y la fecha es dd.MM.yyyy. A veces el archivo trae también líneas del inventario (INV;…), que no son devoluciones. Escribe un traductor que entregue al dominio devoluciones con la fecha en formato ISO (2026-10-15) y rechace en voz alta toda línea que no sea una devolución.
La entrada es RET;1042;HP-01;1;15.10.2026, RET;1043;CH-07;2;16.10.2026 e INV;HP-01;3.
devolución 1042: HP-01 × 1 el 2026-10-15devolución 1043: CH-07 × 2 el 2026-10-16✗ no es una devolución: INV;HP-01;3Más tareas
- Vendedores externos. Conecta también los vendedores al hub: quieren la línea
ORDER 1042 HP-01. ¿Cuántas líneas cambian en los pedidos? - Contabilidad responde. Contabilidad devuelve XML
<paid no="1042" cents="5900"/>. Escribe el traductor hacia dentro: el dominio quiere un pago en euros.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Eventos y colas · Diseño de API: contrato, versiones, solicitudes repetidas · Cuando el vecino es lento: timeout, retry, circuit breaker · Una transacción entre servicios (Saga)
- Continúa en: Cambiar un sistema en marcha (Strangler Fig, Expand–Contract) · El panorama de la empresa y sus capacidades · Tu primera tarea de arquitectura
- Ejercicios sobre el tema: El arquitecto en acción