Saltar al contenido

Deuda técnica: medir y priorizar lo que de verdad importa

Métodos prácticos para cuantificar la deuda técnica, priorizar su pago según el impacto de negocio y crear estrategias que el equipo pueda seguir.

6 min de lectura
Panel que muestra métricas de deuda técnica con puntuaciones de prioridad y gráficos de análisis de impacto

Todos los equipos de ingeniería hablan de la deuda técnica. Pocos la miden de verdad. Y todavía menos la priorizan con datos en lugar de dejarse llevar por la intuición. El resultado es un aplazamiento perpetuo o "sprints de deuda técnica" improvisados que terminan arreglando lo que no toca.

Cuantificar la deuda técnica la convierte de una queja vaga en una prioridad de ingeniería accionable. Cuando puedes asignarle números a la deuda, puedes tomar decisiones racionales sobre cuándo pagarla y qué pagar primero.

Qué hace que la deuda técnica sea medible

La deuda técnica no es solo "código desordenado". Es cualquier decisión de implementación que aumenta el costo futuro del cambio. La palabra clave es "costo", y los costos se pueden medir.

tstypescript
// ❌ Treating all technical debt as equal
const techDebt = [
  "Refactor user service",
  "Update dependencies",
  "Fix database schema",
  "Rewrite auth module",
];
 
// Just pick whatever feels important
const nextSprint = techDebt[0];
tstypescript
// ✅ Quantifying debt with impact scores
interface TechDebtItem {
  id: string;
  description: string;
  impactScore: number;      // 1-10: how much it slows development
  frequencyScore: number;   // 1-10: how often teams hit this
  fixEffort: number;        // story points or days
  riskScore: number;        // 1-10: likelihood of causing incidents
  lastIncidentDate?: Date;
  affectedTeams: string[];
}
 
function calculatePriority(item: TechDebtItem): number {
  const painScore = item.impactScore * item.frequencyScore;
  const riskAdjusted = painScore * (1 + item.riskScore / 10);
  const roi = riskAdjusted / item.fixEffort;
  return roi;
}
 
const prioritizedDebt = techDebt
  .map(item => ({ ...item, priority: calculatePriority(item) }))
  .sort((a, b) => b.priority - a.priority);

La fórmula no necesita ser perfecta. Lo que importa es que estés comparando los elementos de deuda según dimensiones consistentes, en lugar de depender de quien grite más fuerte en la planificación del sprint.

El framework RICE adaptado a la deuda técnica

Los equipos de producto usan RICE (Reach, Impact, Confidence, Effort) para priorizar funcionalidades. El mismo framework funciona igual de bien para la deuda técnica si adaptas las dimensiones.

tstypescript
interface DebtRICE {
  reach: number;        // Number of developers affected per sprint
  impact: number;       // Time lost per encounter (hours)
  confidence: number;   // How sure are we about the estimates (0-1)
  effort: number;       // Person-weeks to fix
}
 
function riceScore(item: DebtRICE): number {
  return (item.reach * item.impact * item.confidence) / item.effort;
}
 
// Example: Legacy authentication module
const authModuleDebt: DebtRICE = {
  reach: 8,           // 8 developers touch auth weekly
  impact: 3,          // Each loses ~3 hours per encounter
  confidence: 0.8,    // We've measured this in time tracking
  effort: 4,          // 4 person-weeks to modernize
};
 
// Example: Inconsistent error handling
const errorHandlingDebt: DebtRICE = {
  reach: 12,          // Entire team encounters this
  impact: 0.5,        // Minor friction each time
  confidence: 0.6,    // Rough estimate
  effort: 2,          // 2 person-weeks
};
 
console.log("Auth module ROI:", riceScore(authModuleDebt));       // 4.8
console.log("Error handling ROI:", riceScore(errorHandlingDebt)); // 1.8

Los números cuentan una historia clara: arreglar el módulo de autenticación aporta 2.6 veces más valor por unidad de esfuerzo que estandarizar el manejo de errores. Sin cuantificación, los equipos suelen perseguir la solución "más elegante" en lugar de la más impactante.

Cómo medir directamente la fricción de los desarrolladores

La medida más honesta de la deuda técnica es el tiempo de desarrollo que se pierde. Hay que registrarlo de forma sistemática en lugar de suponerlo.

