wandres.dev
NIVEL DIOS: SÍNTESIS · construir y sostener

Sostenerla en el tiempo: deuda, ciclo anual y código vivo

Escribir una app es un proyecto; mantenerla cinco años es una disciplina distinta con su propia teoría. Esta lección trata la deuda técnica como un instrumento financiero con intereses, el calendario anual de Apple como una restricción de planificación previsible, la matriz de versiones soportadas como una decisión de producto, y el borrado de código como la única operación de mantenimiento que nunca falla.

⏱ 21 min

Casi todo el material técnico que existe describe cómo construir algo que aún no existe. Sin embargo, la abrumadora mayoría del trabajo real de esta profesión consiste en modificar software que ya está en manos de otras personas, con datos que no puedes perder y con una historia que no escribiste. Ese trabajo tiene sus propias leyes y no son las de la construcción. La deuda técnica se comporta como un crédito con intereses compuestos que a veces conviene pagar y a veces conviene refinanciar. El calendario de Apple impone un pulso anual tan regular que puede planificarse con un año de antelación. La lista de versiones de iOS que soportas es una decisión de producto disfrazada de detalle técnico. Y la operación de mantenimiento con mejor retorno no es refactorizar: es borrar.

🎯 Al terminar esta lección sabrás
  • Distinguir deuda deliberada de deuda accidental y decidir con criterio cuándo se paga y cuándo se refinancia.
  • Planificar el año contra el ciclo previsible de Apple: junio, septiembre y las ventanas de riesgo que generan.
  • Elegir la matriz de versiones soportadas como decisión de producto medida con datos, no como reflejo o costumbre.
  • Aplicar las tres disciplinas que mantienen vivo el código a cinco años: contratos probados, dependencias contadas y borrado sistemático.

Conviene empezar por desmontar una metáfora perezosa. Se habla de deuda técnica como si fuera una mancha moral, algo sucio que el equipo debería avergonzarse de tener, y esa lectura impide gestionarla. La formulación original era financiera y precisa: contraer deuda es tomar prestada velocidad presente a cambio de pagar intereses futuros, y en ciertos contextos es una decisión racional. Nadie considera un fracaso pedir una hipoteca. El fracaso es no saber qué debes, a qué tipo de interés, ni cuándo vence.

Deuda deliberada, deuda accidental y refinanciación

La distinción operativa que sirve tiene dos ejes. Uno separa la deuda deliberada —sé que esto no es la solución correcta, la tomo por una razón y la anoto— de la accidental, que es la que se descubre después: código que era razonable cuando se escribió y dejó de serlo porque el mundo cambió. El otro eje separa la deuda que genera intereses altos —se toca cada semana, bloquea funciones nuevas, produce incidencias— de la que genera intereses casi nulos: un módulo feo que nadie abre desde hace dos años y funciona.

Cruzar ambos ejes da la política completa. La deuda cara se paga pronto y se paga entera. La deuda barata no se paga, y esa es la parte que cuesta aceptar a los equipos con vocación de limpieza: refactorizar un módulo estable que nadie toca consume el mismo tiempo que arreglar el que sí duele y no produce beneficio alguno, salvo el riesgo de introducir un fallo en algo que llevaba dos años sin dar problemas. Hay además una tercera vía, la refinanciación: no arreglar el código malo, sino aislarlo tras un protocolo del dominio para que deje de contaminar y pueda sustituirse cuando toque. Envolver es más barato que reescribir y convierte una deuda perpetua en una deuda con vencimiento.

// Refinanciar en lugar de reescribir: el codigo viejo sigue ahi,
// pero deja de ser visible para el resto del sistema.
protocol PasarelaDePagos: Sendable {
    func iniciarCompra(_ id: String) async throws -> Recibo
}

// Implementacion heredada, con toda su suciedad contenida.
// Marcada, medida y con fecha de vencimiento en el backlog.
struct PasarelaHeredada: PasarelaDePagos { /* ... */ }
💡
El registro de deuda que sí se usa

Una lista de deuda técnica sin cifras se ignora en dos semanas. La que funciona anota, por cada partida, tres datos: qué bloquea concretamente, cuántas veces se ha tocado en los últimos seis meses y qué costaría pagarla. La segunda columna es la clave, porque es el interés medido en lugar de opinado. Al ordenar por ella, la conversación deja de ser estética y pasa a ser económica, que es la única en la que un equipo y un responsable de producto pueden ponerse de acuerdo.

El pulso anual del ecosistema

El desarrollo en plataformas de Apple tiene una regularidad que ninguna otra plataforma ofrece y que muy pocos equipos aprovechan para planificar. En junio se anuncian las versiones nuevas y salen las primeras betas; en septiembre se publican junto con el hardware nuevo y la adopción es rapidísima comparada con cualquier otro ecosistema; entre medias hay un verano de betas donde tu app se rompe en formas que nadie documentó. Ese calendario no es una sorpresa anual: es una restricción tan previsible como una estación, y merece aparecer en la planificación con la misma antelación.

