El Patrón Strangler Fig en la Práctica: Cómo Migramos un Hospital de MUMPS a SQL en 4 Años
Gonzalo Monzón
Fundador & Arquitecto Principal
En 2004, Martin Fowler publicó un breve ensayo sobre la Strangler Fig Application — un patrón inspirado en las higueras estranguladoras de las selvas tropicales que lentamente envuelven su árbol huésped, reemplazándolo gradualmente hasta que el original muere. La idea: en lugar de la catastrófica reescritura "big bang", reemplazas incrementalmente un sistema legacy pieza a pieza mientras ambos coexisten. Es una metáfora elegante. También es, en la mayoría de artículos que encontrarás, completamente teórica.
Este no es un artículo teórico. Entre 2013 y 2017, ejecuté este patrón contra uno de los entornos más hostiles imaginables: un Sistema de Información Hospitalaria (HIS) ejecutándose sobre InterSystems Caché/MUMPS — 8 servidores en múltiples centros hospitalarios, sirviendo a miles de profesionales sanitarios, con datos de pacientes que no podían tener un solo segundo de inactividad. Así es exactamente cómo lo hicimos.
Lo Que Estábamos Estrangulando
La Bestia: InterSystems Caché con MUMPS
MUMPS (Massachusetts General Hospital Utility Multi-Programming System) es un lenguaje de programación y sistema de base de datos diseñado en 1966. En 2013 tenía 47 años y seguía ejecutando infraestructura hospitalaria crítica en todo el mundo — incluyendo el Departamento de Asuntos de Veteranos de EE.UU. (VA), que opera la mayor instalación MUMPS del planeta.
InterSystems Caché es la evolución comercial de MUMPS. Añade ObjectScript (una capa OOP), un servidor web y soporte SQL atornillado encima. Pero por debajo, el modelo de datos no cambia: globals — árboles clave-valor jerárquicos y multidimensionales.
// Un registro de paciente en Caché/MUMPS:
^PACIENTE("12345","NOMBRE") = "García López, María"
^PACIENTE("12345","FNAC") = "19580315"
^PACIENTE("12345","ALERGIAS",1) = "Penicilina"
^PACIENTE("12345","VISITAS","20130415","DEPT") = "URG"
^PACIENTE("12345","VISITAS","20130415","DIAG",1) = "J06.9"
^PACIENTE("12345","VISITAS","20130415","LAB","GLUCOSA") = "127"
// Recorrido: $ORDER devuelve la siguiente clave hermana
// $ORDER(^PACIENTE("12345","VISITAS","20130415","LAB","")) → "GLUCOSA"
No hay tablas. No hay columnas. No hay claves foráneas. No hay esquema. El "esquema" es implícito en el código de aplicación — miles de rutinas MUMPS que saben qué claves leer y escribir. Si quieres consultar "todos los pacientes con glucosa > 200 en el último mes", no puedes escribir una consulta SQL. Tienes que recorrer el árbol.
La Escala
- 8 servidores Caché en diferentes centros hospitalarios y de clínicas de la red Xarxa Santa Tecla (Tarragona, España)
- ~15 años de datos acumulados — era el mismo sistema que yo había ayudado a desplegar durante la migración del Y2K a finales de los 90 cuando estaba en el lado del proveedor (Valen Computer)
- Miles de usuarios diarios — médicos, enfermeras, administrativos, técnicos de laboratorio
- Integraciones HL7 y DICOM — radiología (RIS/PACS), laboratorio, farmacia, admisiones
- Requisito de disponibilidad 24/7 — las urgencias no cierran
Por Qué Yo Era la Persona Correcta (y la Única) para Esto
Tenía una ventaja única que ningún consultor externo podía igualar: había ayudado a construir el sistema que ahora estaba reemplazando.
A finales de los 90, trabajando para Valen Computer, había hecho la migración del Y2K para muchos de estos hospitales — incluyendo migrarlos a Caché. Sabía por qué cada dato estaba estructurado como estaba. Había escrito algunas de las rutinas. Había mantenido el sistema durante años, dando soporte a más de 250 clientes en rotación 24/7. Cuando dos consultoras reputadas de Barcelona intentaron construir soluciones de BI sobre estos datos y fracasaron (trataron Caché como SQL estándar y obtuvieron basura), yo fui el que descubrió cómo extraer datos con sentido.
Esta es la verdad incómoda sobre las migraciones legacy: la persona más cualificada para matar el sistema legacy es la persona que lo construyó.
La Arquitectura de la Coexistencia
El enfoque ingenuo para reemplazar un sistema legacy es el "big bang" — congelar el sistema viejo, desarrollar el nuevo durante 18-24 meses, cambiar un fin de semana. Esto falla casi siempre, y en sanidad falla catastróficamente:
- El nuevo sistema no puede replicar 15 años de reglas de negocio implícitas
- La migración de datos nunca es limpia — casos extremos por todas partes
- Los usuarios se rebelan porque sus flujos de trabajo cambian de la noche a la mañana
- Si algo sale mal, no tienes plan B
Nuestra estrategia fue la Higuera Estranguladora: ambos sistemas funcionarían simultáneamente el tiempo necesario, con sincronización de datos bidireccional en tiempo real, y la funcionalidad se migraría módulo a módulo. En cualquier momento, si algo fallaba, el sistema viejo seguía plenamente operativo.
La Arquitectura Objetivo
┌─────────────────────────────────────────────────────────┐
│ SISTEMA NUEVO │
│ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ Angular │→ │ API REST │→ │ SQL Server │ │
│ │ Frontend │ │ (Bottle) │ │ (Esquema normaliz.)│ │
│ └──────────┘ └──────────┘ └────────┬───────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ TABLA DE EVENTOS │ │
│ │ (SQL → Cache sync) │ │
│ └─────────┬──────────┘ │
├────────────────────────────────────────┼────────────────┤
│ MOTOR CDC PYTHON │ │
│ ┌─────────────────────────────────────┼──────────┐ │
│ │ ┌──────────┐ ┌─────────────▼────────┐ │ │
│ │ │ Lector │ │ Consumidor eventos │ │ │
│ │ │ Journal │ │ (SQL→Cache escrituras)│ │ │
│ │ │ (Cache→ │ │ Usa mapeos primitivas │ │ │
│ │ │ SQL) │ │ MUMPS vía C++ │ │ │
│ │ └────┬─────┘ └──────────────────────┘ │ │
│ └───────┼────────────────────────────────────────┘ │
├──────────┼─────────────────────────────────────────────┤
│ │ SISTEMA LEGACY │
│ ┌───────▼─────────────────────────────────────────┐ │
│ │ InterSystems Caché (MUMPS) │ │
│ │ Mirror/Journaling → Flujo de eventos │ │
│ │ 8 Servidores × Múltiples centros hospitalarios │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Paso 1: Hablar MUMPS desde Python
El primer desafío fue acceder a datos de Caché desde Python a velocidad nativa. InterSystems proporcionaba un puente C++ (interfaz callin/callout), pero exponía llamadas API de Caché sin procesar — no exactamente Pythónico.
Implementé las primitivas de recorrido MUMPS fundamentales como funciones Python:
# Ejemplo simplificado de implementación de $ORDER
def cache_order(global_name, *subscripts):
"""
Equivalente a $ORDER de MUMPS — devuelve la siguiente
clave hermana en el nivel de subíndice dado. Es la
operación fundamental de recorrido para globals (B-trees
multidimensionales).
"""
result = cache_connection.call_class_method(
"CacheTraversal", "Order",
global_name, *subscripts
)
return result if result != "" else None
def cache_get(global_name, *subscripts):
"""Equivalente a $GET de MUMPS — devuelve valor en clave exacta."""
return cache_connection.call_class_method(
"CacheTraversal", "Get",
global_name, *subscripts
)
# Recorrer todos los pacientes:
patient_id = ""
while True:
patient_id = cache_order("^PACIENTE", patient_id)
if patient_id is None:
break
nombre = cache_get("^PACIENTE", patient_id, "NOMBRE")
# ... procesar paciente
La clave: cada llamada a método de clase Caché pasa por el puente C++ a velocidad nativa. Python es solo la capa de orquestación. El acceso a datos es tan rápido como MUMPS nativo — milisegundos por recorrido. Esto me daba lo mejor de ambos mundos: velocidad MUMPS para acceso a datos, flexibilidad Python para diseño de APIs.
Paso 2: API REST como Capa de Traducción
Con las primitivas en su sitio, construí una API REST usando Bottle — un micro-framework Python que es esencialmente un solo archivo. La API traducía entre el mundo jerárquico de MUMPS y el mundo plano de JSON:
@app.route('/api/pacientes/<patient_id>')
def get_paciente(patient_id):
"""
Detrás de este endpoint REST limpio, estamos recorriendo
globals MUMPS y ensamblando una respuesta JSON normalizada.
"""
return {
"id": patient_id,
"nombre": cache_get("^PACIENTE", patient_id, "NOMBRE"),
"fnac": cache_get("^PACIENTE", patient_id, "FNAC"),
"alergias": get_todas_alergias(patient_id),
"visitas_recientes": get_visitas(patient_id, limit=10),
}
def get_todas_alergias(patient_id):
"""Recorrer el subárbol de alergias usando $ORDER."""
alergias = []
idx = ""
while True:
idx = cache_order("^PACIENTE", patient_id, "ALERGIAS", idx)
if idx is None:
break
alergias.append(cache_get("^PACIENTE", patient_id, "ALERGIAS", idx))
return alergias
La API era deliberadamente heavy en lectura y light en escritura. Las lecturas (búsquedas de pacientes, listados de visitas, resultados de laboratorio) estaban segmentadas y eran rápidas — nunca buscabas "todos los pacientes", solo registros específicos o conjuntos filtrados para un día dado. Las escrituras iban por otro camino (más sobre eso en el Paso 3).
Esta capa de API permitió que Mirth Connect (el motor estándar de integración HL7/DICOM en sanidad) se comunicara con el sistema legacy a través de HTTP estándar — convirtiendo una caja negra sellada en un servicio interoperable.
Paso 3: CDC Bidireccional — La Parte Difícil
Aquí es donde fallan la mayoría de proyectos de migración. Pueden construir el sistema nuevo. Pueden leer del sistema viejo. Pero no pueden hacer que ambos sistemas reflejen los cambios del otro en tiempo real mientras ambos están en producción.
Necesitaba Change Data Capture (CDC) bidireccional: cada escritura en el sistema legacy debía aparecer en el nuevo, y cada escritura en el nuevo debía aparecer en el legacy. Ambos sistemas debían comportarse como si fueran el único sistema — los usuarios no debían saber (ni importarles) cuál estaban usando.
Legacy → Nuevo: Secuestrando el Mirror
Caché tiene un sistema integrado de mirroring/replicación para alta disponibilidad. Funciona transmitiendo un journal — un log secuencial de cada operación de datos (sets, kills, transacciones) — del servidor primario al mirror. Este journal es el equivalente en Caché del WAL (Write-Ahead Log) de PostgreSQL.
"Pinché" en este mecanismo — no en el mirror en sí, sino en el flujo del journal. Un proceso Python leía las entradas del journal, filtraba las operaciones sobre globals específicos (datos de pacientes, visitas, resultados de laboratorio, registros administrativos), y traducía cada operación al correspondiente INSERT/UPDATE/DELETE SQL contra el esquema de SQL Server.
# Flujo conceptual (simplificado):
# 1. Entrada journal Caché: SET ^PACIENTE("12345","ALERGIAS",3) = "Látex"
# 2. Parser Python identifica: global=PACIENTE, id=12345, campo=ALERGIAS, idx=3
# 3. Mapea a SQL: INSERT INTO alergias (paciente_id, alergia, seq) VALUES (12345, 'Látex', 3)
# 4. Ejecuta contra SQL Server vía pyodbc
La belleza de leer del journal en vez de hacer polling a la base de datos: cero carga en el servidor Caché de producción. El journal ya se generaba para propósitos de mirroring. Yo simplemente añadí otro consumidor.
Nuevo → Legacy: Triggers SQL + Tabla de Eventos
La dirección inversa usaba un mecanismo diferente. Cuando el nuevo frontend Angular guardaba datos en SQL Server, un trigger capturaba el cambio e insertaba un registro en una tabla de eventos:
-- Trigger SQL Server (conceptual)
CREATE TRIGGER trg_paciente_update ON pacientes AFTER INSERT, UPDATE
AS
BEGIN
INSERT INTO sync_eventos (entidad, entidad_id, operacion, payload, created_at)
SELECT 'PACIENTE', i.id, 'UPDATE',
(SELECT i.* FOR JSON PATH), GETDATE()
FROM inserted i
END
Un proceso Python separado consumía esta tabla de eventos, y usando los mapeos de primitivas MUMPS (el mismo puente C++ del Paso 1), escribía los cambios de vuelta en Caché exactamente como lo habría hecho la aplicación legacy. Esto era crítico: las escrituras tenían que pasar por la misma lógica de validación que la aplicación Delphi/MUMPS imponía, o los datos serían inconsistentes.
La Latencia: 100ms
El viaje completo de ida y vuelta — desde un cambio en un sistema hasta su reflejo en el otro — tardaba 100 milisegundos. Esto era lo suficientemente rápido como para ser efectivamente invisible para los usuarios humanos:
- Un médico modifica un registro de paciente en el terminal viejo → 100ms después, es visible en la nueva interfaz Angular
- Una enfermera actualiza información de triaje en el sistema nuevo → 100ms después, se refleja en el terminal legacy
- La ventana para un conflicto de datos (dos personas modificando el mismo campo en menos de 100ms) era estadísticamente cero en un flujo de trabajo clínico — los médicos no actualizan el mismo registro simultáneamente a la décima de segundo
100ms era nuestro mecanismo de seguridad. A esa latencia, la "consistencia eventual" era efectivamente "consistencia inmediata" para todos los efectos prácticos.
Los Datos: 40TB de Historia Clínica
Mientras el motor CDC manejaba la sincronización en tiempo real, la migración de datos históricos era su propio proyecto. 40 terabytes de registros médicos acumulados necesitaban ser limpiados, transformados y cargados en el nuevo esquema SQL Server.
Si nunca has limpiado datos médicos, esto es a lo que te enfrentas:
- Notas clínicas en texto libre con codificación inconsistente (Latin-1, UTF-8, a veces bytes en crudo)
- PDFs incrustados en globals — informes de radiología, formularios de consentimiento, resúmenes de alta almacenados como cadenas base64 dentro de nodos MUMPS
- Imágenes escaneadas — notas manuscritas, registros en papel antiguos digitalizados hace décadas
- Décadas de datos acumulados con evolución del esquema — la misma clave global podía significar cosas distintas dependiendo de cuándo se creó el registro
- Registros duplicados — pacientes que se mudaron, cambiaron de seguro, o fueron registrados en diferentes centros con variaciones ligeras del nombre
Usé Pentaho Data Integration (Spoon/PDI) para la orquestación ETL — cientos de diagramas de transformación, cada uno manejando un dominio de datos específico (demografía, visitas, resultados de laboratorio, radiología, farmacia, facturación). Los trabajos de Pentaho se ejecutaban incrementalmente: carga completa inicial, luego deltas nocturnos, reconciliando constantemente con el flujo CDC en tiempo real.
La Línea Temporal de 4 Años
| Año | Hito | Sistemas Activos |
|---|---|---|
| 2013 | Motor CDC operativo, capa BI sobre SQL Server, comienza migración histórica | Caché (primario) + SQL Server (solo lectura) |
| 2014 | Primeros módulos Angular en producción (no críticos: informes, agenda). Sync bidireccional activa | Ambos activos, Caché aún primario para clínica |
| 2015 | Módulos clínicos migrados progresivamente — admisiones, farmacia, consultas externas. Usuarios en ambos sistemas | Ambos activos, carga compartida |
| 2016 | Urgencias, laboratorio y módulos críticos restantes migrados. Uso de Caché declinando | SQL Server primario, Caché secundario |
| 2017 | Validación final. 8 servidores Caché apagados permanentemente | Solo SQL Server |
La disciplina clave: nunca apresures una migración de módulo. Cada módulo funcionó en paralelo durante semanas, a veces meses, antes de retirar la versión legacy. A los usuarios se les daba la opción de usar cualquiera de los dos sistemas durante la transición, lo que proporcionaba una señal de calidad natural — si seguían volviendo al terminal viejo, algo faltaba en la nueva interfaz.
El Coste Humano: 3,5 Años de Guardia 24/7
Esta es la parte que los diagramas de arquitectura no muestran.
Durante 3,5 años, fui el punto único de fallo para la sincronización entre ambos sistemas. Nadie más entendía el motor CDC, los mapeos de primitivas MUMPS, el parsing del journal y el esquema SQL lo suficientemente bien como para diagnosticar un fallo de sincronización a las 3 de la madrugada.
Cuando digo guardia 24/7, me refiero a: si el parser del journal encontraba una estructura de global MUMPS que no había visto antes (porque alguna rutina antigua se disparaba por un escenario clínico inusual), yo era el que tenía que diagnosticarlo, mapearlo y desplegar el fix — mientras el hospital estaba operativo y dependía de que ambos sistemas estuvieran sincronizados.
Era el arquitecto, el albañil y el bombero. El Bus Factor era 1. Este es el antipatrón del que todos los blogs de ingeniería advierten, y también es la realidad de la mayoría de migraciones legacy exitosas en infraestructura crítica: la persona que construyó el puente es la única que puede repararlo mientras el tráfico está cruzando.
El día que los servidores Caché se apagaron finalmente, el puente ya no era necesario. Y 15 días después, me despidieron — habiendo logrado con éxito hacerme obsoleto. El sistema que construí para reemplazarme funciona perfectamente. Todavía lo hace.
Lecciones para Tu Migración
1. El que conoce el legacy gana. Las dos consultoras que fracasaron antes que yo intentaron tratar Caché como "una base de datos más". Se conectaron vía ODBC y ejecutaron consultas SQL contra un almacén clave-valor multidimensional. Los resultados no tenían sentido. Entender las primitivas del sistema legacy — cómo $ORDER recorre globals, cómo funciona el journaling, cómo la lógica de aplicación está incrustada en la estructura de datos — no es opcional. Es el cimiento.
2. La sincronización bidireccional no es negociable en sistemas críticos. Una migración unidireccional (viejo → nuevo) significa que no puedes volver atrás. En sanidad, no puedes permitirte "lo arreglamos el lunes". Ambos sistemas deben estar plenamente operativos en todo momento. El motor CDC es el componente más complejo de construir, pero es lo que hace viable la Higuera Estranguladora en entornos de alto riesgo.
3. La latencia es tu estrategia de resolución de conflictos. Con 100ms de latencia de sincronización, no necesitábamos un algoritmo de resolución de conflictos. La probabilidad de que dos clínicos modificaran el mismo campo de un paciente en menos de 100ms era efectivamente cero. Es un tradeoff: si tu latencia fuera de 10 segundos, necesitarías last-write-wins o transformaciones operacionales. Conoce la tolerancia de tu dominio.
4. Migra los datos, no el esquema. No intentes replicar el modelo de datos legacy en el sistema nuevo. Los globals MUMPS son árboles; las tablas SQL son planas. El trabajo del motor CDC es traducir entre representaciones, no preservar la estructura vieja. Tu nuevo sistema debería tener esquemas limpios y normalizados que tengan sentido independientemente del legacy.
5. Deja elegir a los usuarios. Durante la transición, los usuarios podían trabajar en cualquiera de los dos sistemas. Esto era aterrador para los jefes de proyecto ("¿y si nunca cambian?") pero invaluable para asegurar la calidad. Cuando las enfermeras cambiaban voluntariamente al nuevo sistema, sabíamos que estaba listo. Cuando no lo hacían, sabíamos qué faltaba. Tus usuarios son tu mejor equipo de QA — déjales votar con su flujo de trabajo.
6. Planifica para el Bus Factor. Mi mayor error fue ser un equipo de uno durante 3,5 años. El motor CDC debería haber sido documentado y enseñado a al menos dos ingenieros más. Pagué por ello con años de burnout por guardias. Si estás construyendo una capa de sincronización para un sistema crítico, dótalo de personal como el sistema crítico que es.
7. El éxito significa hacerte obsoleto. El mejor resultado posible de una migración Strangler Fig es que un día, alguien desenchufar el sistema viejo y no pasa nada. Sin alarmas. Sin pérdida de datos. Sin llamadas frenéticas. Solo silencio. Eso no es fracaso — es la definición de éxito. Aunque, como aprendí yo, la organización puede que ya no te necesite a ti una vez que tu puente ya no es necesario.
Etiquetas
Sobre el Autor
Gonzalo Monzón
Fundador & Arquitecto Principal
Gonzalo Monzón es Arquitecto de Soluciones Senior e Ingeniero IA con más de 26 años construyendo sistemas críticos en Sanidad, Automatización Industrial e IA empresarial. Fundador de Cadences Lab, está especializado en conectar infraestructura legacy con tecnología de vanguardia.
Artículos Relacionados
Edge Computing: Por Qué lo Apostamos Todo a Cloudflare (Y Qué Consigues por $65/Mes)
Sin servidores, sin contenedores, sin Kubernetes. Corremos 14+ productos interconectados en 9 productos Cloudflare — Workers, D1, R2, Durable Objects, Pages, KV, Vectorize, Workers AI y WAF. $65/mes por lo que costaría $400-600 en AWS. La arquitectura completa.
SQLite Es la Base de Datos de Producción Que Ya Conoces (Solo Que Aún No Lo Sabes)
DHH alcanzó 30.000 escritores concurrentes en SQLite. Nosotros ejecutamos 14+ productos en Cloudflare D1 (SQLite en el edge) por 5$/mes. SQLite no es la base de datos de juguete que crees — está impulsando todo, desde ONCE de 37signals hasta toda nuestra plataforma multi-tenant. Así es como la industria está convergiendo en la base de datos más desplegada del mundo.
Vanilla JS Es el Lenguaje Ensamblador del Navegador (Y Por Eso Lo Usamos)
Gmail envía 20MB de JavaScript. Slack 55MB. Nuestro sistema de cookie consent? 4KB, cero dependencias. Cuando entiendes las primitivas de bajo nivel — vanilla JS, CSS puro, APIs nativas del DOM — no necesitas un framework que te diga lo que el navegador ya sabe. Pero no somos anti-frameworks: usamos Astro para SSG y React islands donde realmente ayudan. La diferencia es que elegimos nuestras herramientas — no nos eligen a nosotros.