Volver al blog

Ingeniería

Deuda Técnica: Cuándo Aceptarla y Cuándo Pagarla

8 min lecturaEnviaIT Engineering

Toda base de código tiene deuda técnica. La tuya, la nuestra, la de Google. La pregunta no es si tienes deuda técnica — la pregunta es si sabes cuánta tienes y si es la correcta.

En EnviaIT gestionamos la deuda técnica de múltiples proyectos simultáneamente. Algunos son MVPs donde la velocidad manda. Otros son plataformas maduras donde la estabilidad es crítica. El approach no puede ser el mismo. Este es el framework que usamos para decidir cuándo aceptar deuda, cuándo pagarla y cuándo vivir con ella.

Qué es (y qué no es) deuda técnica

Ward Cunningham acuñó el término en 1992 como analogía financiera: tomar atajos técnicos es como pedir un préstamo. Puedes avanzar más rápido hoy, pero pagarás intereses mañana en forma de código más difícil de cambiar.

Lo que es deuda técnica:

  • Decisiones conscientes de simplificar para ir más rápido
  • Código funcional que necesitará refactoring cuando los requisitos crezcan
  • Abstracciones que sabes que son limitadas pero suficientes por ahora

Lo que NO es deuda técnica:

  • Código mal escrito por falta de conocimiento (eso es mala calidad)
  • Bugs conocidos que no se arreglan (eso es negligencia)
  • Falta de tests (eso es riesgo operacional)

La distinción importa porque la solución es diferente. La deuda técnica se paga con refactoring planificado. La mala calidad se corrige con formación, code reviews y estándares.

El cuadrante de la deuda técnica

Martin Fowler propuso un modelo de cuatro tipos de deuda que usamos como base para clasificar:

| | Prudente | Imprudente | |---|---|---| | Deliberada | "Sabemos que esto no escala, pero necesitamos lanzar esta semana" | "No tenemos tiempo para diseñar esto bien" | | Accidental | "Ahora que entendemos el dominio, vemos que la estructura debería ser otra" | "¿Qué es la inyección de dependencias?" |

Deuda prudente y deliberada: la buena

Esta es la deuda que queremos tener. Es una decisión informada con trade-offs claros.

Ejemplo real: En un proyecto de marketplace para un cliente, necesitábamos lanzar el sistema de pagos en 3 semanas. Teníamos dos opciones:

  1. Implementar un sistema de pagos completo con split payments, reembolsos parciales y multi-moneda
  2. Integrar Stripe Checkout directamente, sin abstracción, solo EUR, reembolsos manuales

Elegimos la opción 2. Documentamos la deuda:

## DEUDA: Sistema de pagos simplificado
- **Fecha:** 2025-03-15
- **Decisión:** Integración directa con Stripe Checkout sin capa de abstracción
- **Limitaciones:** Solo EUR, sin split payments, reembolsos manuales
- **Trigger para pagar:** Cuando necesitemos multi-moneda o volumen > 500 tx/día
- **Estimación de pago:** 3-4 sprints para abstraer la capa de pagos
- **Responsable:** Equipo Backend

El marketplace lanzó a tiempo, validó el modelo de negocio, y 6 meses después refactorizamos el sistema de pagos con datos reales sobre qué necesitaban los usuarios.

Deuda prudente y accidental: la inevitable

Esta deuda aparece cuando aprendes algo nuevo sobre el dominio. No podías haberla evitado porque no tenías la información cuando escribiste el código.

Ejemplo: Diseñamos un modelo de datos para eventos asumiendo que cada evento tiene una única sede. Tres meses después, un cliente grande necesitaba eventos multi-sede. El modelo no lo soportaba. No fue una mala decisión — fue una evolución natural del producto.

Deuda imprudente: la peligrosa

Esta es la deuda que hay que evitar o pagar inmediatamente:

  • Copiar y pegar código sin entender qué hace
  • Ignorar patrones de seguridad por "ir más rápido"
  • Hardcodear credenciales, URLs o configuraciones
  • No manejar errores porque "ya lo haremos después"

La deuda imprudente no genera velocidad — genera la ilusión de velocidad. Lo que ahorras hoy lo pagas con intereses compuestos cuando explota en producción un viernes a las 22:00.

Cómo medimos la deuda técnica

