The go-to resource for upgrading Ruby, Rails, and your dependencies.

Identifying and Remediating Technical Debt Hotspots Before a Rails Upgrade


In 1990, the Leaning Tower of Pisa was closed to the public; its tilt had reached a dangerous 5.5 degrees, and engineers feared an imminent collapse. The solution to this crisis was not to add new supports to the leaning side, nor was it to rebuild the tower from scratch. Instead, engineers undertook a decade-long project to carefully remove 38 cubic meters of soil from underneath the raised end, addressing the foundational instability that was the root cause of the problem.

This approach — stabilizing the foundation before attempting further structural work — has a direct parallel in software engineering. When we prepare to upgrade a large Ruby on Rails application, we often find that years of rapid feature development have left the codebase structurally unsound. These architectural compromises concentrate in specific areas, creating what we call technical debt hotspots.

Before we get into that, though, we must recognize why these hotspots become critical bottlenecks. A major framework upgrade forces us to interact with the foundational dependencies of our application. If the code relying on those dependencies is tangled, undocumented, or fragile, the stress of the upgrade process can cause cascading failures throughout the system.

In this article, we will outline a practical workflow for identifying these hotspots and remediating them, ensuring that your application has a stable foundation before you embark on a Rails upgrade.

What Constitutes a Technical Debt Hotspot?

Strictly speaking, not all technical debt is equally dangerous. An isolated, messy background job that runs once a month and rarely changes is technical debt, but it poses little risk to your overarching architecture.

A hotspot, on the other hand, is defined by the intersection of high complexity and high churn. It is a file or module that is both difficult to understand and frequently modified by developers. When a Rails upgrade introduces changes to core APIs — such as Action Record or Action View modifications — these high-churn files are the most likely to break, causing cascading failures throughout your system.

One may wonder: if these files are so complex, why not rewrite them entirely? The answer relates to risk management. Rewriting a critical system component right before a framework upgrade introduces two simultaneous vectors of change, dramatically increasing the risk of regressions. Instead, our goal is targeted remediation to stabilize the code enough to survive the upgrade process.

Identifying Hotspots Pragmatically

To manage this risk, we need concrete metrics rather than developer intuition. We can utilize ecosystem tools to quantify both complexity and churn.

For example, the churn gem helps identify files that are modified most frequently in your Git history. Let’s install it and run an analysis. The churn command analyzes your Git repository’s history to count how many times each file has been committed:

$ gem install churn
$ churn

You might see output resembling the following:

**********************************************************************
* Revision Changes
**********************************************************************
Files
+---------------------------------------------+---------------+
| file_path                                   | times_changed |
+---------------------------------------------+---------------+
| app/models/user.rb                          | 142           |
| app/services/payment_processor.rb           | 98            |
| app/controllers/api/v1/orders_controller.rb | 75            |
+---------------------------------------------+---------------+

You also may notice that the files with the highest churn are often core business logic models or central API controllers. Of course, high churn alone does not guarantee that a file is a hotspot; a configuration file might change frequently without being complex.

To find our true hotspots, we must cross-reference this churn data with complexity metrics. We can use the skunk gem, which calculates a “stink score” based on code complexity, code smells, and a lack of test coverage.

$ gem install skunk
$ skunk app/services/payment_processor.rb

When we run this, we might see output indicating a high score:

Running skunk...
+-----------------------------------+-------------+-------------+----------+
| file                              | skunk_score | churn_times | coverage |
+-----------------------------------+-------------+-------------+----------+
| app/services/payment_processor.rb | 145.23      | 98          | 32.5%    |
+-----------------------------------+-------------+-------------+----------+

When we identify files that appear at the top of both our churn list and our complexity reports, we have found the technical debt hotspots that require our attention.

Evaluating Security Risks

Of course, technical debt is not limited to messy code. Outdated dependencies represent a different, but equally severe, type of technical debt: security vulnerabilities. Before embarking on a major framework upgrade, we must audit our dependencies.

We can use bundler-audit to cross-reference our Gemfile.lock against the Ruby Advisory Database. Let’s install and run it:

$ gem install bundler-audit
$ bundle audit check --update

