Why do we need to version our microservices?

Microservices are basically APIs. These are consumed by the clients. We should be able to evolve them without any impact to the clients. We should not force the clients to use the new changes in services and more importantly this should not break the clients. There must be a contract between services and clients. What should the contract contain? Let’s try to answer that. But first some insights on why we need to version them.

We cannot use one version because that means that every microservice must have the same version. If we introduce changes in one and bump its version, we need to bump the versions of the others as well. That means, we need to deploy them all. Well, we break one of the rules: the ability to deploy independently. We are coupling the microservices by this fact. This is not what we want.

The changes that we introduce could be minor, in which case the clients could start using the new version immediately, or they could be major which means that two versions must run simultaneously for a determined amount of time. This comes in handy also when we need to actually deploy in production; think of blue/green deployment or canary releases.

We are already versioning our libraries!!! Just look at a maven dependency. It works with specific versions of dependent libraries in order not to break as it cannot control the release of those libraries. So we are already doing it for ages. We are the clients. Whenever we migrate to a higher version basically we need to check if the update will break or not your code. This way of thinking must be used and enhanced at microservice level.

Okay, hopefully I’ve convinced you by now that versioning is a must. Obviously next question is

How do we version our microservices?

A good and widely adopted approach is semantic versioning. It has a three parts string: MAJOR.MINOR.PATCH. The MAJOR version should increment only when the changes are breaking the clients. The MINOR version should increment when the changes are not breaking the clients. A PATCH version change signals bug fixes.

I will focus on REST here as it is the common choice for many developers. As Led Zeppelin say “Yes, there are two paths you can go by” in their “Stairway to Heaven”: either version the URL or hide this into a header.

URL

If we are versioning the URL it will look something like:

http://item-service/v1.9.0/do-something http://item-service/v2.1.0/do-something-better

then the service can be accessed with the url. The version is visible and the access is straightforward. Ok, but is this part of the contract? Well, it used to be. Not anymore. Some would say that the client should know only one URL for the service. It should be able to find out more by interacting with it. Cool. How?

At this point it’s obvious that coding is required.

One choice could be the spring boot actuator. The /info endpoint is intentionally left blank. What information we can put there? Lots. Service name, git information and of course service version. A custom property(our case here) must be prefixed with info. Spring Boot Maven plugin can out of the box capture information emitted at build time. Your yaml file will contain:

info: project: version: @project.version@ artifactId: @project.artifactId@

Okay, we have now the ability to offer information by accessing the /info endpoint. The client will know to look there and check the version.

http://item-command/info

The response could be

{ "project": { "version": "2.1.0", "artifactId": "item-service-command-web" } }

But how does this help me as a client? What if I’m still at version 1.x.x and since it’s a major release I don’t wanna accept it. Where is the older version which was working fine for me? We could enhance the response to contain more versions(see InfoContributor).

{ "project":[ { "version":"1.9.0", "artifactId":"item-service-command-web", "url":"http://item-service/v1.9.0" }, { "version":"2.1.0", "artifactId":"item-service-command-web", "url":"http://item-service/v2.1.0" } ] }

Okay, well it seems that we’re back to links, but with one extra step. I needed first to interact with the service. And now the clients are informed that a new version was released and major changes were developed. They need to determine the amount of work required to use this new version and settle a timeframe for update. Those who wanna stick with the old one they can, depending on their SLA(Service Level Agreement). Eventually the older version will be removed as all the clients will use the newer one.

You don’t really need here actuator. You could just define your own endpoint. This is just a first step in the direction of semantic communication, the links must be manually hardcoded in the clients. Or one can create a way to interpret this response and dynamically link the version to the url. But this needs a fairly amount of work on clients. Not really reasonable.

Custom header

Second solution. You could have a endpoint in your microservice which will receive a version number (that the client expects) and determine the path from there. We could put the version number in a custom header, but that would mean each request from the client needs to contain it and this introduces complexity on the client side. Eventually you will find out that the service endpoint must be on the gateway, and you need to pass every time through it. If you are using an egde server like Zuul this means a new pre filter that should run always and with the highest precedence.

@Component public class VersionFilter extends ZuulFilter { private Logger logger = LoggerFactory.getLogger(this.getClass()); private VersionService versionService; @Autowired public VersionFilter(VersionService versionService) { this.versionService = versionService; } @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); String version = ctx.getRequest().getHeader("App-Version"); String appContext = ctx.getRequest().getServletPath().split("/")[1]; String appWithVersion = versionService.findUrl(appContext + version); if (appWithVersion == null) { throw new InvalidVersionException(); } String url = ctx.getRequest().getRequestURL().toString().replace(appContext, appWithVersion); ctx.set("requestURI", url); return null; } }

The big problem with this is that we create a single point of failure, and that is the gateway. The services behind it should not need it for communication. Also in the gateway we must keep a data structure that contains all the services with their versions.

