Here at freevision we often inherit legacy Ruby on Rails projects with framework and language versions not corresponding to their current stable versions, or packages that are outdated (gems). In IT this is called technical debt. In this article we list a few examples of ways to approach technical debt. We will explain how we avoid technical debt in our own projects and why keeping all versions up to date is good practice for all parties involved.
Unfortunately, Ruby version upgrades are not making Rails code noticeably faster (although gradual updates still do make a difference). Unlike code execution, however, development time is sped up more significantly by upgrades. Here are some examples of Rails upgrades actually reducing development time from days or hours to just a few minutes.
As a developer, the first thing I need to do for each new project is to set it up locally so that I can start making changes and testing them.
The time needed to set up a new project depends on its type, but generally it should take as little time as possible - a good project should be easy to set up.
One time we received a project so old and unmaintained, that the Ruby version it was running was no longer available - we literally couldn’t find it in any Ruby version manager. It was deprecated and removed, because of how unsafe it was to use. Running the project locally required us to first upgrade Ruby in order to update to the newest “existing” Ruby version. Together with other software pitfalls within this project, it took us at least one day just to set it up and run it locally. Projects that keep their versions up-to-date usually have a setup time within one hour. This is the most extreme example, but it shows how technical debt gets back at you after just a few years of neglecting the software.
Another example is a legacy project which came with Rails 5 (at that time Rails 7 was the current stable version = not great, not terrible). The project used daily background tasks to sync the internal model with an external data source. When running the code I noticed that it was pretty slow, because it was creating records one-by-one without any particular reason. It could be drastically improved if we ran it as one bulk insert query. In Rails 6, this change would be super simple and would only require changing a few lines - using the new ActiveRecord method insert_all.
Stuck in Rails 5, however, the solution would require more changes - we would need to add a new gem to our Gemfile, which handles multiple record importing (such as activerecord-import) and then adjust the code to be able to use this gem.
This was the case in almost every new project we had - we had to use ActiveStorage to store custom image files, and then create variants in order to be able to use different image sizes in our views. In the past we had to develop a custom module which used metaprogramming in order to assign custom variants directly to desired models.
All this is now included in Rails 7+, allowing us to declare custom variants with just a few lines of code - Rails unifies this feature instead of having custom code scattered all throughout the code / projects. Again, newer version = shorter development time.
For each of our projects we use a bundle-audit gem to make sure the gems we use are safe. Depending on the number of dependencies a project uses, new vulnerabilities emerge almost on a weekly basis. Leaving a project without upgrades for more than a few months leaves users as well as the business vulnerable to attacks.
Upgrading new software always comes with lots of unknowns. Here is a list of reasons why upgrading Rails is usually pretty easy and straightforward, including reasons why it makes sense to regularly upgrade each project. This list of reasons is highly inspired by the Rails Doctrine:
It’s documented very well - the Rails upgrade article contains steps for upgrading between each major version. These tutorials usually cover the things that are most important to keep an eye on during an upgrade fairly well, but in case it’s not enough, Changelogs are also an option.
New versions are created by developers for developers - Rails is developed by a community which often solves general web development problems similar to those we solve. As a result of this, each new version usually provides us with cool new solutions and features. As already mentioned, this may significantly speed up our own development time.
This ties in nicely with Rails philosophy of convention over configuration. Creators of Rails often find better ways to do things and they slowly implement them to newer versions, slowly deprecating the older ways.
Deprecation happens safely and slowly, usually over a two-version cycle. That means that each breaking change is first only introduced with a deprecation warning. This gives developers enough time to stop using this feature and remove the warning. Only in the next major version does this deprecated feature start raising an error.