We often think that backward compatibility is hard, but actually it’s rather easy. Any project with users has a built-in mechanism for maintaining backward compatibility — people complain when it’s broken. That’s how you know. And, most of the time they let you know quickly.
The problem with this is that users can force unwanted backward compatibility on you.
Hyrum’s Law states:
With a sufficient number of users of an API,
it does not matter what you promise in the contract,
all observable behaviors of your system
will be depended on by somebody.
You might have noticed that in the first paragraph of this post, I didn’t mention APIs. That’s because backward compatibility is a force and a consideration in all kinds of systems — not all of them software. But, let’s stick with software for a moment. What should clients of an API depend upon? Ideally, they should only depend upon what we’d like them to depend upon. Interfaces should have a contract, either programmatic or documentary, which states the behavior clients should expect and what they should not count upon. Everything outside of that contract should be fair game.
I could offer a stock example of a REST API along with an associated contract, but let’s jump out of the box for a second and took at a very different example. Let’s imagine that we have a set of classes in an application and they have quite a few public methods. Some of these methods could be ones that we consider part of the API and others aren’t. They are public for convenience, or perhaps we are using a language without a module system — one where we can’t programmatically hide the methods from the interface and still make them available to other classes.
What does Hyrum’s Law say about this? Well, Hyrum would tell us that we should expect those non-API public methods to become immobilized. People will use them even if we tell them not to. We’ll no longer be able to change them without someone complaining. This can be very tough for systems evolution. The space behind a published API should be free space. We should be able to change that code to make it more maintainable or, really, for any other reason we like as long as we don’t violate the contract we offer. Think of the contract as a dependency that others have on our API and a dependency that the code implementing the API has on the API. We want to minimize dependency. What can we do?
One thing we can do is just decide not to play - make non-contract behavior undependable. On Twitter, Johnathan Hartley told a story about something he’d done to make sure that clients didn’t depend on the order of fields.
Nice, eh? And, simple.
He adopted this practice for new APIs rather than changing the existing ones, but with forgiving clients, and enough notice, moving to something like this with existing APIs might be acceptable.
Compatibility is a choice. We can decide how much we want to support. Microsoft famously attempted to maintain an extraordinary amount of backward compatibility in the early days of Windows, even to the point of keeping MS-DOS alive for decades. They did it to keep market share. At one point, Steve Jobs broke backward compatibility completely to give Apple OSes a fresh start.
Breaking compatibility forces work on the user but sometimes you need to when you are trying to a keep a product viable. It’s better not to have to break it at all. The way to avoid having to is to keep the contracts narrow, and find ways to prevent clients from expanding them beyond what is good for your software.