When you build an interface — an API, a message format, a file schema, a function signature that other teams call — you’re making a promise, whether or not you meant to. The promise is that the interface will keep working the way the people who depend on it expect. As soon as one other system starts consuming it, the interface has left your sole control: you can still change it, but changing it now has consequences on the other side of a boundary you can’t see, and those consequences are the whole reason backward compatibility is hard.

The trap is that breaking changes often don’t look like breaks from the inside. You rename a field to something clearer, tighten a validation rule that was too loose, change a default that was always a little wrong, remove a parameter nobody should have been using. Each of these is an improvement from where you sit, and each of them is a break from where a consumer sits — their code sends the old field, relies on the old leniency, expects the old default, passes the old parameter. The change that reads as cleanup in your repository reads as an outage in theirs, and the gap between those two readings is exactly the space where compatibility gets broken by accident.

The discipline that prevents this is to treat additive changes and breaking changes as fundamentally different operations. Adding a new optional field, a new endpoint, a new parameter with a safe default — these extend the interface without invalidating anything that already works, and they’re safe to ship freely. Removing or renaming a field, making an optional thing required, changing the meaning of an existing value — these invalidate existing usage, and they can’t be shipped the same way. The distinction isn’t about how big the change is; it’s about whether anything that worked before stops working after. A one-character rename is a breaking change; a whole new subsystem behind a new endpoint is not.

When a breaking change is genuinely necessary, the way to make it safely is to not make it in place. Introduce the new version alongside the old one, run both in parallel, migrate consumers over on their own schedule, and remove the old version only once nothing depends on it anymore. This is more work than editing the interface directly, and the extra work is the cost of the promise you made when the first consumer showed up. Versioned endpoints, new fields alongside deprecated ones, a deprecation period with a real deadline and real communication — these are the mechanisms that let an interface evolve without breaking the systems built on top of it.

The deeper point is that an interface’s value comes precisely from being depended on, and being depended on is what makes it expensive to change. This is not a problem to be solved so much as a tension to be managed: the more useful an interface becomes, the more consumers it accumulates, and the more its stability matters. The teams that manage this well aren’t the ones who never change their interfaces — it’s the ones who internalized early that the interface stopped being theirs the moment someone else built on it, and who treat every change through the eyes of the consumer who can’t see it coming.