Skip to content

Instantly share code, notes, and snippets.

@laughinghan
Last active January 6, 2024 07:22
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save laughinghan/009e300626f328792815 to your computer and use it in GitHub Desktop.
Save laughinghan/009e300626f328792815 to your computer and use it in GitHub Desktop.
Interface Versioning - Never break backcompat, keep the API nimble

Interface Versioning (InterVer)

Never break backcompat, keep the API nimble

An extension of SemVer with a stricter (yet more realistic) backcompat guarantee, that provides more flexibility to change the API, for libraries that are packaged and downloaded (not services accessed remotely over the Internet (see Note 4)).

Description

  1. API consumers must specify an "interface version" before any other use of the API.

  2. Each release of your library, in addition to a traditional version number MAJOR.MINOR.PATCH, has a range of "supported" interface versions. Fail as early as possible if the consumer specifies an interface version outside this range.

  3. When you want to make a breaking change to the API, instead increment the max supported interface version, and only behave differently when the consumer specifies the new interface version, with one exception (Description part 5.3, below).

  4. SemVer is easy when times are good:

    1. increment MAJOR when you drop support for an interface version
    2. increment MINOR when you add features
    3. increment PATCH for other changes
  5. But SemVer is unhelpful when (not if) you discover a bug that's observable to API clients, that they might be relying on (maybe the workaround breaks if the bug is fixed). NeVer's guiding dictum: for a given interface version, every feature in the latest release should be backwards-compatible with the first release of that feature for this interface version.

    1. So, if you accidentally break the API and hence this dictum, fix it in a new release with an incremented PATCH.
    2. However, if the feature was broken (observably) in the release that introduced it, don't fix it for that interface version! The feature should remain bug-compatible for that interface version, instead you should fix the bug in a new interface version, as per Description part 3 (above).
    3. This dictum has an exception: if there is a major bug that no API consumer could be relying on (security vulnerability, for example), you may make the minimal change to the API to fix the bug in existing interface versions and release it with an incremented MINOR version (and deprecate the bad one).

Notes

  1. Strict, well-defined backcompat expectation:
    The main point of all this is that if an API consumer writes working code against your library, they have an expectation with well-defined limitations that they can upgrade to any future release (of the same MAJOR version) and the integration will continue working identically, without making any changes to their code or even reading any changelogs. If it doesn't and there was no earth-shattering vulnerability that had to be patched backwards-incompatibly, then it's categorically, unambiguously a bug in your library that they can expect fixed.

  2. Forward-compatibility:
    Of course, for most libraries, API consumers have an even stronger guarantee that the integration will continue working identically if they simply don't upgrade. What NeVer lets API consumers do that's otherwise impossible is safely upgrade to get the latest optimizations and even the latest new backwards-compatible features, without being forced to update any existing calls to your library.
    (In fact, if you allow API consumers to override the effective interface version on a granular, per-feature basis, API consumers can even use new backwards-incompatible features without updating their use of other features of your library.)

  3. The problem with SemVer:
    To me, the fundamental problem with SemVer is that it's unrealistic to require all releases (of a given MAJOR version) to be perfectly backwards-compatible. What do you do about the inevitable observable bug? The SemVer FAQ has 2 mutually exclusive answers, either fix it in a new minor release (violating backcompat with the broken release), or fix it in a new major release (letting the last release of the old major version break backcompat from all previous releases of that major version): "Use your best judgement" (see below).
    By contrast, NeVer has a clear answer: if the bug was a regression, fix it; if the bug was there from the beginning, it's now a misfeature, fix it only for a new interface version.

  4. This is all sounds great, but why only libraries not services?
    Services are way easier! There's no notion of immutable snapshots the way we expect releases of a library to be immutable, your only version number is the interface version, which you should definitely preserve backcompat for. But if you ever accidentally break the API, you just fix it! No worries about compat between releases for the same interface version.
    NeVer basically applies that blogpost to libraries with the conceit that being on the latest release of a library is kinda like remotely accessing a service, in that you can promise that there will generally not be backcompat bugs with the understanding that you can't guarantee there are never ever any bugs.

Inspiration

EMILY (programming language project homepage) by @mcclure Andi McClure

\version 0.2 This feature is small, but I believes it solves a somewhat fundamental problem with programming languages. Each Emily program is encouraged to start with a line identifying the language version it was developed against. When an Emily interpreter---current or future---encounters the \version line, it makes a decision about whether that code can be run or not. If the hosting interpreter is backward-compatible with the code's version, it just runs. But if backward-incompatible changes have been made to the language since then, it will either enter a compatibility mode or politely refuse to run. At some point, it will be possible to install these compatibility modes as pluggable modules.

