Nadie le había dicho la verdad a ese cliente en 4 años.
Yo fui el primero. Y casi lo pierdo por eso.
Me asignaron lo que parecía una tarea simple: agregar una funcionalidad nueva a una aplicación que llevaba años en producción. Estimación del equipo anterior: una semana. Lo que nadie me dijo es que ese equipo anterior había renunciado exactamente por este proyecto.
Abrí el repositorio y lo entendí de inmediato.
4 años de código sin documentación. Dependencias desactualizadas con vulnerabilidades conocidas. Lógica de negocio mezclada directamente en la UI. Un "fix rápido" aplicado encima de otro "fix rápido", 23 veces. El sistema funcionaba, pero de la misma forma en que funciona un edificio con grietas en los cimientos: mientras no lo toques.
Eso tiene nombre: deuda técnica. Y en este caso, el techo ya había colapsado.
Tuve que ser honesto con el cliente: no era posible agregar esa feature así. Primero había que sanear la base. Su respuesta me dejó frío:
"Pero nadie me había dicho eso antes."
Ahí estaba el problema real. No era un problema de código. Era un problema de comunicación. Cuatro años de equipos que veían el desastre pero nunca lo tradujeron a lenguaje de negocio. El cliente asumía que "el software es así".
No lo es.
Lo que siguió fue un proceso de 7 meses. La feature original se entregó en la semana 7 —después de 6 semanas rediseñando la arquitectura. Este artículo documenta ese proceso: los 5 pasos exactos que seguimos para salir de ese hoyo sin hundir el proyecto.
Paso 1: Hacer visible la deuda (antes de tocar una sola línea)
El primer error que cometen la mayoría de los equipos es empezar a refactorizar sin hacer un mapa del territorio. Entras al código, ves algo que te molesta, lo arreglas, introduces un bug en otro lado que no conocías, y ahora el problema es peor.
Antes de escribir una sola línea nueva, audita.
Lo que yo hice fue dedicar los primeros 3 días a solo leer y documentar:
- Mapa de dependencias: ¿Qué módulos dependen de qué? Herramientas como
madge(para Node.js) opydeps(Python) te generan un grafo. Si el grafo parece un plato de espagueti, ya sabes lo que hay. - Cobertura de tests: Corre el coverage report. En este proyecto era 0%. Cero. No había un solo test automatizado.
- Dependencias desactualizadas:
npm auditopip-audit. Tuve 47 vulnerabilidades conocidas en producción. - Puntos de mayor churn: ¿Cuáles son los archivos que más se han modificado en el historial de git?
git log --stat | grep "file changed" -A 1te lo dice. Esos archivos son usualmente donde vive la deuda más dolorosa. - Complejidad ciclomática: Herramientas como
complexity-reporto SonarQube identifican las funciones con demasiadas rutas de ejecución posibles. Números arriba de 10 son una señal de alerta.
Al final de este paso tienes algo crítico: un inventario con números. No opiniones. Números.
Paso 2: Traducir al lenguaje del negocio (el paso que nadie hace)
Este fue el paso que cambió todo.
Los equipos anteriores habían visto el mismo desastre. Lo sabían. Pero nunca lo comunicaron de forma que el cliente pudiera entender qué estaba en riesgo. Hablaban de "código legacy" y "arquitectura monolítica" —términos que para un gerente de producto son ruido blanco.
La deuda técnica es invisible para quien no la vive. Sus consecuencias, no.
Antes de la reunión con el cliente, traduje el inventario técnico a impacto de negocio:
0% cobertura de tests → Cada deploy es una apuesta. El riesgo de caída en producción es alto.
47 vulnerabilidades conocidas → Riesgo legal y de seguridad. Los datos de usuarios están expuestos.
Cycle time de 3 semanas por feature → Mientras tu competencia lanza en días, tú tardas semanas.
Alta churn en el módulo de pagos → El módulo más crítico del negocio es el más inestable.
Con ese mapeo, la conversación cambió completamente. Ya no era "el código está mal". Era "tienes un riesgo operacional concreto, y estos son los números".
El cliente aprobó el presupuesto esa misma semana.
La lección: No pidas permiso para arreglar código. Presenta el riesgo que representa no arreglarlo.
Paso 3: Priorizar con criterio (no todo necesita arreglarse)
Uno de los errores más comunes en proyectos de rescate técnico es querer arreglar todo. Eso es una receta para el fracaso. Siempre hay más deuda de la que el tiempo y el presupuesto permiten atender.
Usé una matriz simple de dos ejes: impacto en el negocio vs esfuerzo de resolución.
ESFUERZO BAJO ESFUERZO ALTO
┌─────────────────────────────────────┐
IMPACTO ALTO │ ✅ HAZLO YA 📅 PLANIFÍCALO │
│ │
IMPACTO BAJO │ 🧹 MEJORA OPORTUNISTA ❌ IGNÓRALO │
└─────────────────────────────────────┘
✅ Hazlo ya (impacto alto, esfuerzo bajo): Actualizar dependencias con vulnerabilidades críticas, agregar CI/CD básico, escribir tests para el flujo de pago. Estas cosas dan confianza inmediata y desbloquean el trabajo futuro.
📅 Planifícalo (impacto alto, esfuerzo alto): Refactorizar la arquitectura central, separar la lógica de negocio de la UI, migrar la base de datos. Estas van al roadmap con estimaciones realistas.
🧹 Mejora oportunista (impacto bajo, esfuerzo bajo): Renombrar variables confusas, limpiar imports muertos, mejorar mensajes de error. Aplica la Boy Scout Rule: deja el código un poco mejor cada vez que lo tocas.
❌ Ignóralo (impacto bajo, esfuerzo alto): Hay código que funciona, nadie toca, y "arreglarlo" no mueve ninguna aguja de negocio. No lo toques.
En este proyecto, el 80% de los beneficios vinieron del 20% de los cambios. Identifica ese 20%.
Paso 4: Refactorizar incrementalmente (jamás un rewrite total)
La tentación en proyectos con tanta deuda es tirar todo y empezar de cero. Es el instinto natural de cualquier developer que ve código sucio. Resístelo.
Los rewrites totales son uno de los errores más costosos en la historia del software. Netscape lo intentó y casi muere. El proyecto tarda el doble de lo estimado, el negocio se detiene, y al final el nuevo código tiene sus propios problemas.
La alternativa es el Strangler Fig Pattern: rodeas el código viejo con código nuevo, y vas migrando tráfico gradualmente hasta que el sistema viejo muere de forma natural.
En la práctica, esto se ve así:
// ANTES: Lógica mezclada, imposible de testear
function guardarPedido(data) {
// 300 líneas de validación + DB + email + logs + pagos
// Todo junto. Todo mezclado. Todo frágil.
}
// DESPUÉS: Responsabilidades separadas, testeable
class PedidoService {
constructor(
private validator: IPedidoValidator,
private repo: IPedidoRepository,
private notificaciones: INotificacionService,
private pagos: IPagoGateway,
) {}
async procesar(dto: PedidoDTO): Promise<PedidoResult> {
const errores = this.validator.validar(dto);
if (errores.length) throw new ValidationError(errores);
const pedido = await this.repo.guardar(dto);
await this.pagos.cobrar(pedido);
await this.notificaciones.confirmar(pedido);
return pedido;
}
}
El módulo viejo sigue funcionando. El nuevo se construye a su lado. Cuando el nuevo está testeado y verificado, el tráfico migra. El viejo se elimina.
Regla práctica: Nunca dejes el proyecto en un estado peor del que lo encontraste. Cada PR debe mejorar algo, aunque sea pequeño.
Paso 5: Prevenir que vuelva a acumularse
Resolver la deuda técnica existente sin cambiar los procesos que la generaron es como vaciar un bote que tiene agua entrando por un agujero. En unas semanas estás de vuelta en el mismo lugar.
Estas son las prácticas que implementamos para que el problema no reapareciera:
Tests como requisito de merge: Ningún PR llega a main sin tests. La cobertura del módulo afectado no puede disminuir. Esto se configura en el CI en menos de una hora y cambia completamente la cultura del equipo.
20% del sprint para deuda técnica: No se negocia, no se sacrifica por features de último momento. Es la inversión en velocidad futura. Los equipos que hacen esto consistentemente entregan más, no menos.
Tech Debt como ítem en el backlog: La deuda técnica tiene tickets, tiene estimaciones, tiene prioridad. Deja de ser invisible. Cuando el cliente puede verla en el backlog, la conversación sobre prioridades es completamente diferente.
Definition of Done actualizada: Una feature no está "done" si dejó más deuda de la que había. Incluye criterios como "el módulo afectado tiene tests" y "no hay dependencias nuevas sin documentar".
ADRs (Architecture Decision Records): Cada decisión técnica relevante se documenta en un archivo Markdown en el repositorio. No para burocracia, sino para que el developer que llegue en 2 años entienda por qué las cosas son como son. La falta de estos registros es exactamente lo que genera los 23 "fix rápidos" apilados.
El resultado
7 meses de proyecto en total. Las primeras 6 semanas fueron de trabajo arquitectural puro —sin entregar una sola feature nueva. La feature original, la que había motivado todo, se entregó en la semana 7 de esa fase de rescate.
Al final del proyecto, el cliente tenía:
- Cobertura de tests del 78% (desde 0%)
- Tiempo de deploy reducido de "manual y aterrador" a 12 minutos automatizados
- Cero vulnerabilidades críticas conocidas
- Un equipo que podía hacer cambios con confianza
Pero más importante que todo eso: por primera vez en 4 años, alguien le había explicado exactamente en qué estado estaba su software y qué significaba para su negocio.
Lo que aprendí
El trabajo técnico más valioso a veces no es escribir código. Es traducir el problema para que quien decide entienda qué está en riesgo y por qué.
Los equipos anteriores habían visto el mismo desastre. Lo que faltaba no era habilidad técnica —era comunicación. Cuatro años de profesionales competentes que nunca encontraron las palabras para hacer visible algo que solo ellos podían ver.
Si estás en un proyecto con deuda técnica acumulada, el primer paso no es abrir el editor. Es abrir una conversación honesta con quien toma las decisiones.
El código puede esperar un día. La conversación no.
¿Tu proyecto tiene deuda técnica acumulada?
Si reconoces alguno de estos síntomas:
- Cada feature nueva tarda más que la anterior
- Temes tocar ciertos módulos por miedo a romper algo
- Tu equipo rota más de lo que debería
- Los bugs en producción son recurrentes
Puedo ayudarte a auditar el estado actual, diseñar un plan de rescate realista y ejecutarlo sin interrumpir el negocio.
