API design fundamentals: do the evolution
The API versioning vs evolution is an old feud and you will find a good number of posts defending both sides of the argument. I will try to do the most objective analysis I can below, but it’s important to clarify that I too have already picked a side, which is made clear in the title.
So let’s start with the flamewarish quote, by yours truly:
Sticking a v1 in your API URL is like introducing your other half as your first spouse.
With that out of the way, shall we look into both approaches?
A quick recap: both approaches are about API providers introducing contract changes:
- in versioning, clients are notified of a new version and encouraged to point to it (most common pattern is to use the base URL, but query strings and headers can also be used). The old version is (hopefully) kept running in parallel for some time, to allow time for any required changes on consumers. Clients still running on an old version are broken once the deprecation period is over;
- in evolutionary design, clients are informed from day they should ignore fields they do not know (more here and here). That way, if a field gets introduced later in the process, nothing breaks;
Easy peasy, right? Not so much…
Remember I said I was not a fan of versioning? Here’s why:
- At the end of the day, every single client that gets left behind after a version is shut down will be broken, and this is quite bad customer experience;
- Statement #1 means you should maintain older versions of your API, at least for a while. Depending on your pace of change and deprecation window, this can mean maintaining multiple previous versions. This can ultimately cripple dev speed;
- Documentation gets more involved, especially if your API is successful enough to have high visibility, external blog posts describing its usage;
- If you are doing hypermedia APIs, versioning has to be taken into account and may be conflicting (eg: your orders endpoint pointing to customer v1 and your shipment endpoint pointing to v2);
Evolution is easier said than done
Having given versioning such a harsh treatment, a word of warning: don’t get sold short on how easy it is to evolve an API in a non breaking way. This post and this report call out a number of changes that may introduce breaks. Besides that, what are the catches?
- Your clients have implement a must ignore pattern. But quite frankly, not such a big deal these days;
- Things may feel awkward if you have to introduce versioning later on. And as we discuss later on, there’s a good chance this will eventually happen;
To optimize your chances in implementing a successful evolutionary pattern, there’s a handful of practices you can adopt.
I won’t go there again, but the very first one is to base your design on usage, not assumptions. This is so critical that it’s impossible to overstate. Practices such as dog fooding, early betas, etc are a great way for a team to explore new API grounds.
Second, in good old Darwinian style, the secret is to start small (and focusing on a handful of core use cases) and evolve gradually, only adding features that have proved, immediate, beneficial impact. In other words, you should work with minimal contracts and evolve them as iteratively as you can.
What if I have a breaking change?
Despite all arguments above and good practices you adopt, there’s a risk you will need to introduce a breaking change. The major causes for that are:
- API design mistakes: hey, happens to us all. As long as the mistakes are manageable and the team learns from them, this is just API business as usual.
- Shiny new tech: this is actually a case of “shit changes”, but we have seen enough in the past decades that de facto standards for APIs (and their surroundings, such as auth and formats) have a shelf life.
- Shit changes: no matter how rock solid your API is right now, there’s no guarantee it is future proof. A simple thing such as product realignment could mean the use cases you so diligently catered for have now been turned upside down. The only constant in life yadda-yadda…
The reason for the break is not as relevant as its breadth, as deprecating a single field is way less traumatic than completely revamping the API spec (say, a more from REST to GraphQL). While some of these breaks can be isolated (eg: GraphQL has built-in functionality to communicate field deprecation), some are big enough to require a major version bump. Ouch.
While this may sound like a nail on evolutionary design’s coffin, please notice that the three cases above should rarely happen, so you can buy your team years of easy evolution until the next breaking wave comes.