No puedes gestionar lo que no mides. Usamos una combinación de métricas cuantitativas y cualitativas:

Métricas automatizadas

# Métricas que recogemos en CI/CD
metricas_deuda:
  cobertura_tests:
    umbral_minimo: 70%
    accion: bloquear PR si baja del umbral

  complejidad_ciclomatica:
    herramienta: ESLint complexity rule
    umbral: 15 por función
    accion: warning en PR review

  duplicacion_codigo:
    herramienta: jscpd
    umbral: 5% máximo
    accion: report semanal

  dependencias_desactualizadas:
    herramienta: npm audit + npm outdated
    frecuencia: semanal
    accion: ticket automático si hay vulnerabilidades

  tiempo_build:
    umbral: 5 minutos máximo
    accion: investigar si sube más de 20%

Métricas cualitativas

Cada dos semanas, en la retrospectiva del sprint, preguntamos:

  1. "¿Qué parte del código te da miedo tocar?" — Señala zonas de alto riesgo
  2. "¿Dónde tardaste más de lo esperado?" — Indica código difícil de cambiar
  3. "¿Qué harías diferente si empezaras de cero?" — Revela deuda accidental descubierta

Mantenemos un registro de deuda en el repo — un archivo TECH_DEBT.md que cualquier dev puede actualizar:

# Registro de Deuda Técnica

## Alta prioridad
- [ ] **Migrar auth a JWT con refresh tokens** — El sistema actual usa sesiones
      en memoria, no escala horizontalmente. Impacta: escalado, deploys.
      Estimación: 2 sprints. Trigger: antes de pasar a 2+ instancias.

## Media prioridad
- [ ] **Extraer servicio de notificaciones** — Actualmente acoplado al
      monolito. Añadir un nuevo canal (SMS, push) requiere tocar código
      core. Estimación: 1 sprint.

## Baja prioridad
- [ ] **Migrar de Moment.js a date-fns** — Moment está deprecated y pesa
      300KB. No bloquea nada, pero infla el bundle. Estimación: 3 días.

Framework de priorización

No toda la deuda merece ser pagada. Usamos una matriz de impacto vs esfuerzo adaptada a deuda técnica:

Paso 1: Clasificar por impacto

  • Impacto alto: Bloquea o ralentiza features de negocio, causa incidentes en producción, afecta a la seguridad
  • Impacto medio: Ralentiza el desarrollo pero no lo bloquea, degrada la experiencia de developer
  • Impacto bajo: Molesto pero funcional, cosmético, no afecta al usuario final

Paso 2: Estimar esfuerzo de pago

  • Bajo: Menos de 1 día — un dev, sin riesgo de regresión
  • Medio: 1-5 días — requiere planning, posible impacto en otras áreas
  • Alto: Más de 1 sprint — proyecto dedicado, requiere migración gradual

Paso 3: Decidir la acción

| | Esfuerzo bajo | Esfuerzo medio | Esfuerzo alto | |---|---|---|---| | Impacto alto | Pagar ya | Planificar este sprint | Proyecto dedicado | | Impacto medio | Incluir en PR relacionado | Backlog priorizado | Evaluar trimestralmente | | Impacto bajo | Boy Scout Rule | Solo si hay tiempo | Ignorar conscientemente |

La Boy Scout Rule ("deja el código mejor de como lo encontraste") es nuestra herramienta favorita para deuda de bajo esfuerzo: si tocas un archivo para una feature, mejora lo que veas.

Estrategias de refactoring

Cuando decidimos pagar deuda, no siempre significa "reescribir". Tenemos un toolkit de estrategias según la situación:

1. Strangler Fig Pattern

Para reemplazar un sistema complejo gradualmente. Construyes el nuevo sistema al lado del viejo, redirigiendo tráfico poco a poco.

// Fase 1: Wrapper que delega al sistema viejo
class NotificationService {
  async send(notification: Notification) {
    // TODO: Migrar al nuevo sistema event-driven
    return this.legacyService.sendNotification(
      notification.userId,
      notification.message,
      notification.channel
    );
  }
}

// Fase 2: Feature flag para redirigir al nuevo sistema
class NotificationService {
  async send(notification: Notification) {
    if (await this.featureFlag.isEnabled('new-notifications', notification.userId)) {
      return this.eventBus.publish('notification.send', notification);
    }
    return this.legacyService.sendNotification(
      notification.userId,
      notification.message,
      notification.channel
    );
  }
}

