How we handle breaking changes in Whoz API thanks to Spring Cloud Gateway

Thomas Martin
WhozApp
Published in
6 min readJun 3, 2021
Deprecating leaves - source: Chris Lawton on Unsplash

At Whoz, like most SaaS services, we expose an API so our clients can import or export their data and interconnect all their software. And at Whoz, like most API providers, we had to face the difficulty of API versioning and managing breaking changes.

In this article, I’ll present the solution we came up with, based on Spring Cloud Gateway and a bit of our self-cooked sauce. Of course, it has its limitations and has been subject to a lot of internal debates, but this is the best way we found, knowing that at the end of the day API versioning has no “right” way.

API versioning really?

“Why ?” is always a good question

Before getting into the details, you could legitimately wonder why API versions are needed? Until a few months ago, we were dealing with breaking changes without any versioning. Breaking changes were avoided unless absolutely needed, and when they were, the deprecated properties were kept with some in-app code to maintain them by translating the new properties into the old ones. Because our API is consumed by customer clients but also and mainly our front-end app, things were getting tricky.

Developers were not happy with all this backward compatibility code mixed with the new shiny code of their latest feature. Product owners were not happy with developers telling them that their awesome feature is “impossible” or costs three times the usual cost. Customers would not have been happy if the app was getting slower because payload sizes were always increasing because of deprecated fields. Basically, technical debt was increasing without much we could do about it without versioning.

Design goals

Once we decided to implement an API versioning system, we settled on several goals to reach.

  • Keep It Simple
  • Keep the business code free from backward compatibility considerations
  • Handle all the back-end microservices
  • Be seamless for current API customers
  • Have a “private” API that is subject to breaking changes every other week (we work in two-weeks-long sprints)

We also had issues to tackle about monitoring deprecated versions usage, documentation, release cycle, or communication but that’s for another story.

The “Accept-Version” header

Clients choose the API version thanks to the Accept-Version header.

It is assumed to mean “V1” version when no header is passed. That way, clients implemented prior to versioning still work.

To avoid splitting our API into a private and a public one, increasing the maintaining cost, we use a special API version that is unstable and is called latest. latest is translated by the versioning system into the next version.

For instance, if the current stable version is “V3”, we have the following API versions:

  • V1 (default if no Accept-Version header)
  • V2
  • V3
  • V4 (unstable, also targetted if Accept-Version="latest")

Note that “unstable” here does not mean beta, it is a fully functioning version used by the front-end. However, it may introduce breaking changes without notice.

When the release date of V4 is reached, we simply need to update the documentation and introduce a V5 API version into the system, becoming de facto the new unstable version.

Successive transformations

Step by step — source: my Instagram

The general idea of the versioning system is to transform requests and responses from one version to the next one, reaching step by step the latest API version which is the current state of the back-end services.

That means that we only have to transform payloads from the last stable version to the unstable one when introducing breaking changes.

Transformations are handled by Spring beans implementing the VersionTransformer interface, which the Kotlin code is:

The first two methods are the obvious transformation methods for requests and responses.

The last two methods will be called by the ApiVersioningManager to determine if the transformer must be applied or not. The accept()method is a condition to apply the transformer, typically on the beginning of the path which indicates the business object managed by the endpoint. The sourceVersionNumber() method defines which API version the input of the transformer uses. Since transformers handle only the transformations to the next version, it is unnecessary to specify the target version. The same is true for the name of the transformer classes. For instance, the TaskV2Transformer handles call to all endpoints under the path /api/task with a V2 API version.

Note that the TaskV2Transformer would also be called in the case of a request with Accept-Version="V1" because the transformers are called successively. Let’s pretend that there is a breaking change on task endpoints between V2 and V3, and another one between V3 and V4, the transformers would be applied in that order by the ApiVersionManager:

  1. TaskV2Transformer.transformRequestObjectNode()
  2. TaskV3Transformer.transformRequestObjectNode()
  3. The back-end service
  4. TaskV3Transformer.transformResponseObjectNode()
  5. TaskV2Transformer.transformResponseObjectNode()
TaskV1Transformer does not exist because there is no breaking change between V1 and V2

For now, this covers all our use cases.

The accept() method is quite generic and future-proof, allowing other eligibility criteria, such as payload patterns or precise endpoint names. Breaking changes are thankfully sparse, so one transformer for each version and business object is manageable, but the interface is generic enough to have one transformer per modified field.

One more questionable design choice is the fact that transformers handle request and response transformation. What about read-only fields? Just do nothing in the request part? As long as we don’t encounter such issues, we’ll keep the interface as is for simplicity's sake, but I can sense we’ll have to change it sooner or later.

The gateway

Gateway — source: Laila Gebhard on Unsplash

We naturally have a gateway between clients and back-end services to intercept the requests and responses. Historically, that gateway was based on Zuul. Since then, Spring has released the great Spring Cloud Gateway and has deprecated Zuul. Our stack is based on Spring Boot, so we decided to port the gateway to that new technology.

Like Zuul, Spring Cloud Gateway allows creating filters to be applied pre and post-backend calls. There are two filters to handle the transformations : ApiVersioningInput and ApiVersioningOutput.

In the following groovy code, you can see that we configure a ModifyRequestBodyGatewayFilterFactoryto call our ApiVersioningManager into a Mono, because Spring Cloud Gateway is based on Spring Web Flux that is itself built upon the Reactor reactive library. This is right for our API, though I’m not sure a Monowould work in the case of web sockets.

In the configuration, the list of filters will have an automatically assigned order (1 for the first one, 2 for the second, etc.). The filters will be applied in ascending order in the request processing, and in descending order in the response processing. We have this configuration:

spring:
cloud:
gateway:
default-filters:
- SomethingAuthRelated
- SomethingUnrelatedToVersioning
- APIVersioningInput
- APIVersioningOutput

This works well for the request part, but not the response part. Indeed, the filter writing the response has the order, and we need to transform the payload before that. To do so, we set the order ourselves with an OrderedGatewayFilter.

Transformers

Sorry but I had to :) — source: Netflix

Interface of VersionTransformer is fairly simple, so are their implementations. As I write those lines, all they do is manipulating JSON payloads thanks to Jackson'sObjectNode and its subclasses.

We don’t do serialization to POJOs because:

  1. that would become painful to maintain to have a different POJO for each version of a business object
  2. the less overhead the better. Keep in mind that the transformers are part of a gateway that is passed through at least twice for each API call, so we want them to be as fast as possible.

For example, look at the code to rename the aphotecary property to drugstore.

We have not needed anything more than JSON transformations for the first few months. We’ll probably have to do trickier things in the future like endpoint splitting, but for as long as this simple design will work, we’ll keep it simple.

API versioning can be headache-inducing. I hope you’ve enjoyed reading our adventure with tackling it as much as I enjoyed writing it and that it will be helpful for your own journey. And if you already have gotten through this, feel free to share your experience too!

--

--