tstypescript
// Simple friction tracking system
interface FrictionEvent {
  timestamp: Date;
  developerId: string;
  category: string;
  description: string;
  minutesLost: number;
  codeArea: string;
}
 
class FrictionTracker {
  private events: FrictionEvent[] = [];
 
  record(event: Omit<FrictionEvent, "timestamp">): void {
    this.events.push({ ...event, timestamp: new Date() });
  }
 
  getWeeklyReport(): Map<string, { count: number; totalMinutes: number }> {
    const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
    const recentEvents = this.events.filter(
      e => e.timestamp > oneWeekAgo
    );
 
    const report = new Map<string, { count: number; totalMinutes: number }>();
 
    for (const event of recentEvents) {
      const existing = report.get(event.codeArea) ?? {
        count: 0,
        totalMinutes: 0,
      };
      report.set(event.codeArea, {
        count: existing.count + 1,
        totalMinutes: existing.totalMinutes + event.minutesLost,
      });
    }
 
    return report;
  }
}
tstypescript
// ❌ Vague friction estimates
// "The payment service is annoying to work with"
// "Tests are slow"
// "Deployments take forever"
 
// ✅ Concrete friction data
const tracker = new FrictionTracker();
 
tracker.record({
  developerId: "dev-42",
  category: "slow-tests",
  description: "Payment integration tests take 12 min locally",
  minutesLost: 45,
  codeArea: "payment-service/tests",
});
 
tracker.record({
  developerId: "dev-17",
  category: "unclear-api",
  description: "Had to read source code to understand order API response",
  minutesLost: 30,
  codeArea: "order-service/api",
});

Tras dos semanas de seguimiento de la fricción, surgen patrones que ninguna revisión de arquitectura lograría revelar. Los datos suelen sorprender a los equipos: la peor deuda no siempre está en el código más antiguo.

Cómo construir un registro de deuda técnica

Un registro de deuda es un inventario vivo de la deuda técnica conocida, que se actualiza de forma continua. Sirve como la única fuente de verdad sobre qué existe, por qué importa y cuándo abordarlo.

tstypescript
interface DebtRegisterEntry {
  id: string;
  title: string;
  createdDate: Date;
  category: "architecture" | "code" | "test" | "infrastructure" | "dependency";
  owner: string;
  status: "identified" | "measured" | "scheduled" | "in-progress" | "resolved";
  businessImpact: string;
  metrics: {
    developerHoursPerMonth: number;
    incidentFrequency: number;
    deploymentImpact: number;
  };
  repaymentPlan?: {
    estimatedEffort: number;
    proposedSprint: string;
    dependencies: string[];
  };
}
 
function generateQuarterlyReport(
  register: DebtRegisterEntry[]
): {
  totalMonthlyHoursLost: number;
  topOffenders: DebtRegisterEntry[];
  resolvedThisQuarter: number;
  trend: "improving" | "stable" | "worsening";
} {
  const active = register.filter(e => e.status !== "resolved");
  const totalHours = active.reduce(
    (sum, e) => sum + e.metrics.developerHoursPerMonth,
    0
  );
 
  const topOffenders = [...active]
    .sort(
      (a, b) =>
        b.metrics.developerHoursPerMonth - a.metrics.developerHoursPerMonth
    )
    .slice(0, 5);
 
  const resolvedThisQuarter = register.filter(
    e =>
      e.status === "resolved" &&
      e.createdDate > new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
  ).length;
 
  return {
    totalMonthlyHoursLost: totalHours,
    topOffenders,
    resolvedThisQuarter,
    trend: totalHours > 100 ? "worsening" : totalHours > 50 ? "stable" : "improving",
  };
}

El informe trimestral convierte quejas abstractas en cifras que un directivo puede entender. Decir "nuestro equipo pierde 120 horas de desarrollo al mes por deuda técnica" es un argumento mucho más convincente que "necesitamos tiempo para refactorizar".

Cómo integrar el seguimiento de la deuda en tu flujo de trabajo

El mejor seguimiento de deuda ocurre de forma automática, como parte del flujo de trabajo existente, en lugar de ser un proceso aparte que se abandona a las dos semanas.

ymlyaml
# .github/PULL_REQUEST_TEMPLATE/default.md
## Technical Debt Assessment
 
<!-- Check all that apply -->
- [ ] This PR introduces new technical debt
- [ ] This PR repays existing technical debt
- [ ] No debt impact
 
