Library Versioning

Libraries should be versioned and packaged such that they are easy to use over time, and in combination. The best way I have found to do this is to abide by three rules:

  • Use APR versioning
  • Re-namespace on major version changes
  • Change the artifact ID on major version changes

Use APR style versioning

APR versioning basically defines the meanings of changes for versions like {major}.{minor}.{bugfix}.

A bugfix release is both forwards and backwards compatible. It is a drop in, binary compatible, replacement for anything with the same {major}.{minor} numbers. Going from 2.28.0 to 2.28.1 would be a bugfix release.

A minor release is backwards compatible but not forwards compatible. That is, a 2.29.7 version can be dropped in to replace any other 2.29.X, or earlier minor version numbers such as 2.27.4 or 2.1.0. It would not be a drop in replacement for 2.30.0, though. Typically minor releases add new functionality through additions to the API.

A major release is not backwards compatible with anything lower – a 3.0.0 cannot be dropped in to replace a 2.30.7 – it has a different API. Nor can it replace a 4.2.89 release, which has a higher major version number.

Version numbers are used to encode compatibility for the API.

Re-namespace on major version changes

When making a major version change, that is a backwards incompatible change, always use a new namespace. In Java or C# use a new package name, in Ruby use a new module name, in C use a new function prefix, etc.

Re-namespacing allows you to use both the old and new versions in the same process. This is particularly important for transitive dependencies. To look at a concrete counter-example demonstrating the pain of not doing this let’s look at a personal mistake I made in jDBI. jDBI uses a StatementContext to make information available to to extensions, such as custom statement rewriters. Between 2.15 and 2.16 I changed StatementContext from an abstract class to an interface, but did not change it’s API. I believed this was a backwards compatible change because I thought the same bytecode was generated for method invocations against interfaces and abstract classes.

I was wrong, different bytecode is generated. Heavy users of jDBI tend to create small libraries which bundle up their extensions, and then they rely on their small libraries. At Ning we call ours ning-jdbi. If rely on ning-jdbi 1.3.2 which relies on jdbi 2.14, and I rely on jdbi 2.28 then I am in trouble, as ning-jdbi will get runtime errors when trying to run against the more recent version of jDBI. I have to go cut a new version of ning-jdbi, which is now backwards incompatible, and the chain continues. By introducing an accidental backwards incompatible binary change I forced backwards incompatible changes on the whole dependency chain.


Change the artifact ID on major version changes

Using a seperate namespace for backwards incompatible changes is not enough on its own in most circumstances. Yes, both versions can coincide in the same process, but most build and packaging tools cannot handle loading two libraries with the same name and different versions. As I don’t want to write yet-another-dpkg or yet-another-build-tool merely to work around this issue, you save tons of grief by also changing the library identifier as fas as build and packaging goes.

Once again, transitive dependencies are the main driver here. If I depend on and a library I use depends on (using maven coordinates) I am only going to get 10.0.1. Unfortunately, Guava is not backwards compatible between r9 and 10.0.1, and the library I depend upon may or may not work. (I pick on Guava here because it is such a damned useful library, so it tends to get used everywhere, at least by me).

I used a Java example here, but it applies everywhere. For example, in the Ruby world most gems are pretty lackadaisical about compatibility, not to mention packaging. Heck, just look at the history of rubygems itself.

Working around bad libraries

Even if you strictly follow these rules, when you rely on a library which does not follow them, you break. Your only options are to stop using it or to repackage it. In general you can repackage by just grabbing the source and changing the namespace, then building against and including your renamespaced version. This locks you into a specific version, but it protects you and your users from an unhygienic dependency. Depending on how you build you may renamespace the source at build time, such as by using the maven shade plugin, or during development by just pulling the source into your source tree.