Faisalaffan Blog
|
🇬🇧
Faisalaffan Blog
Retrospectives18 min read

The $55K/Year Tech Debt Nobody Noticed — How I SOLD a Massive Refactor to Management (And WON)

Your MVP was built in record time, but now every new feature takes 3X longer? This is the raw, unfiltered true story of how silent tech debt secretly devoured $55,000/year — and the EXACT framework I used to convince management that refactoring is an explosive investment, not a cost.

Faisal Affan
Faisal Affan
Published
Gallery
1 / 3

Refactoring & Compounding Tech Debt

"Move fast and break things. Unless you are breaking things faster than you can fix them." — Adapted from Mark Zuckerberg's later reflection

Context of this Article

This article is not an academic theory about tech debt. This is a real retrospective — the moment a team realized that their initial development speed (MVP) had become a technical debt that choked business iteration. We will dissect the true cost, migration strategy, and how to convince management that pausing to fix the foundation is the smartest business decision they can make.


1. The Reckoning: When MVP Becomes a Monster

1.1 A Familiar Story

Every startup has been at this point. The first month, everything feels magical — features are shipped in days, pivots happen in hours, and "we'll fix it later" becomes the team's mantra.

Then "later" arrives.

graph TD
    A["Month 1-3: MVP Phase<br/>🚀 Ship everything fast"] --> B["Month 4-6: Growth Phase<br/>📈 Features grow, team expands"]
    B --> C["Month 7-12: Friction Phase<br/>⚠️ Bugs increase, deploy slows down"]
    C --> D["Month 12-18: Crisis Phase<br/>🔥 Developers frustrated, business stalled"]
    D --> E{Choices}
    E -->|"Ignore"| F["💀 Technical Bankruptcy<br/>Rewrite from scratch"]
    E -->|"Act"| G["🔧 Strategic Refactoring<br/>Fix as you go"]

    style A fill:#d1fae5,stroke:#10b981,color:#000
    style B fill:#fef3c7,stroke:#f59e0b,color:#000
    style C fill:#fed7aa,stroke:#f97316,color:#000
    style D fill:#fee2e2,stroke:#ef4444,color:#000
    style F fill:#fecaca,stroke:#dc2626,color:#000
    style G fill:#bbf7d0,stroke:#22c55e,color:#000

1.2 Signs that Tech Debt is Compounding

The Silent Killer

Tech debt never arrives suddenly. It compounds — like unpaid loan interest. Every sprint spent without paying down the debt increases the interest. And one day, the interest is larger than the team's capacity to pay.


2. The Cost of Bad Code: Calculating Business Losses

2.1 Where Developer Time Actually Goes

One of the most awakening moments was when we conducted a time audit for 4 consecutive sprints and found this data:

pie title "Developer Time Distribution — Healthy vs Troubled Codebase"
    "Feature Development" : 15
    "Merge Conflict Resolution" : 25
    "Bug Fixing (Regression)" : 20
    "Workaround & Patching" : 15
    "Debugging Spaghetti Code" : 10
    "Code Review (Due to Complexity)" : 10
    "Waiting (Build/Deploy)" : 5
A Shocking Data Point

Developers spent 70% of their time just resolving code conflicts, fixing regressions, and navigating a fragile architecture — instead of building features. This means out of 5 engineers, only 1.5 engineers were truly productive in building new value.

2.2 Quantifying the Loss in Currency

Let's quantify this real loss so we can speak in an language that management understands. (Using IDR conversions originally, translated contextually).

analysis/healthy-team.ts
const healthyTeam = {
  engineers: 5,
  monthlyRate: 30_000_000, // per engineer, fully loaded
  totalMonthlyCost: 5 * 30_000_000, // = Rp 150,000,000

  timeDistribution: {
    featureDevelopment: 0.55, // 55%
    bugFix: 0.1, // 10%
    codeReview: 0.1, // 10%
    techDebt: 0.1, // 10% (proactive)
    meetings: 0.1, // 10%
    mergeConflicts: 0.05, // 5%
  },

  effectiveFeatureOutput: 5 * 0.55, // = 2.75 engineer equivalents
  monthlyFeatureValue: 2.75 * 30_000_000, // = Rp 82,500,000 worth of features
  velocityTrend: "accelerating",
};