### If introducing debt:
- **Debt ID**: (link to debt register entry)
- **Justification**: 
- **Estimated repayment effort**: 
 
### If repaying debt:
- **Debt ID(s) resolved**: 
- **Measurable improvement**: 
tstypescript
// Automated debt detection in CI
interface DebtSignal {
  type: string;
  severity: "low" | "medium" | "high";
  location: string;
  message: string;
}
 
function analyzeForDebtSignals(
  prDiff: string
): DebtSignal[] {
  const signals: DebtSignal[] = [];
 
  // Detect TODO/HACK/FIXME additions
  const todoPattern = /\+.*(?:TODO|HACK|FIXME|WORKAROUND)(?::?\s*)(.+)/gi;
  let match: RegExpExecArray | null;
 
  while ((match = todoPattern.exec(prDiff)) !== null) {
    signals.push({
      type: "code-comment-debt",
      severity: "medium",
      location: extractFileLocation(prDiff, match.index),
      message: match[1].trim(),
    });
  }
 
  // Detect growing file complexity
  const addedLines = (prDiff.match(/^\+[^+]/gm) ?? []).length;
  const removedLines = (prDiff.match(/^-[^-]/gm) ?? []).length;
 
  if (addedLines > 200 && removedLines < 20) {
    signals.push({
      type: "growing-complexity",
      severity: "low",
      location: "overall",
      message: `Large addition (${addedLines} lines) with minimal removal`,
    });
  }
 
  return signals;
}
 
function extractFileLocation(diff: string, index: number): string {
  const upToIndex = diff.substring(0, index);
  const fileMatch = upToIndex.match(/\+\+\+ b\/(.+)/g);
  return fileMatch ? fileMatch[fileMatch.length - 1].replace("+++ b/", "") : "unknown";
}

Cuando cada PR saca a la luz decisiones sobre deuda, el equipo desarrolla el hábito de gestionarla de forma consciente. La deuda nueva no está prohibida: se reconoce y se programa su pago.

La regla del 20% y el pago sostenible

Muchos equipos intentan destinar un porcentaje fijo de cada sprint al pago de deuda. El número mágico varía, pero el principio importa más que el porcentaje exacto.

tstypescript
interface SprintAllocation {
  totalCapacity: number;       // story points
  featureWork: number;
  debtRepayment: number;
  bugFixes: number;
  debtPercentage: number;
}
 
function planSprintAllocation(
  totalCapacity: number,
  debtBudgetPercent: number,
  criticalBugs: number
): SprintAllocation {
  const bugAllocation = criticalBugs * 3; // ~3 points per bug
  const debtAllocation = Math.floor(
    (totalCapacity - bugAllocation) * (debtBudgetPercent / 100)
  );
  const featureAllocation = totalCapacity - bugAllocation - debtAllocation;
 
  return {
    totalCapacity,
    featureWork: featureAllocation,
    debtRepayment: debtAllocation,
    bugFixes: bugAllocation,
    debtPercentage: (debtAllocation / totalCapacity) * 100,
  };
}
 
// Example: 40-point sprint, 20% debt budget, 2 critical bugs
const plan = planSprintAllocation(40, 20, 2);
// { featureWork: 27, debtRepayment: 7, bugFixes: 6, debtPercentage: 17.5 }

La idea clave es que el pago de la deuda es acumulativo. Arreglar los tests lentos en este sprint significa ciclos de feedback más rápidos en el próximo. Limpiar la capa de API significa menos errores introducidos el mes siguiente. Cuantifica estos efectos de segundo orden para justificar la inversión continua.

Conclusiones clave

Gestionar la deuda técnica no consiste en eliminarla por completo, sino en tomar decisiones informadas sobre qué deuda mantener y cuál pagar. Los equipos que lo logran comparten prácticas comunes: miden la fricción en lugar de suponerla, puntúan los elementos de deuda con criterios consistentes y tratan el pago como una inversión continua en lugar de un sprint ocasional.

Empieza por el seguimiento de la fricción. Dos semanas de datos te dirán más sobre los costos reales de tu base de código que cualquier revisión de arquitectura. Construye un registro, intégralo en tu flujo de trabajo de PRs y destina un porcentaje sostenible de cada sprint al pago de deuda. Los números hablarán por sí solos cuando los stakeholders pregunten por qué la velocidad mejora trimestre tras trimestre.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX