Expert Advice on How to Manage Dependencies in Ruby on Rails
If you’re a seasoned RoR developer, you might remember what a hassle it used to be to manage multiple applications’ dependencies. There were all sorts of problems and hacks to handle various versions of Ruby and having multiple versions of the same gem.
DISCLAIMER: I’m going to assume you know about semantic versioning (https://semver.org/). Please familiarize yourself with it even if you’re not going to read this article.
A Bit of History
But then Ruby Version Manager (RVM) arrived with gemsets that somewhat mitigated the problem. Still, there wasn’t enough control, and that’s how bundler was born. The now established dependency manager for Ruby applications is used in Rails by default.
With bundler you can clearly define which Ruby version your application uses and list all the gems you need. You can also restrict gem versions to not update beyond a certain PATCH or MINOR version number. And here come the tough decisions.
If you’ve worked on a few applications with other developers, you probably saw several different approaches to version locking (or no approach at all). Some Gemfiles hard-lock most gem versions, some lock everything to patch version, others don’t lock anything except Rails itself, and still others mix and match.
So What’s the “Correct” Approach?
Of course, as you probably guessed, the “correct” approach doesn’t exist. There are just too many variables to consider. Now you might think this article can’t be helpful or maybe you have your own (DHH-strong) opinions about this, but please bear with me for a moment, and hopefully, I can give you a comprehensive guide on what to keep in mind when adding stuff to your Gemfile.
Rule #1: Don’t
I’m not saying you should avoid adding dependencies at all cost (I kind of am), but too often developers do this without proper consideration. Do you really need the whole thing? Have you looked at alternatives? Does it follow semantic versioning? How many additional dependencies does it bring? Was it updated since 2011? Can it be replaced by writing ten lines of your own code?
Too many times I have seen entries in the Gemfile added by devs who didn’t ask themselves even one of these fundamental questions.
Much of this comes with experience, and when you’re new to Ruby on Rails, you’re tempted to use a gem for everything, but if that’s the case, just trust me here—you’ll benefit in the long run by keeping the Gemfile as short as possible.
Even so, it’s possible to work upwards of ten years in Rails and still make bad choices at that stage. I suspect it has to do with the way new developers are introduced to the framework and the community itself. DRY is good—you shouldn’t repeat yourself (or anybody else), but it doesn’t mean that whenever you see something vaguely similar to what you’re trying to do, you should just include and customize it.
DRY is superseded by DISH—Don’t Inflict Self-Harm.
Rule #2: Read
OK, so you took the 30 seconds required to ask the important questions and check the gem’s GitHub page. You’re reasonably sure it’s not a trap.
Which version should you use?
A good general rule of thumb is to include it the way the author advises. Most README files will have a section on how to include the project in your application and a ready-to-copy line for your Gemfile. Some will restrict version, some won’t, but if they do, they will most likely provide an explanation. This should work reasonably well for new (or up-to-date) applications, but if you’re dealing with legacy code, a harsher version restriction might be needed.
So I’m just going to say it—you shouldn’t hard-lock versions in the Gemfile. That’s what Gemfile.lock is for. The versions will stay the same until somebody runs bundle update and somebody in the project should run bundle update regularly. What’s the use scrambling to fix bugs and security issues if you’re depending on something you haven’t updated in a couple years? That’s the thing about having many dependencies—they might have bugs and security flaws you don’t even know about and have little control over.
So read and always know what you’re including. Also, refresh that knowledge from time to time because sometimes gems become abandoned or replaced. And if keeping this knowledge up to date becomes a hassle and takes too much of your time—you have too many dependencies.
Rule #3: Write
Another thing I’ve seen very often is the lack of comments. I would find a couple of hard-locked gems in an otherwise decent Gemfile, but their names wouldn’t indicate anything about what they do, and there was no comment with a reason for locking either. Comment your Gemfile! At least link to the project’s GitHub, but preferably write a short note on what the gem does. If you lock version harder than MINOR, also write a comment to the person that will find this in a couple of years so that they hate you a little less. It might be you we’re talking about!
I would recommend you don’t lock versions at all unless you have a specific reason for doing so. The most common case here would be an application that uses an older version of Rails. Actively developed gems that depend on Rails will usually target the newest version of the framework and use a pessimistic lock themselves, but not all gem developers will be this proactive, so you might need such a lock there. And you should write a comment saying the lock is for reasons of legacy Rails compatibility.
Rule #4: Be Pessimistic
Obviously not always, but when you’re adding dependencies, you should be skeptical and pessimistic. By that I mean you should use pessimistic locking: ~> MAJOR.MINOR (without PATCH version). If your gems follow semantic versioning, this will ensure you’re up to date on security and features and further updates won’t introduce breaking changes.
For gems that don’t follow it, you need to be on the ball a little more. You could use >= if you’re feeling adventurous, but generally you’d want to follow the gems’ development to know how they version their code.
Just a quick reminder: ~> will only allow for upgrading the last version number specified and >= will allow for an upgrade to the newest version available (and not constrained by other dependencies) only ensuring it’s greater than or equal to the version specified.
But there is another thing to consider. We’re talking about a Rails application and most likely all the gems you include will rely on Rails in some capacity. You can take advantage of that by a pessimistic lock to a MINOR or PATCH version of Rails itself and not apply any locking to the gems that depend on it.
That way bundler should resolve most gems to the latest version possible. And if you do get conflicts, just research the conflicting gems. Perhaps the newest version targets a newer version of Rails in which case you should apply a pessimistic lock to the version that does support it. In any case, unless you really know what you’re doing and you document it properly, you should use these rules and this kind of versioning in your projects.
That’s all folks.
I hope this article helps you make better decisions about your projects. Please keep in mind that the “rules” I mentioned are more like guidelines. Rules of thumb if you will. Also, it should go without saying that none of this is a valid replacement for a decent test suite which will catch many incompatibilities and other problems. I think that’s a nice downer to end on.
But I do have a few bonus tips before I go:
- This advice works for gems that depend on Rails, but there are more major gems like it—for example, refinerycms—and you should apply the same sort of lock to their version too so gems that depend on it and not on Rails are resolved properly as well.
- If you’re using default libraries in your application (like OpenSSL), it’s a good idea to include them in your Gemfile explicitly so that you have control over their version as well.
- You can use bundle update specific_gem when you face conflicts running bundle update to narrow the scope and tackle conflicts one by one. Also, bundle outdated is worth running from time to time to give you an idea of where the hotspots are in your Gemfile.
And remember, all of this was written by a Ruby on Rails dev. Other environments and communities might have similar tools but different approaches and philosophies, so apply this to non-Ruby projects at your own risk.
Polcode is an international full-cycle software house with over 1,300 completed projects. Propelled by passion and ambition, we’ve coded for over 800 businesses across the globe. If you share our passion and want to become a part of our team, contact our HR department. We’ll be happy to answer all your questions and even happier to welcome you aboard 🙂 Or maybe you have an interesting project in mind? If so, drop us an email and let’s talk over the details.