Technical Debt: 6 Months vs 10 Years of Legacy Code

Quick answer
Technical debt isn’t about the age of your code, it’s about how fast hidden risks pile up. A system written by hand over ten years accumulates debt slowly: someone read every line and worried about it. A system built on code autogeneration racks up the same volume of problems in one or two sprints, because the speed of writing outpaces the speed of understanding. Industry research on AI code generation shows technical debt growing 30-41% after tool adoption, refactoring share dropping below 10%, and test coverage sitting around 12% instead of the usual 68%. Debt gets measured with static analysis and git churn metrics, not gut feeling. Next: how to diagnose it, count it, and not drown in it.

1. Diagnosis

You launched a production system six months ago. Small team, fast tools, code generated from prompts. Six months later nobody can explain why date validation exists in three different files, written three different ways. Sound familiar?

Technical debt is Ward Cunningham’s metaphor: you take a shortcut now, you pay interest later. Interest used to accrue over years. Now it accrues over weeks, because the speed of code generation jumped by an order of magnitude, while the speed of understanding it didn’t move at all.

This article is a working framework for engineers and team leads: how to diagnose debt, measure it in concrete numbers, break it down by type, and pay it off without stopping your roadmap. Nothing esoteric here. Commands, tables, a process diagram, tools that actually work.

What you’ll get out of this:

  • A method to measure debt through git churn and static analysis, 30-40 minutes for the first run
  • A classification of debt by type (security, test, duplication, comprehension) with a payoff order
  • A ready-to-use CI/CD checklist that stops debt from growing further
  • A table: “problem type → cause → solution → verification command”

You’ll need: access to a repository with commit history, Docker for running static analysis locally, and about twenty minutes without a deadline breathing down your neck.

Developer prompt

AI generates code

Linter and tests pass

Merge without architecture review

Debt builds up in the code

Production incident

Quick hotfix

Look closely at this diagram. The loop closes on itself. Until there’s a node with a green border in it, a point where someone actually stops and checks the architecture, debt grows exponentially, not linearly.

2. Causes

Seven reasons why six months on autogeneration beat ten years of hand-written legacy code. Not five, because in reality there are always more causes than fit into a round number.

2.1. Duplicated logic instead of reuse

AI solves the specific prompt in front of it. It doesn’t search the codebase for an existing utility that does the same thing, and even when it finds one, it doesn’t always use it. The result: a date formatting function lives in four variations, the same database query pattern gets copied and slightly tweaked across six controllers. Analysis of code duplication in projects built on generation shows multiples of the duplication found in hand-written code of the same size.

2.2. Only the “happy path” gets handled

The code handles the case you described in your prompt. Cases you didn’t describe, a database unavailable for a second, an external API returning a 429, a file one kilobyte over the limit, go unhandled. It works in tests. It fails in production at 3 AM, when none of the tested conditions apply.

2.3. “It works, but nobody knows why”

The most expensive type of debt. The code produces the correct result through a chain of operations that made sense to the model at generation time, but has no obvious logic for a human reading it a month later. Refactoring feels risky: nobody knows what will break.

2.4. Test coverage fails by default

AI tools write code that gets past the prompt. They write tests only if explicitly asked, and not always the right tests. In practice, average coverage in projects built on autogeneration drops to a fraction of the industry norm.

2.5. Refactoring share drops

Developers used to clean up surrounding code while adding a feature. When code is generated rather than written, that habit disappears, generating something new is easier than digging into the old. The share of refactored lines among changed lines drops year over year by a wide margin.

2.6. Organizational pressure to move faster

The “features per sprint” metric keeps climbing. The “architecture reviews per feature” metric doesn’t. Nobody budgets time for a pause, because a pause doesn’t show up in a velocity report.

2.7. The illusion of cleanliness

The code compiles, passes the linter, looks tidy. The reviewer sees a neat diff and digs less deep, everything looks fine on the surface. That doesn’t make the debt any smaller. It just makes it invisible to a human and visible only to an incident profiler at 3 AM.

True story
A drop of nicotine kills a horse. One normal-looking function with zero tests kills your Friday night release.

3. The recipe: how to measure and prioritize debt

Preparation

You need three things before you start:

  • A git repository with full commit history, at least 3-6 months
  • Docker for running static analysis locally, no cloud subscription needed
  • Read access to CI metrics, if a pipeline exists, tests and coverage are already tracked somewhere

The first step isn’t a tool, it’s a question: how much of the code in this project was written by a human, and how much was generated and merged without changes. If you don’t have an answer, that’s already a diagnosis.

