The branch nobody wants to touch
Somebody started the upgrade. It might have been Rails 6 to 7, or 7 to 8, or a Ruby version that’s about to lose security support. They got partway: a few gems bumped, a pile of deprecation warnings fixed. Then a customer needed something, and the upgrade branch sat.
Now it’s months behind the main branch. Merging it is its own project. A key gem hasn’t been updated for the new version. Every week the gap grows, and every week someone says “we should really finish that.”
Your team made the right call. Customer work pays the bills. But an outdated framework has a real cost: security patches stop, new gems won’t install, hiring gets harder, and the eventual jump gets bigger. Your developers know the app well. What they haven’t had is uninterrupted time and a method that doesn’t need one big risky weekend.
What I do: small steps, always shippable
I don’t do big-bang upgrades. The app stays deployable at every step, and each step goes to production on its own.
- Assess the gap. Which Rails and Ruby versions you’re on, which gems block the next step, how much test coverage you have, and what changed between versions that affects your code.
- Fill test gaps first. An upgrade is only as safe as the tests that tell you it worked. Where coverage is thin around critical flows, such as payments, logins and imports, I add tests before changing versions.
- Dual-boot. The app runs on both the current and next Rails version from the same branch, so the upgrade work happens on main in small merges instead of on a branch that drifts away.
- One version at a time. Rails 6.1 to 7.0 to 7.1 to 7.2 to 8.0, never skipping, with Ruby upgraded alongside where it makes sense.
- Gem conflicts. Update, replace or patch the gems that hold you back. When a gem is abandoned, I find a maintained alternative or contribute the fix upstream.
- Deprecations fixed, not silenced. Each warning gets resolved in the code, so the next upgrade starts clean.
- Production rollout with monitoring, and a way back if something unexpected shows up.
A process you can repeat
The upgrade shouldn’t be a once-a-decade event. Rails ships a new version roughly every year, and staying close to current is much cheaper than catching up. So the written output matters as much as the upgrade itself:
- An upgrade log: every step, every gem change, every surprise and how it was solved, in order.
- A repeatable process for the next version: the dual-boot setup left in place, a checklist, and the commands to run.
- A system overview that reflects the app as it is after the upgrade.
- Decision notes on gems replaced, patches applied, and anything deliberately left for later.
- Performance numbers before and after, because upgrades occasionally change query behaviour and you should know.
- Instructions for AI assistants, updated for the new version, so agents don’t suggest code from an old Rails.
You own all of it. The next upgrade can be done by your own team, a freelancer, or me, from the same log.
Why me
For seven years I was a senior engineer on a multi-tenant B2B SaaS that grew from about $4M to $13M in annual revenue, which meant keeping a large, long-lived Rails app current without stopping the business. I also built and run a live product on Rails 8 on my own, and I have a merged contribution to faraday, one of the Ruby HTTP libraries many apps depend on. I’ve written about zero-downtime migrations at scale and related work on my writing page.
Take a business like this one
The business in this section is a composite of the kind of company I talk to, not a named client. The numbers are the shape of the problem, not a case study.
Picture a B2B scheduling platform on Rails 6.1 and an older Ruby, with around four hundred models and a test suite that covers about half the app. An upgrade branch to Rails 7 was started fourteen months ago and is now hundreds of commits behind. Two gems the app depends on have no release for newer Rails, and one of them handles the calendar sync that customers rely on most.
What I’d do for a business shaped like this:
- Week one: assess. List every blocking gem, read the changelogs for each Rails step, and estimate the work per step. Decide whether to salvage anything from the stale branch.
- Tests around calendar sync, billing and login, the flows where a quiet failure hurts customers.
- Set up dual-booting on main, so both versions run in CI.
- Replace or patch the two stuck gems. For the calendar gem, either a maintained fork or a small in-house replacement covered by tests.
- Step through 7.0, 7.1, 7.2 and 8.0, each deployed to production on its own, with Ruby upgraded alongside.
- Hand over the upgrade log and the process, so the team runs the next upgrade themselves.
The team keeps shipping features the whole time, because the upgrade happens on main in small pieces instead of on a branch that blocks everyone.
What it costs
It starts with a small fixed-price assessment: a written upgrade plan with every blocker, every step and an honest estimate for each. Some teams take the plan and do the work themselves. Otherwise the upgrade is scoped and quoted step by step from the plan, so you can stop after any version. No retainer, and no lock-in.
What I need from you
Access to the code, CI and a staging environment. A developer on your side to review merges and learn the dual-boot setup. Your release schedule, so upgrade steps land at quiet times. And a list of the flows customers notice first when something breaks.
