Negocios 9 min de lectura

Legacy Code: El monstruo que te da de comer

Publicado el 10 de octubre de 2025

Legacy Code: El monstruo que te da de comer

Todos tenemos ese código. Ese código que escribiste hace años, cuando eras menos experimentado, cuando las prioridades eran diferentes. Ese código que hoy te da vergüenza, que viola todos los principios que ahora conoces, pero que sigue funcionando en producción, generando ingresos, sirviendo a clientes.

Ese es el legacy code. Y es el monstruo que te da de comer.

Todos los proyectos tienen legacy code. Código que funciona pero que no seguirías escribiendo hoy. Código que te da vergüenza pero que sigue generando valor.

En este artículo, compartiré cómo gestionar legacy code de manera profesional: cuándo refactorizar, cuándo dejar en paz, y cómo trabajar con código que no te enorgullece pero que sigue siendo valioso.

¿Qué es Legacy Code?

Legacy code no es solo código viejo. Es código que:

  • Funciona en producción y genera valor
  • Es difícil de entender o modificar
  • No tiene tests (o tiene tests pobres)
  • Usa tecnologías o patrones obsoletos
  • Fue escrito cuando los requisitos eran diferentes

Pero sigue funcionando. Y eso es lo importante.

La paradoja del Legacy Code

El legacy code es una paradoja:

  • Te da vergüenza: Sabes que no es tu mejor trabajo
  • Te da de comer: Genera ingresos, sirve a clientes
  • Quieres reescribirlo: Pero reescribir es riesgoso y costoso
  • Tienes que mantenerlo: Pero mantenerlo es difícil y frustrante

Esta tensión es real, y gestionarla correctamente es crucial para el éxito de tu proyecto.

¿Cuándo refactorizar? ¿Cuándo dejar en paz?

No todo legacy code necesita ser refactorizado. A veces, el mejor código es el código que no tocas.

Deja en paz cuando:

  1. Funciona perfectamente: Si no hay bugs y no necesitas agregar features, déjalo en paz
  2. No hay cambios planeados: Si no vas a modificar esa parte, no hay razón para refactorizar
  3. El costo es alto: Si refactorizar es más costoso que mantener, no lo hagas
  4. Está aislado: Si el código problemático está aislado y no afecta otras partes, déjalo

Refactoriza cuando:

  1. Necesitas agregar features: Si necesitas modificar código legacy frecuentemente, refactoriza
  2. Hay bugs frecuentes: Si el código legacy causa bugs constantemente, refactoriza
  3. Está bloqueando desarrollo: Si el código legacy hace difícil agregar nuevas features, refactoriza
  4. Puedes hacerlo incrementalmente: Si puedes refactorizar sin riesgo, hazlo

Estrategias para trabajar con Legacy Code

1. Estrategia del “Strangler Fig”

En lugar de reescribir todo de una vez, reemplaza partes gradualmente.

// ❌ Antes: Todo en un archivo legacy
class LegacyOrderProcessor {
    processOrder(order) {
        // 500 líneas de código legacy
        // ...
    }
}

// ✅ Después: Extraer partes gradualmente
class LegacyOrderProcessor {
    constructor() {
        this.validator = new OrderValidator();  // Nuevo
        this.calculator = new PriceCalculator(); // Nuevo
    }
    
    processOrder(order) {
        // Validación nueva
        this.validator.validate(order);
        
        // Cálculo nuevo
        const total = this.calculator.calculate(order);
        
        // Lógica legacy (aún)
        // ... código legacy que funciona
    }
}

// Eventualmente, reemplazar completamente
class OrderProcessor {
    processOrder(order) {
        this.validator.validate(order);
        const total = this.calculator.calculate(order);
        // Nueva implementación limpia
    }
}

Ventajas:

  • Riesgo bajo: Cambios pequeños y graduales
  • Sin downtime: El sistema sigue funcionando
  • Aprendizaje: Entiendes el código legacy mientras lo reemplazas

2. Agregar tests antes de refactorizar

Antes de tocar código legacy, agrega tests que verifiquen el comportamiento actual.

// Código legacy (sin tests)
function calculateTotal(items, discount) {
    // Lógica compleja y difícil de entender
    let total = 0;
    for (let i = 0; i < items.length; i++) {
        total += items[i].price * items[i].quantity;
    }
    if (discount > 0) {
        total = total * (1 - discount);
    }
    return Math.round(total * 100) / 100;
}

// ✅ Agregar tests primero
describe('calculateTotal', () => {
    it('should calculate total without discount', () => {
        const items = [
            { price: 10, quantity: 2 },
            { price: 5, quantity: 1 }
        ];
        expect(calculateTotal(items, 0)).toBe(25);
    });
    
    it('should apply discount correctly', () => {
        const items = [{ price: 100, quantity: 1 }];
        expect(calculateTotal(items, 0.1)).toBe(90);
    });
    
    // Más tests para cubrir edge cases
});

// Ahora puedes refactorizar con confianza
function calculateTotal(items, discount) {
    const subtotal = items.reduce(
        (sum, item) => sum + (item.price * item.quantity),
        0
    );
    const discountedTotal = subtotal * (1 - discount);
    return Math.round(discountedTotal * 100) / 100;
}

Ventajas:

  • Seguridad: Los tests te alertan si rompes algo
  • Documentación: Los tests documentan el comportamiento esperado
  • Confianza: Puedes refactorizar sin miedo

3. Aislar código legacy

Si no puedes refactorizar, al menos aísla el código legacy del resto de la aplicación.

// ❌ Mal: Código legacy mezclado con nuevo código
class UserService {
    createUser(userData) {
        // Código nuevo y limpio
        const user = this.validateAndCreate(userData);
        return user;
    }
    