Step 1. Measure churn, how often code gets rewritten two weeks after the commit

The churn metric shows how many lines change or get deleted in the first two weeks after they appear. High churn is a clear marker of code that was written without much thought and rewritten right away.


# List of files changed in the last 6 months, with commit counts
git log --since="6 months ago" --name-only --pretty=format: | sort | uniq -c | sort -rn | head -30

Result: a list of files that top the change frequency chart. If the top 10 files aren’t configs or migrations, but business logic, that’s your first list of candidates for review.

Step 2. Run static analysis

The fastest route is a local SonarQube in a container. It calculates technical debt in hours-to-fix using the SQALE method, adding up the fix time for every flagged issue.


docker run -d --name sonarqube -p 9000:9000 sonarqube:community

# After startup (a minute or two), run analysis on the project
sonar-scanner \
  -Dsonar.projectKey=my-project \
  -Dsonar.sources=. \
  -Dsonar.host.url=http://localhost:9000 \
  -Dsonar.login=<your_token>

Result: a dashboard with code smell counts, duplication, test coverage, and an overall debt estimate in person-hours.

Step 3. Split what you found into four types

Not all debt is equally dangerous. A practical way to split it is by the type of risk it carries:

Debt type What it is Why it’s dangerous
Security debt Vulnerabilities and unsafe patterns Exploitable from outside, maximum cost of failure
Test debt Missing coverage for critical paths Users find the bugs, not CI
Duplication debt One piece of logic living in multiple places You fix a bug in one spot, it stays alive in three others
Comprehension debt Code works, but the logic is opaque Refactoring gets postponed indefinitely out of fear of breaking something

This classification echoes Martin Fowler’s technical debt quadrant, which splits debt into deliberate and inadvertent, reckless and prudent. For code built on autogeneration, nearly all debt lands in the “inadvertent, but not always reckless” quadrant, the decision looked reasonable at the time, nobody checked the consequences.

Step 4. Pay off debt in order of risk, not in order of annoyance

The temptation is to fix whatever bothers you most. The correct order is different: security debt first, then test debt, then duplication, comprehension last. It’s the most unpleasant kind, but the one least likely to take down production directly.


# Simple script: flags files with high churn and low coverage
# as first-priority review candidates
import subprocess
import json

def get_churn(path="."):
    result = subprocess.run(
        ["git", "log", "--since=6 months ago", "--name-only", "--pretty=format:"],
        cwd=path, capture_output=True, text=True
    )
    files = [f for f in result.stdout.splitlines() if f.strip()]
    counts = {}
    for f in files:
        counts[f] = counts.get(f, 0) + 1
    return counts

def flag_candidates(churn, coverage_map, churn_threshold=10, coverage_threshold=40):
    candidates = []
    for f, c in churn.items():
        cov = coverage_map.get(f, 0)
        if c >= churn_threshold and cov < coverage_threshold:
            candidates.append({"file": f, "churn": c, "coverage": cov})
    return sorted(candidates, key=lambda x: x["churn"], reverse=True)

if __name__ == "__main__":
    churn = get_churn()
    # coverage_map comes from a SonarQube report or coverage.py
    coverage_map = {}
    print(json.dumps(flag_candidates(churn, coverage_map), indent=2))

Result: a concrete list of files where high change frequency meets low test coverage, exactly where the most expensive debt hides.

4. Verification

Debt goes down not because things “feel calmer”, but because three numbers move in the right direction. Check them weekly, not once a quarter.


# Re-run analysis to compare against the previous run
sonar-scanner -Dsonar.projectKey=my-project -Dsonar.host.url=http://localhost:9000

# Churn trend over the last 4 weeks vs the previous 4
git log --since="4 weeks ago" --name-only --pretty=format: | sort | uniq -c | wc -l
git log --since="8 weeks ago" --until="4 weeks ago" --name-only --pretty=format: | sort | uniq -c | wc -l
Metric What an increase means What a decrease means
Debt in hours (SonarQube) Accumulating faster than it’s paid off Payoff is outpacing accumulation
Churn in top files Unstable logic, rewritten by trial and error Logic has stabilized
Test coverage Tests lag behind the code Tests keep up with the code
Duplication share Copy-pasting instead of reuse Shared modules are actually being reused

If three out of four metrics move the wrong way for two months straight, panic is premature, but you already have a reason to carve out a dedicated sprint for paying down debt.

5. Complications

One format throughout: the problem type, why it happens, what to do, how to verify the fix.