This will download the latest vulnerability definitions and scan your project. If your application relies on a gem with known Common Vulnerabilities and Exposures (CVEs), bundler-audit will output a warning:

Name: nokogiri
Version: 1.10.9
Advisory: CVE-2020-26247
Criticality: Medium
URL: https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-vvrm-jmxg-xwqw
Title: XML external entity injection in Nokogiri
Solution: upgrade to >= 1.11.0.rc4

Vulnerabilities found!

That dependency is a hotspot that must be remediated.

Addressing these security risks early is crucial. Often, patching a CVE requires updating a gem, which may, in turn, force a cascade of other minor dependency updates. By resolving these security issues before the primary Rails upgrade, we ensure that we are not fighting both security patches and framework deprecations simultaneously.

Strategies for Remediation

Once we have identified our hotspots, we must apply targeted fixes. There are three major approaches to stabilizing these areas of the codebase; depending on your team’s bandwidth and the specific nature of the debt, you may employ one or more of these strategies.

The first approach is to extract isolated logic from framework-coupled classes. The second is to fortify the existing code with targeted tests. The third option is to delegate the remediation to an external maintenance service.

Generally speaking, the first option is the most effective long-term solution. The second option, though, will often make more sense if you are under severe time constraints.

Extracting Isolated Logic

Large applications often suffer from “God Models” — Active Record classes that handle database interactions, API integrations, and business logic all at once. When upgrading Rails, changes to Active Record callbacks or validations can cause these models to fail unpredictably.

For example, if you had a User model that handled Stripe payment synchronization directly in a callback:

class User < ApplicationRecord
  after_commit :sync_with_stripe, on: [:create, :update]
  
  def sync_with_stripe
    # Complex external API logic mixed with Active Record
    Stripe::Customer.create(email: email, description: name)
    # ...snip...
  end
end

To remediate this, we can extract non-Active Record logic into Plain Old Ruby Objects (POROs) or Service Objects. By isolating the business logic from the framework, we reduce the surface area that is vulnerable to framework changes:

class User < ApplicationRecord
  # Active Record responsibilities only
end

class StripeCustomerSync
  def initialize(user)
    @user = user
  end
  
  def call
    Stripe::Customer.create(email: @user.email, description: @user.name)
    # ...snip...
  end
end

By removing the callback and extracting the logic, we make both classes easier to test and far less likely to break when Rails updates its internal callback chain. This is often the most effective long-term solution, though it requires significant developer effort.

Fortifying with Targeted Tests

If a hotspot lacks test coverage, we cannot safely upgrade the underlying framework. However, achieving comprehensive test coverage across a legacy application before an upgrade is rarely feasible.

Instead, we can focus our testing efforts entirely on the identified hotspots. By writing high-level integration tests or system tests that verify the expected inputs and outputs of a complex file, we provide a coarse but effective safety net. For instance, if a deprecation removal in a newer Rails version alters the behavior of our payment_processor.rb, our targeted tests will catch the regression. This approach is generally faster than extracting logic, making it a pragmatic choice when time is short.

Delegating Maintenance Tasks

For teams with limited bandwidth, executing a comprehensive code audit and remediating years of technical debt can halt product development entirely. In these scenarios, engaging an external maintenance service is a pragmatic alternative.

These services typically pair your internal team with external experts who systematically address technical debt hotspots and perform gradual version bumps. This approach ensures steady progress toward the target version without overwhelming your internal resources, though, of course, it involves additional financial cost.

Beyond the Upgrade

A successful framework upgrade is not the end of the process. Even with rigorous hotspot remediation, we must plan for post-upgrade support. Deprecation warnings will continue to surface in our logs, and subtle performance changes may occur as the new framework version interacts with our application.

Monitoring tools are essential during this phase. By profiling the application to monitor performance and memory usage, we can ensure that the upgraded application remains stable under production loads.

By identifying technical debt hotspots early, quantifying the risk with tools like churn and skunk, and applying targeted remediation, we transform a potentially chaotic migration into a predictable, manageable process. Just as engineers stabilized the Leaning Tower of Pisa by addressing the soil beneath it, we build long-term technical health not through massive, risky rewrites, but through deliberate, strategic improvements to our foundation.

Sponsored by Durable Programming

Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.

Hire Durable Programming