API versioning needs actual semantics
There’s this Doc Hammer sketch about those crappy, low-motion marvel cartoons from the 90s – not the awesome saturday morning X-men but some under-produced monstrosities. His takeaway is profound dissonance. ‘I should be liking this because it’s marvel, right? But it’s really bad’.
That’s how I feel about semantic versioning as implemented in semver. It’s something that uses a familiar and beloved brand (typed interfaces) to advertise something much shittier.
At best, semver encodes semantic expectations and breaking changes into a number. It doesn’t say what changed or let me upgrade worry-free if the breaking change is in a method I don’t consume.
What should semantic versioning mean
At minimum it should declare a typed interface (method names and argument types). But we can do better.
Let’s also declare actual semantics of methods – natural language descriptions of properties that API routes are expected to have. For example:
- idempotent
- reversible (on a delete route)
- logs-username
- works-with-impersonate
- hipaa-compliant
These ‘properties’ should be verified in the API’s test suite, asserted in client code, and shared in some kind of manifest.
This improves docs as well – there are a ton of important properties that are asserted in test suites but are difficult to communicate to API consumers.
Operational details
I imagine provider codebases (the API / library) generating a manifest in CI and bundling it with client libraries.
A codebase consuming the API can declare, at the callsite for an API method, which semantics it expects from that method. We can come up with a list of compatible API versions via static verification (or at startup in dynamic languages).
The goal is maintainability and correctness
These ‘semantics’ aren’t quite the same as programmatic proofs like e.g. coq would provide. That said, the goal is to let API providers express their intent and verify it via the test suite.
I think this would save a lot of misunderstandings, improve docs, and make it easier to safely make changes.
The postgres community had an incident last year they’re calling fsyncgate:
When fsync() returns success it means “all writes since the last fsync have hit disk” but we assume it means “all writes since the last SUCCESSFUL fsync have hit disk”.
Declaring and asserting these expectations with semantics is a step towards preventing this kind of thing.