Make an Old Rails App Safer to Change

Make an Old Rails App Safer to Change

Inheriting a Rails codebase is one of the most common experiences in professional Ruby development, and one of the least taught. The application works—mostly. The tests pass—some of them. The gems are outdated—many of them. And nobody who wrote the original code is available to explain the decisions. This path gives you a structured approach to making an inherited Rails application safer to change, without the risk of a big-bang rewrite. Each step builds confidence incrementally, and each can be completed independently. For the broader context on maintenance strategy, see the Debugging and Maintenance topic page.

Who this is for

  • Developers who have inherited a Rails application and need to make changes safely
  • Teams preparing for a major version upgrade on an established codebase
  • Technical leads assessing the health of an existing Rails project
  • Anyone who has opened a Rails codebase, seen the Gemfile, and felt a wave of concern

Prerequisites

  • Working knowledge of Rails (controllers, models, views, migrations)
  • Ability to run the application's test suite
  • Access to the application's production logs and error tracker (if available)
  • Version control set up (Git, with a clean working tree)

Step 1: Establish the current state

What you will do: Create a comprehensive inventory of the application's health: Ruby and Rails versions, gem freshness, test coverage, known vulnerabilities and deployment state.

Why it matters: You cannot improve what you have not measured. This step gives you a baseline and reveals the highest-priority risks.

Key actions:

  • Record the Ruby version, Rails version and Bundler version
  • Run bundle audit and document known vulnerabilities
  • Run bundle outdated and categorise gems by update urgency
  • Run the full test suite and record pass/fail counts, total time and coverage percentage
  • Check the last successful deployment date and method
  • Document any environment-specific configuration or secrets management
  • List the application's external dependencies: databases, Redis, external APIs, file storage

Expected time: 45-60 minutes

Step 2: Dependency security audit

What you will do: Address known security vulnerabilities in gems, starting with the most critical.

Why it matters: Vulnerable gems are the highest-priority maintenance item because they represent known, exploitable risks. Fixing them first reduces the surface area for the most dangerous category of problems.

Key actions:

  • Update gems with known security vulnerabilities (patch versions first)
  • For each update, run the test suite and verify no regressions
  • For gems where a safe update is not possible, document the vulnerability and assess whether the vulnerable code path is actually used in your application
  • Record all changes in a maintenance log
  • If major gem updates are required, defer them to Step 6 and note the dependency

Common blocker: A security update requires updating another gem which requires updating Rails itself. Flag this as a constraint and continue with the gems that can be updated independently.

Expected time: 30-60 minutes

Step 3: Test coverage assessment

What you will do: Analyse the test suite to understand what is covered, what is not, and where the coverage gaps are most dangerous.

Why it matters: Test coverage tells you where you can make changes safely and where you are operating without a net. The goal is not 100% coverage—it is knowing where the gaps are.

Key actions:

  • Run SimpleCov and generate a coverage report
  • Identify the ten most-changed files (from Git history) with the lowest coverage
  • These are the highest-risk areas: frequently modified code with no test safety net
  • Write characterization tests for the three most critical untested code paths
  • Record the coverage baseline for future comparison

Common blocker: The test suite is so slow that running it fully is impractical. Identify the slowest tests with --profile and consider parallelisation or skipping integration tests for the characterization phase.

Expected time: 45-60 minutes

Step 4: Dead code identification and removal

What you will do: Find and remove code that is never executed, reducing the codebase surface area and making remaining code easier to understand.

Why it matters: Dead code adds cognitive load, increases test suite time, creates false positives in security scans and makes dependency analysis harder. Removing it is pure upside.

Key actions:

  • Run debride for static analysis of unused methods
  • Review unused routes with bin/rails routes compared to traffic logs
  • Check for feature-flagged code where the flag has been removed
  • Search for commented-out code blocks
  • Identify unused model scopes, helper methods and mailer templates
  • Remove dead code in small, focused commits with clear commit messages
  • Run the test suite after each removal

Common blocker: Uncertainty about whether code is truly dead. Use coverband in production for a week to get runtime evidence before removing suspicious methods.

Expected time: 30-45 minutes

Step 5: Safe refactoring of high-risk areas

What you will do: Improve the structure of the highest-risk code areas identified in Step 3, using characterization tests as a safety net.

Why it matters: This is where you start building confidence in the codebase. Each refactoring makes the code more understandable, more testable and safer to change in the future.

Key actions:

  • Select the highest-risk method or class from Step 3
  • Ensure characterization tests exist for its current behavior
  • Apply one refactoring pattern: extract method, extract class, replace conditional with polymorphism, or introduce service object
  • Run the test suite after each change
  • Review the diff to confirm behavior is preserved
  • Repeat for two more high-risk areas

Common blocker: Fear of breaking something. This is why Steps 3 and 4 come first. With characterization tests and reduced dead code, the risk of each refactoring is bounded.

Expected time: 45-60 minutes

Step 6: Dependency updates

What you will do: Update outdated gems systematically, starting with the easiest and progressing to gems that require code changes.

Why it matters: Outdated gems accumulate tech debt. Each major version gap makes future updates harder. Systematic updates, done incrementally, prevent the "too far behind to update" trap.

Key actions:

  • Update all gems that have safe patch-level updates available
  • Run tests after each update
  • Update gems with minor version bumps, checking changelogs for breaking changes
  • For major version bumps, read migration guides and plan changes
  • Update one major gem version at a time, with a full test run between each
  • If Rails itself needs updating, see the Rails 8 Upgrade Checklist for a structured approach

Common blocker: A gem update breaks something and the fix is not obvious. Pin the gem at its current version, document the issue, and move to the next update. Return to stuck updates after the rest of the dependency tree is current.

Expected time: 45-90 minutes (varies widely by codebase)

Step 7: Establish maintenance routines

What you will do: Set up ongoing processes to prevent the codebase from degrading back to its inherited state.

Why it matters: Maintenance is not a one-time project. It is a continuous practice. Without routines, the improvements from Steps 1-6 erode over time.

Key actions:

  • Add bundle audit to CI so vulnerable gems fail the build
  • Set up a monthly dependency review cadence
  • Configure Dependabot or Renovate for automated gem update PRs
  • Add coverage tracking to CI with a ratchet (coverage cannot decrease)
  • Document the maintenance playbook for the next developer
  • Schedule quarterly dead code reviews
  • Set up error tracking if not already present

Expected time: 30-45 minutes

Expected outcomes

After completing this path, you will have:

  • A documented baseline of the application's health
  • Zero known security vulnerabilities in gems (or documented exceptions)
  • Characterization tests covering the highest-risk code paths
  • Less dead code and a cleaner codebase surface area
  • Refactored high-risk areas with improved structure
  • An up-to-date dependency tree (or a documented plan for remaining updates)
  • Maintenance routines that prevent regression