Development

API Migration

So, you’ve got a load of Microservices all talking over HTTP. This is nice and dandy, with everything communicating smoothly in the void of cyber space between your running services. But, as with most software projects, there’s going to be a push for change. And with change, comes a whole host of challenges and difficulties. What services need to change? How do I ensure service up-time while we’re changing things? How will this affect downstream projects?

That last point – affecting downstream projects – is what we’re going to discuss today, and specifically how we handle the changes in your defined URLS, requests bodies, and JSON responses. Your services external API.

The problem

Lets assume you’re a developer who has two services – a biscuit service that does all things biscuit related, and a consuming service thats called “Tea” (though not overly important): Lets start with a generic request:

GET http://www.yourservice.com/biscuits/digestive

At this point, we’d expect a 200 OK as a response, with a body along the lines of:

{
    "guid":"123123-asdkfasd-8",
    "info": "Digestive - A versatile biscuit that can be used for cooking, dunking, or taking for long romantic walks on the beach. What I'm trying to say is; it's very dependable."
}

So far, pretty normal. But now you have a requirement where the info section needs to be broken down into name and description. So the future response would look something like this:

{
    "guid":"123123-asdkfasd-8",
    "name": "Digestive",
 "description" "A versatile biscuit that can be used for cooking, dunking, or taking for long romantic walks on the beach. What I'm trying to say is; it's very dependable."
}

Note that  info has now gone, and the data is spread across fields. This is what’s known as a breaking api change, which has some knock on consequences:

  • All consuming applications have to be ready for this change

  • Consuming services have to be deployed at the same time as the providing

  • Providing service is held up waiting for the consumers to change / prepare

  • One Big Bang change

It’s advisable to avoid a breaking change at all costs. Think of it like when a library changes an object to require new constructor arguments; it’s annoying and time consuming to fix. It’s best practise to provide an upgrade path where appropriate, as well as give time for consumers to adapt.

Solutions

So, how do we deal with a API change? There are 3 main ways of handing this.

Big Bang

The first one is one we’ve already discussed – just do a big bang change. This is a valid option for some services, as their traffic could be low enough, or the number of consuming services so small, that it’s easy to enable this. It still has the draw back of needing to orchestrate a number of services together into one release, but it’s conceptually simple. Though, you should also watch out for bugs in consuming services; they can’t be rolled back without rolling back everything.

This is the hammer approach to API changes, and is the very definition of deploying a breaking API change. But sometimes a hammer is appropriate.

Versioning

You could follow the approach of versioning your API. Lets take a look at an example:

GET http://www.yourservice.com/v2/biscuits/digestive

Which would then return your new body, while the original URL is still in use with the old body. Also, you don’t have to define the version in your URL; you could instead put in a header and subsequently return a 400 BAD REQUEST if a version isn’t specified (though I’d argue this conflicts with Postel’s law).

This has some real advantages; different services can use different versions, and consumers can slowly migrate to the new version, much like with libraries. It’s also quite simple to get your head around, and consuming services can roll back whenever they want to (as long as the previous version of the API is still available). This also allows for the owners of the providing team to define a “cut off” point – a time and date when a version of the API will stop working. This works very well when you don’t own the consuming service, such as Google and their Maps API.

The real flaw to this is the maintenance of code, extra documentation, and general overhead of having duplicates of URLs. You’ve now got more endpoints to look after, with their own backing code. You’re also at the mercy of consuming services and their speed of movement. Depending on how much leverage you have over your consuming services, you might find it very hard to deprecate and remove the v1 of your API.

Slow Migration

The idea behind slow migration is to add and remove fields from your API over time, so that you only ever have one URL, but the body of the responses changes over time. Lets take a look at some examples:

So, we have our URL:

GET http://www.yourservice.com/biscuits/digestive

With the response body of:

{
    "guid":"123123-asdkfasd-8",
    "info": "Digestive - A versatile biscuit that can be used for cooking, dunking, or taking for long romantic walks on the beach. What I'm trying to say is; it's very dependable."
}

From here, we now need to add in the new fields to the response:

{
    "guid":"123123-asdkfasd-8",
    "info": "Digestive - A versatile biscuit that can be used for cooking, dunking, or taking for long romantic walks on the beach. What I'm trying to say is; it's very dependable.",
    "name":"Digestive",
    "description" "A versatile biscuit that can be used for cooking, dunking, or taking for long romantic walks on the beach. What I'm trying to say is; it's very dependable."
}

So we have now duplicated data on the response body, but under different fields.

Consuming services can now start to slowly change over to using the new fields, but still have the freedom to revert back to the previous format in case of issues or bugs.

Once the provider is 100% certain that no consuming service is still using the info field, this can be removed.

No need for a version number to be sent and understood, or big bang releases, the consumers get to migrate at their own pace, and the produce has less code to worry about, as it’s “just” the formatting of data on the current endpoint.

This, of course, does make the assumption that consumers aren’t going to explode if new fields are added to an endpoint (they shouldn’t), and also assumes you have test coverage and visibility on what service is using what endpoint with what field. This is more complex and difficult to set up, but if you can manage it, it leads to much more elegant and clean code, as well as more simplistic yet versatile API’s.

When thinking about API testing and Slow Migration, it’s also a good idea to read about Consumer Driven Contracts (CDC). Rightmove invested in CDC when we made the jump to microservices a few years ago. Since then, they have stopped us from deploying breaking changes by accident on numerous occasions.

There is one large downside to this approach; slow migration doesn’t work for large, external facing API’s. You and your team have less control over how the API will be and is being used. Slowly migrating API’s requires conversation and tool facilitation, that isn’t always possible with external applications and customers.

Summary

Above are three ways of handling API migration. From a personal stand point, if you’re working with internal API’s and microservices, it makes much more sense to aim for the Slow Migration approach. You can communicate with teams directly who are using your services, as well as cut development time and increase flexibility doing it this way. However, the moment those API’s need to be accessible externally, I’d probably move to versioning of the API. I’d avoid big bang migration altogether, unless your system is simplistic enough that either of the above is overkill. Each situation is different, and requires thought.

While we’ve spoken about the above in a HTTP style, the concepts apply well to language and library API methods. When changing an internal object, you could slowly migrate the code over to using new fields on an Object, for example. This would enable you to commit, deploy, and release individual sections of changes, without needing to do it all in one go. Think about breaking library changes – this is API versioning at its core.

When you’re setting out to start building your microservices, think about how you’re going to manage this migration first. It’s much easier to set up a process at the start, than to retrofit one across teams.

Leave a Reply