Sostenerla en el tiempo: el calendario, las migraciones y la deuda
Una aplicación publicada entra en un régimen que no se parece en nada al de su construcción: la plataforma saca una versión al año, el nivel de API objetivo se vuelve obligatorio con fecha, cada subida activa cambios de comportamiento que rompen exactamente lo que funcionaba, las bibliotecas se abandonan, el esquema de la base local acumula versiones y el equipo que escribió el código ya no está. Esta lección trata el mantenimiento como la disciplina de ingeniería que es: cómo se lee el calendario de la plataforma, cómo se sube el nivel objetivo sin apostar la reputación del producto, cómo se diseñan migraciones reversibles de datos y de bibliotecas, y cómo se convierte la deuda técnica en un presupuesto explícito en lugar de un lamento recurrente.
Hay una asimetría cruel en el desarrollo móvil que nadie menciona en los cursos: construir una aplicación es un proyecto con final, y mantenerla es una condición permanente. El primer año se escribe el producto; los siguientes siete se dedican a que el producto siga siendo el mismo mientras todo lo que hay debajo cambia. Cada otoño aparece una versión nueva del sistema con sus cambios de comportamiento. Cada verano el nivel de API objetivo mínimo para publicar actualizaciones sube un escalón y deja de ser opcional. Las bibliotecas que elegiste con criterio se declaran obsoletas, el lenguaje introduce una forma mejor de hacer lo que ya hacías, el compilador cambia de versión y arrastra al complemento, y el esquema de la base local acumula una decena de migraciones que nadie se atreve a tocar porque hay medio millón de dispositivos con datos reales dentro. Nada de esto es un accidente ni una mala racha: es el régimen normal de operación, y la diferencia entre un equipo que lo lleva con soltura y uno que vive en crisis permanente no está en la velocidad con que programan, sino en si tratan el calendario como una entrada planificable del trabajo o como una interrupción que siempre les pilla desprevenidos.
- Leer el calendario anual de la plataforma y traducirlo a trabajo planificado con meses de antelación.
- Subir el nivel de API objetivo con una estrategia de verificación por fases y una vía de retirada real.
- Diseñar migraciones de esquema, de biblioteca y de lenguaje que se puedan detener a mitad sin dejar el producto roto.
- Convertir la deuda técnica en un presupuesto explícito con criterios de admisión y de cobro.
El calendario que no controlas
El ciclo tiene una forma estable que conviene tener dibujada. A comienzos de año aparecen las primeras compilaciones para desarrolladores de la versión siguiente; en primavera se congela el comportamiento; en otoño se publica y empieza a llegar a dispositivos reales; y aproximadamente un año después de esa publicación, el nivel de API objetivo correspondiente pasa a ser el mínimo exigido para poder publicar actualizaciones en la tienda. Eso deja una ventana de unos dieciocho meses entre el primer aviso y la obligación, que suena a muchísimo tiempo y se consume entero porque nadie mira el asunto hasta que la consola de publicación empieza a mostrar la advertencia.
Conviene además tener presente que el calendario de la plataforma no es el único que corre. La tienda publica cambios de política con sus propias fechas, y esos cambios afectan a cosas que no están en el código: declaraciones de uso de datos, justificaciones de permisos delicados, requisitos de cuentas de desarrollador, obligación de ofrecer eliminación de cuenta desde la propia aplicación. Un equipo que solo vigila el nivel de API objetivo se queda a mitad, porque la retirada de una aplicación por incumplimiento de política es tan definitiva como la imposibilidad de publicar por nivel obsoleto, y llega con menos aviso.
// Tres cifras, tres significados y una sola de ellas es una promesa de comportamiento.
android {
compileSdk = 36 // contra que API compilo: siempre la mas reciente estable
defaultConfig {
minSdk = 24 // a quien excluyo: decision de negocio con datos de campo
targetSdk = 35 // que reglas acepto: la unica sujeta a calendario obligatorio
}
}
La lectura profesional de ese calendario tiene dos entradas distintas que no conviene mezclar. Los cambios que afectan a todas las aplicaciones se activan por versión del sistema, con independencia de tu nivel objetivo, y por tanto te alcanzan aunque no toques nada: si el sistema endurece una restricción de red, de almacenamiento o de trabajo en segundo plano para todo el mundo, tu aplicación lo nota cuando el usuario actualiza su teléfono. Los cambios que se activan al subir el nivel objetivo son voluntarios hasta que dejan de serlo, y son los peligrosos, porque llegan todos juntos el día que subes una cifra. Un equipo maduro mantiene dos listas separadas, revisa la primera en cuanto sale la versión preliminar y planifica la segunda como un proyecto con nombre propio.
La práctica que más sufrimiento ahorra es mantener una variante de compilación permanente con el nivel de API objetivo del año siguiente, ejecutando la suite completa en integración continua desde el primer día en que existe la versión preliminar. No se publica: solo informa. Cuando el cambio obligatorio llega, el equipo lleva meses viendo las pruebas rojas que produce y ha ido resolviéndolas de una en una, en lugar de descubrir cuarenta problemas simultáneos tres semanas antes de la fecha límite.
Subir el nivel objetivo sin apostar el producto
Subir esa cifra no es un cambio de configuración sino una migración con riesgo, y merece el tratamiento de una. El procedimiento que funciona tiene cuatro fases y no admite atajos. Primero se lee la lista de cambios de comportamiento de esa versión y se traduce cada uno a una pregunta concreta sobre el código propio: si cambia el modelo de acceso a almacenamiento, dónde escribimos ficheros; si cambia el arranque en segundo plano, qué trabajo iniciamos sin que el usuario esté delante; si cambian los intents implícitos, qué componentes exportamos. Segundo, se activa el cambio de forma selectiva en compilaciones internas para observar el efecto real antes de comprometerse. Tercero, se distribuye por fases con vigilancia de métricas. Y cuarto, se conserva la capacidad de volver atrás durante todo el proceso.
// Verificacion escalonada: el codigo tolera ambos regimenes durante la migracion.
val puedeProgramarExactas: Boolean =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
alarmManager.canScheduleExactAlarms()
} else {
true
}
if (puedeProgramarExactas) {
programarRecordatorioExacto()
} else {
// Degradacion honesta: aviso aproximado y explicacion en la interfaz.
programarRecordatorioAproximado()
registrarEvento("recordatorio_degradado")
}
La cifra mínima admitida merece su propia decisión y casi nunca la recibe. Subir el nivel mínimo elimina ramas de código, permite usar API nuevas sin comprobaciones y reduce la superficie de prueba, pero excluye a usuarios reales, y esa exclusión no se reparte al azar: recae sobre dispositivos viejos y baratos, es decir, sobre la gente con menos renta. La decisión correcta se toma con los datos de distribución del propio producto y no con los del ecosistema global, se acompaña de una estimación del ingreso y del uso que se pierde, y se comunica antes de aplicarla. Lo que no es defendible es subirla porque una dependencia nueva lo exige y descubrir después a quién se dejó fuera.
La distribución por fases es la herramienta de gestión de riesgo más infravalorada del ecosistema, y su valor solo se materializa si va acompañada de una métrica que decida. Publicar al uno por ciento sin saber qué número miraremos ni qué umbral nos hará detenerlo es teatro. La pareja mínima es tasa de sesiones sin fallos y tasa de éxito de la operación crítica del producto, ambas segmentadas por versión del sistema, con un umbral escrito antes de publicar. Y conviene recordar la asimetría del mecanismo: detener una distribución es instantáneo y gratuito, mientras que revertirla una vez que un usuario ha actualizado no existe como operación, porque la tienda no degrada versiones. Lo único que se puede hacer es publicar una corrección, que tarda lo que tarde en revisarse y en llegar. Diseñar con esa asimetría en mente significa poner banderas de funcionalidad remotas en todo lo que la subida de nivel objetivo pueda romper, para que apagar el camino nuevo no requiera publicar nada.
Los cambios de comportamiento que lanzan una excepción se detectan el primer día. Los que degradan en silencio —una alarma que ya no es exacta, un trabajo que ahora se aplaza horas, una notificación que ya no se muestra por falta de permiso, una escritura que va a un directorio distinto— no aparecen en ningún informe de fallos y se manifiestan semanas después como una caída lenta de la retención que nadie sabe atribuir. Por eso la lista de cambios de comportamiento se audita entera a mano, y por eso las métricas de producto importan tanto como las de estabilidad.
Migraciones: datos, bibliotecas y lenguaje
Las migraciones de esquema son las únicas de esta lección que no admiten fallo, porque un error borra datos de usuarios reales de forma irreversible. La disciplina mínima es exportar el esquema al repositorio, escribir una migración explícita por cada cambio, probar cada camino de versión a versión con datos poblados y no confiar nunca en la destrucción y recreación como salida. Y conviene recordar que los caminos posibles no son lineales: un usuario que no abre la aplicación en dos años salta de la versión tres a la nueve de una sola vez, de modo que la prueba que importa no es la de cada escalón sino la de cada origen posible hasta el destino actual.
val MIGRACION_7_8 = object : Migration(7, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE pedido ADD COLUMN cuentaId TEXT NOT NULL DEFAULT ''")
db.execSQL("UPDATE pedido SET cuentaId = (SELECT id FROM cuenta LIMIT 1)")
db.execSQL("CREATE INDEX IF NOT EXISTS idx_pedido_cuenta ON pedido(cuentaId)")
}
}
// La prueba que importa: desde cada version publicada hasta la actual, con datos dentro.
@Test
fun migracionDesdeCualquierVersionPublicada() {
for (origen in 3..8) {
helper.createDatabase(NOMBRE, origen).apply { poblarConDatosReales(this); close() }
helper.runMigrationsAndValidate(NOMBRE, 9, true, *TODAS_LAS_MIGRACIONES)
}
}
Merece la pena decir en voz alta lo que muchos equipos hacen y no admiten: usar la destrucción y recreación de la base local ante un fallo de migración. Es una decisión legítima solo si todo lo que hay dentro es caché reconstruible desde el servidor, y es una pérdida de datos silenciosa en cualquier otro caso. Como la frontera entre ambas situaciones se desplaza con cada funcionalidad nueva —el día que se añade un borrador sin enviar, la base deja de ser caché—, la política tiene que revisarse cada vez que se añade una tabla y no una sola vez al principio.
Las migraciones de lenguaje y de cadena de compilación tienen una tercera lógica y una trampa propia. Kotlin, el compilador de Compose, el complemento de Gradle y las herramientas de generación de código forman un conjunto acoplado en el que la versión de uno restringe la de los otros, de modo que actualizar no es una operación aislada sino un movimiento coordinado del bloque entero. La estrategia sana es actualizar el bloque completo con frecuencia y en pasos cortos, en su propio cambio, sin mezclarlo con trabajo funcional y con la suite completa como única prueba de aceptación. Los equipos que agrupan seis meses de actualizaciones en un único cambio gigante descubren siempre lo mismo: cuando algo se rompe, no hay forma de saber cuál de las quince actualizaciones fue.
Las migraciones de biblioteca siguen otra lógica: aquí el enemigo no es la pérdida de datos sino la migración a medias que se eterniza. La estrategia que termina es la de la fachada: se introduce una interfaz propia sobre la biblioteca antigua, se migra el código de llamada a esa interfaz, y solo entonces se sustituye la implementación de un golpe. La estrategia que no termina es la de migrar pantalla por pantalla usando las dos bibliotecas a la vez, porque el proyecto queda indefinidamente en un estado híbrido que duplica el conocimiento necesario para trabajar en él y que la primera urgencia congela para siempre. Si una migración no se puede completar en un trimestre, hay que dividirla en migraciones que sí, cada una con su estado final coherente.
flowchart TD
A[Version preliminar en invierno] --> B[Auditar cambios de comportamiento]
B --> C[Variante permanente con targetSdk nuevo en CI]
C --> D{Pruebas en verde}
D -->|no| E[Corregir de una en una durante meses]
E --> C
D -->|si| F[Distribucion interna con banderas remotas]
F --> G[Publicacion por fases al uno por ciento]
G --> H{Metricas dentro del umbral}
H -->|no| I[Detener y apagar la bandera]
I --> E
H -->|si| J[Ampliar por fases hasta el cien por cien]
J --> K[Retirar codigo del regimen antiguo]La deuda como presupuesto y no como lamento
Hay una última categoría de mantenimiento que ningún calendario anuncia y que llega igual: la rotación del equipo. El conocimiento de por qué algo está escrito como está no vive en el repositorio salvo que alguien lo escriba, y se evapora con cada persona que se va. Un producto de siete años suele haber renovado su equipo entero dos veces, de modo que la pregunta relevante no es si el código está bien escrito sino si alguien que llega hoy puede modificarlo con seguridad mañana. Contra eso solo funcionan dos cosas, y ninguna es un diagrama: pruebas que documenten el comportamiento esperado y decisiones escritas con su contexto y su fecha.
La deuda técnica se discute mal porque se discute en términos morales. No es un pecado ni una prueba de descuido: es un instrumento financiero, y como tal tiene principal, intereses y condiciones de amortización. Hay deuda deliberada y bien invertida —salir tres meses antes con un atajo consciente que capturó el mercado— y hay deuda involuntaria y tóxica —una decisión tomada sin entender sus consecuencias que ahora encarece todo lo que se toca. La única manera de gestionarla es hacerla explícita: registrarla como se registra cualquier otro trabajo, con una estimación del interés que paga el equipo cada mes en forma de lentitud, y con la condición concreta que la haría urgente.
Para que ese registro sea utilizable hace falta una taxonomía corta y estable, porque las categorías largas nadie las mantiene:
- Obligatoria por calendario. Nivel objetivo, política de la tienda, biblioteca sin soporte. No se prioriza: se planifica.
- De interés compuesto. Dependencias atrasadas y pruebas que faltan. Cada mes que pasa encarece el resto del trabajo.
- Localizada. Un módulo feo que nadie toca. Interés casi nulo mientras no se toque; se amortiza solo si aparece trabajo encima.
- Estructural. Una decisión de arquitectura equivocada que encarece todo. Cara de amortizar y catastrófica de ignorar.
Sobre el reparto hay dos escuelas y una sola respuesta que funciona. Un porcentaje fijo de la capacidad reservado a mantenimiento —entre el quince y el veinticinco por ciento según la edad del producto— sostenido durante trimestres, produce resultados que ninguna semana de limpieza anual consigue, porque la deuda se acumula de forma continua y solo se amortiza de forma continua. La partida debe cubrir tres frentes distintos que compiten entre sí: el calendario obligatorio de la plataforma, que no es negociable y siempre va primero; la actualización de dependencias, que es barata si se hace cada mes y carísima si se hace cada dos años; y la refactorización de lo que duele, que es lo único opcional y por tanto lo primero que se sacrifica cuando hay presión.
Calendario
Dos listas separadas: lo que te alcanza por versión del sistema y lo que se activa al subir el nivel objetivo. Se auditan en momentos distintos.
Variante futura
Una compilación permanente con el nivel objetivo del año siguiente en integración continua convierte una crisis anual en cuarenta correcciones repartidas.
Migraciones probadas
El camino que importa no es de la versión anterior a la actual, sino desde cualquier versión publicada hasta la actual, con datos reales dentro.
Presupuesto fijo
Un porcentaje sostenido de capacidad para mantenimiento gana siempre a la semana heroica de limpieza que se cancela cuando hay prisa.
Conviene enfrentarse a una idea que la cultura del oficio se resiste a admitir: escribir código nuevo es la parte pequeña y atípica de esta profesión, y sin embargo es la única para la que se entrena a la gente, la única que se celebra en las presentaciones y la única que aparece en las ofertas de empleo. Un producto que sobrevive pasa quizá el diez por ciento de su vida siendo construido y el noventa por ciento siendo sostenido, y esa proporción invertida explica por qué tantos equipos técnicamente brillantes producen resultados mediocres a los tres años. Han optimizado el diez por ciento. La razón de fondo es que el mantenimiento tiene una estructura de incentivos perversa: su éxito es invisible. Cuando un equipo sube el nivel objetivo a tiempo, migra sus bibliotecas y mantiene sus migraciones probadas, no ocurre absolutamente nada, y no ocurrir nada no genera reconocimiento, no aparece en ninguna métrica de negocio y no se recuerda en ninguna evaluación. Cuando ese mismo equipo lo desatiende, tampoco ocurre nada durante dieciocho meses, y después ocurre todo a la vez en forma de una crisis que sí es visible, sí moviliza recursos y paradójicamente sí genera reconocimiento para quien la resuelve. El sistema premia estructuralmente al bombero por encima del inspector de incendios, y ningún discurso sobre calidad corrige ese desequilibrio si no se acompaña de mecanismos concretos: presupuesto reservado que no se pueda canibalizar, calendario público de obligaciones de plataforma visible para quien decide prioridades, y una definición de terminado que incluya la capacidad de mantener lo entregado. Hay algo más profundo todavía. La longevidad de un sistema no depende de lo elegante que fuera su diseño inicial sino de lo barato que resulte cambiarlo, y esas dos propiedades no solo son distintas sino que a veces se oponen: un diseño muy ajustado a los requisitos de hoy es difícil de doblar mañana. Por eso el criterio del veterano no es cuánto le gusta un diseño, sino cuántas decisiones deja abiertas y con qué facilidad se pueden revisar cuando la realidad cambie. Programar para que otro pueda cambiarlo dentro de cinco años, sin conocerte y sin poder preguntarte, es la forma más alta del oficio, y es exactamente lo contrario de escribir el código más ingenioso posible.
- Escribe el calendario de los próximos veinticuatro meses con las fechas de versión preliminar, publicación y obligatoriedad del nivel objetivo.
- Crea una variante de compilación con el nivel objetivo del año siguiente, ejecútala en integración continua y documenta cada prueba que se rompe.
- Diseña una migración de esquema que añada una columna obligatoria y pruébala desde las cinco versiones anteriores con datos poblados.
- Elige una biblioteca de tu proyecto, introduce una fachada propia sobre ella y demuestra que sustituir la implementación toca un solo fichero.
- Registra la deuda técnica de un módulo con principal, interés mensual estimado y condición de urgencia, y defiende un presupuesto trimestral con esos números.