Let’s start with a simple question. Do you need to version microservice APIs? I think the near-unanimous answer here is ‘yes’. You need to version microservice APIs, because each service is independent, you can’t update all the services that call a certain API all at once. You need a mechanism to introduce some new functionality, while keeping the old stuff around, so that you, or others, can make the changes to other services as the need arises, without breaking them.
If your APIs are versioned, this means that the clients of those APIs must declare which version they want to use. Most seem to do this implicitly, where the client doesn’t actually specify a version, and they are just defaulted to the oldest version (the oldest so that new changes do not by default break everybody). Clients should explicitly call out the version they use, but we all get lazy sometimes. There also needs to be some way of tracking what clients are using an old version of the API, and some way of notifying those clients. In a microservices world, this may be digging into logs, and sending emails to teams asking them to update. Enter the world of corporate bureaucracy.
The next question is how do you request a version, and how do you handle routing when you are supporting multiple versions? There seem to be three common approaches to specifying a version
- In the URL
- Custom request header
- Accept header
Each have different benefits and drawbacks. Many go with option 1 because it is very simple. You can request different versions in your web browser (e.g. www.example.com/api/v1/
or www.example.com/api/v2/
) without any special tools. The other two mechanisms typically will need some other tools installed, such as a REST client, or some good knowledge of other browser-based developer tools. Personally, I prefer option 3, specifically using an ‘Accept-Version’ header, based on what Restify does.
Routing requests to a particular version of the API I think is the trickiest part of this whole thing. There are so many different ways to handle the problem. If the changes you make to an API are additive and non-breaking, it may be easiest to just update the service, and basically have it implicitly handle the different versions (i.e. if the new field exists, call this new method, otherwise fall back to the old method). When you are making breaking changes, this problem gets harder. Usually you are making breaking changes because you do not want to support the ‘old’ way any longer, so there is a huge mental block to simply updating the existing service to handle both the old and new version. That may still be the most simple approach, but it does carry around the feeling that you are writing throw-away code. Another approach is to just introduce a new service entirely, and find a way to support running both the old version and the new version side-by-side for some time, and offload the decision making to some routing layer.
I am currently in the process of some kind of hybrid model here, where the goal is to introduce a new service that completely rewrites and replaces the old version of the service, but to do it in such a way as to be continuously deliverable. My approach is basically a Strangler App, where the new service will take on more and more of the functionality (that I want) from the old service, until there is nothing left of the old service. To start out with, my new service will basically just be a proxy to the old service, and it will slowly grow until the new service actually becomes useful.
Originally Posted on my Blogger site June of 2017