4 min read
When talking to developers building HTTP APIs the subject of versioning comes up regularly. A quick web search will reveal hundreds of articles promoting guidance on the subject. Unfortunately, many of those “best practices” contain information that is contradictory. For example, some will say, always include a v1 in your URL in your first release. Others say this is a really bad idea. The challenge our team faced was how to build a version management capability that could help all developers, regardless of the approach they take.
We quickly found that supporting the different version identification mechanics is fairly easy; the real challenge is knowing when a change needs to be deployed as a new version and when it does not. We needed to support both breaking and non-breaking changes in a way that does not hinder developers. But what do we mean when we say that?
What is a breaking change?
It's almost impossible to have a conversation about versioning APIs for more than a few minutes without someone using the term breaking change. While everyone understands the consequences of a breaking change, trying to come to consensus on what is a breaking change is much harder to do. Most developers tends to carry their definition of breaking change over from their preferred programming language. Within that space, there is a consistent set of rules that define what will “break” an interface, usually defined by the compiler or runtime engine. However, HTTP APIs cross boundaries that prevent tooling from applying any set of standardized rules. Relying on the notion of breaking change, borrowed from the software component world, to help developers manage their HTTP APIs seemed problematic.
HTTP APIs are different
There are two primary reasons that HTTP APIs behave differently than component APIs:
- Client code dictates what will break it
- API Provider chooses if changes are opt-in or transparent
HTTP Clients control coupling
HTTP APIs enable calls between vastly different systems, with different operating systems, different, languages, different compilers, different libraries. An API provider has no control over the tools a consumer may use to interpret an API response. The tolerance for change those tools have also varies widely. When an API provider changes a HTTP response, which clients break depends entirely on the the client's choice of tooling. One of the reasons that JSON has largely replaced XML in recent years, is because most of the tooling in the JSON space takes a very tolerant approach to data structure changes, especially compared to XML tooling. This indiscriminate tolerance does come at a performance cost though, which is one reason we are seeing some movement back to binary formats like Protobuf, Thrift and Bond, in performance sensitive scenarios.
API Providers control timing
Reusable software components are generally distributed via package managers. Clients that consume software components have to explicitly opt-into a new version of a component, even if it is identified as a patch update. With HTTP APIs it doesn't have to be that way. API providers can update their API and changes to responses will immediately impact client consumers. However, by updating some kind of version identifier in the expected request message, such as a version number in a path segment, an API provider can allow clients to opt-in to the change when they are ready. Azure API Management enables you to take an existing API definition and create a new Version of it. The new Version will be identified in the request message via an identifier in the path, query string or HTTP header.
If we accept that every client, potentially, has a different tolerance for change, does that mean that every change we deploy must be opt-in to avoid breaking clients? The answer is no. The key is to explicitly set expectations with the API consumers.
Consumers need to know what types of API changes they will be able to opt-in to and what types of changes they will need to adapt to transparently. For example, most APIs expect to be able to add a new property to a JSON object without a client failing to be able to parse the object. The expectation is that the client will simply ignore that new property, or perhaps round-trip the value. But this expectation is built around the default behavior of commonly used tooling. It needs to be explicitly documented in order to ensure long term inter-operability.
Revisions enable transparent updates
In order to manage the deployment of transparent updates, Azure API Management is introducing a second capability, distinct from the ability to create Versions of API definitions. Now, you can create a Revision of an API definition and change that independently. All APIs and Versions of APIs defined in Azure API Management can have a set of Revisions. The key distinction is that only one Revision is considered current and all share the same public URL(and version identifer if used). Marking a new Revision as current provides the ability to deploy changes to consumers transparent. Rolling back to a previous Revision is as simple as marking it as current again.
To enable API publishers to test and debug non-current Revisions, a special matrix parameter can be used in the URL. For example, if an API had two revisions and the rev=1 was considered the current Revision, then rev=2 could be tested using the following URL:
A different perspective on change
Managing change in HTTP APIs will never be easy. However, by taking a different perspective on change, the Azure API Management Versions and Revisions feature makes managing change simpler for developers of all perspectives. By being explicit about the delivery mechanism for different types of changes, API consumers can be confident in deploying clients that will not break, and API providers can be confident in making changes that will not break their clients.