2.3 The Compound Interest of Tech Debt

xychart-beta
    title "Velocity Projection: Without vs With Refactoring Investment"
    x-axis ["Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "Q7", "Q8"]
    y-axis "Story Points per Sprint" 0 --> 80
    line "Without Refactoring" [40, 35, 28, 22, 18, 14, 12, 10]
    line "With Refactoring (20% sprint)" [40, 35, 32, 38, 45, 52, 58, 65]

Notice the graph above. Both teams start at the same point (40 story points per sprint). The team that does not invest in refactoring experiences a continuous velocity decline — from 40 to 10 in 8 quarters.

The team that allocates 20% of their sprints for tech debt does experience a slight dip initially (due to reduced bandwidth), but then their velocity explodes as the codebase becomes cleaner and easier to extend.

The Math

After 2 years, the team investing in refactoring produces 6.5x more output than the team ignoring tech debt — even though they "only" spend 80% of their time on features.


3. Large-Scale Migration: Zero Downtime Transformation

3.1 Anatomy of a Large-Scale Migration

The following retrospective is from a real project: migrating a legacy frontend to a modern architecture without disrupting active customers.

PropertyTypeDescription
From Legacy Frontend jQuery + server-rendered templates, monolithic CSS, tightly coupled with backend APIs, zero test coverage, 4 years of accumulated code without standardization.
To Modern SPA/SSR React/Next.js (or Vue/Nuxt, Flutter Web — context dependent), component-based architecture, design system, API contract via OpenAPI, >80% test coverage.
Constraint Zero Downtime 50K+ daily active users. There can be zero downtime or UX degradation during the migration process. Business must continue normally.
Timeline 6-9 months Parallel to feature development. The team cannot 'freeze' new features during migration because the business still needs to iterate.

3.2 The Strangler Fig Pattern

The strategy we used: Strangler Fig Pattern — gradually replacing legacy components with new components, without ever performing a "big bang" switch.

graph TD
    subgraph "Phase 1: Foundation (Month 1-2)"
        A1["Setup new project<br/>(React/Next.js)"] --> A2["Design System &<br/>Component Library"]
        A2 --> A3["API Gateway /<br/>BFF Layer"]
        A3 --> A4["Shared Auth &<br/>Session Management"]
    end

    subgraph "Phase 2: Migration (Month 3-6)"
        B1["Route-by-route migration<br/>Start from the simplest page"] --> B2["Reverse proxy routing<br/>Legacy vs New"]
        B2 --> B3["Feature flag per route<br/>Gradual rollout 1% → 100%"]
        B3 --> B4["Parallel running &<br/>comparison testing"]
    end

    subgraph "Phase 3: Completion (Month 7-9)"
        C1["Migrate remaining<br/>complex pages"] --> C2["Performance optimization<br/>& monitoring"]
        C2 --> C3["Decommission legacy<br/>frontend"]
        C3 --> C4["Post-migration<br/>cleanup & documentation"]
    end

    A4 --> B1
    B4 --> C1

    style A1 fill:#dbeafe,stroke:#3b82f6,color:#000
    style A2 fill:#dbeafe,stroke:#3b82f6,color:#000
    style A3 fill:#dbeafe,stroke:#3b82f6,color:#000
    style A4 fill:#dbeafe,stroke:#3b82f6,color:#000
    style B1 fill:#fef3c7,stroke:#f59e0b,color:#000
    style B2 fill:#fef3c7,stroke:#f59e0b,color:#000
    style B3 fill:#fef3c7,stroke:#f59e0b,color:#000
    style B4 fill:#fef3c7,stroke:#f59e0b,color:#000
    style C1 fill:#d1fae5,stroke:#10b981,color:#000
    style C2 fill:#d1fae5,stroke:#10b981,color:#000
    style C3 fill:#d1fae5,stroke:#10b981,color:#000
    style C4 fill:#d1fae5,stroke:#10b981,color:#000

3.3 Reverse Proxy: The Key to Zero Downtime

The most critical concept: using a reverse proxy (Nginx/Caddy/Traefik) to route traffic between the legacy and new apps based on paths or feature flags.

nginx/migration-routing.conf
# Phase 2: Route-based migration
# Migrated pages → New App
# Unmigrated pages → Legacy App

upstream legacy_app {
    server 127.0.0.1:3000;
}

upstream new_app {
    server 127.0.0.1:4000;
}

server {
    listen 80;
    server_name app.example.com;

    # Pages migrated to new app
    location /dashboard {
        proxy_pass http://new_app;
    }

    location /settings {
        proxy_pass http://new_app;
    }

    location /reports {
        proxy_pass http://new_app;
    }

    # All other pages remain on legacy
    location / {
        proxy_pass http://legacy_app;
    }
}
Gradual Rollout

With this approach, you can migrate one page per sprint, validate with real users, and rollback in seconds if an issue arises — simply by modifying the proxy config. Zero downtime, zero drama.

3.4 Lesson: What Went Well and What Didn't

Design System First

Building the component library and design system before starting the migration ensured UI consistency and exponentially accelerated the development of subsequent pages.

Feature Flag per Route

Using feature flags allowed for phased rollouts (1% → 10% → 50% → 100%) and instant rollbacks. Not a single incident of downtime occurred during the migration.

Parallel Comparison Testing

Running both versions in parallel and comparing output helped catch behavioral discrepancies prior to full rollout.

API Contract via OpenAPI

Defining API contracts before migrating ensured backend and frontend could evolve independently.


4. Selling the Refactor to Management

4.1 Why This Is Hard

graph LR
    subgraph "Management Perspective"
        M1["Refactoring = <br/>No new features"] --> M2["No new features = <br/>No growth"]
        M2 --> M3["No growth = <br/>Disappointed investors"]
    end

    subgraph "Engineering Perspective"
        E1["No refactoring = <br/>Velocity keeps dropping"] --> E2["Velocity drops = <br/>Features slow down"]
        E2 --> E3["Features slow down = <br/>Competitors win"]
    end

    M3 ---|"Eternal Tension"| E3

    style M3 fill:#fee2e2,stroke:#ef4444,color:#000
    style E3 fill:#fee2e2,stroke:#ef4444,color:#000
The Communication Gap

When engineers say "we need a refactoring", management hears "we want to play around with code without producing anything visible to customers." This communication gap is the root of the problem. The solution isn't avoiding the conversation — it's changing the language.

4.2 Framework: The Refactoring Business Case

Never approach management with "we need to refactor because the code is bad." Come with a structured business case.

Quantify the Problem (Data, Not Opinions)

Gather data over 2-4 sprints:

business-case/problem-quantification.ts
const currentState = {
  // Velocity metrics
  avgVelocity: {
    sixMonthsAgo: 42, // story points per sprint
    current: 18, // story points per sprint
    trend: "declining",
    projectedIn6Months: 8,
  },

  // Time distribution
  timeOnFeatures: 0.15, // 15% — should be 50%+
  timeOnBugFixes: 0.25, // 25% — should be <15%
  timeOnMergeConflicts: 0.2, // 20% — should be <5%

  // Business impact
  avgFeatureDeliveryTime: {
    sixMonthsAgo: "5 days",
    current: "18 days",
  },

  // Incident frequency
  productionIncidents: {
    sixMonthsAgo: "1 per month",
    current: "4 per month",
  },

  // Team health
  developerSatisfaction: 3.2, // out of 10
  attritionRisk: "HIGH",
};

Calculate the Cost of Inaction

business-case/cost-of-inaction.ts
const costOfInaction = {
  // Wasted engineer time per month
  wastedTimePerMonth: {
    engineers: 5,
    wastedPercentage: 0.55, // 55% time wasted
    monthlyCost: 5 * 30_000_000 * 0.55,
    // = Rp 82,500,000 per month wasted
  },

  // Projected over 12 months
  annualWaste: 82_500_000 * 12,
  // = Rp 990,000,000 (Almost 1 Billion Rupiah!)

  // Attrition cost if developers resign
  attritionCost: {
    probabilityOfResignation: 0.4, // 40% risk
    costPerReplacement: 150_000_000, // 5x monthly salary
    expectedAttritionCost: 0.4 * 2 * 150_000_000,
    // = Rp 120,000,000 (2 engineers likely to leave)
  },

  // Opportunity cost: unshipped features
  lostRevenue: {
    featuresDelayed: 8, // per quarter
    avgRevenuePerFeature: 50_000_000,
    quarterlyLoss: 8 * 50_000_000,
    // = Rp 400,000,000 per quarter
  },

  totalAnnualCost: 990_000_000 + 120_000_000 + 400_000_000 * 4,
  // = Rp 2,710,000,000 (Rp 2.71 Billion!)
};

Present the Investment (Not "Cost")

business-case/refactoring-investment.ts
const refactoringInvestment = {
  duration: "8 weeks focused refactoring",
  teamAllocation: "3 of 5 engineers (2 remain on features)",
  investmentCost: 3 * 30_000_000 * 2, // 3 eng x 2 months
  // = Rp 180,000,000

  expectedOutcomes: {
    velocityRecovery: "18 → 35 story points (+94%)",
    featureDeliveryTime: "18 days → 7 days (-61%)",
    bugRate: "-60% production incidents",
    mergeConflicts: "-80% conflict resolution time",
    developerSatisfaction: "3.2 → 7.5 (out of 10)",
  },

  roi: {
    annualSavings: 990_000_000 * 0.6, // 60% waste recovered
    // = Rp 594,000,000 per year
    investmentCost: 180_000_000,
    roiMultiple: 594_000_000 / 180_000_000,
    // = 3.3x ROI in year 1
    paybackPeriod: "~4 months",
  },
};

Show the Timeline with Milestones

%%MDX_BLK_0_5%%

Propose Risk Mitigation

4.3 Pitch Deck: The One Convincing Slide

If you only have one slide to convince management, use this:

Without Refactoring With Refactoring
Investment Rp 0 Rp 180 Million (8 weeks)
Annual Loss Rp 2.71 Billion Rp 990 Million
Feature Delivery 18 days/feature (worsening) 7 days/feature (improving)
Production Incidents 4x/month (rising) 1x/month (stable)
Developer Attrition Risk 40% (2 people) 10% (normal)
Velocity Trend Down 50% per 6 months Up 20% per quarter
Net Impact (Year 1) -Rp 2.71 Billion +Rp 594 Million saving
ROI N/A 3.3x
Pitch Closing Statement

"Pausing feature releases for 8 weeks to fix the foundation is not stalling the business — it's saving Rp 1.72 Billion per year and restoring the team's ability to move fast. Without this investment, in 12 months we will require a total rewrite costing 5-10x more."


5. Playbook: Measurable Refactoring Strategy

5.1 Prioritization Matrix

Not all tech debt is equal. Use this matrix to prioritize:

quadrantChart
    title Tech Debt Prioritization Matrix
    x-axis "Low Effort to Fix" --> "High Effort to Fix"
    y-axis "Low Business Impact" --> "High Business Impact"
    quadrant-1 "Plan & Schedule"
    quadrant-2 "Quick Wins — Do First!"
    quadrant-3 "Ignore / Accept"
    quadrant-4 "Strategic Project"
    "Merge conflicts": [0.3, 0.9]
    "Slow CI/CD": [0.2, 0.7]
    "No tests on Module A": [0.4, 0.8]
    "Legacy CSS": [0.7, 0.3]
    "Old logging format": [0.2, 0.2]
    "Database migration": [0.8, 0.85]
    "Monolith decomposition": [0.9, 0.7]
    "Outdated docs": [0.3, 0.15]

5.2 The 20% Rule

Sustainable Debt Management

Once a major refactoring is complete, allocate 20% of every sprint to tech debt prevention. This is like paying mortgage installments — small, consistent, and preventing the debt from piling up again. 20% today prevents a 100% rewrite tomorrow.

strategy/sprint-allocation.ts
interface SprintAllocation {
  featureDevelopment: number;
  techDebtPaydown: number;
  bugFixes: number;
}

const sustainableAllocation: SprintAllocation = {
  featureDevelopment: 70, // New features for business
  techDebtPaydown: 20, // Pay down tech debt installments
  bugFixes: 10, // Buffer for unforeseen bugs
};

// Monitor: If bugFixes consistently > 20%,
// it's a sign tech debt is still too high

5.3 Metrics Dashboard: Track the Recovery

After refactoring begins, track these metrics every sprint:

PropertyTypeDescription
Velocity Trend Story Points per Sprint Target: Return to baseline within 3-4 sprints post-refactoring. If it hasn't risen after 4 sprints, re-evaluate the refactoring scope.
Cycle Time Hours from first commit to production Target: < 2 days. This metric reveals how smooth the development pipeline is. A cycle time drop is the earliest indicator of successful refactoring.
Merge Conflict Rate Number of conflicts per PR Target: < 1 conflict per 5 PRs. If the rate remains high, focus on better modularization and clear domain boundaries.
Bug Escape Rate Bugs slipping to production per sprint Target: < 2 per sprint. Decreasing this metric indicates improving test coverage and code quality.
Developer Satisfaction Survey score (1-10) Target: > 7. A frequently ignored metric but highly predictive of attrition. Conduct anonymous surveys every 2 weeks.

6. Lessons Learned: What We'd Do Differently

Don't Wait Until It's a Crisis

We started refactoring when velocity had already sunk 57%. We should have started when the first signs appeared — when velocity started constantly plateauing or merge conflicts began increasing. The earlier it begins, the cheaper the cost. Rule of thumb: if velocity drops 15% for 2 consecutive sprints, that's an alarm.

Refactoring Needs Ownership, Not a Committee

We originally formed a "tech debt committee" that met weekly to decide what to refactor. The result: endless meetings, little progress. What worked: appointing 1 senior engineer as the "Refactoring Lead" with the authority to make daily decisions. The committee only reviewed progress at sprint's end.

Measure Before, During, and After

Without baseline metrics, you cannot prove that refactoring succeeded. We almost failed to justify the investment because we lacked solid "before" data. Always collect 2-4 sprints of data prior to initiating a refactor.

Overcommunication is Better Than Under

Every Friday, we sent updates to a Slack channel followed by management: what was fixed, changed metrics, and before/after screenshots. This built trust and prevented demoralizing questions like "when will refactoring finish?"

Don't Refactor Everything at Once

The greatest temptation: "let's just fix everything while we're at it." This is a recipe for disaster. Focus on 2-3 areas of highest impact first. The resulting momentum provides evidence to push refactoring in subsequent areas.


7. Conclusion

TL;DR

Tech debt is not a technical problem — it's a business problem that happens to manifest in code. The way to overcome it is not with technical jargon, but with business language: measurable losses, quantifiable investments, and accountable ROI.

Acknowledge That MVP Speed Has a Price

Every past shortcut is a loan that now must be repaid. It wasn't a mistake — it was the correct decision at the time. But now contexts have changed, and the debt must be managed.

Quantify the Loss, Don't Complain

Don't say "the code is messy." Say: "The team loses Rp 720 Million manually per year because 70% of time gets wasted navigating fragile architectures." Data beats opinions.

Gradual Migration, Not Big Bang

Strangler Fig Pattern. Feature flags. Reverse proxies. Zero downtime. There is absolutely no reason to risk a running business just to refactor.

Sell it with ROI, Not Ego

Management doesn't care that your code is "cleaner." They care that a Rp 180 Million investment generates Rp 594 Million in annual savings. Frame it as an investment, not an expense.

Prevent, Don't Await a Crisis

Allocate 20% of every sprint for tech debt. Pay the installment. Don't wait until the interest eclipses the principle.

"The best time to refactor was 6 months ago. The second best time is now." Every passing day absent of action compounds the tech debt interest. Start today — not with a massive rewrite, but with one measured step proving that fixing the foundation is a powerful investment, not a cost.