I write a lot of Python, and a huge running problem is compatibility between versions. In Python, as in most programming languages, the implementation version is the same as the language version. Python 2.4 runs Python 2.4, Python 2.7 runs Python 2.7, Python 3.1 runs Python 3.1, etc. Meanwhile Python 2.7 can run Python 2.4 code, but Python 3.1 can't run Python 2.7 code, which means Python is competing with itself and nobody uses Python 3 because all the code's written for 2.7. (And even before the big 3.0 switch, forward compatibility created a huge problem all by itself: If you had a program that used a feature from 2.5, but what you had installed was 2.4, you wouldn't know it until you tried to run it and something would break strangely, possibly at runtime.)

This is all silly! Language versions define interfaces, and interpreters are engines. We shouldn't be holding back on upgrading our engines because the interface changed (and if there's some reason a new engine can't handle the old interface, it should at least fail very early). It's generally possible at least in principle to convert between these interfaces, so it should be possible to install something that does conversion for an incompatible past interface (probably even a future one!). It should be possible to mix code written against different interfaces in the same program---maybe even the same file. There's surely a point at which this becomes untenable (library cross compatibility probably gets awkward quick), but language implementors not being able to get updates adopted because nobody wants to lose back compatibility with 15-year-old versions doesn't sound very tenable either.

Anyway, for now: Just tag each file with its version, and all this becomes a lot easier to sort out later.

http://emilylang.org#markdown-header-what-else

MOVE FAST, DON'T BREAK YOUR API (blogpost) by @amfeng Amber Feng

When a user starts implementing Stripe for the first time they don't need to worry about API versions. Instead it's invisible---they'll innocently make their first API request, we'll record what internal version they're on, and from then on our code takes care of making sure we never break their integration.

http://amberonrails.com/move-fast-dont-break-your-api/

CONFIGURATION VERSION (Vagrant docs)

Configuration versions are the mechanism by which Vagrant 1.1+ is able to remain backwards compatible with Vagrant 1.0.x Vagrantfiles, while introducing dramatically new features and configuration options.

http://docs.vagrantup.com/v2/vagrantfile/version.html

Problems with SemVer

Semantic Versioning 2.0.0 by @mojombo Tom Preston-Werner

What do I do if I accidentally release a backwards incompatible change as a minor version?

As soon as you realize that you've broken the Semantic Versioning spec, fix the problem and release a new minor version that corrects the problem and restores backwards compatibility. Even under this circumstance, it is unacceptable to modify versioned releases. If it's appropriate, document the offending version and inform your users of the problem so that they are aware of the offending version.

[...]

What if I inadvertently alter the public API in a way that is not compliant with the version number change (i.e. the code incorrectly introduces a major breaking change in a patch release)

Use your best judgment. If you have a huge audience that will be drastically impacted by changing the behavior back to what the public API intended, then it may be best to perform a major version release, even though the fix could strictly be considered a patch release. Remember, Semantic Versioning is all about conveying meaning by how the version number changes. If these changes are important to your users, use the version number to inform them.

http://semver.org/spec/v2.0.0.html

Why Semantic Versioning Isn't aka Romantic Versioning by @jashkenas Jeremy Ashkenas

If you pretend like SemVer is going to save you from ever having to deal with a breaking change — you're going to be disappointed. It's better to keep version numbers that reflect the real state and progress of a project, use descriptive changelogs to mark and annotate changes in behavior as they occur, avoid creating breaking changes in the first place whenever possible, and responsibly update your dependencies instead of blindly doing so.

https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e

"ferver": Fear-Driven Versioning by @jonathonong Jonathon Ong

  • No patch version bumps are allowed for any changes that have a possibility of being a breaking change.
  • "minor" breaking changes are allowed in minor version bumps.

[...]

Disclaimer: this is just an idea. Please don't actually use this.

https://github.com/jonathanong/ferver#readme

@jwmerrill
Copy link

In fact, if you allow API consumers to override the effective interface version on a granular, per-feature basis, API consumers can even use new backwards-incompatible features without updating their use of other features of your library.

Is this a part of the concrete proposal, or a speculative extension? What would it actually look like?

@laughinghan
Copy link
Author

It's a speculative extension but I don't think it would be that crazy.

Usage:

MathQuill.interfaceVersion(1, {
  handlerArgs: 2
})

Implementation:

var INTERFACE_VERSIONS = {}; // this needs to be global within the module
MathQuill.interfaceVersion = function(v, overrides) {
  'handlerArgs otherVersionedFeature'.split(' ').forEach(function(feature) {
    INTERFACE_VERSIONS[feature] = overrides[feature] || v;
  });
};

// ...

if (INTERFACE_VERSIONS.handlerArgs < 2) {
  // ...
}
else {
  // ...
}

// ...

if (INTERFACE_VERSIONS.otherVersionedFeature < 3) {
  // ...
}
else {
  // ...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment