Saltar al contenido

Gestión sistemática de la deuda técnica

Un marco para identificar, clasificar, priorizar y reducir la deuda técnica sin frenar el producto: inventarios, asignación de capacidad y retorno medible.

5 min de lectura
Tablero Kanban que muestra elementos de deuda técnica clasificados por gravedad y retorno estimado

La deuda técnica no es una sensación vaga de que el código está mal. Es el costo medible de elegir ahora una solución más rápida que exigirá trabajo adicional más adelante. Al igual que la deuda financiera, genera intereses: cada funcionalidad tarda más en construirse, cada error tarda más en diagnosticarse y cada incorporación de nuevos integrantes tarda más en completarse.

El problema no es que exista deuda técnica. Parte de la deuda es intencional y estratégica. El problema surge cuando la deuda no se mide, no se registra y no se prioriza, cuando se acumula en silencio hasta que la base de código se vuelve tan frágil que hasta los cambios pequeños resultan riesgosos.

Clasificación de la deuda técnica

No toda la deuda técnica es igual. Un índice de base de datos faltante cuesta segundos por consulta. Un grafo de dependencias enredado cuesta días por funcionalidad. Clasificar la deuda ayuda a priorizar qué resolver primero.

tstypescript
// Technical debt classification framework
interface TechnicalDebtItem {
  id: string;
  title: string;
  description: string;
  category: DebtCategory;
  severity: 'low' | 'medium' | 'high' | 'critical';
  interestRate: InterestAssessment;
  estimatedPayoffEffort: string;     // "2 hours", "1 sprint", "1 quarter"
  affectedAreas: string[];
  createdDate: string;
  lastAssessedDate: string;
}
 
type DebtCategory =
  | 'architecture'      // Wrong abstraction, tight coupling, missing boundaries
  | 'code-quality'      // Duplicated logic, unclear naming, missing types
  | 'testing'           // Missing tests, flaky tests, slow test suite
  | 'infrastructure'    // Outdated dependencies, manual deployments, missing monitoring
  | 'documentation'     // Missing or stale docs, tribal knowledge, unclear APIs
  | 'performance'       // Slow queries, missing caching, unoptimized assets
  | 'security';         // Known vulnerabilities, missing auth checks, weak encryption
 
interface InterestAssessment {
  // How much does this debt slow us down per week?
  weeklyTimeCost: string;            // "2 hours/week on workarounds"
  // Is the interest rate increasing, stable, or decreasing?
  trend: 'increasing' | 'stable' | 'decreasing';
  // What happens if we never pay this off?
  worstCase: string;                 // "Migration becomes impossible"
}
tstypescript
// ❌ Vague debt tracking
const vagueDebt = [
  "Code is messy",
  "Need to refactor auth",
  "Database is slow",
];
// Nobody knows what to fix, how long it'll take, or why it matters
 
// ✅ Specific, measurable debt items
const specificDebt: TechnicalDebtItem[] = [
  {
    id: 'DEBT-042',
    title: 'User service has no integration tests',
    description: 'The /api/users/* endpoints have 0% integration test coverage. ' +
      'Last production incident (INC-127) was caused by a regression that unit ' +
      'tests did not catch because they mock the database layer.',
    category: 'testing',
    severity: 'high',
    interestRate: {
      weeklyTimeCost: '4 hours/week debugging regressions',
      trend: 'increasing',
      worstCase: 'Data corruption incident affecting all users',
    },
    estimatedPayoffEffort: '1 sprint',
    affectedAreas: ['user-service', 'api-gateway'],
    createdDate: '2022-01-15',
    lastAssessedDate: '2022-03-20',
  },
];

El inventario de deuda

Mantén un inventario vivo de toda la deuda técnica conocida. Este inventario funciona como la única fuente de verdad sobre lo que existe, qué tan grave es y qué planea hacer el equipo al respecto.

tstypescript
class DebtInventory {
  private items: Map<string, TechnicalDebtItem> = new Map();
 
  add(item: TechnicalDebtItem): void {
    this.items.set(item.id, item);
  }
 
  // Prioritization: score items by impact and effort
  prioritize(): TechnicalDebtItem[] {
    return [...this.items.values()]
      .map(item => ({
        item,
        score: this.calculatePriorityScore(item),
      }))
      .sort((a, b) => b.score - a.score)
      .map(({ item }) => item);
  }
 
  private calculatePriorityScore(item: TechnicalDebtItem): number {
    const severityWeight: Record<string, number> = {
      critical: 4,
      high: 3,
      medium: 2,
      low: 1,
    };
 
    const trendMultiplier: Record<string, number> = {
      increasing: 1.5,   // Getting worse — fix sooner
      stable: 1.0,
      decreasing: 0.7,   // Getting better on its own — lower priority
    };
 
    const effortDiscounting: Record<string, number> = {
      // Prefer quick wins — low effort, high impact
      '2 hours': 2.0,
      '1 day': 1.5,
      '1 sprint': 1.0,
      '1 quarter': 0.5,
    };
 
    const severity = severityWeight[item.severity] ?? 1;
    const trend = trendMultiplier[item.interestRate.trend] ?? 1;
    const effort = effortDiscounting[item.estimatedPayoffEffort] ?? 1;
 
    return severity * trend * effort;
  }
 
