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
Define un boundary claro
- Nuevas features → Swift
- Código legacy → Objective-C
- Comunicación a través de interfaces bien definidas
Introduce Swift de forma incremental
- Nuevos módulos o frameworks escritos en Swift
- Uso controlado del
@objcy@objcMemberssolo donde sea necesario
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:
UIHostingControllerpara introducir SwiftUIUIViewControllerRepresentablepara reutilizar UIKit existente
Estrategia típica
Nuevas pantallas en SwiftUI
- Onboarding
- Ajustes
- Features nuevas
Pantallas legacy permanecen en UIKit
- Flujos críticos
- Zonas con mucha deuda técnica
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