Changing the Rails version in a Gemfile is easy. Knowing what the change will disturb is the work.
An older application may depend on framework behavior that nobody remembers choosing. A gem may patch an internal Rails API. A serializer may be consumed by a mobile app that is hard to update. A migration may be harmless on a laptop and dangerous against a large production table.
Before estimating an upgrade, build a risk map. It is faster than learning the system through failed deploys.
Start with the supported path, not the desired destination
Record the current versions of Ruby, Rails, Bundler, database, Redis and other runtime services. Then identify the intermediate Rails releases between the current application and the target.
The official Rails upgrade guide recommends moving one minor version at a time so that deprecation warnings can guide the work. That does not mean every step needs a long production stay. It means you should not skip the compatibility information each release provides.
Create a simple upgrade ladder:
| Step | Ruby | Rails | Bundler | Main compatibility questions |
|---|---|---|---|---|
| Current | ||||
| Step 1 | ||||
| Step 2 | ||||
| Target |
Fill this from the application and primary documentation. Do not start with a generic compatibility chart copied from an old article.
Map dependencies by replacement difficulty
bundle outdated tells you which versions are behind. It does not tell you which dependency can stop the project.
Group direct dependencies:
- framework extensions and authentication;
- database adapters and search clients;
- background jobs and scheduling;
- file processing;
- payments and external APIs;
- assets and frontend integration;
- testing and developer tooling;
- internal or Git-sourced gems.
For each dependency, record:
- current and candidate version;
- maintenance status;
- Rails/Ruby compatibility;
- use of public or internal framework APIs;
- replacement option;
- amount of application code coupled to it;
- owner of the external integration.
A small abandoned gem used in one formatter may be easy to remove. A maintained authentication library with custom monkey patches may carry much more upgrade risk.
Find framework assumptions in application code
Search for code that reaches into framework behavior:
- initializers copied from old releases;
- overrides of Rails or gem classes;
- monkey patches;
- custom middleware;
- autoloading assumptions;
- serialization and time-zone behavior;
- controller callbacks and parameter handling;
- deprecated Active Record APIs;
- direct use of internal constants.
Do not label every old pattern as a defect. Some are deliberate compatibility choices. The purpose of the inventory is to identify code that needs an explicit test when the framework changes.
Treat tests as evidence, not a checkbox
A green suite helps only if it covers the behavior that matters.
Before the first version change, make sure you can answer:
- Does the suite run from a clean checkout?
- Which tests cover login, billing, permissions and data exports?
- Are background jobs exercised with realistic arguments?
- Are external integrations represented by contract tests or only mocks?
- Can you run a production-like smoke test against staging?
- Which important flows have no automated coverage?
Add characterization tests around high-risk behavior before changing it. These tests describe what the application does today, including awkward behavior that customers or integrations may rely on.
Avoid the temptation to rewrite the test suite and upgrade Rails at the same time. If assertions, fixtures, factories and application behavior all move together, failures stop telling you which change caused them.
Separate code compatibility from data and deployment risk
An application can boot and still be unsafe to release.
Review migrations for:
- table rewrites or long locks;
- new indexes on large tables;
- column type changes;
- backfills inside schema migrations;
- application code that assumes a new column exists immediately;
- rollback behavior when old and new app versions overlap.
Plan expand-and-contract changes when the old and new application need to run during the same deployment window. Add the compatible shape first, backfill separately, move application behavior, verify it, then remove the old shape in a later release.
The same thinking applies outside PostgreSQL. A serializer, webhook payload or cache key can be a data contract even if it never appears in db/schema.rb.
Turn deprecations into a managed queue
Deprecation warnings are part of the upgrade path. Capture them in test and staging environments, group duplicates, and assign each warning to a code path or dependency.
A useful deprecation record contains:
- warning text;
- source location;
- whether the source is application or dependency code;
- behavior at the target Rails version;
- proposed change;
- test that protects the behavior.
Do not hide a warning globally merely to make CI quiet. Suppress it only when you understand why it is safe and when the suppression can be removed.
Choose upgrade slices that can be reviewed and reversed
A single branch containing Ruby, Rails, frontend tooling, deployment and large application refactors is hard to review and harder to roll back.
Prefer slices such as:
- make the current application reproducible;
- update the oldest blocking dependencies;
- add characterization tests for risky behavior;
- move one Rails step and resolve its deprecations;
- verify staging and operational behavior;
- repeat for the next step;
- perform optional refactors after compatibility is established.
The exact sequence depends on the dependency graph. The principle is stable: keep compatibility work separate from cleanup that does not need to happen now.
Define evidence for each release
Before a staging or production rollout, define what must remain true:
- boot and migration completion;
- error and request-latency behavior;
- job processing and queue latency;
- database connection and lock behavior;
- login, billing and permission flows;
- critical external integrations;
- rollback or disable procedure.
An upgrade is complete when the team has verified the application in its operating environment, not when the test suite compiles under a new version number.
What the risk map gives you
The output should be compact enough to use:
- version ladder;
- dependency and compatibility inventory;
- high-risk behavior without coverage;
- data and deployment hazards;
- bounded work slices;
- acceptance and rollback criteria;
- open decisions and owners.
That is a better basis for cost and sequencing than a line count or a promise that the upgrade “should be straightforward.”
Our modernization process starts with this kind of evidence before implementation scope is fixed. If you already know the current versions and the production constraint, you can send us that context for a focused first conversation.
Source
Checked 13 August 2026:
