Prioritizing Technical Debt in PHP Projects
AI generated
<?php
8.4
PHP · Legacy · Technical Debt · Prioritization
Prioritizing Technical Debt in PHP Projects
A traceable method instead of gut feeling

Every grown PHP project accumulates technical debt, the question is never whether but which item should be fixed first. This article shows a practical method for scoring technical debt by change frequency, risk of failure and fix effort, and deriving a defensible order from it.

18 min read Hotspot analysis · Scoring system · Refactoring backlog PHP 8.x · Legacy projects

1. Why a list of known problems is not enough

Technical debt in PHP projects is rarely documented, it usually exists only as shared knowledge in the team about which class is best left untouched and which file causes anxiety on every change. The problem with this implicit knowledge: it is not prioritized, not measurable, and disappears with every developer who leaves the project. A mere list of known problems, as many teams keep on a wiki page, only partly solves this because it gives no order and quickly goes stale.

The actual bottleneck is not recognizing technical debt, any experienced developer can name a dozen problem spots within an hour. The bottleneck is prioritization: which of the twenty known problems should be fixed this quarter, if there is only time for three? Without a traceable method, this decision is made either by the loudest developer in the room or by whoever is currently most annoyed at a particular module.

A defensible prioritization needs three ingredients, built up step by step in the following sections: an objective method to measure how often a code area is actually touched, an assessment of the risk a problem poses, and a realistic estimate of the fix effort. Only the combination of these three factors turns a list of subjective opinions into a traceable order for technical debt.

2. Distinguishing three kinds of technical debt

Not every form of technical debt deserves the same attention. It is worth distinguishing at least three categories: deliberately incurred debt, such as a pragmatic workaround under time pressure that was marked temporary from the start; unintentionally incurred debt, which only becomes visible as a problem through new insights or growing requirements; and outdated debt, arising from the evolution of PHP itself or the libraries used, such as relying on a function now considered unsafe.

This distinction is not an academic exercise, it directly affects prioritization. Deliberately incurred debt with clear documentation is often easier to fix because its context is known. Unintentional debt first requires analysis to understand the actual scope. Outdated debt, triggered for example by a PHP version upgrade, often has a hard deadline because an old function is removed in the next major version, which automatically bumps it up in priority regardless of other criteria.

3. Hotspot analysis: deriving change frequency from git

The most important objective factor in prioritizing technical debt is not how bad a file looks, but how often it is actually changed. An ugly file that has not been touched for years costs the team little, because nobody touches it. A mediocre file changed several times a week, on the other hand, continuously costs time and nerves. This insight, often called hotspot analysis, can be derived directly from the git history, without any subjective assessment.

The following script counts, for every PHP file in the project, how many commits touched it over the last twelve months, providing a first, objective ranking of the most frequently changed files, independent of their perceived code quality.


#!/usr/bin/env bash
# hotspot-analysis.sh — rank PHP files by change frequency (last 12 months)
set -euo pipefail

git log --since="12 months ago" --name-only --pretty=format: -- '*.php' \
  | grep -v '^$' \
  | sort \
  | uniq -c \
  | sort -rn \
  | head -30 \
  | awk '{printf "%-6s %s\n", $1, $2}'

# Output example:
#   142    app/code/Legacy/Checkout/Model/OrderProcessor.php
#   98     app/code/Legacy/Catalog/Model/PriceCalculator.php
#   67     app/code/Legacy/Customer/Model/AccountManager.php

Combining this change frequency with a complexity metric for each file produces a far more meaningful prioritization than pure gut feeling: a file that is both frequently changed and highly complex is likely causing the largest ongoing harm and should be prioritized accordingly.

4. Measuring complexity with static metrics

Change frequency alone still says nothing about a file's actual risk. A simple, well structured file that is changed frequently is unproblematic as long as changes can be made easily and safely. Only the combination of high change frequency and high cyclomatic complexity turns a file into a real risk for technical debt prioritization.

Tools such as PHPMD (PHP Mess Detector) or PHPStan with complexity rules enabled provide a cyclomatic complexity score per method, the number of independent paths through the code. A method with complexity above fifteen is considered a clear refactoring candidate in most teams, regardless of how often it changes. Combined with the change frequency from section three, this yields a matrix with four quadrants: high complexity with high change frequency as a clear immediate candidate, high complexity with low change frequency as worth watching but not urgent, and the two correspondingly mirrored, less critical cases.


#!/usr/bin/env bash
# complexity-report.sh — run PHPMD cyclomatic complexity rule on hotspots
set -euo pipefail

vendor/bin/phpmd app/code/Legacy/Checkout/Model/OrderProcessor.php \
  text cyclomatic-complexity