// Fase 3: Eliminar el código viejo cuando el 100% está migrado
class NotificationService {
  async send(notification: Notification) {
    return this.eventBus.publish('notification.send', notification);
  }
}

2. Branch by Abstraction

Cuando necesitas reemplazar una dependencia o componente sin parar el desarrollo:

  1. Crea una abstracción sobre el componente actual
  2. Haz que todo el código use la abstracción
  3. Implementa la nueva versión detrás de la abstracción
  4. Cambia la implementación
  5. Elimina la vieja

3. Refactoring incremental

Para deuda dispersa (naming inconsistente, patrones mixtos, etc.), dedicamos un porcentaje fijo del sprint:

  • 20% del sprint dedicado a pagar deuda en sprints normales
  • 1 sprint de "tech health" cada trimestre para deuda mayor
  • Boy Scout Rule siempre activa para mejoras pequeñas

Cuándo reescribir vs refactorizar

La tentación de "reescribir desde cero" es fuerte. Casi siempre es un error.

Reescribir es justificable cuando:

  • La tecnología base está end-of-life y sin soporte
  • El modelo de datos fundamental no soporta los requisitos actuales
  • El coste de mantener el sistema viejo supera el de construir uno nuevo (con datos que lo demuestren)
  • El equipo que escribió el código ya no está y nadie entiende el sistema

Refactorizar es mejor cuando:

  • El sistema funciona y genera valor
  • Los problemas son de estructura, no de concepto
  • Puedes aislar las partes a mejorar
  • El negocio no puede permitirse meses sin features nuevas

Joel Spolsky lo dijo hace 25 años y sigue siendo cierto: reescribir desde cero es "el peor error estratégico que una compañía de software puede cometer". No porque nunca sea necesario — sino porque se subestima brutalmente el esfuerzo.

El business case para pagar deuda

El mayor reto no es técnico — es convencer a stakeholders de que invertir en algo invisible tiene ROI. Estos son los argumentos que funcionan:

1. Velocidad de desarrollo medible

Sprint 1 (con deuda):    5 features, 3 bugs encontrados en QA
Sprint 2 (con deuda):    4 features, 5 bugs encontrados en QA
Sprint 3 (pagando deuda): 2 features + refactoring
Sprint 4 (post-pago):    7 features, 1 bug encontrado en QA
Sprint 5 (post-pago):    8 features, 0 bugs en QA

Los números hablan solos. Tracking del velocity antes y después del refactoring demuestra el ROI.

2. Reducción de incidentes

Cada incidente en producción tiene un coste: tiempo de ingeniería para diagnosticar, impacto en usuarios, daño reputacional. Si puedes correlacionar incidentes con zonas de deuda conocida, el argumento se construye solo.

3. Retención de talento

Los buenos desarrolladores no quieren trabajar en código que les hace infelices. Si el equipo menciona constantemente que el código es "frustrante" o "frágil", hay un coste de rotación que nadie está calculando.

4. Time-to-market futuro

La deuda técnica es un impuesto invisible sobre cada feature futura. Si añadir un campo a un formulario toma 2 días en vez de 2 horas, el negocio está pagando intereses sin saberlo.

Nuestro proceso en la práctica

Resumiendo cómo gestionamos la deuda técnica en proyectos de EnviaIT:

  1. Documentar al crear: Cada atajo deliberado se documenta con fecha, razón, limitaciones y trigger para pagar
  2. Medir continuamente: Métricas automatizadas en CI + retrospectivas cualitativas
  3. Priorizar trimestralmente: Review formal del registro de deuda con el equipo y stakeholders
  4. Dedicar tiempo fijo: 20% del sprint para mantenimiento, 1 sprint/trimestre para deuda mayor
  5. Medir el impacto: Velocity, incidentes y satisfacción del equipo antes y después

La deuda técnica no es el enemigo. Es una herramienta. Como cualquier herramienta, el problema no es usarla — es no saber cuándo dejarla.


¿Tu proyecto tiene deuda técnica que está frenando el desarrollo? Hablemos — te ayudamos a diagnosticarla, priorizarla y crear un plan de acción realista.