Versioning doesn't make it any easier to manage change in APIs
When you publish an API you need to manage change whilst providing a stable contract to clients. After all, an API consumer has a right to use your service without having to deal with breaking change that undermines their code.
Engineers often adopt versioning strategies in the mistaken belief that it will help to make change easier to manage. When you create a new version of a contract it can take some time for all the consumers to switch to a new version. Even if you ruthlessly deprecate versions and force consumers to upgrade, there will inevitably be a period where you have to support more than one version of the contract.
This means that you need a solution for running both the old and new versions of the contract in production. Do you run multiple versions of the application, or do you implement mapping logic between different versions of the contracts? This can greatly complicate understanding, testing, and maintaining a system. Brandon Byers summed this up by borrowing Jamie Zawinski's dig at regular expressions:
Some people, when confronted with a problem, think "I know, I'll use versioning." Now they have 2.1.0 problems.
Whatever approach you take, versioning will lead to a more complicated solution without doing anything at all to help you manage change. You still have to deal with the complication of getting consumers to change their implementations. Aligning roadmaps within an enterprise is hard enough, but managing external consumers is harder still. In many cases, an external integration could be something you will be expected to support indefinitely.
Managing breaking change
What really matters here is how you handle breaking change. If you are adding new mandatory fields, or removing some existing parts of a data structure, is it really still the same API? When an interface changes to the extent that it’s no longer compatible with existing clients, then perhaps you are creating a completely new resource or service as opposed to a version of the original one.
Versioning is often a lazy response to change. Creating a new version saves you from the inconvenience of having to figure out how to implement change without disrupting existing integrations. It bakes in obsolescence to API contracts and implies that change can be managed purely by incrementing a version number.
API contracts require a mature and carefully managed process of change management. It is surprisingly easy for engineers to check in seemingly innocuous changes to APIs, only to have them cause mayhem once they reach production. Note that semantic changes in the meaning of existing fields can be just as disruptive as changes to their structure.
Alternatives to versioning
Some contracts try to anticipate change by implementing flexible contracts based on more generic data structures. This includes wildcards fields or key\value pair properties that seek to enable dome degree of “forwards compatibility” in the contract.
The problem with this approach is that it creates a weak and indistinct contract. It places a burden on the consumer to understand what all the various switches and settings can mean. It doesn’t even work a lot of the time, as it’s almost impossible to draw up a structure that can anticipate any future changes.
Bertrand Meyer’s Open\Closed principle suggests that software entities should be “open for extension, but closed for modification”. When applied to API contracts the implication is that you can augment your resources but not change them.
This approach could offer the certainty of stricter versioning without the regression risks involved in backwards compatibility. Augmentation is not without its problems though as it can give rise to bloated contracts. Without careful discipline an API can become littered with duplicate methods or resources that provide several slightly different ways of achieving the same thing.
A shared responsibility
You can make change management more of a shared responsibility between clients and consumers. Postel's law, often referred to as the Robustness principle, states that you should be “liberal in what you accept and conservative in what you send”. In terms of APIs this implies a certain tolerance in consuming services.
For example, strict serialization techniques can be unnecessarily intolerant of change. A more tolerant reader should only be concerned with data that it needs and ignore every other part of the response. This means that the majority of changes are unlikely to break the integration.
Another approach could be for the consumer to declare the data they are interested in as part of a request. This consumer-driven contract pattern does not specify the form that these consumer assertions should take, but an implementation could allow an API to detect when a request is out of date.
Unfortunately, these approaches can only be applied to relatively closed communities of services. Public-facing APIs rarely have the luxury of being able to dictate the style of client integration. The only enforceable contract you have between service and client is made up of the data and protocol.
This is why careful discipline is at the heart of any sensible change strategy. A good API doesn’t come into being by accident. It has to be curated. Versioning is no substitute for consistent and disciplined governance over the evolving contract.