Problem Cause Solution Verification
Same bug gets fixed, then resurfaces somewhere else Duplicated logic across multiple files Extract a shared function, find all copies by searching for the signature grep -rn "validation_function" --include=*.py .
Incident from a rare case “nobody tested” AI only generated the happy path Add edge case tests manually, don’t rely on autogenerated tests for this Run coverage on the specific module
Refactoring gets postponed for months Nobody understands the module’s logic as a whole Pair code reading before making changes, document invariants directly in the code Review with a mandatory “why this works this way” comment
Security scanner finds vulnerabilities in fresh code AI generation without context on the project’s security policies Mandatory security gate in CI before merge, not after Run a SAST scanner in the pipeline on every PR
Warning
Never push security debt to “next sprint”. A vulnerability your scanner finds today might be found by someone else before your team gets to it.

6. Alternatives

There are at least four strategies for paying off debt. Which one fits depends on your team size and how much time is left until the next release.

  • Big-bang rewrite. Stop shipping features, rewrite the module from scratch. Fast on paper, almost always slower and more expensive in reality, rewrite estimates are chronically optimistic.
  • Strangler pattern, gradual replacement. New code gets written alongside the old, the old gets phased out piece by piece. Slower, but production never stops for a day.
  • Freeze sprint for debt payoff. Features get paused for one iteration, the whole team works through the list from section 3. Works for teams up to 10-15 people, organizationally nearly impossible at larger scale.
  • Governance layer over the process. Permanent quality gates in CI, mandatory architecture review for generated code above a certain volume. Not a one-time fix, a permanent rule of the game.

For teams where code gets generated at scale by AI tools, the default that works best is a combination: strangler pattern for existing debt, governance layer for new code. A full stop for a rewrite usually loses the competition, while you’re rewriting, the market isn’t waiting.

7. Prevention

Three things keep debt under control without Friday-night heroics.

Monitor debt as a metric, not a feeling

Churn, coverage, and code smell counts should be numbers on a dashboard that get reviewed regularly, the same way you check uptime. Debt that isn’t measured doesn’t get managed.

Quality gate in CI/CD, a block, not a suggestion


# Example CI quality gate thresholds (pseudo-config)
new_code_coverage: minimum 70%
new_duplicated_lines: maximum 3%
new_security_hotspots: 0 unresolved

The merge doesn’t go through if new code fails the threshold. Not a “warning in chat”, a red status in the pipeline.

Keeping expertise inside the team

Part of the industry is cutting junior hiring, leaning on AI generation instead. That creates a gap: there’s nobody left to pay off debt by hand exactly when there’s the most of it. A reviewer who came up through the ranks spots a suspicious pattern faster than any linter.

8. FAQ

Why does technical debt keep growing even after code review?

Because review looks at the diff, not the whole architecture. Neatly formatted code passes review faster, even if it duplicates logic from another module underneath. Review catches style, not systemic consequences.

How do you verify that a refactor actually reduced debt?

Compare three numbers before and after: debt hours from SonarQube, churn of the affected files over the following month, test coverage of the changed modules. A subjective “it feels cleaner” doesn’t count.

What do you do if the whole debt backlog doesn’t fit in a sprint?

Don’t try to close everything at once. Take the top 20% from the “complications” section ranked by risk, that usually delivers most of the impact. Spread the rest across future iterations with a fixed time quota per sprint.

How is vibe coding different from regular technical debt?

Speed and shape. Regular debt grows linearly, like interest on a loan. Debt from mass AI generation grows in chunks: changes inside one module look locally correct, but together they form an inconsistent system. The nature of the debt isn’t different, the counter just ticks several times faster.

Do you need to stop using AI code generation to avoid debt?

No, and that would be about as pointless as giving up compilers. What you need is a governance layer: mandatory review, tests, and a security gate specifically for generated code, because it carries a different risk profile.

9. Outlook

Debt isn’t going anywhere, it exists in any living system, old or young. The difference between a team that has things under control and a team that’s putting out fires every Friday isn’t who writes code faster. It’s who set up a dashboard with churn and coverage first, and who doesn’t let security debt survive until the next release.

It’s not as bad as it sounds after reading the causes section. It’s actually simpler: three metrics, one governance layer in CI, prioritize by risk, and debt stops growing faster than the team can pay it off. If you implemented this and it didn’t work out, drop a comment, we’ll dig into your specific case.

Оставайтесь на связи

Рецепты от IT-боли. Без воды, без рекламы, без маркетинговой шелухи.

Подписаться на IT-Аптеку →

Мы ВКонтакте

IT-Аптека — советы, новости и помощь рядом.

Вступить в группу ВКонтакте →
Поделитесь:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top