Skip to content
GitHub Twitter

Strangler Fig Pattern: reduciendo el riesgo al modernizar el código legacy de tu aplicación mobile

Modernizar una aplicación mobile con años de historia suele generar vértigo. Código legacy, decisiones tomadas en otro contexto, dependencias obsoletas y un producto que no puede dejar de evolucionar porque sigue en producción y generando valor.

Aquí es donde entra en juego el Strangler Fig Pattern, un patrón de arquitectura que permite evolucionar sistemas legacy de forma incremental, reduciendo riesgos y evitando reescrituras masivas que rara vez salen bien.

Inspirado en la higuera estranguladora —una planta que crece alrededor de un árbol hasta reemplazarlo por completo—, este patrón propone rodear progresivamente el sistema antiguo con nuevas implementaciones, hasta que el código legacy deja de ser necesario.


¿Qué es el Strangler Fig Pattern?

El Strangler Fig Pattern consiste en introducir nuevas funcionalidades o versiones de componentes en paralelo al sistema existente, redirigiendo gradualmente el tráfico o el uso hacia la nueva implementación.

En lugar de:

  • ❌ Reescribir toda la app
  • ❌ Parar el desarrollo durante meses
  • ❌ Asumir un riesgo alto de regresiones

El patrón propone:

  • ✅ Cambios pequeños y controlados
  • ✅ Validación continua en producción
  • ✅ Capacidad de rollback
  • ✅ Convivencia temporal entre lo viejo y lo nuevo

Martin Fowler lo describe como una estrategia de evolución, no de reemplazo abrupto.


¿Por qué es especialmente relevante en Mobile?

En aplicaciones mobile, este patrón encaja especialmente bien por varios motivos:

  • Las apps viven muchos años: no es raro encontrar proyectos con más de 7–10 años de código.
  • El negocio no puede parar: hay releases constantes, métricas, experimentos y bugs que corregir.
  • La UI evoluciona rápido: nuevas APIs (SwiftUI), nuevos patrones y cambios de diseño.
  • El coste de errores es alto: una regresión puede impactar a millones de usuarios.

Aplicar el Strangler Fig Pattern permite modernizar sin dejar de entregar valor.


Caso 1: Migrar de Objective-C a Swift de forma segura

El problema

Muchas aplicaciones iOS comenzaron en Objective-C y fueron creciendo durante años. Reescribir todo a Swift es inviable, pero mantener Objective-C indefinidamente tampoco es una buena opción.

Aplicando Strangler Fig

  1. Define un boundary claro

    • Nuevas features → Swift
    • Código legacy → Objective-C
    • Comunicación a través de interfaces bien definidas
  2. Introduce Swift de forma incremental

    • Nuevos módulos o frameworks escritos en Swift
    • Uso controlado del @objc y @objcMembers solo donde sea necesario
  3. Sustitución progresiva

    • Refactor de pantallas o servicios críticos uno a uno
    • Eliminación gradual de dependencias Objective-C

Ejemplo real

  • App iniciada en Objective-C (2014)
  • Swift introducido en 2018 para nuevas features
  • En 2022:
    • El 80% de la lógica de negocio ya estaba en Swift
    • Objective-C reducido a capas muy específicas
  • Resultado: migración sin grandes reescrituras ni congelar el desarrollo

Objective-C no se eliminó de golpe: fue estrangulado progresivamente.


Caso 2: De UIKit legacy a SwiftUI sin romper la app

El problema

SwiftUI ofrece enormes ventajas:

  • Código más declarativo
  • Menos estado compartido
  • Mejor testabilidad visual
  • Menos boilerplate

Pero una app grande en UIKit no puede migrarse de una sola vez.

Aplicando Strangler Fig

SwiftUI está diseñado para convivir con UIKit, lo que lo hace ideal para este patrón:

  • UIHostingController para introducir SwiftUI
  • UIViewControllerRepresentable para reutilizar UIKit existente

