I didn’t realize until recently that something as mundane as versioning was such a controversial topic. There are conflicting opinions on every aspect of versioning APIs.
“Every API must be versioned!” “Avoid versioning your APIs!” “Version only when needed.” “Version right from the beginning.” “Version the URL.” “Version the Headers”. And on it goes. So I dug into it a bit, to make sense of it all and lay down some ground rules to follow when dealing with versioning. So here are my musings.
To borrow from the much hackneyed phrase – change is the only constant in life. In the life of software developers that is. Especially so if the said developers are building microservices. The most important raison d’être for microservices adoption, in general, is the speed and agility it provides to meet changing business needs. Thus the environment in which microservices live and breathe makes it inevitable that at some point during their lifecycle, the service APIs will need to change, perhaps even numerous times. Therefore mechanisms to support change must be inherent in the architecture of the APIs rather than being an afterthought. To that end, it is prudent to have a standardized strategy in place, one that can be used by all services, right from the get go.
Every API change can be seen from the broad lens of whether it breaks the contract with the consumers or not. API changes that break the consumers are obviously the ones that are problematic and complex to deal with. So the first step should be to avoid making breaking changes to begin with. Easier said than done, as in practice such changes are not avoidable at all times. However, good design principles can help mitigate the number of such changes. To that end, the advice – “Be conservative in what you do, be liberal in what you accept from others.” (Postel’s law) is a good one to follow. As a consumer of APIs be generous in ignoring aspects that are not required by your service. Seeing an additional new field in the response, for example, should not cause your service to break. As long as the API still honors what is essential to your service, all other additional or expansive changes should not affect the service behavior. Similarly if you are the provider of APIs, maintaining backwards compatibility must be highly prioritized over other options. Breaking changes should only be made if all other options are indeed exhausted.
In the case where backward-incompatible changes are made, then there must be a path for the consumers to smoothly integrate with the changes without surprises. There are two common ways to go about it.
The first standard recommended practice is to version the APIs. The versioning scheme can be something home cooked or the standard Semantic Versioning. In brief, it proposes the version number to be of the format:
The rules for incrementing the versions are:
- MAJOR version when you make incompatible API changes,
- MINOR version when you add functionality in a backwards-compatible manner, and
- PATCH version when you make backwards-compatible bug fixes.
Using versioning numbers, each version is handled by a corresponding API endpoint and the recommended approach is to co-exist the versions in the same service. As consumers of the old version migrate to the newer version, the old API is removed and only the newer version remains in the service (alluded to as the expand and contract pattern). Having multiple service instances for different versions is more complex than co-existing endpoints in the same service instance and hence is generally not recommended.
If using HTTP/REST APIs there are two factions on how to go about implementing versioning. There are proponents for carrying the version information in the URL, mainly for the ease with which it can be implemented, tested and integrated. The other faction recommends using the content negotiation feature of HTTP using the Accept and Content-Type headers and leaving the URL unchanged. I personally prefer the second approach.
The second recommended practice is to not use any external version numbers, but handle the changes internally in the service. The API endpoint remains unchanged, but the service internally adapts to the request depending on the context of the caller and handles both the legacy and new API callers. This obviates the need for the consumers to deal with versioning, thus leading to a cleaner approach.
In either case, the number of such versions actively supported must be kept to a minimum, ideally just two at a time and the old API must be retired as soon as reasonably possible. The consumers must be provided a deprecation path to migrate to the new APIs.
An interesting view of versioning as an anti-pattern is presented in the book “Production Ready Microservices“. To quote,
“A microservice is not a library (it is not loaded into memory at compilation-time or during runtime) but an independent software application. Due to the fast-paced nature of microservice development, versioning microservices can easily become an organizational nightmare, with developers on client services pinning specific (outdated, unmaintained) versions of a microservice in their own code. Microservices should be treated as living, changing things, not static releases or libraries. Versioning of API endpoints is another anti-pattern that should be avoided for the same reasons.”
It further goes on to state that:
“Microservice versioning is often discouraged because it can lead to other (client) services pinning to specific versions of a microservice that may not be the best or most updated version of the microservice.”
All valid and reasonable points but not entirely convinced that this can be put into strict practice. Most often supporting multiple versions becomes a necessity and the best one can aim for in that situation, is to keep the number of concurrent supported versions to a minimum (ideally just two) and have a path for consumers to migrate without resorting to lockstep releases.
Netflix presents an interesting, albeit extreme, use case (Check the below link to hear them talk about it). They use the SemVer format for versioning everything and the intriguing part is where they state that they maintain versions indefinitely (the immutability feature) and so use version based routing to connect the consumers to the correct versions. Maintaining 1000’s of versions is not for the faint of heart and Netflix has the muscle to support it, but for the rest of us, keeping the versions to a minimum is the only viable path.
Note that this article is part of the Microservices series. You can read the previous ones here : Prelude, Introduction, Evolution, Guiding Principles, Ubiquitous Language, Bounded Contexts, Communication Part 1, Communication Part2, Communication Part 3, Communication Part 4, Communication Part 5, Kafka, Time Sense, Containers, API Gateways, Service Mesh, Caching Part1, Caching Part2