Armario metálico oxidado envuelto por raíces que bajan por dentro, con cables que cuelgan de sus huecos, sobre fondo oscuro con tonos púrpura

Patrón Strangler Fig: migrar legacy sin parar el servicio

Estuve unos seis meses en un proyecto de migración de un sistema legacy en .NET, sin parar el servicio en ningún momento. En su momento no seguíamos ningún patrón con nombre: protegíamos cada cambio con tests antes de tocar nada, sacábamos primero los servicios que más pesaban para el negocio, y manteníamos todo funcionando mientras durase el proceso. Solo años después, leyendo sobre arquitectura de migraciones, descubrí que a eso se le llama patrón strangler fig. Este post cuenta el método, no los detalles del sistema de nadie: lo que hicimos de verdad, no una teoría aplicada a posteriori.

Qué es el patrón Strangler Fig

El nombre viene de la higuera estranguladora, una planta que crece alrededor de un árbol existente hasta sustituirlo por completo, sin que el árbol original deje de sostener nada mientras tanto. Aplicado a software, este patrón consiste en construir el sistema nuevo alrededor del legacy, desviando funcionalidad hacia lo nuevo pieza a pieza, hasta que lo viejo deja de tener trabajo que hacer y se puede retirar.

La alternativa —parar el sistema, reescribirlo entero, y lanzar la versión nueva de golpe— es la que casi siempre sale mal en proyectos reales. Un «big bang rewrite» acumula todo el riesgo en un único momento de lanzamiento, justo cuando menos se puede permitir un fallo. El patrón strangler fig reparte ese riesgo en muchas decisiones pequeñas en vez de una sola grande.

Lo hicimos sin saber que tenía nombre

No planteamos la migración como la aplicación consciente de un patrón de arquitectura. Nadie en el equipo dijo en su momento «vamos a aplicar un strangler fig» — ese nombre no formaba parte de nuestro vocabulario entonces. Decidimos, por sentido común y por necesidad, no tocar el sistema legacy de golpe: proteger cada cambio con tests antes de moverlo, sacar primero los servicios que más pesaban para el negocio, y mantener todo funcionando durante los seis meses que duró el proceso.

Reconocí el patrón mucho después, leyendo sobre arquitectura de migraciones: lo que habíamos hecho por necesidad tenía nombre y una explicación formal detrás. Prefiero contarlo así, y no como si hubiéramos seguido un manual desde el primer día, porque es la verdad — y porque lo que hizo que funcionara no fue conocer el nombre del patrón: fue blindar cada cambio con tests y priorizar lo que más importaba al negocio antes de tocar el resto.

El problema real: un sistema legacy que frenaba cada cambio

El sistema con el que trabajábamos corría sobre una base legacy que frenaba el desarrollo y complicaba el mantenimiento del equipo: cada cambio pequeño tardaba más de lo razonable, y el riesgo de romper algo al tocar código antiguo pesaba sobre cada decisión.

No era un sistema roto ni caído. Era un sistema que funcionaba, pero que costaba cada vez más mantener y ampliar — el escenario típico donde una reescritura total suena tentadora y es, casi siempre, la decisión equivocada.

Por qué no hicimos una reescritura total

Definimos la arquitectura desde cero para la parte nueva, con logging estructurado para poder diagnosticar en producción desde el primer día, pero decidimos no apagar el sistema legacy hasta que la nueva arquitectura estuviera demostrando, con datos reales, que hacía lo mismo que el sistema viejo o mejor.

Parar la operación de un sistema que ya está en producción, usado a diario por sus usuarios, para migrarlo de golpe, era un riesgo que no tenía sentido asumir cuando existía una alternativa que reducía ese riesgo sin alargar el proyecto de forma desproporcionada. Lo que hoy sé que se llama patrón strangler fig permite exactamente eso: migrar sin parar el servicio.

Los tests como red de seguridad, no como trámite

Esta es la parte que casi nadie cuenta cuando explica el patrón strangler fig en abstracto. Migrar por fases solo reduce el riesgo si, en cada fase, tienes forma de comprobar que lo nuevo hace lo mismo que lo viejo antes de apagar lo viejo. Sin esa comprobación, «migrar por fases» es simplemente «romper las cosas más despacio».

Antes de mover cada pieza de funcionalidad del sistema legacy a la implementación nueva, escribí tests que capturaban el comportamiento real del sistema viejo para esa pieza concreta: mismas entradas, mismas salidas esperadas. No son tests que describen cómo «debería» comportarse el sistema en teoría — son tests que describen cómo se comporta hoy, de verdad, aunque ese comportamiento no sea perfecto. Ese tipo de test se suele llamar test de caracterización, y su función no es validar que el código es bueno: es validar que el código nuevo no cambia el comportamiento sin que alguien lo decida a propósito.

Un ejemplo simplificado de cómo se ve esa comprobación entre las dos implementaciones, no código real de producción:

public class ConfiguracionEquivalenciaTests
{
    [Theory]
    [MemberData(nameof(CasosDeConfiguracionReales))]
    public void NuevaImplementacion_DebeCoincidirConLegacy(
        ParametrosEntrada entrada)
    {
        var resultadoLegacy = _clienteLegacy.GenerarConfiguracion(entrada);
        var resultadoNuevo = _servicioNuevo.GenerarConfiguracion(entrada);

        Assert.Equal(resultadoLegacy.Checksum, resultadoNuevo.Checksum);
    }