    // Código legacy mezclado
    processLegacyUser(legacyData) {
        // 200 líneas de código legacy
        // ...
    }
}

// ✅ Bien: Código legacy aislado
class UserService {
    createUser(userData) {
        const user = this.validateAndCreate(userData);
        return user;
    }
    
    processLegacyUser(legacyData) {
        // Delegar a clase legacy aislada
        return LegacyUserProcessor.process(legacyData);
    }
}

// Código legacy en su propio módulo
class LegacyUserProcessor {
    static process(legacyData) {
        // Todo el código legacy aquí
        // Aislado del resto de la aplicación
    }
}

Ventajas:

  • Separación clara: Código legacy no contamina código nuevo
  • Fácil de encontrar: Sabes dónde está el código legacy
  • Fácil de reemplazar: Cuando estés listo, reemplazas el módulo completo

4. Documentar comportamiento, no implementación

Documenta qué hace el código legacy, no cómo lo hace (especialmente si es confuso).

/**
 * Calcula el precio total de un pedido usando la lógica legacy.
 * 
 * NOTA: Esta función usa lógica legacy que no sigue nuestros estándares actuales.
 * No modificar sin agregar tests primero. Ver: tests/legacy/order-pricing.test.js
 * 
 * @param {Order} order - El pedido a procesar
 * @returns {number} El precio total calculado
 * 
 * Comportamiento esperado:
 * - Aplica descuentos en orden específico (no conmutativo)
 * - Redondea a 2 decimales usando método legacy
 * - Maneja edge cases de manera específica (ver tests)
 */
function calculateLegacyPrice(order) {
    // Implementación legacy (no documentar detalles internos)
    // ...
}

Estrategias en la práctica

Código que funciona pero necesita mejoras

Cuando tienes código que funciona pero que no seguirías escribiendo hoy:

Estrategia: No lo toques a menos que necesites agregar features. Cuando necesites modificarlo, primero agrega tests, luego refactoriza incrementalmente.

Resultado: El código sigue funcionando, y gradualmente se vuelve mejor.

Lógica compleja pero funcional

Cuando tienes lógica compleja que funciona pero es difícil de mantener:

Estrategia: Usa la estrategia del Strangler Fig. Extrae partes a nuevas clases, pero mantén la lógica legacy funcionando.

Resultado: El sistema es más mantenible sin riesgo de romper funcionalidad existente.

Código crítico (finanzas, salud)

Cuando tienes código crítico que no puedes permitirte romper:

Estrategia: Agrega tests exhaustivos primero, luego refactoriza muy gradualmente.

Resultado: Código más seguro y mantenible, sin riesgo de romper funcionalidad crítica.

Errores comunes con Legacy Code

1. Reescribir todo de una vez

// ❌ Mal: "Voy a reescribir todo este módulo"
// - Alto riesgo
// - Tiempo largo sin features nuevas
// - Posibilidad de introducir bugs

// ✅ Bien: Refactorizar incrementalmente
// - Bajo riesgo
// - Features nuevas continúan
// - Bugs se detectan temprano

2. Ignorar código legacy completamente

// ❌ Mal: "No voy a tocar ese código nunca"
// - Se vuelve más difícil de mantener con el tiempo
// - Bloquea desarrollo futuro
// - Acumula deuda técnica

// ✅ Bien: Refactorizar cuando tenga sentido
// - Mantiene código mantenible
// - Facilita desarrollo futuro
// - Reduce deuda técnica gradualmente

3. Refactorizar sin tests

// ❌ Mal: Refactorizar código legacy sin tests
// - No sabes si rompiste algo
// - Miedo constante
// - Bugs en producción

// ✅ Bien: Tests primero, luego refactorizar
// - Sabes si rompiste algo
// - Confianza al refactorizar
// - Bugs detectados temprano

La mentalidad correcta

Trabajar con legacy code requiere una mentalidad específica:

1. Acepta que existe

No te avergüences del legacy code. Es parte del proceso. Todo código se vuelve legacy eventualmente.

2. Valora lo que funciona

El legacy code funciona. Eso tiene valor. No lo descartes solo porque no es perfecto.

3. Mejora gradualmente

No necesitas arreglar todo de una vez. Mejora gradualmente, cuando tenga sentido.

4. Prioriza impacto

Refactoriza código que:

  • Se modifica frecuentemente
  • Causa bugs
  • Bloquea desarrollo

No refactorices código que:

  • Funciona perfectamente
  • No se modifica
  • Está aislado

Mi perspectiva personal

Legacy code es el monstruo que te da de comer. Es código que no te enorgullece, pero que genera valor. Gestionarlo correctamente es crucial.

He trabajado con legacy code en proyectos de todos los tamaños. Código que funciona pero que no seguirías escribiendo hoy. Código que te da vergüenza pero que sigue generando valor.

He visto proyectos que intentaron reescribir todo de una vez, resultando en meses sin features nuevas y bugs en producción. He visto proyectos que ignoraron el legacy code completamente, resultando en código cada vez más difícil de mantener.

No intentes reescribir todo de una vez. No ignores el legacy code completamente. En su lugar:

  • Refactoriza cuando tenga sentido: Cuando necesites modificar código, cuando cause problemas, cuando bloquee desarrollo
  • Agrega tests primero: Antes de tocar código legacy, agrega tests que verifiquen el comportamiento
  • Refactoriza incrementalmente: Usa la estrategia del Strangler Fig para reemplazar partes gradualmente
  • Aísla cuando no puedas refactorizar: Si no puedes refactorizar, al menos aísla el código legacy

El legacy code no es tu enemigo. Es parte de tu código. Trátalo con respeto, mejóralo cuando tenga sentido, y no te avergüences de él. Al final del día, es el código que te da de comer, y eso tiene valor.