It’s been a long couple of sprints, but your team has managed to pull together and complete the features promised for delivery. Your unit tests have passed, and you feel confident in rolling your changes out to production. However, there’s a last minute ask to fix a minor bug by updating one of your project dependencies. There’s no change to your code base, and everyone thinks it should be a low risk change, so it’s pulled in last minute. But as soon as the change is included, all the tests start failing, and chaos ensues. After a day of painful troubleshooting, it’s determined that the newer dependency broke backwards compatibility with your application, and your application stopped working as a result. Good version management could have helped you avoid this situation.
Software under active development evolves over time as bugs are fixed and new features are added. Simply releasing every change as soon as it's finished has a high potential of causing chaos. To help keep things simple, changes are usually bundled into a known good build and released together. However, since there are now multiple releases of the same piece of software, it becomes important to know what features and bugs are present in any given release. To distinguish these releases, software teams use various versioning schemes to tag their releases so they can easily communicate to their users what to expect in any given release.
The many different versioning schemes all have their own quirks, but almost always have the core goal of providing information about the relative age two releases and little else beyond. As a fun example, the TeX typesetting language derives its version numbers from the digits of π. Every time there is a new release, another digit from π is added. From this it's pretty easy to tell that version 3.14 is an older release than version 3.1415.
Such versioning schemes are simple, and can work well when the project is independent and isolated, and no other software depends on it. If a user encounters a bug or needs a feature, the solution is simple: just tell them to upgrade to a later version of the software! A good versioning scheme should provide a way for users to know what features and bugs are present with the software release that they are using. If a user encounters a bug, they can hopefully upgrade to a later version where the bug is fixed; or barring that, at least wait for a version which has fixed the bug.
However, these simple versioning systems become lacking for more complex use cases. When trying to build software on top of other software, it becomes important to know exactly how much has changed between two versions of a piece of software you depend upon. How can you reliably know if a new version of a software dependency is compatible with what you've built? It's no longer sufficient to know if a piece of software is newer – there is also a need to know if the software you depend on is compatible with what you've built.
Tom Preston-Werner, a Github co-founder, devised SemVer in an attempt to resolve these versioning issues. Since its creation, SemVer has grown in popularity tremendously, and has taken place as the de facto standard way to version software for a large number of projects. SemVer allows you to not only signal the relative ages of release, but also whether users should expect it to be compatible with their software. With SemVer, changes are categorized into one of three categories: patches (usually bug fixes), minor changes (usually new backwards compatible features), and major changes (usually changes that break backwards compatibility). SemVer makes it possible for users to tell at a glance what to expect between releases.
The numbers in SemVer signal the scope of what has changed between two releases.
This makes managing software dependencies much easier — a user can configure their software build to only pull in the latest stable version of their dependency that maintains compatibility with their codebase. Rather than needing to juggle exact versions, dependencies can be pinned to "the latest compatible release with the features that I need."
A user can quickly identify what has changed in a release by checking which version numbers have incremented.
At Element, we maintain a large number of microservices with interdependencies, and our use of SemVer is vital. Proper use of SemVer allows us to minimize the risk of deploying new software by ensuring compatibility between our services. We will automatically deploy new releases as long as they're compatible with our other software. For breaking changes, a more careful approach is needed, and we will flag such releases to give them more attention. With this too, SemVer provides the ability to gate breaking changes and prevent them from accidentally being released before the services that depend on them are also updated.
Using SemVer will highlight potential incompatibility issues before they have rolled out. The major version change in the Auth API should raise a flag with the developers of the Users API.
In practice, it's almost impossible to coordinate updating every microservice to be compatible with a breaking change all at once, and so when it comes time to update a microservice with a breaking change, there are essentially two viable options: maintain multiple versions of the microservice in parallel, or publish a single microservice that supports older backwards compatible versions of the APIs.
An example of using the path based routing to have multiple versions of a microservice running in parallel. This allows users to choose the major release they are compatible with, and allows a gradual transition to new major releases.
Bad version management can have a high cost in several dimensions, and usually at the worst possible time to pay that cost. It can result in spending weekends and all-nighters resolving dependency hell issues that come up after a seemingly minor deployment.
Proper use of SemVer for versioning software can contribute to a healthy work-life for software development teams. Its use at Element has proven to be extremely valuable, and we would highly recommend it to any organization attempting to develop interdependent software systems – or for that matter, as a versioning scheme for any piece of software. It will provide improved stability, and allow issues to be flagged and caught early on, when the cost to fix them is low.
What will you do with the extra time and energy you get from using SemVer? Build an airboat with parts that you find around your house (yes, that has actually happened here!). Drop us a note and let us know!