How to modernise a legacy Rails application without rewriting the business
A rewrite replaces the risky parts and the valuable parts at the same time. Staged modernisation lets you separate them.
Written by Tarun Mookhey · CTO / Principal Engineer ·
A legacy application is rarely risky because it is old. It is risky because the framework, runtime, data layer and operating system underneath it have stopped receiving fixes, while the business behaviour on top of it still matters every day. A rewrite treats those two things as one problem. Staged modernisation treats them as two.
Start by separating the stack from the behaviour
Before choosing between an upgrade and a rewrite, write down two lists.
What is obsolete: the framework version, the Ruby runtime, the ORM or data layer, unsupported gems, the operating system, deployment steps that live in one person’s head.
What is valuable: the rules, edge cases and workflows that years of production use have taught the application. These are usually undocumented, and they are the part a rewrite is most likely to lose.
If the second list is long and the first is the real source of risk, an incremental path is usually the lower-risk answer.
When incremental modernisation is viable
Staged upgrades tend to work when:
- the application still runs and is understood well enough to be tested;
- the business logic is worth keeping;
- the obsolete layers can be replaced one at a time;
- the team can keep shipping while the work happens.
A programme that moved a Rails 2/Merb application to Rails 7.1 is an example of the shape this takes: framework, Ruby runtime (1.9 to 3.3), data layer (DataMapper to ActiveRecord) and operating system (Ubuntu 10 to 22.04) were each moved on their own terms rather than in one big-bang cutover. You can read the details in the case study.
Get test safety before you change anything
The most useful early work is often unglamorous: characterise how the application behaves today. Even a thin layer of tests around the workflows that matter lets you tell the difference between “the upgrade broke something” and “it always did that”. Without it, every later step is a leap of faith.
Sequence the layers deliberately
Order matters because the layers depend on each other.
- Runtime and dependencies first, where possible. Move to a supported Ruby and replace abandoned gems so later steps are not blocked.
- Framework in steps. Major Rails versions have documented upgrade paths; take them one version at a time rather than jumping.
- Data layer when you can isolate it. Swapping an ORM touches every query. Do it behind tests, and ideally not in the same release as a framework change.
- Infrastructure alongside, not after. Operating-system and deployment changes are coupled to the runtime, so plan them together.
Keep every step observable and reversible. If a step cannot be rolled back, make it smaller.
When a rewrite is justified
Sometimes it is. A rewrite makes sense when the business behaviour itself is changing, when the existing logic is not worth preserving, or when the cost of understanding the old system exceeds the cost of rebuilding it. The point is that this is a business-risk decision, not a preference for newer tools.
A quick test
Ask: if we replaced everything at once, what would we have no way to verify? If the answer is “most of what the business relies on”, staged modernisation is probably the safer route.
Related: Application modernisation · Ruby on Rails development · Legacy modernisation case study
Have software that needs building, connecting or untangling?
Describe the problem as it exists today. A finished specification is not required.
You will speak directly with the senior engineer responsible for assessing the work.