# Example output:
# OrderProcessor.php:42  The method calculateTotal() has a Cyclomatic
#   Complexity of 28. The configured cyclomatic complexity threshold is 10.
# OrderProcessor.php:118 The method applyDiscounts() has a Cyclomatic
#   Complexity of 19. The configured cyclomatic complexity threshold is 10.

5. A scoring system of risk, frequency and effort

The pure combination of change frequency and complexity is still not enough for a defensible prioritization, because it ignores fix effort. A high risk problem that can only be fixed with two weeks of effort genuinely competes with a medium risk problem that can be solved in one day. A simple scoring system resolves this trade-off by explicitly weighing risk and effort against each other.

The following PHP script computes a priority value for a list of recorded technical debt items using the formula risk times frequency divided by effort, modeled after the Weighted Shortest Job First (WSJF) principle known from project management. The higher the value, the better the ratio of benefit to cost of fixing it.


<?php

declare(strict_types=1);

// Weighted Shortest Job First style prioritization for technical debt items
final class TechnicalDebtItem
{
    public function __construct(
        public readonly string $description,
        public readonly int $risk,       // 1 (low) to 5 (critical)
        public readonly int $frequency,  // changes per quarter, from git log
        public readonly int $effort,     // estimated days to fix
    ) {
    }

    public function priorityScore(): float
    {
        // Higher score = better ratio of benefit to cost
        return ($this->risk * $this->frequency) / max(1, $this->effort);
    }
}

$items = [
    new TechnicalDebtItem('OrderProcessor: no test coverage', risk: 5, frequency: 12, effort: 8),
    new TechnicalDebtItem('PriceCalculator: cyclomatic complexity 28', risk: 4, frequency: 9, effort: 3),
    new TechnicalDebtItem('AccountManager: deprecated mysql_* calls', risk: 3, frequency: 4, effort: 2),
];

usort($items, static fn (TechnicalDebtItem $a, TechnicalDebtItem $b): int =>
    $b->priorityScore() <=> $a->priorityScore());

foreach ($items as $item) {
    printf("%.2f  %s\n", $item->priorityScore(), $item->description);
}

The value of the script is not mathematical precision, risk and effort remain estimates. The real benefit is that everyone involved must discuss the same three numbers instead of negotiating a ranking by feeling. A team arguing about the risk assessment of OrderProcessor is arguing about a concrete, checkable number instead of a diffuse unease.

6. Building and maintaining the refactoring backlog

Once a scoring system is established, technical debt needs a fixed place in project management, instead of only being mentioned in commit comments as "TODO: refactor". A dedicated backlog, maintained alongside the feature backlog, makes debt visible and discussable, rather than letting it disappear into code comments where nobody systematically sees it anymore.

It is important to re-evaluate this backlog regularly, at least once a quarter, because both change frequency and risk shift over time. A file that was a hotspot a year ago may drop off the list after a successful refactoring, while a new file suddenly becomes a new hotspot due to increased usage. A static backlog, created once and never updated, quickly loses its explanatory power if it is not kept in sync with current git data.


# technical-debt-backlog.yaml — reviewed quarterly, kept next to the code
items:
  - description: "OrderProcessor: no test coverage"
    risk: 5
    frequency: 12
    effort_days: 8
    priority_score: 7.5
    status: "in_progress"
    owner: "checkout-team"

  - description: "PriceCalculator: cyclomatic complexity 28"
    risk: 4
    frequency: 9
    effort_days: 3
    priority_score: 12.0
    status: "backlog"
    owner: "catalog-team"

  - description: "AccountManager: deprecated mysql_* calls"
    risk: 3
    frequency: 4
    effort_days: 2
    priority_score: 6.0
    status: "backlog"
    owner: "customer-team"

7. A fixed time budget instead of endless debates

One of the most effective organizational measures against growing technical debt is a fixed percentage of development time reserved for refactoring as a matter of principle, commonly between ten and twenty percent per sprint. This approach avoids the recurring debate over whether there is any time at all for refactoring in this sprint, and replaces it with a fixed rule that leaves only the choice of the concrete measure open.

What matters is actually using this budget for the highest priority entries from the backlog, instead of spontaneous, unprioritized cleanup work. A team that spends its refactoring budget on whatever file is randomly annoying right now instead of the highest scored hotspot gives away most of the structural benefit of the prioritization work from the previous sections.

8. Communicating technical debt to management

The scoring system from section five has an important side effect: it translates a technical discussion into language understandable even without PHP knowledge. Instead of "the code in this module is bad", one can say "this module is changed twelve times per quarter, carries a high failure risk, and fixing it costs three days, one of the best cost-benefit ratios in the current backlog." This translation is crucial for justifying refactoring budget to stakeholders who are primarily measured on feature progress.

