By Najaf Ali
Ruby has a rich ecosystem of libraries. There's a gem available for any requirement you could think of. Building Rails applications often feels like stringing together a series of appropriate gems rather than meaningful programming.
But using gems come at a cost. They become dated. When unmaintained they don't integrate with the latest versions of the rest of your libraries. They may not work with the latest Ruby version. They're often general-purpose, so you load unnecesary code into your application. Their API's may change in future versions and you will have to upgrade. All of these costs are multiplied by the number of dependencies each gem requires.
Gems reach an end of life
On a long enough timescale, all gems will stop being maintained. Humanity will have better things to do at the heat-death of the universe than keep
devise working with Rails version
2 x 10^9871. On a shorter timescale, gems stop being maintained all the time. Maintainers move on to other things and newer libraries become popular for solving the same problem. For better or for worse, the Ruby community evolves through this continuing upheaval and iteration.
If you're a consultant and your focus is only on the start of projects, the long term prospects of a gem aren't of interest to you. You don't care if the gem you used to implement a critical business function will stop being maintained in three years.
If you're instead working long-term on a Rails codebase, the upkeep of the gems you use is important to the continuing health of your application. Out of date gems can hold back upgrades to new Ruby or Rails versions. Security vulnerabilites in parts of gems that you don't even use will still need patching to keep your codebase secure.
Picking gems for the long term
One way to avoid these problems is to pick your gems with this knowledge in mind.
If you want to guarantee that your gem will be maintained by someone else long-term, your safest bet is to pick the most popular gems for the most common functionality. Gems like
devise are de facto defaults in Rails for implementing things as common as authentication. With so much critical mass, someone will be available to take the reigns and make sure that it stays up to date with the latest versions of Ruby/Rails.
Another option is to pick extremely small gems that do hardly anything for you.
Most gems try to take care of any possible requirement you might have in a domain. They come with their own opinionated ways of doing things. They sometimes include a DSL. They often integrate directly with the internals of Rails. This is surprising for a community that pays so much lip service to "simplicity".
Gems change. They add new features and their API's are modified on the whims of the developer leading the project. That developer may not make decisions that you fully agree with, but your code will have to change to match their inclinations. We even use how recently a gem has been worked on as a proxy for it's quality. A gem where the last commit was more than a year ago is seen as old and unmaintained.
Does it have to be this way? What if we had gems that did one, tiny thing well and had a stable API? Gems that were finished. The only modifications they would need would be to keep up to date with versions of Ruby and the standard library. Their only integration would be by using the classes, objects and methods they make available to you. They wouldn't need to keep up with Rails versions because they wouldn't be coupled to Rails at all.
This sounds like fantasy, but there exists a Ruby minimalist counterculture. You can find a list of libraries that aspire to minimalism at microrb.com. The maintenance overhead for all of these libraries will be considerably less than what you'll get from using more popular gems. Because they do so little they have few dependencies, so the overall code footprint in your project will be low.
Another option is to forego using a gem alltogether. If the functionality you need is small subset of what available gems provide you might as well do it yourself. If what you need doesn't quite fit into the model that gems give you then it's probably not worth tying yourself to them.
Developers sometimes seem gun-shy about adding code to their
models/ directory. This is especially true for developers where Rails is their first exposure to programming. In tutorials, you only put subclasses of
models/, so developers assume you can't just add arbitrary code to that directory.
If the code is to do with what response to send it belongs in
controllers/. If the code is to do with rendering the body of a particular response it belongs in
views/. There are various "patterns" that prescribe other directories in
app/ that you might use too.
Everything else belongs in
models/. If you want to write a stripped down version of a finite state machine or delegate the details of authorization in your app to a bunch of dedicated classes,
models/ is the natural place for that. You don't need a gem's
README.md or a tutorial somewhere to tell you to do this. You just write a test for your new class, make it pass by implementing it in
models/ and repeat until your app does what you need it to.
This means more code for you to maintain but it removes all of the downsides of using a gem. You can modify the code at will to do exactly what you need and nothing more. You can keep it up to date with it's dependencies as and when required. If you have the discipline to keep it decoupled from the rest of your code, you might even decide to pull it out into a private gem.
Keeping gems maintained
Sharing work as we do in open-source software is unheard of in any other industry. You don't see car manufacturers sharing the findings of their R&D departments. In web development at least, companies can use open source code build software faster, effectively for free.
However, gems don't appear out of the ether. Like with any codebase, someone has to plan them, write the code, write the documentation and do everything else required to keep the project functional. This takes a serious commitment of time and effort, usually outside of working hours.
There's a reason that companies pay for things. Paying for things allows you to set up a legal framework where there's a provider and a customer. Both parties can have expectations of the relationship (ongoing support and maintenance for a set monthly fee for example).
This sort of thinking tends to be completely forgotten when it comes to open source software. It's free, no one's stopping you, so you go ahead and use it. Whether or not to pay for it doesn't enter into the equation. The author of the library lacked the business acumen to ask for a recurring fee for support and maintenance, so paying for the library isn't considered.
The cheapest way to keep a third-party gem maintained is to pay the original author to do it for you. There might be a donation link but there's no guarantee that the author will offer support and maintenance for the fee. If there's a library you depend heavily on, chase the author down over email and make them an offer. You pay them a set fee per month or year in return for their ongoing support and maintenance of the project.
Not all developers will be open to this. Another way to keep a gem up-to-date and maintained is to have someone on your team spend time dedicated to the gem during working hours. Done right, the engenders goodwill with the gem maintainer and allows you to influence the future direction of the gem.
Maintaining the original gem might not be an option either. The original developer may have disappeared, lost interest or be uncooperative. As a last resort you can fork the gem and maintain your fork instead of the original. You've now lost almost all of the benefit of delegating the work the code does, but you at least have control of the code that you bring into your application.
There are a number of scenarios which require your attention:
- New functionality is added to the gem.
- The API of a gem has been changed.
- The gem has been made to work with a newer version of a dependency (e.g. Rails, the Ruby standard library)
- A security vulnerability in the gem has been fixed.
- A gem reaches end of life.
In all of these cases (especially the last two) you want to upgrade your version of the gem (or migrate to something else) sooner rather than later.
The longer you leave an upgrade the more difficult it gets. As more features grow up on a foundation of out-of-date dependencies, it becomes difficult to keep track of what quirks of the current versions you're relying on. Upgrades become a goal pushed further and further into the future.
Wait at most a few weeks for minor/major versions to stablize and just upgrade. Upgrade early and upgrade often. Make upgrading dependencies a completely routine maintenance task that everyone in your organisation is used to. Don't turn it into a big refactoring project that needs special attention.
Provided you have acceptance tests that exercise the critical parts of your application, upgrading your gem versions is a small task every few weeks that individually will have no impact on the business. Taken in aggregate though, it will give you access to the latest versions of all the software you use, leading at the very least to a more performant codebase.
What's the alternative? You have out-of-date gems that over the years stop being maintained by anyone. Security vulnerabilities will go unfixed. You'll have to shoulder more and more of the burden of keeping a gem working with rest of your dependencies. It's far cheaper in time and effort to upgrade your dependencies in small, regular doses.
Dealing with out-of-date gems
It may be a bit late for all the above advice. You might five or six years into the project with a good mix of well-maintained and completely deprecated gems. What do you do from here?
If a gem you rely on has been deprecated, it's likely that someone has the same problem as you. Someone may already have forked a gem you use after it reached end of life. Have a look at the GitHub fork graph to see if you can identify a de facto official fork that appears to be popular. You might be able to use this in place of your older version and get a more up-to-date version of the gem. As mentioned before, you're also free to fork the gem. This puts all of the burden of maintaining it on you.
Rather than fork the gem, it might make more sense to vendor all of the gem's code into your codebase. Especially if you need to make modifications specific to your requirements, there's very little point in keeping the codebase in a separate repository. You can then of course make any modifications you need to the code to upgrade e.g. to future versions of Ruby or Rails.
- Gems give you extreme power but increase the maintenance costs of your codebase.
- Gems can go out of date and hold back upgrades in other parts of your system.
- Choose the most popular gems for common use cases, they will definitely be maintained.
- Choose small, finished gems that do one thing well for minimum maintenance overhead.
- For small enough requirements, consider not using a gem at all.
- Keep gems maintained by paying the original developer, doing it yourself or forking.
- Upgrade your gems early and often to avoid sticky situations.
- To manage abandoned gems, use maintained forks, fork them yourself or vendor the code.
Gems are major source of pain when it comes to the long-term maintenance of Rails applications. With a bit of regular attention however, you can avoid the worst of it. Think hard before using gems, keep them up to date and don't let unmaintained code linger in your application. You'll thank yourself in three years' time.