Get to know 4 microservices versioning techniques
Are you struggling to apply updates consistently across distributed services? Here are four microservices versioning techniques that can help.
As applications grow and evolve, businesses often need to update or add functionalities that customers can consume consistently across multiple applications and services. Most development shops require some type of versioning strategy to ensure updates are built and deployed consistently across the board.
Versioning techniques are especially important as organizations move away from monolithic applications in favor of a microservices architecture, since developers will need to target individual services for updates rather than whole applications.
Versioning a microservices-based application is not as straightforward as with a traditional app. Microservices enable developers to design, build, test, deploy and update services independent of one another -- or simultaneously, if they choose to do so.
While this is great for architectural flexibility, it also means that versioning one service can cause it to lose compatibility with another if updates are not properly planned or synchronized. As such, architects must implement the right methods and tools to version services consistently and allow quick rollbacks when needed.
This article is part of
What are microservices? Everything you need to know
There are many approaches to microservices versioning that developers will encounter at some point in their careers. In this article, however, we examine four of the fundamental version management techniques that development teams should not only understand, but know how to put into action:
- URI versionin
- header versioning
- semantic versioning
- calendar versioning
URI versioning
In this approach, developers add version information directly to a service's URI, which provides a quick way to identify a specific version of the service by simply glancing at either the URL or URN. Here's an example of how that looks:
http://productservice/v1.1.2/v1/GetAllProducts
http://productservice/v2.0.0/GetProducts
Assuming that there are two versions of the service -- v1 and v2 -- you can copy the old data from v1 into the new v2 database, making sure that these two databases are entirely separate. Or you can update the service's schema and subsequently alter the source code of v1 to handle the new schema.
The following code snippet shows how you can allow two versions of a service (named "Product") to reside side-by-side using unique URIs and route array parameters:
[ApiController] [ApiVersion("1.0")] [Route("api/{version:apiVersion}/product")] public class EmployeeV1Controller : ControllerBase { [HttpGetbn] public IActionResult Get() { return new OkObjectResult("Inside Product v1 Controller"); } } [ApiController] [ApiVersion("2.0")] [Route("api/{version:apiVersion}/product")] public class EmployeeV2Controller : ControllerBase { [HttpGet] public IActionResult Get() { return new OkObjectResult("Inside Product v2 Controller"); } }
The downside of this approach is that a large URI footprint can become unmanageable over time, leading to mismatched naming conventions and lost versions. URI versioning is generally used to update the public APIs associated with a service. However, since it is purely a surface-level naming convention, this approach is less likely to cause any accidental breaking changes to the back-end data store.
Header versioning
This microservices versioning approach passes version information through an HTTP protocol header attribute known as content-version. Header-driven versioning takes advantage of the content-version attribute in the HTTP header to specify a particular service.
The following code snippet shows how you can configure header-based versioning in ASSP.NET Core (note that if you call your service from an HTTP client such as Postman, you'll also need to add the header x-api-version):
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddApiVersioning(config => { config.DefaultApiVersion = new ApiVersion(1, 0); config.AssumeDefaultVersionWhenUnspecified = true; config.ReportApiVersions = true; config.ApiVersionReader = new HeaderApiVersionReader("x-api-version"); }); }
In contrast to URI versioning, the main benefit of this technique is that the names and locations of application resources remain the same across updates. This helps make sure the URI doesn't become cluttered with versioning information and that API names retain their semantic meaning to the developers who regularly work with them. One downside of this approach is that information cannot be readily encoded into the hypermedia links.
Semantic versioning
Semantic versioning is an ideal practice for projects containing public-facing or communally shared APIs that experience frequent updates. This technique uses three non-negative integer values to identify version types: major, minor and patch. The format for this is often written as MAJOR.MINOR.PATCH.
Here's what each of these means:
- This denotes an update that will cause breaking changes to services or the related APIs. You should always increase this number if previous versions are incompatible with the new, updated version.
- Alter this number every time the previous version is compatible with the new version, but some of the logic behind the service has changed.
- This number indicates if the previous is compatible with the new version, but developers need to adjust the new version of the service to address a bug. Increase this number every time an error in the code is fixed and patches are added.
Let's look at an example of this. If a new version is named v1.0.1, this indicates there was a small change and there was one patch that a developer needed to add. Once this service undergoes a major change, the new version number should be v2.0.0. Here's how you can use semantic versioning to invoke an API:
http://api.product.com/v1.2.3/GetProducts
Keep those old versions ready
It's essential to keep the previous versions of your microservices intact, and there are a few reasons for this:
- First, some users may prefer not to update and run the old service instead of the new one, while other users will want the version with new or extended features -- either way, you need to support both.
- Second, it's a good practice to keep some users on the old version of an application or service alongside those using the new version. This provides a little insurance in case of any errors, since a failure in the new version will only affect a segment of customers, rather than the entire user base.
- Finally, keeping the original version in production provides a mechanism to revert to previous versions quickly. In case a new version of a service fails, you'll be able to easily swap users back to the original version and continue operations unhindered. This is especially important if the failure is one that can bring other dependent services down with it.
Calendar versioning
Although it is similar to semantic versioning, this approach uses calendar dates in lieu of non-negative integers. While calendar versioning doesn't strictly require a particular date format, it should clearly and consistently indicate the year, month or day -- or all three -- when a new version was released.
This date format enables developers to find previous or existing versions simply by searching for the release date. Calendar versioning is a good choice when applications operate on regularly scheduled updates.
Below, you can see the format calendar versioning typically follows. However, developers should note, while semantic versioning indicates the actual version, calendar versioning populates these integer values with the numeric date of a release. Note that the MICRO value means the same thing as PATCH in semantic versioning:
MAJOR.MINOR.MICRO
So, if a developer released a major version of a service in 2020, but went back to patch two errors without a minor version in between, they might identify the version with the date of the major release and the number of patches added within that version:
4.0.2
Alternatively, if the developer adds a minor update to that same major version of the service, the format would indicate the major release year and the number of minor releases within. Since there is a new minor version, the patch number will reset to zero until an error is addressed.
4.1.0
Finally, if that major version goes through a second minor update that subsequently required two separate patches, the version ID would indicate as such:
4.2.2
Best practices for microservices versioning
No matter the approach developers choose to implement, there are a few basic rules that always apply when versioning microservices.
First, never use version information in the service or API name; this will result in service call problems. For instance, if the version information resides in the name of your API (i.e., Customer_1_2_1 or Product_1_1_2), the microservices that call it will need their code adjusted with every new version of that API. And so, if you want to call older versions of that API, you'll now have to maintain either two separate services or two call methods within one service.
Second, always maintain proper documentation and ensure that versioning and documentation processes work hand in hand. In fact, it should be mandatory for developers to update service documentation following each new version of that service.
Finally, the service URL and version number should be easily configurable. Make sure that the service URL as well as the version number is not hard coded directly into any part of the application's back-end codebase. Instead, focus on enabling back-end developers and application managers to focus only on business-logic code without needing to sift through and understand a myriad of ever-changing version names and numbers.