Equally important is documenting concrete, already incurred costs: how many production incidents in the last six months trace back to a particular hotspot, how many hours were spent on hotfixes in this module. These historical numbers are often more convincing than any forecast about future risk, because they document already realized, undeniable harm.


<?php

declare(strict_types=1);

// Turns raw incident data into a stakeholder-readable summary line
final class DebtImpactReport
{
    public function __construct(
        private readonly string $module,
        private readonly int $incidentsLastSixMonths,
        private readonly float $hotfixHoursSpent,
    ) {
    }

    public function summaryLine(): string
    {
        return sprintf(
            '%s: %d production incidents, %.1f hotfix hours in six months',
            $this->module,
            $this->incidentsLastSixMonths,
            $this->hotfixHoursSpent
        );
    }
}

$report = new DebtImpactReport(
    module: 'OrderProcessor',
    incidentsLastSixMonths: 7,
    hotfixHoursSpent: 34.5,
);
echo $report->summaryLine();

9. Prioritization methods compared

There are several established approaches to prioritizing technical debt. The following table compares the most important methods by introduction effort and by how meaningful the results are.

Method Setup Effort Explanatory Power Fit
Team gut feeling None Low, subjective and inconsistent Tolerable only for very small projects
Hotspot analysis alone Low, a git script Medium, ignores fix effort Good first step for any team
Scoring system (risk/frequency/effort) Medium, requires estimates High, traceable and discussable Recommended for medium to large teams
Formal WSJF (SAFe) High, requires its own process Very high, but often overkill Only where SAFe processes are already established

For most PHP projects, the scoring system from section five is the best compromise: it does not require a full process rebuild like formal WSJF, yet delivers noticeably more defensible results than pure hotspot analysis or gut feeling. The easiest entry point is combining the git hotspot script with the simple scoring formula from the previous section.

Mironsoft

PHP legacy modernization and Magento development

Ready to finally prioritize technical debt in a traceable way?

We run a hotspot analysis of your codebase, systematically assess risk and effort, and deliver a prioritized refactoring backlog you can defend directly to management.

Hotspot Analysis

Derive change frequency and complexity from git and PHPStan

Prioritization

Build a scoring system out of risk, frequency and effort

Communication

Prepare defensible metrics for stakeholder conversations

10. Summary

Prioritizing technical debt in PHP projects does not succeed through a pure list of known problems, but through a combination of objective data: change frequency from git history, complexity from static analysis tools such as PHPMD or PHPStan, and a realistic estimate of fix effort. A simple scoring system following the principle risk times frequency divided by effort turns subjective discussions into a traceable, discussable order.

A dedicated refactoring backlog, a fixed time budget per sprint, and understandable communication toward management ensure that prioritization does not stay theoretical but actually translates into continuous improvement. The biggest mistake with technical debt is not having it, every growing project does, it is letting it grow unprioritized and invisible until a single hotspot becomes a genuine operational risk.

Prioritizing Technical Debt — Key Takeaways

Hotspot Analysis

Change frequency from git objectively shows which files actually cause ongoing cost.

Scoring System

Risk times frequency divided by effort provides a traceable, discussable priority.

Fixed Budget

Reserve ten to twenty percent of sprint time firmly for the highest priority backlog items.

Communication

Concrete metrics on incidents and hotfix hours convince stakeholders more than forecasts.

11. FAQ: Prioritizing Technical Debt in PHP Projects

1Technical debt vs. bad code?
Technical debt describes deliberate or unintentional trade-offs with long-term cost. Mostly secondary for prioritization.
2How do I find the biggest hotspots?
Git script counting commits per file, combined with complexity metrics from PHPMD or PHPStan.
3Do I need an expensive tool?
No, git log, PHPMD and a homegrown scoring system are fully sufficient for most teams.
4How often to re-evaluate?
At least once a quarter, as frequency and risk shift over time.
5How much time to reserve?
Ten to twenty percent of sprint time, planned in firmly rather than as leftover.
6How to convince management?
Concrete costs like incidents and hotfix hours per hotspot convince more than abstract forecasts.
7Must I fix every debt item?
No, rarely used, uncomplicated low risk files are often not worth the effort.
8What is WSJF?
Weighted Shortest Job First from SAFe. This article's scoring system adopts the idea without the full SAFe process.
9How do I estimate effort?
Most reliably with historical comparison data, otherwise a rough three-point estimate.
10Does prioritization prevent new debt?
No, that additionally requires code reviews, PHPStan in CI and clear architecture guidelines.