Refactorización de código legado: cómo cambiar código viejo sin riesgo
Cómo refactorizar código legado con seguridad: pruebas de caracterización, objetivos por riesgo, pasos pequeños y reversibles, y cuándo migrar por partes.

En esta página · 6 secciones
- ¿Qué hace que el código legado sea difícil de refactorizar?
- ¿Cómo agregas pruebas antes de tocar nada?
- ¿Por dónde deberías empezar a refactorizar?
- ¿Cómo refactorizas en pasos pequeños mientras lanzas funcionalidades?
- ¿Cuándo debería la refactorización dar paso a una migración?
- ¿Quieres una segunda opinión sobre tu base de código?
Puntos clave
- Refactorizar significa cambiar la estructura, no el comportamiento. Si el comportamiento cambia, es una funcionalidad o una corrección de bug, y debe ir en un commit aparte.
- Escribe primero pruebas de caracterización. Registran lo que el código hace hoy, incluso cuando ese comportamiento parece incorrecto.
- Empieza donde los cambios son frecuentes y riesgosos, no donde el código es más feo.
- Trabaja en pasos pequeños que puedan fusionarse y revertirse por separado. Las ramas de refactorización de larga duración son donde mueren los proyectos.
- Si el stack mismo es el problema (runtime sin soporte, nadie disponible para contratar, ninguna forma de probar), deja de refactorizar en el lugar y pasa a una migración incremental con un interruptor delante de todo.
La refactorización de código legado es segura cuando cambias la estructura del código sin cambiar lo que hace, y puedes demostrarlo con pruebas antes de tocar nada. El orden es: fijar el comportamiento actual, elegir las partes que más duelen, cambiarlas en pasos pequeños y reversibles, y seguir lanzando funcionalidades mientras tanto.
Sale mal cuando los equipos ordenan código que no entienden, sin red de seguridad, en una sola rama gigante.
¿Qué hace que el código legado sea difícil de refactorizar?
El código legado es difícil de cambiar porque nadie puede predecir qué romperá un cambio; la fealdad es secundaria. En las bases de código que he tomado, aparecen siempre las mismas cosas:
- No hay pruebas, o hay pruebas que no prueban nada. No puedes saber si un cambio preservó el comportamiento.
- Acoplamiento oculto. Una función que parece autocontenida lee una variable global, escribe en una tabla compartida o depende del orden de las llamadas.
- Comportamiento que nadie documentó. Las condiciones de aspecto raro a menudo codifican un caso real de un cliente o un incidente pasado. Borrarlas como código muerto es un error clásico.
- Falta de contexto. El desarrollador original o la agencia ya no está, y el historial de commits dice cosas como “fix” y “update”.
- Flujos de dinero mezclados con todo lo demás. La lógica de facturación, checkout o permisos está enredada con el código de UI o de datos, así que una edición pequeña tiene un radio de impacto grande.
El primer objetivo no es limpiar nada, sino hacer que verificar un cambio sea barato: si no puedes correr el proyecto localmente, compilarlo de forma reproducible y ver el resultado de una prueba en minutos, arregla eso primero. Cuando tomo una base de código heredada, sigo siempre el mismo orden: primero los accesos, luego el build, luego los flujos de dinero, luego los riesgos. La refactorización empieza cuando los dos primeros están sólidos.
¿Cómo agregas pruebas antes de tocar nada?
Agregas pruebas de caracterización: pruebas que capturan lo que el código realmente hace ahora, no lo que debería hacer. Michael Feathers popularizó este enfoque en Working Effectively with Legacy Code.
Empieza por fuera, no por dentro
Las pruebas unitarias sobre partes internas enredadas son lentas de escribir y frágiles. Empieza en un límite que puedas observar:
- Un endpoint HTTP: envía una solicitud, registra la respuesta.
- Un estado de la base de datos: ejecuta una operación, haz aserciones sobre las filas que cambiaron.
- Una página renderizada o un flujo de punta a punta: registrarse, pagar el carrito, postularse, pagar.
Estas pruebas son gruesas, pero detectan cambios que los usuarios pueden ver. Siempre cubro primero los flujos de dinero: checkout, facturación, registro, cualquier cosa que mueva fondos o deje a la gente sin acceso.
Registra y luego haz la aserción
El proceso es simple:
- Llama al código con entradas realistas.
- Anota la salida, aunque parezca incorrecta.
- Convierte esa salida en una aserción.
- Repite con los casos límite que encuentres en logs, tickets de soporte y reportes de bugs antiguos.
Si encuentras un bug real mientras haces esto, anótalo, deja la prueba verificando el comportamiento actual y corrígelo después en su propio commit. Mezclar correcciones de bugs en una refactorización hace imposible saber qué cambió.
Usa producción como fuente de datos de prueba
Las muestras sanitizadas de solicitudes de producción muestran los casos que el código realmente maneja. Quita los datos personales antes de que lleguen a un fixture.
Rompe dependencias solo lo necesario
Cuando el código es imposible de probar porque llama a una API de pagos o al reloj del sistema, introduce una costura delgada: pasa la dependencia como parámetro o envuélvela en una función pequeña que puedas reemplazar en las pruebas. Mantén estos cambios de costura mínimos y mecánicos, ya que son el único tipo de edición que haces antes de tener pruebas.
¿Por dónde deberías empezar a refactorizar?
Empieza donde el código cambia a menudo y los errores son caros. El código feo que nadie toca puede seguir siendo feo.
Cuando ordeno los candidatos, estas son las señales que miro, más o menos en este orden:
- Frecuencia de cambio. El historial del control de versiones muestra qué archivos se editan en cada sprint; una estructura desordenada ahí te cobra una y otra vez.
- Concentración de bugs. Los tickets de soporte y las notas de incidentes señalan los módulos que se rompen una y otra vez.
- Riesgo. Los flujos de dinero y la autenticación, donde las fallas cuestan dinero o confianza.
- Bloqueos del roadmap. Si los próximos tres elementos del roadmap tocan el mismo módulo, empieza ahí.
Mi regla práctica: si un módulo se cambia con frecuencia y además da miedo cambiarlo, va primero.
Por dónde no empezar
- Renombrar y reformatear toda la base de código. Genera diffs ruidosos y conflictos de merge, y no arregla nada.
- El código más antiguo, solo porque es el más antiguo.
- Código que planeas borrar pronto. No pulas lo que está por desaparecer.
La refactorización debe guiarse por evidencia sobre costo y riesgo, no por gusto.
¿Cómo refactorizas en pasos pequeños mientras lanzas funcionalidades?
Haces que cada paso de refactorización sea lo bastante pequeño para fusionarlo en la rama principal por sí solo, e intercalas esos pasos con el trabajo en funcionalidades en lugar de pausar el roadmap. En mi experiencia, los equipos que detienen todo para un “trimestre de refactorización” a menudo pierden la confianza de los stakeholders y terminan con una rama a medio terminar.
Aplica la regla de “déjalo mejor” en el punto de trabajo
Cuando una funcionalidad te obliga a tocar un módulo desordenado, dedica un esfuerzo acotado a hacer primero esa zona más fácil de cambiar y luego construye la funcionalidad. Kent Beck lo resume así: “haz que el cambio sea fácil, y luego haz el cambio fácil.” La funcionalidad justifica la refactorización, y el negocio ve ambas cosas.
Mantén los pasos reversibles
Los buenos pasos pequeños se ven así:
- Extrae una función, corre las pruebas, fusiona.
- Mueve una función a un módulo mejor, corre las pruebas, fusiona.
- Introduce una nueva implementación detrás de un flag, envíale un poco de tráfico, compara resultados, luego cambia y borra la antigua.
Ese último patrón es la forma más segura que conozco de cambiar lógica riesgosa: si la ruta nueva se comporta mal, normalmente puedes volver atrás sin un despliegue.
Separa los commits por intención
Mantén separados los commits de refactorización y los de cambio de comportamiento. Así, quien revisa puede leer un commit de refactorización sabiendo que nada debería cambiar, y una prueba que falla te avisa de inmediato que algo sí cambió.
Cómo se vio esto en mi equipo
En Inovaula construí la v2 de la plataforma web y luego lideré el equipo, y alcanzamos el flujo de entregas más estable que el equipo había tenido. Mirando atrás, se lo atribuyo menos a una gran limpieza que al hábito descrito arriba: cambios pequeños, revisados con cuidado, fusionados a menudo, con la estructura mejorando como parte del trabajo normal.
¿Cuándo debería la refactorización dar paso a una migración?
La refactorización mejora el código dentro de un stack, pero no puede arreglar un stack que es en sí mismo el obstáculo. Considera una migración cuando:
- El runtime, el framework o librerías clave ya no reciben actualizaciones de seguridad.
- No puedes contratar, o la incorporación tarda demasiado, por culpa de la tecnología.
- La arquitectura impide probar en absoluto, por ejemplo, lógica que vive en la base de datos o en plantillas sin costuras.
- Cada refactorización necesita tanto andamiaje que en la práctica ya estás reconstruyendo.
Aun así, prefiero la migración incremental a la reescritura big-bang. La regla que sigo: poner un interruptor delante de todo antes de escribir código nuevo. Una capa de enrutamiento, un proxy o feature flags te permiten enviar una porción del tráfico al sistema nuevo mientras el antiguo sigue atendiendo a todos los demás. Una reescritura suele costar más y tardar más, porque el sistema antiguo tiene que seguir funcionando en paralelo todo el tiempo.
En avanzzada lideré la migración completa de una plataforma que postula automáticamente a ofertas de empleo por sus usuarios. Toda la arquitectura pasó de un stack legado a uno nuevo mientras la gente seguía usándola todos los días. Lo que lo hizo viable fueron los hábitos de esta guía: saber qué hace el sistema actual, poner el interruptor primero y mover una pieza a la vez.
¿Quieres una segunda opinión sobre tu base de código?
Si tienes enfrente una base de código antigua y no sabes si refactorizar o migrar, escríbeme a me@filipeeduardo.dev con una descripción breve o un enlace, y te diré qué revisaría primero.
Preguntas frecuentes
¿Cuál es la diferencia entre refactorizar y reescribir?
Refactorizar cambia la estructura en pasos pequeños mientras el comportamiento se mantiene igual y el sistema sigue funcionando. Una reescritura reemplaza el código por completo, a menudo en paralelo, y normalmente tarda más y conlleva más riesgo, porque tienes que redescubrir cada comportamiento que manejaba el código antiguo.
¿Puedo refactorizar código legado sin pruebas?
Solo para los cambios mecánicos más pequeños, como renombrar con una herramienta en la que confías o agregar una costura. Para todo lo demás, escribe primero pruebas de caracterización, empezando por las rutas de mayor riesgo. Refactorizar a ciegas significa que te enterarás de las fallas por tus usuarios.
¿Cuánto tiempo debería dedicar un equipo a refactorizar?
No hay un número universal. Trátalo como parte del trabajo en funcionalidades, más trabajo dirigido en los módulos que causan incidentes repetidos, y evita proyectos de refactorización separados sin un resultado de negocio visible.
¿Debería usar herramientas de IA para refactorizar código legado?
Pueden ayudar con ediciones mecánicas y a explicar código desconocido, pero no conocen tus reglas de negocio ocultas. Trata lo que producen como el pull request de un junior: córrelo contra tus pruebas de caracterización y revisa cada diff antes de fusionarlo.
¿Cómo sé que la refactorización no rompió nada?
Las pruebas que pasaban antes siguen pasando después, más el monitoreo de producción. Para lógica riesgosa, compara la ruta antigua y la nueva detrás de un flag antes de hacer el cambio. Ningún proceso elimina el riesgo, pero este lo mantiene pequeño y visible.