Estrategia típica

  1. Nuevas pantallas en SwiftUI

    • Onboarding
    • Ajustes
    • Features nuevas
  2. Pantallas legacy permanecen en UIKit

    • Flujos críticos
    • Zonas con mucha deuda técnica
  3. Migración progresiva

    • Reemplazo de pantallas UIKit por SwiftUI una a una
    • Reducción gradual del código legacy

Ejemplo real

  • App con más de 120 UIViewController
  • SwiftUI introducido primero en Settings
  • Posteriormente en flujos secundarios
  • Tras 18 meses:
    • El 60% de la UI ya era SwiftUI
    • UIKit reducido a casos muy específicos

Sin big-bang, sin reescritura total.


Ejemplo aplicado: migrar un perfil sin parar la app

Imagina una app de banca que lleva 9 años en producción. El perfil del usuario está en Objective-C y hace una llamada a una API antigua que devuelve un NSDictionary. Queremos migrar solo el perfil a Swift sin tocar el resto de la app.

La idea es crear un boundary claro: un protocolo en Swift que define “cómo obtener el perfil”. Luego ofrecemos dos implementaciones: una moderna (Swift + API nueva) y otra legacy (Objective-C). Un router elige cuál usar en tiempo de ejecución con un feature flag.

Primero definimos el modelo y el contrato compartido:

struct UserProfile: Equatable {
    let id: String
    let name: String
}

protocol UserProfileProviding {
    func fetchProfile(userId: String) async throws -> UserProfile
}

Después escribimos la implementación moderna. Esta ya usa la API nueva y es completamente Swift:

final class SwiftUserProfileProvider: UserProfileProviding {

    private let api: ProfileAPI

    init(api: ProfileAPI) {
        self.api = api
    }

    func fetchProfile(userId: String) async throws -> UserProfile {
        let dto = try await api.getProfile(userId: userId)
        return UserProfile(id: dto.id, name: dto.name)
    }
}

Y mantenemos la implementación legacy sin reescribirla. Solo la “adaptamos” al contrato Swift:

@interface LegacyProfileService : NSObject
- (void)fetchProfileWithUserId:(NSString *)userId
                    completion:(void (^)(NSDictionary * _Nullable profile,
                                         NSError * _Nullable error))completion;
@end
final class ObjCLegacyUserProfileProvider: UserProfileProviding {

    private let legacyService: LegacyProfileService

    init(legacyService: LegacyProfileService) {
        self.legacyService = legacyService
    }

    func fetchProfile(userId: String) async throws -> UserProfile {
        try await withCheckedThrowingContinuation { continuation in
            legacyService.fetchProfile(withUserId: userId) { dict, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }

                guard
                    let dict,
                    let id = dict["id"] as? String,
                    let name = dict["name"] as? String
                else {
                    continuation.resume(
                        throwing: NSError(domain: "LegacyMapping", code: 0)
                    )
                    return
                }

                continuation.resume(
                    returning: UserProfile(id: id, name: name)
                )
            }
        }
    }
}

Por último, un router decide qué implementación usar. Así podemos activar la versión Swift primero para QA, luego para un 10% de usuarios y, si todo va bien, para el 100%:

protocol FeatureFlags {
    func isEnabled(_ key: String) -> Bool
}

final class UserProfileProviderRouter: UserProfileProviding {

    private let legacy: UserProfileProviding
    private let modern: UserProfileProviding
    private let flags: FeatureFlags

    init(
        legacy: UserProfileProviding,
        modern: UserProfileProviding,
        flags: FeatureFlags
    ) {
        self.legacy = legacy
        self.modern = modern
        self.flags = flags
    }

    func fetchProfile(userId: String) async throws -> UserProfile {
        if flags.isEnabled("profile_modern_provider") {
            return try await modern.fetchProfile(userId: userId)
        } else {
            return try await legacy.fetchProfile(userId: userId)
        }
    }
}

El resultado es una migración realista: el equipo puede probar la nueva implementación en producción, medir errores y rendimiento, y volver atrás en segundos si algo falla. Sin “big bang”, sin congelar el desarrollo.

UIKit + SwiftUI: convivir por pantallas