  // Summary for stakeholder reporting
  summary(): DebtSummary {
    const items = [...this.items.values()];
    return {
      total: items.length,
      bySeverity: {
        critical: items.filter(i => i.severity === 'critical').length,
        high: items.filter(i => i.severity === 'high').length,
        medium: items.filter(i => i.severity === 'medium').length,
        low: items.filter(i => i.severity === 'low').length,
      },
      byCategory: this.groupByCategory(items),
      estimatedWeeklyInterest: this.totalWeeklyInterest(items),
    };
  }
 
  private groupByCategory(items: TechnicalDebtItem[]): Record<string, number> {
    return items.reduce((acc, item) => {
      acc[item.category] = (acc[item.category] ?? 0) + 1;
      return acc;
    }, {} as Record<string, number>);
  }
 
  private totalWeeklyInterest(items: TechnicalDebtItem[]): string {
    // Aggregate the weekly cost across all items for reporting
    const totalHours = items.reduce((sum, item) => {
      const match = item.interestRate.weeklyTimeCost.match(/(\d+)/);
      return sum + (match ? parseInt(match[1]) : 0);
    }, 0);
    return `~${totalHours} hours/week`;
  }
}

Asignación de capacidad: la regla 80/20

El enfoque más práctico consiste en asignar un porcentaje fijo de la capacidad de ingeniería a la reducción de deuda. Una división habitual es 80% funcionalidades y 20% deuda. Esto garantiza un progreso constante sin detener el desarrollo de funcionalidades.

tstypescript
// Sprint planning with debt allocation
interface SprintPlan {
  totalCapacity: number;         // Story points available
  featureAllocation: number;     // 80% for features
  debtAllocation: number;        // 20% for debt reduction
  features: WorkItem[];
  debtItems: TechnicalDebtItem[];
}
 
function planSprint(
  capacity: number,
  featureBacklog: WorkItem[],
  debtInventory: DebtInventory
): SprintPlan {
  const debtAllocation = Math.floor(capacity * 0.2);
  const featureAllocation = capacity - debtAllocation;
 
  // Pick the highest-priority debt items that fit in the allocation
  const prioritizedDebt = debtInventory.prioritize();
  const selectedDebt: TechnicalDebtItem[] = [];
  let debtPointsUsed = 0;
 
  for (const item of prioritizedDebt) {
    const points = estimatePoints(item);
    if (debtPointsUsed + points <= debtAllocation) {
      selectedDebt.push(item);
      debtPointsUsed += points;
    }
  }
 
  return {
    totalCapacity: capacity,
    featureAllocation,
    debtAllocation,
    features: featureBacklog.slice(0, featureAllocation),
    debtItems: selectedDebt,
  };
}

Reducción oportunista de deuda

Más allá de la asignación dedicada, fomenta las mejoras según la "regla del boy scout": deja el código mejor de como lo encontraste. Cuando modifiques un archivo para una funcionalidad, corrige los pequeños elementos de deuda de ese archivo mientras estés ahí.

tstypescript
// ❌ Opening a separate PR just to rename a variable
// Overhead of review, CI, deployment for a trivial change
// Gets deprioritized forever
 
// ✅ Fixing small debt while working on a related feature
// PR title: "Add user notification preferences"
// Commit 1: Add notification preferences API
// Commit 2: Clean up user service imports and naming (while here)
//
// The debt fix ships for free alongside the feature
// Reviewer sees the cleanup is scoped and relevant
 
// Guidelines for opportunistic fixes:
const opportunisticRules = {
  do: [
    'Rename unclear variables in files you are modifying',
    'Add types to untyped functions you are calling',
    'Remove dead code you encounter while navigating',
    'Fix linting warnings in changed files',
  ],
  doNot: [
    'Refactor entire modules while fixing a bug',
    'Change unrelated files in the same PR',
    'Reformat code outside your changes',
    'Upgrade dependencies as a side effect',
  ],
};

Comunicar la deuda a las partes interesadas

Los equipos de ingeniería suelen tener dificultades para explicar a los responsables de producto y a los ejecutivos por qué importa la deuda técnica. La clave está en traducir la deuda en impacto de negocio: entregas más lentas de funcionalidades, mayor frecuencia de incidentes y más tiempo de incorporación de personal.

tstypescript
// Framing debt in business terms
interface DebtBusinessImpact {
  featureVelocityDrag: string;
  // "Features that should take 1 sprint are taking 2 sprints
  //  due to workarounds in the payment module"
 
  incidentFrequency: string;
  // "3 of the last 5 production incidents trace back to
  //  the untested user service endpoints"
 
  onboardingCost: string;
  // "New engineers need 2 extra weeks to become productive
  //  because the build system has 14 undocumented manual steps"
 
  opportunityCost: string;
  // "We cannot adopt the new auth provider until we decouple
  //  the auth module from the user service — blocking the
  //  SSO feature that 40% of enterprise prospects request"
}

Conclusiones clave

  1. Registra la deuda explícitamente — mantén un inventario vivo con gravedad, categoría, costo semanal y esfuerzo estimado de retorno
  2. Prioriza según la tasa de interés — corrige primero la deuda que empeora más rápido y que tiene el mayor costo semanal para sortearla
  3. Asigna el 20% de la capacidad de forma constante — el progreso constante importa más que las sesiones ocasionales de refactorización heroica
  4. Corrige la deuda pequeña de forma oportunista — limpia el código en los archivos que ya estás modificando por trabajo de funcionalidades
  5. Mide el retorno — después de corregir un elemento de deuda, mide si la mejora prevista realmente se materializó
  6. Comunica en términos de negocio — traduce la deuda en freno a la velocidad de entrega de funcionalidades, frecuencia de incidentes y costo de incorporación para las partes interesadas
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX