Zum Inhalt springen

Technische Schulden: Der Business Case für Refactoring

Ein praxisnaher Rahmen, um technische Schulden in Zahlen zu fassen, die bei Stakeholdern ankommen: Code-Metriken, Velocity und Vorfallskorrelation.

5 Min. Lesezeit
Dashboard mit Kennzahlen zu technischen Schulden neben Trends der Entwicklungsgeschwindigkeit

Warum "Wir haben technische Schulden" nicht ausreicht

Jedes Engineering-Team weiß, dass es technische Schulden hat. Der Code enthält Workarounds aus einer Deadline vor zwei Jahren. Das Datenbankschema spiegelt das erste Produkt des Startups wider, nicht das aktuelle. Die Testabdeckung im Payment-Modul liegt bei 12 %.

Das Problem ist nicht das Bewusstsein dafür. Das Problem ist die Übersetzung. Aus "dieser Code ist schlecht" ein "dieser Code kostet uns $47,000 pro Monat durch verzögerte Features und Incident-Response" zu machen – das ist der Unterschied zwischen bloßem Beschweren und tatsächlicher Refactoring-Zeit auf der Roadmap.

Business-Stakeholder interessieren sich nicht für Codequalität als abstraktes Prinzip. Sie interessieren sich für Liefergeschwindigkeit, Zuverlässigkeit und Kosten. Die Quantifizierung technischer Schulden schließt diese Lücke, indem sie Codeprobleme in Begriffen ausdrückt, die Stakeholder bereits wertschätzen.

Den Einfluss auf die Entwicklungsgeschwindigkeit messen

Die überzeugendste Schulden-Metrik ist ihre Auswirkung auf die Liefergeschwindigkeit. Wenn das Hinzufügen eines Features zu Modul A dreimal länger dauert als ein Feature gleicher Komplexität in Modul B, lässt sich dieser Unterschied quantifizieren.

tstypescript
interface VelocityMetrics {
  module: string;
  avgCycleTimeDays: number;
  avgReviewRounds: number;
  reworkPercentage: number;
  incidentFrequency: number;
  onboardingTimeDays: number;
}
 
function calculateDebtCost(
  metrics: VelocityMetrics,
  baselineMetrics: VelocityMetrics,
  engineerDailyCost: number
): {
  module: string;
  cycleCostOverhead: number;
  monthlyOverhead: number;
  annualOverhead: number;
} {
  const cycleOverhead = metrics.avgCycleTimeDays - baselineMetrics.avgCycleTimeDays;
  const featuresPerMonth = 30 / metrics.avgCycleTimeDays;
  const monthlyOverheadDays = cycleOverhead * featuresPerMonth;
  const monthlyOverhead = monthlyOverheadDays * engineerDailyCost;
 
  return {
    module: metrics.module,
    cycleCostOverhead: cycleOverhead * engineerDailyCost,
    monthlyOverhead,
    annualOverhead: monthlyOverhead * 12,
  };
}
 
// Example: Payment module vs. a healthy module
const paymentModuleMetrics: VelocityMetrics = {
  module: "payments",
  avgCycleTimeDays: 12,
  avgReviewRounds: 4.2,
  reworkPercentage: 35,
  incidentFrequency: 3.5, // per month
  onboardingTimeDays: 21,
};
 
const healthyModuleMetrics: VelocityMetrics = {
  module: "notifications",
  avgCycleTimeDays: 4,
  avgReviewRounds: 1.8,
  reworkPercentage: 12,
  incidentFrequency: 0.5,
  onboardingTimeDays: 5,
};
 
const cost = calculateDebtCost(paymentModuleMetrics, healthyModuleMetrics, 600);
// cycleCostOverhead: $4,800 per feature
// monthlyOverhead: $12,000
// annualOverhead: $144,000

Die Zahl muss nicht exakt sein. Sie muss begründbar sein. Ziehe die Cycle-Time-Daten aus deinem Projektmanagement-Tool, zähle die Review-Runden aus deiner Git-Historie und berechne die Differenz gegenüber deinem gesündesten Modul. Selbst grobe Schätzungen machen das Unsichtbare sichtbar.

Code-Komplexitätsmetriken, die mit Bugs korrelieren

Statische Analyse liefert objektive Messwerte, die mit der Fehlerdichte korrelieren. Zyklomatische Komplexität, Kopplungsmetriken und Änderungsraten (Churn) identifizieren die Dateien, die die meisten Probleme verursachen.

tstypescript
import { execSync } from "child_process";
 
interface FileDebtScore {
  path: string;
  complexity: number;
  churnCount: number;
  couplingScore: number;
  bugCorrelation: number;
  combinedDebtScore: number;
}
 
function getFileChurn(filepath: string, months: number = 6): number {
  const since = new Date();
  since.setMonth(since.getMonth() - months);
  const dateStr = since.toISOString().split("T")[0];
 
  const result = execSync(
    `git log --since="${dateStr}" --oneline -- "${filepath}"`,
    { encoding: "utf-8" }
  );
 
  return result.trim().split("\n").filter(Boolean).length;
}
 
function calculateDebtScore(files: FileDebtScore[]): FileDebtScore[] {
  // Normalize each metric to 0-1 scale
  const maxComplexity = Math.max(...files.map((f) => f.complexity));
  const maxChurn = Math.max(...files.map((f) => f.churnCount));
  const maxCoupling = Math.max(...files.map((f) => f.couplingScore));
 
  return files
    .map((f) => ({
      ...f,
      combinedDebtScore:
        (f.complexity / maxComplexity) * 0.3 +
        (f.churnCount / maxChurn) * 0.4 +
        (f.couplingScore / maxCoupling) * 0.3,
    }))
    .sort((a, b) => b.combinedDebtScore - a.combinedDebtScore);
}

Die Kombination aus hoher Komplexität und hohem Churn ist das stärkste Signal. Eine komplexe Datei, die niemand anfasst, ist stabile Schuld – lästig, aber nicht aktiv schädlich. Eine komplexe Datei, die sich in jedem Sprint ändert, ist eine tickende Zeitbombe. Priorisiere Refactoring dort, wo sich Komplexität und Änderungshäufigkeit überschneiden.

Analyse der Incident-Korrelation

Produktions-Incidents sind die teuerste Ausprägung technischer Schulden. Wenn man Incidents auf Codebereiche zurückführt, werden aus vagen "Stabilitätsbedenken" konkrete Risikobewertungen.

tstypescript
interface Incident {
  id: string;
  date: string;
  severity: "low" | "medium" | "high" | "critical";
  rootCauseModule: string;
  timeToResolveMins: number;
  engineersInvolved: number;
  customerImpact: boolean;
}
 
interface ModuleIncidentProfile {
  module: string;
  totalIncidents: number;
  criticalIncidents: number;
  avgResolutionMins: number;
  totalEngineerHours: number;
  estimatedCost: number;
}
 
function analyzeIncidentsByModule(
  incidents: Incident[],
  hourlyEngineerCost: number = 75,
  customerIncidentCost: number = 5000
): ModuleIncidentProfile[] {
  const grouped: Record<string, Incident[]> = {};
 
  for (const incident of incidents) {
    const mod = incident.rootCauseModule;
    if (!grouped[mod]) grouped[mod] = [];
    grouped[mod].push(incident);
  }
 
  return Object.entries(grouped)
    .map(([module, moduleIncidents]) => {
      const totalEngineerMins = moduleIncidents.reduce(
        (sum, i) => sum + i.timeToResolveMins * i.engineersInvolved,
        0
      );
      const totalEngineerHours = totalEngineerMins / 60;
      const customerImpactCount = moduleIncidents.filter(
        (i) => i.customerImpact
      ).length;
 
      return {
        module,
        totalIncidents: moduleIncidents.length,
        criticalIncidents: moduleIncidents.filter(
          (i) => i.severity === "critical"
        ).length,
        avgResolutionMins:
          moduleIncidents.reduce((s, i) => s + i.timeToResolveMins, 0) /
          moduleIncidents.length,
        totalEngineerHours,
        estimatedCost:
          totalEngineerHours * hourlyEngineerCost +
          customerImpactCount * customerIncidentCost,
      };
    })
    .sort((a, b) => b.estimatedCost - a.estimatedCost);
}

Ein Modul mit 12 Incidents pro Quartal, die im Schnitt 3 Stunden mit 2 Engineers zur Behebung brauchen, kostet allein an reiner Reaktionszeit rund 72 Engineer-Stunden pro Quartal – bevor man die Kosten des Kontextwechsels, die Post-Mortem-Meetings und die verdrängte Feature-Arbeit mitzählt.

Das Schuldenregister aufbauen

Ein Schuldenregister ist ein lebendes Dokument, das bekannte Posten technischer Schulden zusammen mit ihren Auswirkungsmessungen erfasst. Es verwandelt Schulden von einem vagen Gefühl in ein priorisiertes Backlog.

tstypescript
interface DebtItem {
  id: string;
  title: string;
  description: string;
  affectedModules: string[];
  estimatedRefactorDays: number;
  velocityImpact: "low" | "medium" | "high";
  incidentCorrelation: number; // incidents per quarter
  monthlyCarryingCost: number;
  paybackPeriodMonths: number;
  priority: number;
}
 
function calculatePriority(item: DebtItem): number {
  const costWeight = item.monthlyCarryingCost / 10000;
  const incidentWeight = item.incidentCorrelation * 2;
  const effortPenalty = item.estimatedRefactorDays / 30;
 
  return (costWeight + incidentWeight) / effortPenalty;
}
 
function buildDebtReport(items: DebtItem[]): string {
  const sorted = items.sort((a, b) => b.priority - a.priority);
  const totalMonthly = items.reduce(
    (sum, i) => sum + i.monthlyCarryingCost,
    0
  );
 
  let report = `# Technical Debt Register\n\n`;
  report += `**Total Monthly Carrying Cost:** $${totalMonthly.toLocaleString()}\n`;
  report += `**Total Annual Carrying Cost:** $${(totalMonthly * 12).toLocaleString()}\n\n`;
  report += `| Priority | Item | Monthly Cost | Refactor Days | Payback |\n`;
  report += `|----------|------|-------------|---------------|----------|\n`;
 
  for (const item of sorted) {
    report += `| ${item.priority.toFixed(1)} | ${item.title} | $${item.monthlyCarryingCost.toLocaleString()} | ${item.estimatedRefactorDays} | ${item.paybackPeriodMonths}mo |\n`;
  }
 
  return report;
}

Die Amortisationszeit ist die überzeugendste Kennzahl für Gespräche mit dem Business. "Dieses Refactoring braucht 15 Engineer-Tage, spart aber $8,000 pro Monat und amortisiert sich in 7 Wochen" – das ist eine Sprache, die Product Manager verstehen.

Die Präsentation vor Stakeholdern

Das Format der Präsentation zählt genauso wie die Daten selbst. Stelle technische Schulden als Geschäftsentscheidung dar, nicht als technische Beschwerde.

tstypescript
// ❌ Bad: Engineer-centric framing
const badPitch = {
  title: "We need to refactor the payment module",
  argument: "The code is messy, has high cyclomatic complexity, " +
    "and uses deprecated patterns. It needs to be rewritten properly.",
  ask: "Give us 3 sprints to clean it up",
};
 
// ✅ Good: Business-outcome framing
const goodPitch = {
  title: "Reducing payment feature delivery time by 60%",
  context:
    "Payment features take 3x longer to ship than equivalent " +
    "features in other modules. This gap costs ~$144K annually in " +
    "engineering overhead and contributed to 14 production incidents " +
    "in the last 6 months.",
  proposal:
    "A targeted 15-day refactoring investment reduces cycle time " +
    "from 12 days to 5 days per feature and cuts incident frequency " +
    "by an estimated 70%.",
  roi:
    "Investment: ~$9,000 (15 engineer-days). " +
    "Annual savings: ~$120,000 (velocity) + ~$35,000 (incidents). " +
    "Payback period: 3 weeks.",
  risk:
    "We phase this alongside feature work — no feature freeze required. " +
    "Each phase delivers measurable improvement independently.",
};

Bitte nie um einen "Refactoring-Sprint". Fordere ein konkretes Geschäftsergebnis, das durch Daten belegt ist. Das Gespräch verschiebt sich von "sollten wir den Code aufräumen?" zu "ist diese jährliche Ersparnis von $155K eine dreiwöchige Investition wert?". Das ist ein deutlich leichteres Ja.

Den Schuldenabbau über die Zeit verfolgen

Nachdem die Refactoring-Zeit genehmigt wurde, musst du zeigen, dass es funktioniert hat. Verfolge dieselben Metriken vorher und nachher, um den ROI nachzuweisen.

tstypescript
interface DebtReductionReport {
  period: string;
  module: string;
  metricsBefore: VelocityMetrics;
  metricsAfter: VelocityMetrics;
  incidentsBefore: number;
  incidentsAfter: number;
  investmentDays: number;
  measuredSavingsMonthly: number;
}
 
function generateImpactReport(report: DebtReductionReport): string {
  const cycleImprovement =
    ((report.metricsBefore.avgCycleTimeDays -
      report.metricsAfter.avgCycleTimeDays) /
      report.metricsBefore.avgCycleTimeDays) *
    100;
 
  const incidentReduction =
    ((report.incidentsBefore - report.incidentsAfter) /
      report.incidentsBefore) *
    100;
 
  return `## Refactoring Impact: ${report.module}\n` +
    `**Period:** ${report.period}\n` +
    `**Investment:** ${report.investmentDays} engineer-days\n\n` +
    `| Metric | Before | After | Improvement |\n` +
    `|--------|--------|-------|-------------|\n` +
    `| Cycle Time | ${report.metricsBefore.avgCycleTimeDays}d | ${report.metricsAfter.avgCycleTimeDays}d | ${cycleImprovement.toFixed(0)}% |\n` +
    `| Review Rounds | ${report.metricsBefore.avgReviewRounds} | ${report.metricsAfter.avgReviewRounds} | — |\n` +
    `| Incidents/mo | ${report.incidentsBefore} | ${report.incidentsAfter} | ${incidentReduction.toFixed(0)}% |\n` +
    `| Monthly Savings | — | — | $${report.measuredSavingsMonthly.toLocaleString()} |\n`;
}

Dieser Bericht erfüllt zwei Zwecke: Er validiert die aktuelle Investition und schafft Glaubwürdigkeit für künftige Refactoring-Anfragen. Wenn du zeigen kannst, dass "das letzte Refactoring in drei Monaten einen ROI von 2,5x erzielt hat", wird die Genehmigung für das nächste deutlich einfacher.

Die wichtigsten Erkenntnisse

Technische Schulden sind kein moralisches Versagen – sie sind eine finanzielle Verbindlichkeit. Der Weg von "das sollten wir refactoren" zu "das ist finanziert" führt über die Quantifizierung. Miss die Auswirkung auf die Velocity, korreliere Incidents mit Codebereichen, berechne die laufenden Kosten und stelle das Gespräch in den Rahmen von Geschäftsergebnissen.

Das Schuldenregister ist dein wichtigstes Werkzeug: ein lebendes Dokument, das jeden Schuldenposten mit seinen monatlichen laufenden Kosten und der geschätzten Amortisationszeit erfasst. Aktualisiere es vierteljährlich, teile es mit den Stakeholdern und nutze es, um Refactoring neben der Feature-Arbeit zu priorisieren.

Die Engineers, die Refactoring-Zeit bekommen, sind nicht diejenigen, die am lautesten über die Codequalität klagen. Es sind diejenigen, die Codeprobleme in Geschäftszahlen übersetzen und Refactoring als Investition mit messbarem Ertrag präsentieren.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX