43

We had two major dependency-related crises with two different code bases (Android, and a Node.js web app). The Android repo needed to migrate from Flurry to Firebase, which required updating the Google Play Services library four major versions. A similar thing happened with our Heroku-hosted Node app where our production stack (cedar) was deprecated and needed to be upgraded to cedar-14. Our PostgreSQL database also needed to update from 9.2 to 9.6.

Each of these apps' dependencies sat stale for almost two years, and when some were deprecated and we reached the 'sunset' period, it has been a major headache to update them, or replace them. I've spent over 30 hours over the past month or two slowly resolving all of the conflicts and broken code.

Obviously letting things sit for two years is far too long. Technology moves quickly, especially when you're using a platform provider like Heroku. Let's assume that we have a full-fledged test suite, and a CI process like Travis CI, which takes a lot of the guesswork out of updating. E.g. if a function was removed after an upgrade, and you were using it, your tests would fail.

How often should dependencies be updated, or when should dependencies be updated? We updated because we were forced to, but it seems that some kind of pre-emptive approach would be better. Should we update when minor versions are released? Major versions? Every month if updates are available? I want to avoid a situation like what I just experienced at all costs.

PS - for one of my personal Rails projects, I use a service called Gemnasium which tracks your dependencies so that you can be notified of e.g. security vulnerabilities. It's a great service, but we would have to manually check dependencies for the projects I mentioned.

6 Answers 6

47

You should generally upgrade dependencies when:

  1. It's required
  2. There's an advantage to do so
  3. Not doing so is disadvantageous

(These are not mutually exclusive.)

Motivation 1 ("when you have to") is the most urgent driver. Some component or platform on which you depend (e.g. Heroku) demands it, and you have to fall in line. Required upgrades often cascade out of other choices; you decide to upgrade to PostgreSQL version such-and-so. Now you have to update your drivers, your ORM version, etc.

Upgrading because you or your team perceives an advantage in doing so is softer and more optional. More of a judgment call: "Is the new feature, ability, performance, ... worth the effort and dislocation bringing it in will cause?" In Olden Times, there was a strong bias against optional upgrades. They were manual and hard, there weren't good ways to try them out in a sandbox or virtual environment, or to roll the update back if it didn't work out, and there weren't fast automated tests to confirm that updates hadn't "upset the apple cart." Nowadays the bias is toward much faster, more aggressive update cycles. Agile methods love trying things; automated installers, dependency managers, and repos make the install process fast and often almost invisible; virtual environments and ubiquitous version control make branches, forks, and rollbacks easy; and automated testing let us try an update then easily and substantial evaluate "Did it work? Did it screw anything up?" The bias has shifted wholesale, from "if it ain't broke, don't fix it" to the "update early, update often" mode of continuous integration and even continuous delivery.

Motivation 3 is the softest. User stories don't concern themselves with "the plumbing" and never mention "and keep the infrastructure no more than N releases behind the current one." The disadvantages of version drift (roughly, the technical debt associated with falling behind the curve) encroach silently, then often announce themselves via breakage. "Sorry, that API is no longer supported!" Even within Agile teams it can be hard to motivate incrementalism and "staying on top of" the freshness of components when it's not seen as pivotal to completing a given sprint or release. If no one advocates for updates, they can go untended. That wheel may not squeak until it's ready to break, or even until has broken.

From a practical perspective, your team needs to pay more attention to the version drift problem. 2 years is too long. There is no magic. It's just a matter of "pay me now or pay me later." Either address the version drift problem incrementally, or suffer and then get over bigger jolts every few years. I prefer incrementalism, because some of the platform jolts are enormous. A key API or platform you depend on no longer working can really ruin your day, week, or month. I like to evaluate component freshness at least 1-2 times per year. You can schedule reviews explicitly, or let them be organically triggered by the relatively metronomic, usually annual update cycles of major components like Python, PostgreSQL, and node.js. If component updates don't trigger your team very strongly, freshness checks on major releases, at natural project plateaus, or every k releases can also work. Whatever puts attention to correcting version drift on a more regular cadence.

10

Libraries should be updated when they are required to be updated. That means, if updating brings no value, you shouldn't.

In your particular case, you were migrating from a old tech stack to a new one, and in order to do that you were forced to update your dependencies. That very moment is the correct time to update dependencies.

Had you been updating your dependencies across time, in order to "not have a headache now", you'd have had to invest a lot of work time (coding) for no return value. And when you were to do the last update (the one you're doing now, but updating 1 major version instead of 4), you'd probably still have a headache somewhere (after all, major version means breaking changes). So I think you're on the right path.

However, if you find it too hard to migrate, and have to do a lot of refactor, chances are the problem lies in your codebase. It's pretty common for Android projects not to have an overall architecture in terms of code structure. A good dependency injection framework like Dagger 2, and a couple of software engineering principles like SOLID would have made it easier to change code implementation while keeping the same behavior/requirements.

Also, since we're on refactoring, read a little bit about Unit Testing, as that would help a lot when doing this kind of work.

5

If you are using package management tools (e.g. npm, NuGet) and have a comprehensive automated test suite then upgrading dependencies should be a low-effort activity, simply upgrade the package, run your test suite and see if there are any issues. If there are then rollback and raise a work item to investigate and fix the issue.

As long as the cost of upgrading dependencies is low its well worth keeping up to date:

  • If there are issues with upgrading you want to know sooner rather than later in case upstream changes are required.
  • Leaving dependency upgrades to the last minute often means you are doing those upgrades at crunch time (e.g in response to a security critical bug). Keeping on top of your dependencies means you are in control of when you spend that effort and can perform those upgrades at times when you are not as busy.
  • Newer versions may have productivity improvements e.g. better documentation, easier to use API, bugfixes (although the reverse is also possible).

If upgrading dependencies is not low effort (e.g. because you need to manually test the upgrade or because there are known issues / breaking changes) then you have to weigh up the pros and cons against your other tasks. Old dependencies are a kind of low-interest technical debt, and so should be treated accordingly.

2

I think it must depend on the library in question to some extent, but I have had similar dependency headaches myself.

Common sense says to me that a major version is probably the right time to upgrade, and a minor version that addresses a serious flaw, or includes a significant benefit would supersede that.

Sometimes we don't have the luxury of working on every application that requires maintenance, or even undeploying a mission critical one, but they will bite you eventually and an ounce of prevention often beats a pound of cure!

2

You should not do a release where you knowingly use old versions of your dependencies, unless those versions are supported alternatives.

ie If you are on V1 and it is still supported, you can still use the latest version of v1.

The only time you should be out of date is if:

A: You haven't done a release in some time.

B: You have been on v1 so long that it is no longer supported

Updates are released for a reason, they contain security fixes which you should be taking on board.

If a new version of your dependency comes out, you should also be doing a release

0

Libraries should be updated when offers an advantage that you software will use that compensate the work spent in the change.

Even minor library version upgrades can break or insert inconsistencies in apps. From that perspective there is no minor changes.

There is no shame in using old libs. When the change is needed may be painful but it is part of the job.

1
  • I agree that every upgrade should be well understood. And it is ok to have technical debt if you can pay it back. We are not hired to be on the latest version (and only chasing latest versions all the time, without thought or analysis), but latest versions may help in the things we are hired to do.
    – geoaxis
    Oct 26, 2019 at 17:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.