    public static IEnumerable<object[]> CasosDeConfiguracionReales()
    {
        // Casos extraídos de configuraciones reales ya generadas,
        // no inventados para que el test pase.
        yield return new object[] { ParametrosEntrada.DesdeCasoReal(1) };
        yield return new object[] { ParametrosEntrada.DesdeCasoReal(2) };
    }
}

Mientras ese tipo de test no fallaba para una pieza concreta, la desviaba a la implementación nueva con confianza. Si fallaba, el problema se quedaba contenido en esa pieza, sin arrastrar al resto de la migración.

Cómo lo apliqué: convivencia entre lo viejo y lo nuevo

Durante los seis meses que duró la migración, el sistema legacy y la nueva arquitectura convivieron: una capa de enrutamiento decidía, para cada tipo de funcionalidad, si la atendía el sistema viejo o el nuevo, según qué pieza ya estuviera migrada y validada. El orden en que migrábamos no era arbitrario ni técnico: sacábamos primero los servicios que más pesaban para el negocio, no los más sencillos de tocar, aunque eso significara empezar por lo más delicado en vez de lo más cómodo. Nada de esto era visible para quien usaba el sistema en el día a día — el cambio ocurría por debajo, pieza a pieza.

Esa convivencia es la parte incómoda de este tipo de migración: durante meses hay que mantener y entender dos sistemas a la vez, no solo uno. Es un coste real, y quien aplica este enfoque sin asumirlo desde el principio se lleva una sorpresa a mitad de proyecto.

Cómo medí el avance sin engañarme

Con dos sistemas conviviendo, es fácil perder de vista cuánto queda realmente por migrar. Lo que me sirvió fue llevar la cuenta por pieza de funcionalidad, no por porcentaje de código: cuántas piezas atendía ya el sistema nuevo frente a cuántas seguía atendiendo el legacy, y con qué tests de caracterización estaba cubierta cada una. Un porcentaje de líneas migradas no dice nada sobre el riesgo real que queda; saber exactamente qué piezas concretas siguen dependiendo del sistema viejo, sí.

Qué documenté durante la migración

Con dos sistemas conviviendo durante meses, confiar en la memoria de nadie es un error. Para cada pieza migrada dejé constancia de tres cosas: qué comportamiento del sistema legacy capturaban sus tests de caracterización, qué diferencias conocidas existían (si las había) entre el comportamiento viejo y el nuevo, y cómo revertir esa pieza concreta al sistema legacy si algo fallaba una vez en producción. Ese último punto importa más de lo que parece: un plan de rollback pensado con calma, antes de necesitarlo, evita decisiones apresuradas el día que algo no sale como se esperaba.

Esta documentación no fue un informe extenso escrito al final del proyecto. Fue una lista corta, actualizada pieza a pieza, a medida que cada una pasaba de legacy a nuevo. Cualquiera del equipo podía mirarla y saber, sin preguntar, qué estado tenía cada parte del sistema en un momento dado — quién la atendía, con qué garantía, y qué hacer si fallaba.

El resultado, sin dramatizar

La migración se completó en unos seis meses, con el sistema nuevo en producción, sin parar el servicio en ningún momento del proceso. No fue un lanzamiento con fecha límite fija ni un evento único: fue una sucesión de piezas migradas, validadas y puestas en producción, hasta que no quedó nada que migrar.

Lo que le diría a quien empieza una migración así

Si tuviera que resumir en una frase lo que aprendí en esos seis meses, sería esta: el riesgo no desaparece por migrar por fases, se reparte. Cada pieza que se desvía al sistema nuevo es una decisión pequeña y reversible, no una apuesta grande sin marcha atrás — y esa diferencia es la que permite dormir tranquilo mientras dura el proyecto.

También aprendí a no subestimar el coste de mantener dos sistemas vivos a la vez. No es solo escribir código nuevo: es seguir entendiendo el viejo lo bastante bien como para saber, en cada momento, qué parte sigue dependiendo de él. Subestimar ese coste es la forma más habitual de que una migración por fases se alargue mucho más de lo previsto, aunque cada pieza individual avance bien.

Cuándo NO usar el patrón Strangler Fig

Este enfoque no es gratis ni universal. Si el sistema legacy es pequeño y se puede reescribir por completo en un plazo corto con riesgo asumible, mantener dos sistemas conviviendo durante meses puede costar más de lo que ahorra. Y si no es posible construir ese tipo de comprobación de equivalencia entre lo viejo y lo nuevo —por ejemplo, porque el sistema legacy no es determinista o no se puede instrumentar—, la red de seguridad que hace funcionar el patrón strangler fig simplemente no existe, y migrar por fases pierde buena parte de su ventaja frente a una reescritura controlada.

Si arrastras un sistema legacy que te frena y necesitas migrarlo sin detener el negocio, en Modernización de sistemas legacy aplico este mismo criterio: evaluar antes de tocar, migrar por fases, y usar tests como red de seguridad, no como trámite.

Fuente externa citada: Strangler Fig pattern — Azure Architecture Center, Microsoft Learn — la documentación oficial de Microsoft sobre el patrón.