The Art of a Dependency Upgrade

March 18, 2018

RxJava 1.x reaches EOL on March 31, 2018, meaning no further development. That’s not surprising since the 1.x was in a bugfix-only mode since June 1, 2017.

This is an interesting event in a software lifetime, since not so many libraries actually live and prosper long enough to produce a superior version, at the same time handling support for an older version for so long. Fortunately enough, RxJava is one of these lucky projects with maintainers actually caring about users. Thank you, RxJava maintainers, you are real human beings and real heroes.

RxJava 1.x2.x serves as a good example of a major dependency upgrade. Developers actually update dependencies pretty frequently, but that’s mostly the case with minor upgrades, when things are (usually) nice, cozy and feel like a walk in a park. Major upgrades can change API drastically.

Another example of such an upgrade is Retrofit 1.x2.x — basically, everything was reworked, repackaged and restructured. Good times. This kind of change can (and actually will) break your code if the upgrade was reckless. It gets worse with medium-to-large teams and corresponding codebases.

After doing a bunch of impactful upgrades — including RxJava, Retrofit, Spek, Mockito — I’ve pointed out a couple of patterns which might help with major dependency upgrades. These suggestions can be applied to internal refactorings as well — this proved to be the case with removing Dagger from the project, but that’s another story.

Asking the Right Question

Are you sure?

That’s the first question you should ask yourself before migrating to a new shiny dependency. Actually, you will answer this not only to yourself but to your management, since such migrations usually consume a considerable amount of time which can be spent on evolving a product from the consumer perspective.

Let’s split this vague question to simpler ones.

Answers like the new one is just better do not work in real life.

Our example — the RxJava upgrade — unfortunately has a huge impact on a project, especially if it is practically based on RxJava and every component uses it one way or another.

It is always easier to sell huge performance boosts or improved development experience. Know all pros and cons. The truth might be harsh. Every developer wants to have nice new things, but when your backlog is filled with product-oriented tasks the reality kicks in.

Know Your Enemy

At this point, you might ask yourself an interesting question.

Why bother with all this team communication? I can upgrade everything myself!

The answer is… teamwork!

For example, RxJava brings a huge amount of changes. The most notorious one is throwing the NullPointerException if a stream has null value in it. This amount of changes will affect everyone on your team, especially if you use RxJava heavily.

Retrofit might be simpler in that regard even though changes are not so small. The reason is simple — scope of the dependency. Retrofit affects your network layer, but not every developer actually needs to know how network calls work if you have proper abstractions in place.

Brace for Impact

Take a step back. Look at the bigger picture. Do you see some patterns here and there? Good.

The thing is, some preparation actions can be done beforehand. As I’ve mentioned before, RxJava 2.x does not allow null values in streams. You already can refactor the code to prepare for that, most likely using some kind of Optional values.

Using the Retrofit example RequestInterceptor was replaced with OkHttp Interceptor. It is possible to do the refactoring using the Retrofit 1.x doing no harm at all.

Be pragmatic, it helps! Like, in life!

Don’t be a Hero

The obvious approach is to make a huge refactoring, but if you have such thoughts better take a deep breath and save your soul before it’s too late.

Brick by Brick

Do the migration gradually. Even better — pick the area of the project with the least impact and do experiments there. Is is extremely trivial to do so with RxJava. Pretty much the same can be done with Retrofit — just move a subset of API declaration to another interface and you are ready to go. Live with the migrated subset for a couple of releases, take a look at metrics (the most trivial one is the crash rate) and refine your approach. This is not a sprint but a marathon. You should have a single goal in mind — maintain the product quality at all costs.

Side by Side

The gradual migration requires different artifacts and package names to avoid conflicts. Jake has this topic covered pretty well. Both RxJava 2.x and Retrofit 2.x actually apply this policy, allowing using two versions of a library in parallel. Some libraries do not follow it though. In such cases, I suggest to repackage the previous version using a custom package name and publish a local artifact. This way you can just remove the old artifact after the migration is done.

Less is More

Another word of advice related to the gradual migration is following the hierarchy from the bottom to the top level. For example, let’s imagine your project has two virtual layers: service and presentation. A single service can provide data to multiple presentation components. What would you migrate first? Yep, the least impactful component, i. e. a single presentation component. This way you can gradually apply changes. An alternative would be to change a service, but this way you are affecting all components you have.

Celebrate!

Got to say this. The most satisfying part of a long-running migration is deleting obsolete dependencies and realizing that the project works totally fine without them. This is an awesome feeling!


Title is a reference to The Art of War and, of course, to The Deadpool’s Art of War.


Thanks to Artem Zinnatullin for the review!