La política que evita el pánico tiene cuatro movimientos. En junio, compilar la app con el SDK nuevo la primera semana, no para adoptar nada, sino para inventariar qué se rompe y qué se ha marcado como obsoleto. En julio y agosto, arreglar solo lo que se rompe y probar en dispositivo real, resistiendo la tentación de adoptar todas las APIs nuevas en el mismo ciclo. En septiembre, publicar una versión compatible el día del lanzamiento, aunque no contenga ninguna función nueva. Y a partir de octubre, adoptar lo nuevo con criterio, cuando ya existe experiencia de campo sobre qué funciona.

flowchart LR
J[Junio: SDK nuevo y primer inventario de roturas] --> V[Verano: arreglar solo lo roto y probar en dispositivo]
V --> S[Septiembre: publicar version compatible el dia uno]
S --> O[Otono: adoptar APIs nuevas con criterio]
O --> I[Invierno: pagar deuda cara y subir la version minima]
I --> J
style J fill:#89b4fa,color:#11111b
style S fill:#a6e3a1,color:#11111b
style I fill:#fab387,color:#11111b

La tentación que hay que resistir tiene nombre: adoptar en septiembre todo lo anunciado en junio. Las APIs recién nacidas tienen fallos, cambian de comportamiento en las primeras revisiones puntuales y su documentación tarda meses en reflejar la realidad. Un equipo que adopta el primer día se convierte en el departamento de control de calidad de Apple sin cobrar por ello. La excepción legítima es cuando la novedad es el producto —una app cuyo valor es precisamente estar en lo último—; ahí el coste se asume a conciencia y se compensa con capacidad de publicar arreglos rápido.

La matriz de versiones es una decisión de producto

Elegir la versión mínima de iOS que soportas parece un ajuste y es, en realidad, un intercambio explícito entre alcance de mercado y coste de mantenimiento. Cada versión antigua que sostienes multiplica la matriz de pruebas, te prohíbe usar APIs modernas o te obliga a escribir dos caminos y, sobre todo, consume atención en dispositivos que a menudo representan una fracción minúscula de tus ingresos. La decisión debe tomarse con tus datos, no con estadísticas globales: las métricas de App Store Connect te dicen qué versiones usa tu población concreta.

La práctica sana consiste en subir la versión mínima una vez al año, en la ventana de invierno, cuando la adopción de la versión de septiembre ya se ha estabilizado, y en anunciarlo antes en las notas de la versión. Sostener dos versiones mayores hacia atrás es generoso; sostener tres suele ser nostalgia. Y hay un matiz que se olvida: soportar una versión antigua no significa ofrecer en ella todas las funciones. Una función nueva puede exigir el sistema más reciente mientras la app sigue funcionando en el anterior, y esa asimetría es la que permite avanzar sin abandonar a nadie.

// Degradar por disponibilidad, no bifurcar la arquitectura entera.
struct PanelDeResumen: View {
    var body: some View {
        if #available(iOS 26, *) {
            ResumenConNovedades()
        } else {
            ResumenClasico()      // camino estable, sin funciones nuevas
        }
    }
}

Lo importante de ese patrón no es la sintaxis sino dónde se coloca. Las comprobaciones de disponibilidad dispersas por decenas de archivos son deuda accidental garantizada, porque cuando por fin subas la versión mínima nadie sabrá dónde están todas. Concentradas en unos pocos puntos de composición, subir la versión mínima se convierte en borrar ramas, que es una tarde de trabajo y no un proyecto.

⚠️
La deuda de dependencias es la que más sorprende

Cada biblioteca de terceros es una promesa ajena de seguir manteniéndose. Cuando su autor abandona, hereda su trabajo quien la usó. La política razonable es contar las dependencias, exigir a cada una una justificación proporcional a su tamaño, preferir las que envuelven algo que sabrías escribir en una semana y desconfiar de las que se meten en el modelo de datos o en la capa de red, porque son las más caras de extirpar. Una dependencia que solo te ahorra código bonito no vale su riesgo a cinco años.

El código vivo: contratos, borrado y memoria del equipo

Queda la parte que decide de verdad si un proyecto llega vivo al quinto año, y son tres disciplinas poco glamurosas. La primera son los contratos probados. Un proyecto largo no puede sostenerse sobre pruebas que afirman detalles de implementación, porque esas pruebas se borran a la primera refactorización y con ellas se va la red de seguridad. Lo que sobrevive son las pruebas del dominio y de las fronteras: dado este estado y esta acción, este resultado. Esas afirmaciones siguen siendo verdad cuando cambia el framework de interfaz, el motor de persistencia o el patrón de presentación, y son las que permiten cambiar cosas grandes sin miedo.

