Microservice Interoperability

This article is about how to handle diverged Microservices, API changes and how to retain backward compatibility.

The API of a Microservice is it's contract to the outer world and the only single point of failure in case the contract gets broken. E.g. our slidewiki-platform relies on the contract, so will third party developers and other microservices. Thus we want a resilient contract that is very hard to violate. So how will we make sure it is resilient and hard to violate? - Through interface tests and some rules, that are enforced by interface tests.

Example

But let's first come to some examples and general rules: Let's assume that you implemented some API for your super cool service and some other software's are using it. Now you notice that you could add a super cool feature to your API, that modifies one of the routes responses in a way, that it removes some fields, exchanges an array with a object (aka set) and adds some new fields. You deploy your new service and you're fine with the world. But wait! Like one day later, some people will start to complain that your software must be broken, because this one route doesn't work anymore, but it did before....oh damn. You broke the service API! So how to avoid it? By resilient clients and versioned APIs.

Resilient clients and versioned APIs

In case you're adding something to a response (like a new field) of a route, but won't touch the original response, you're fine to go. The client software will just ignore your new field, because the developer that implemented the client was not aware of it in the first place.

In case you have to touch the original response, please think again. If there is no other option, introduce a versioned route (e.g. deckservice/v2/....) and implement your wanted behavior there. This won't break the original route (thus all clients that rely on it, will still work) and clients can start to shift over to the new and improved version. Downside: You have to make sure that you're old route still works (e.g. with your data model).

In case you don't want to introduce a new route, don't implement your feature. Otherwise you WILL break something.

How to build a resilient contract?

By using interface tests. This is not about the aim to increase the coverage (but it will, as a nice side effect) or to appear as a good developer. It's just necessary for a high quality API.

So every time you think that a route is ready for production deployment or is used by some client, you will have to write all kinds of interface tests for this route. Remember to check for:

  • All HTTP response codes (2XX, 3XX, 4XX, 5XX)
  • Structure of your data (e.g. the response needs to match an array, that consists of separate values)
  • Content of your data (e.g. the separate values are typed as String, it should be at least five of them, these strings are not empty and match you're previously inserted data)
  • Always test as open minded (e.g. use object.contains(name) instead of object === {name: "..."}) → this allows non-breaking extensions

For examples, see the integration tests at the user-service .

What if a test fails to execute? 

Well, you probably broke a API method and it's not backward compatible anymore. That's really bad! Instead of fixing or out commenting the test, please fix the API method, so the test succeeds again, or introduce a new version of this method, or discard your changes - because breaking a API method is not allowed!