Why do we bother with multi-part numbers at all? Why not just use use a single revision number, like version 123? The parts of a version number communicate the magnitude of the change between the two versions. In the case of a library, this translates to how difficult the upgrade will be, which in turn tells about the level of source, binary, and serialization compatibility between the two releases.
Setting the right level of compatibility between releases is about striking a balance. Upgrades that are fully source and binary compatible are much easier to take for the client, but much more work for the authors. When releasing frameworks I prefer to use a three part version number of the form major.minor.maintenance. Bug fix (maintenance) releases must be source, binary, and serialization compatible. Minor releases ought to be binary and serialization compatible. Major releases need not be compatible.
When we find a bug, it’s often serious enough that we want clients to upgrade immediately. We don’t want to give them any excuse to avoid it. That means we ought to preserve binary, source, and serialization compatibility. That way the new version can be a drop in replacement, meaning it will work without recompilation.
The fact that a library author bothered to produce a minor version number communicates intent to the client. The author went through the effort to create a fully compatible release in order to make your life easier. The author believes the upgrade is important.
Occasionally fixing a bug will cause an incompatibility. It’s unfortunate but sometimes necessary. If it’s possible to limit the break to the piece of code that wasn’t working anyway, then it seems justified. Some work is required to get things working properly, it’s as simple as that.
Incrementing the major version number indicates that backwards compatibility was broken. The new version can be binary incompatible, source incompatible, and the serialized form of its classes may have changed as well.
It’s like a last resort, but it’s an important one. Some changes are just too big. The authors will not always be able to or want to preserve compatibility. It’s important to schedule major releases so that there is balance. If you schedule them too frequently, clients will resist doing upgrades. Every major upgrade requires recompilation, maybe some development work, and testing. There’s little incentive to upgrade to any particular version at any particular time. It becomes very difficult to synchronize the version used across an entire organization. On the other hand, if you don’t create them frequently enough then clients may have to wait a very long time for certain features.
In my experience it’s good to create a major release roughly every six months.
Minor releases are the most complex because there are a few choices for what they can mean. Minor releases can be binary compatible with the previous release, source compatible, or both. There’s also the choice of whether it should indicate serialization compatibility as well. If you look at what open source projects do, you won’t find a lot of consistency.
I believe that minor version releases should be binary and serialization compatible with the previous release, but not necessarily source compatible. I’m assuming familiarity with binary and source compatibility. If not, you can read about it here.
Let’s dive into the trade-offs between work on the client side and work on the library developer side. Binary compatibility means that no coordinated releases across teams are necessary. Source compatibility means no development effort is required.
Why minor versions should be binary compatible
If your minor versions are binary compatible, it’s easier for everyone to upgrade. More importantly, teams who are slow to upgrade will not hold up other teams from taking the latest version.
Let’s say library B depends on version 1.0.0 of library A, and application C depends on both libraries. When binary compatible version 1.0.1 of library A is released, application C can start using it without involving anyone who works on library B. When a new version of a library is not binary compatible, it can take a very long time to propagate through an organization or through a software stack. With binary compatibility, no coordination across teams is necessary.
Why minor versions should not have to be source compatible
If a release is source compatible, it still needs to be recompiled but there won’t be any compiler errors requiring fixes. However, it doesn’t reduce the overall amount of work, it just pushes it later. If library designers think it’s a good idea to change API in some source breaking way, then they’ll do it some day and the clients will see those compiler errors eventually. Postponing just bunches them all up and makes major versions more painful.
In addition, source compatibility doesn’t affect what can get deployed. You might have to stick with the old jar at compile time to avoid compiler errors, and yet you can still deploy with the new jar if it’s binary compatible.
Making minor version numbers source compatible adds restrictions and requires more work from the library developers without giving clients much benefit. Therefore I recommend that minor versions are binary compatible but not source compatible. This might not be what clients are expecting though so it’s important to communicate the compatibility policy clearly.