zuul: routes: item-command-default: path: /ic/** serviceId: item-command-V1.9.0 retryable: true item-command-v1: path: /ic/v1.9.0/** serviceId: item-command-V1.9.0 retryable: true item-command-v2: path: /ic/v2.1.0/** serviceId: item-command-V2.1.0 retryable: true

We advanced a little bit, in the sense that now the client knows only one URL and requests the version in a custom header. It has no details about how we do the versioning. The route item-command-default is there to provide an entry point in the edge server.

All is fine and well, but the clients can be other microservices and they may not need/want a gateway to communicate. That means each one needs to replicate this function in their code and eventually it will lead to coupling the services, by having an orchestrator which will decide the path. Seems this is not really working for us.

Version resources, not APIs

Huh, it turns out Led Zeppelin were wrong. There are 3 paths you can go by. 🙂

A third solution(and more elegant) is hypermedia (HATEOAS). The definition of these media types is what the contract should consists of. For this to work the client must again send the version it requires. We can do it by following a process called content negotiation. The accept header is useful in this case. The client can request the version in this header.

accept: application/item-service+json.v1.9.0

This signals that the response content type must be a version 1.0.0 and it’s also a json. In return the service should look at this header and match the version. As you can see the values is some kind of MIME type, but we can create our own as vendors of services.

accept: application/item-service+json.v1.9.0

Basically all HTTP stacks can modify by default these headers.

The service has to set the HTTP Vary header to indicate that the response is cacheable based on the URL and Content-Type.

A big advantage is that the clients are not required to change their links to the service endpoints. By adding the rel tag on top of what we have we are not changing the meaning of the resource, but we are versioning its representation. Another advantage is that they are in control of which version they want to use.

Sure, but we said the service needs to look at the version in the header. That means we have ONE service with multiple versioned resources(we want choreography, not orchestration). The clients need to know each resource name and its version. With this in mind the resources may have different versions. Let’s say for example that we have the Item resource with version 1 and Bid resource with version 2 and they are compatible. In this case the client needs to be aware of this and make the correct requests. It could get rapidly confusing when dealing with multiple versioned resources. Mistakes can be easily made. On top on this the client need a caching mechanism for storing the version number and propagate it in a correct way.

How do we version the resources then? Do we need to have ItemV1 and ItemV2 in our code? Well yes. Not necessary this convention but we need to have both since V2 contains breaking changes. What about the business layer? That must be able to support versions of the object as well? What about the database? As you can see it becomes harder to maintain and work with. There are no problems if we are doing minor changes as the business layer can work with the changes and also the database. But if the changes are major than we truly have a problem. We might need to support different signatures of the same method and different structures of the same table. That’s not a pleasant thought to say the least.

Version 2 will look something like

And the response headers

This must be handled in on the same endpoint:

@RequestMapping(value = "/bidsHateoas", headers = HttpHeaders.ACCEPT, method = RequestMethod.POST) public ResponseEntity createBidWithHateoas(@RequestHeader(HttpHeaders.ACCEPT) String accept, @RequestBody BidRequest bid) throws ExecutionException, InterruptedException { logger.info("Creating bid " + bid); String version = accept.split("version=")[1]; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_TYPE, BidServiceMediaType.BID_SERVICE_BID_VALUE + ";version=" + version); headers.add(HttpHeaders.VARY, "Content-Type"); CompletableFuture<EntityWithIdAndVersion> future = bidService.addBid(bid.getItemCode(), bid.getAmount()); EntityWithIdAndVersion result = future.get(); if (version.contains("1.9.0")) { CreateBidResourceV1 resource = new CreateBidResourceV1(result.getEntityId()); resource.add(linkTo(methodOn(BidController.class).createBidWithHateoas(accept, bid)).withRel("bidsHateoas")); return new ResponseEntity(resource, headers, HttpStatus.OK); } if (version.contains("2.1.0")) { CreateBidResourceV2 resource = new CreateBidResourceV2(result.getEntityId(), "I am Groot!"); resource.add(linkTo(methodOn(BidController.class).createBidWithHateoas(accept, bid)).withRel("bidsHateoas")); return new ResponseEntity(resource, headers, HttpStatus.OK); } return new ResponseEntity(HttpStatus.NOT_FOUND); }

I’m sure this can be enhanced in many ways but you get the idea behind it.

QUEUES

If you are using a asynchronous way of communicating(queues) this could be even easier. You can put your messages on specific versioned topics or put the version number in the routing key. Clients will consume the messages that they match their version number. Again coding is required for this one too and web clients cannot communicate directly with the broker. But it could be great for service to service communication.

I’ve always said that microservices are hard. Why versioning them should be any different? We’ve gone through multiple solutions of versioning each with its pros and cons. URL is a quick way of achieving this, but it’s not part of the REST contract. On the other hand hypermedia it is part of the contract but it comes with exponential complexity. Not convinced it’s worth it.