The Contract Is What They Rely On
Every interface has two contracts. There’s the one you wrote down — the documented signature, the stated guarantees, the semantics you intended. And there’s the one that actually governs, which is whatever your users have come to depend on, documented or not. The gap between them is where surprising breakage lives: you change something you never promised, well within your stated rights, and things break anyway, because the behavior you considered incidental was load-bearing for someone downstream. The rule is uncomfortable but reliable — with enough users, every observable behavior of your system is depended upon by somebody.
This happens because people build against what they observe, not what they read. They call the function, look at what comes back, and write code that works with that. If the results happen to come back sorted, code gets written that assumes sorted. If the error message happens to contain an ID, something parses it out. If an operation happens to be fast enough to call in a loop, it gets called in a loop. None of that was promised, but all of it now exists, and the people who wrote it weren’t being careless — they were doing the ordinary thing, which is to use what’s in front of them. Documentation describes intent; behavior teaches expectations, and behavior wins.
Understanding this changes what “breaking change” means. It’s not defined by whether you violated a documented guarantee — it’s defined by whether working code stopped working. Those overlap but aren’t the same, and the second one is what your users experience. You can be entirely correct that a behavior was never part of the contract and still have broken production for people who relied on it. Being right about the documentation is not much comfort to someone whose system is down, and it doesn’t change the practical fact that you shipped something that broke things.
The useful response isn’t to promise everything forever — that would freeze the system solid. It’s to be deliberate about what you expose in the first place, since anything observable becomes a de facto commitment. Return the minimum that serves the purpose. Don’t leak internal identifiers, ordering, or timing that you don’t intend to keep stable. Where a detail is genuinely incidental, consider making it visibly unstable rather than accidentally consistent — randomizing an unspecified order, for example, teaches the true contract far more effectively than a sentence in the docs that says order is not guaranteed. What you don’t reveal, no one can depend on.
And when you do need to change something people have come to rely on, treat it as the migration it actually is rather than a correction they should have anticipated. Find out who depends on it, give notice proportional to the disruption, provide a path forward, and change it once rather than repeatedly. That’s more work than declaring the behavior undocumented and moving on, but it’s the work that maintaining an interface consists of. The contract isn’t what you wrote down. It’s what would break if you changed it — and knowing the difference is most of what it takes to evolve a system people depend on without betraying them.