Ahora imagina que quieres modernizar la pantalla de Ajustes. El resto de la app sigue en UIKit, así que no puedes migrar todo de golpe. La estrategia es simple: presentas una pantalla SwiftUI desde un flujo UIKit y dejas el resto intacto.

En el ejemplo siguiente, SettingsView es SwiftUI y se presenta desde un UINavigationController existente. Esto permite empezar la migración sin tocar el resto del flujo:

struct SettingsView: View {

    let onClose: () -> Void

    var body: some View {
        NavigationStack {
            List {
                Section("Cuenta") {
                    NavigationLink("Perfil") {
                        Text("Perfil en SwiftUI")
                    }
                    NavigationLink("Privacidad") {
                        Text("Privacidad en SwiftUI")
                    }
                }
            }
            .navigationTitle("Ajustes")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Cerrar", action: onClose)
                }
            }
        }
    }
}
final class SettingsCoordinator {

    private let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let view = SettingsView { [weak self] in
            self?.navigationController.dismiss(animated: true)
        }

        let controller = UIHostingController(rootView: view)
        navigationController.present(controller, animated: true)
    }
}

Y cuando una pantalla nueva en SwiftUI necesita reutilizar un componente antiguo (por ejemplo, un mapa legacy), puedes “envolver” ese UIViewController con UIViewControllerRepresentable y seguir avanzando sin reescribirlo:

struct LegacyMapContainer: UIViewControllerRepresentable {

    func makeUIViewController(context: Context) -> UIViewController {
        LegacyMapViewController()
    }

    func updateUIViewController(
        _ uiViewController: UIViewController,
        context: Context
    ) {}
}
struct NearbyStoresView: View {

    var body: some View {
        VStack(spacing: 0) {
            Text("Tiendas cercanas")
                .font(.headline)
                .padding()

            LegacyMapContainer()
                .edgesIgnoringSafeArea(.bottom)
        }
    }
}

Nota clave del patrón, en mobile, el boundary del Strangler Fig suele ser la pantalla, el flujo o el módulo, y el “router” suele vivir en:

  • Coordinators
  • Navegación
  • Feature flags/Remote config

Así evitamos reescrituras masivas y reducimos drásticamente el riesgo.

Claves para que el patrón funcione en Mobile

1. Define límites claros

Sin boundaries claros, el legacy se filtra y el patrón falla.

2. Acepta la convivencia temporal

Durante un tiempo tendrás dos formas de hacer lo mismo, y eso está bien.

3. Mide y valida constantemente

Cada paso debe ser:

  • Observable
  • Reversible
  • Entendible por el equipo

4. No todo debe migrarse

Algunas partes pueden quedarse legacy si:

  • Son estables
  • No aportan valor al cambio
  • El coste de migración no compensa

Riesgos y errores comunes

  • ❌ Empezar sin una estrategia clara
  • ❌ Mezclar responsabilidades sin boundaries
  • ❌ Migrar por moda y no por valor
  • ❌ Subestimar el coste cognitivo para el equipo

El Strangler Fig Pattern reduce el riesgo, pero no elimina la necesidad de buenas decisiones técnicas.


Conclusión

Modernizar una aplicación mobile no debería ser una apuesta de todo o nada. El Strangler Fig Pattern ofrece una vía pragmática, segura y probada para evolucionar código legacy sin frenar el negocio.

Ya sea migrando de Objective-C a Swift o de UIKit a SwiftUI, este patrón permite:

  • Reducir riesgos
  • Mantener entregas constantes
  • Mejorar la calidad del código a largo plazo

No se trata de destruir el árbol antiguo, sino de dejar que el nuevo crezca a su alrededor hasta que ya no lo necesitemos.


Referencias

  • Martin Fowler – Strangler Fig Application
    https://martinfowler.com/bliki/StranglerFigApplication.html
  • Microsoft Azure Architecture Center – Strangler Fig Pattern
    https://learn.microsoft.com/es-es/azure/architecture/patterns/strangler-fig
  • TechTarget – A detailed intro to the strangler pattern
    https://www.techtarget.com/searchapparchitecture/tip/A-detailed-intro-to-the-strangler-pattern