La segunda es el borrado. Es la única operación de mantenimiento con retorno garantizado: el código que no existe no tiene fallos, no se compila, no se prueba y no se migra. Todo producto acumula funciones que nadie usa, banderas que llevan dos años en el mismo valor, pantallas que sobrevivieron a un experimento y capas de compatibilidad de versiones ya abandonadas. Medir el uso real de cada función y eliminar sin ceremonia las muertas reduce la superficie de mantenimiento más que cualquier refactorización, y sin embargo casi ningún equipo lo hace de forma sistemática porque borrar no luce en una demostración.

La tercera es la memoria del equipo. A cinco años, quien escribió una decisión ya no está, y el código explica el qué pero nunca el por qué. Un registro breve de decisiones —una página por decisión irreversible, con el contexto, las alternativas descartadas y las consecuencias aceptadas— es lo que impide que la generación siguiente deshaga por ignorancia algo que se decidió por una razón excelente. Es la diferencia entre un código con historia y un código con arqueología.

🔱

Deuda con intereses

Se ordena por veces que se toca, no por fealdad. La cara se paga, la barata se ignora y la contaminante se envuelve.

📅

Pulso anual

Compilar con el SDK nuevo en junio, publicar compatible en septiembre, adoptar en otoño, subir la versión mínima en invierno.

📉

Versiones soportadas

Decisión de producto con tus datos. Dos versiones atrás es generoso; tres es nostalgia. Concentra las comprobaciones.

🗑️

Borrar

La única operación de mantenimiento con retorno garantizado. Lo que no existe no falla, no se prueba y no se migra.

Un proyecto de cinco años no se mantiene: se hace mantenible una decisión cada vez

Existe una ilusión de perspectiva que arruina a los equipos en el año dos y conviene desmontarla del todo. Se imagina el mantenimiento como una actividad futura y separada —primero se construye, luego se mantiene— cuando en realidad la mantenibilidad no es una fase ni una propiedad que se añada, sino la suma acumulada de miles de decisiones locales tomadas mientras se construía, cada una de las cuales fijó cuánto costaría cambiar de opinión más tarde. Un tipo del dominio que no depende de ningún framework hace que el cambio de framework sea posible dentro de tres años; el mismo tipo con anotaciones de persistencia clavadas encima lo hace imposible sin migración. Una prueba que afirma un comportamiento sobrevive a diez refactorizaciones; una que afirma una implementación muere en la primera y deja el terreno sin cobertura precisamente donde más falta hará. Una comprobación de disponibilidad concentrada en un punto de composición se borra en una tarde; cuarenta esparcidas se convierten en un trimestre de arqueología. Y una función que se elimina cuando se comprueba que nadie la usa deja de consumir atención para siempre, mientras que la misma función conservada por sentimentalismo cobrará su peaje en cada migración de esquema, cada actualización de SDK y cada auditoría de accesibilidad durante el resto de la vida del producto. De ahí se sigue la reformulación que cambia la conducta diaria: la pregunta útil ante cualquier línea nunca es qué es más limpio, que es una discusión sin árbitro, sino qué le costará a la persona que herede esto cambiar de opinión. Esa pregunta ordena las prioridades sin necesidad de doctrina, explica por qué las fronteras importan más que los patrones, por qué borrar rinde más que pulir y por qué el registro escrito de una decisión vale más que el código elegante que la implementa. Los proyectos que llegan sanos al quinto año casi nunca son los que se escribieron con más cuidado estético; son aquellos en los que alguien, sistemáticamente y sin heroísmo, mantuvo bajo el coste de cambiar de opinión.

📝
Lo esencial

La deuda es financiera, no moral: se clasifica en deliberada o accidental y se ordena por interés real —veces que se toca—, se paga la cara, se ignora la barata y se refinancia la contaminante envolviéndola tras un protocolo. El año tiene un pulso previsible: SDK nuevo en junio, arreglar solo lo roto durante el verano, publicar compatible en septiembre, adoptar en otoño y subir la versión mínima en invierno. La matriz de versiones es una decisión de producto con datos propios, con las comprobaciones de disponibilidad concentradas. Y lo que mantiene vivo el código a cinco años son contratos probados, borrado sistemático y un registro escrito del porqué de las decisiones irreversibles.

⚔️ Audita la salud a cinco años
  1. Construye el registro de deuda de tu proyecto con las tres columnas —qué bloquea, veces tocado en seis meses, coste de pago— y ordénalo por la segunda. Compara ese orden con el que habrías propuesto de memoria.
  2. Elige la partida más contaminante y diseña su refinanciación: qué protocolo del dominio la aislaría y qué se podría sustituir después sin tocar a quien la usa.
  3. Reconstruye el último ciclo de junio a septiembre de tu equipo y anota en qué punto del calendario ideal se desvió y qué costó esa desviación.
  4. Saca de App Store Connect el reparto real de versiones de tu población, decide tu versión mínima con ese dato y localiza en el código cuántas comprobaciones de disponibilidad tendrías que borrar al subirla.
  5. Identifica tres funciones cuyo uso real desconozcas, instrumenta su medición y comprométete a eliminar la menos usada si no supera un umbral que fijes por escrito hoy.