The Deprecation That Never Ends
Deprecation is supposed to be a process with an end: mark the old thing deprecated, give consumers time to migrate, then remove it. In practice, most deprecations stop after the first step. The annotation goes on, the changelog notes it, the docs get a warning banner — and then the old thing keeps running, year after year, because removing it requires knowing who still depends on it and getting them to move, and neither of those is as easy as adding the label. The deprecation notice becomes a permanent fixture rather than a countdown.
The cost of this is that you now maintain both versions indefinitely. Every bug fix has to be evaluated against the old path as well as the new one. Every new developer has to learn which is which and why both exist. Every future change has to preserve compatibility with an interface nobody wants and everybody is technically still allowed to use. The deprecated thing was supposed to be temporary debt with a payoff date, and instead it became a permanent tax — and the tax gets larger with each deprecation that joins it, because they compound.
What separates a real deprecation from a decorative one is a removal date that someone is accountable for. Not “deprecated, will be removed in a future version” — that’s a wish, and it never comes true. A specific date, announced up front, with the removal actually happening on it. This is uncomfortable because it forces a real conversation with consumers who haven’t migrated, and that discomfort is precisely why the date works: the deadline is what converts “I’ll get to it” into “I have to get to it,” and without one, migration is always someone’s lowest priority. The date has to be real, though — a deadline that slips twice teaches everyone that deadlines slip, and then you’re back to the wish.
The other prerequisite is knowing who actually depends on the thing. Deprecations stall because nobody can answer “what breaks if I delete this?”, and in the absence of an answer the safe move is always to leave it. This is an argument for instrumenting interfaces before deprecating them: log which consumers call which endpoints, which fields get read, which parameters get passed. When you can see that the deprecated endpoint gets four calls a day from one internal service, removal becomes a conversation with one team instead of an unbounded risk. The visibility is what makes the deadline enforceable, because you can tell exactly who needs to hear about it.
None of this contradicts the discipline of keeping interfaces stable — it completes it. Compatibility means you don’t break consumers without warning; it doesn’t mean you carry every past decision forever. An interface that can never shed anything only accumulates, and an interface that only accumulates eventually collapses under the weight of its own history. The promise worth making to consumers isn’t “nothing will ever be removed.” It’s “nothing will be removed without a clear warning and enough time to move” — and then keeping the second half of that promise, on the date you said.