Migrar una app legacy a un stack moderno hoy en día

Migrar una app legacy —Web Forms, VB6, un monolito .NET Framework con quince años de parches— a un stack moderno es un trabajo que casi nadie quiere hacer y casi todos necesitan hacer. Con un agente de código de por medio (Claude Code, Copilot, Cursor, el que uses), ese trabajo cambia de escala: lo que antes tomaba meses de reescritura manual se puede completar en semanas. Pero acelerar una migración mal planificada solo te hace llegar más rápido a un sistema nuevo con los mismos problemas de siempre, escrito en un lenguaje moderno.
Esto es lo que haría yo, hoy en día, si me tocara encarar esa migración: no una lista de prompts, sino el orden en que conviene tomar las decisiones — que es lo que separa una migración que deja el sistema mejor de una que solo lo deja distinto.
1. Mapear el dominio antes de tocar una línea
Antes de pedirle al agente que traduzca código, hay que entender qué hace ese código — no cómo está escrito, sino qué proceso de negocio resuelve. Un formulario de Web Forms con seis eventos de postback y lógica repartida entre el code-behind, un par de stored procedures y un archivo de JavaScript de 2011 no se migra leyendo el archivo de arriba hacia abajo.
Acá conviene usar la IA al revés de cómo se la usa habitualmente: no para escribir código nuevo, sino para leer el viejo y resumir qué hace. Pedirle que documente cada pantalla, cada regla de validación y cada efecto secundario antes de escribir una sola línea del sistema nuevo. Esa documentación —aunque después se descarte— es lo que permite migrar el comportamiento real del legacy, no la interpretación literal del código que lo implementaba.
2. Migrar por capas, no por pantalla
La tentación es empezar por lo que se ve: convertir cada .aspx en un
componente de React y llamarlo migración. El problema es que la lógica de
negocio real casi nunca vive en la pantalla — vive salpicada en eventos de
UI, procedures de base de datos y algún Session["algo"] que nadie se
anima a tocar.
Conviene invertir el orden: primero el modelo de dominio y las reglas de negocio, expresadas como funciones puras y testeables, sin nada de UI alrededor. Recién después la capa de datos que las alimenta, y al final la interfaz. Migrada así, la lógica queda en un lugar que un agente de código puede leer y modificar con confianza, en vez de quedar otra vez escondida detrás de un framework distinto — que es la diferencia entre llegar a un stack moderno y llegar a un legacy nuevo con mejor tipografía.
3. Tests de caracterización antes de tocar nada
Sin una red de pruebas, migrar con IA es pedirle a alguien que traduzca un libro a un idioma que no domina y confiar en que no cambió el sentido. Y esa red no puede escribirse contra el comportamiento deseado —nadie lo tiene documentado del todo— sino contra el comportamiento real del legacy, tal cual es hoy, bugs incluidos.
Eso son los tests de caracterización: correr la app vieja con casos representativos, capturar la salida tal cual sale, y usar esa salida como referencia.
// Antes de migrar nada: se ejecuta contra el sistema viejo
// y guarda la salida real como referencia (golden master).
const salidaVieja = await sistemaViejo.calcularFactura(pedido123);
guardarComoReferencia("factura-pedido-123", salidaVieja);
// Después de cada paso de la migración: compara contra esa referencia.
const salidaNueva = await sistemaNuevo.calcularFactura(pedido123);
assert.deepEqual(salidaNueva, referencia("factura-pedido-123"));
Un agente de código puede generar decenas de estos casos a partir de datos reales (anonimizados) mucho más rápido que una persona escribiéndolos a mano. Lo que no puede decidir solo es cuáles casos importan — esa parte sigue siendo trabajo de quien conoce el negocio.
4. Convivencia: strangler fig, no big-bang
Reescribir todo de una y recién ahí apagar el sistema viejo suena prolijo en un diagrama y sale mal casi siempre: el día del corte aparecen los casos que nadie migró, y no hay forma de volver atrás sin perder días de datos.
El patrón que funciona es el strangler fig, llamado así por la higuera que crece alrededor de un árbol, lo va envolviendo y termina ocupando su lugar sin que haya un momento en el que no haya árbol. Acá es igual: el sistema nuevo convive con el legacy, y el tráfico se redirige ruta por ruta, pantalla por pantalla, a medida que cada parte queda migrada y validada. Un proxy o un router al frente decide, por endpoint, a cuál de los dos sistemas mandar cada pedido.
Si algo migrado falla, el remedio es apagar esa ruta puntual y volver a mandarla al sistema viejo — no dar marcha atrás con toda la migración. Cada paso queda chico, reversible y fácil de revisar, que es exactamente el tamaño de tarea en el que un agente de código rinde mejor: contexto acotado, criterio de terminado claro, y una prueba que dice si funcionó.
5. Arquitectura objetivo definida antes de empezar
Un agente de código sin arquitectura definida no toma decisiones malas: no toma ninguna decisión de conjunto, porque no tiene de dónde sacarla. Cada sesión resuelve el archivo que tiene enfrente de la forma que le parece razonable a esa sesión, y dos pantallas migradas en momentos distintos terminan con capas distintas, convenciones de nombres distintas y una manera distinta de manejar errores.
Antes de migrar el primer archivo, conviene tener escrito —en un documento corto, no en la cabeza de nadie— qué capas tiene el sistema nuevo, cómo se llaman las cosas, y qué patrón se usa para lo que se repite: manejo de errores, validación, acceso a datos. Ese documento es lo que se le pasa como contexto al agente en cada tarea, para que la arquitectura la decida una persona una sola vez, y no cincuenta sesiones distintas.
6. Revisión humana obligatoria en lógica de negocio crítica
No toda la app pesa igual. El cálculo de impuestos, la lógica de facturación o las reglas de permisos merecen que alguien lea línea por línea lo que salió, aunque los tests de caracterización pasen — un test prueba los casos que se te ocurrió escribir, no todos los que existen.
La regla práctica es clasificar cada módulo antes de migrarlo: lo mecánico (una grilla, un formulario de alta simple) se revisa por arriba; lo que mueve plata, decide permisos o toca datos sensibles se revisa con la misma seriedad que un cambio escrito por una persona nueva en el equipo — porque, en los hechos, es exactamente eso.
7. Documentar decisiones, no solo código
Cada elección que se toma durante la migración —por qué se sacó tal librería, por qué esta pantalla quedó con una estructura distinta al resto, por qué se decidió no migrar tal módulo todavía— se olvida en semanas si no queda escrita en algún lado. Un registro corto de decisiones (ADRs: qué se decidió, por qué, qué alternativas se descartaron) cuesta minutos escribir y ahorra horas más adelante, tanto a la próxima persona como a la próxima sesión del agente que tenga que tocar esa parte.
Es, además, lo único que evita repetir la historia: el legacy que estás migrando hoy también fue alguna vez un stack moderno, y lo que se perdió en el camino no fue el código sino el porqué de cada decisión.
Errores más caros
- Migrar todo de una. Sin pasos intermedios no hay forma de saber en qué momento se rompió algo, ni de volver atrás sin perder todo el trabajo.
- Empezar a migrar sin tests de caracterización. Sin ellos, cada cambio es un salto de fe: puede que funcione, puede que rompa un caso borde que nadie va a notar hasta producción.
- Dejar que el estilo varíe según qué sesión escribió cada archivo. Sin una arquitectura objetivo escrita, el stack moderno termina con la misma falta de consistencia que tenía el legacy, solo que en un lenguaje más nuevo.
- Migrar solo lo fácil. Es tentador quedarse con las pantallas simples y dejar la lógica más vieja y más crítica para el final. Eso es exactamente al revés: lo complejo es lo que más beneficio saca de tests y revisión temprana, y lo que más tiempo lleva descubrir bien.
El orden importa más que la herramienta
Migrar una app legacy con IA no es más rápido por default: lo es cuando el orden de las decisiones es el correcto. Mapear el dominio antes de traducir código, migrar por capas y no por pantalla, escribir tests de caracterización antes de tocar nada, y mantener ambos sistemas conviviendo mientras se migra en pasos chicos y reversibles.
La IA acelera cada uno de esos pasos — pero el orden lo sigue poniendo quien planifica la migración, no el agente. Llegar a un stack moderno es la parte fácil; llegar a uno que siga siendo mantenible dentro de cinco años es lo que se juega en estos siete pasos.
Comentarios
Iniciá sesión para comentar y dar "me gusta".
Todavía no hay comentarios. Sé la primera persona en escribir uno.