The moment someone else writes code against our API, we have made a promise — and it is enforced by software we cannot see, maintained by people we will never meet.
So the first question is whether a change needs a version at all. A new optional field, a new endpoint, a new enum value: existing clients carry on untouched. Removing a field, renaming anything, tightening a validation, changing a default: those break callers, often without us realising.
When a version is genuinely needed, the strategies trade differently. A version in the URL path is obvious and cacheable and gets ugly at deeper paths. A header keeps URLs clean and hides the version from logs, browsers and some tools. A date-based version pins each client to the day it integrated, which is fine-grained and hard for humans to remember.
The mechanism is the easy part. v1 does not retire when v2 ships: both need bug fixes, security patches, documentation, SDKs and tests kept in sync, and some clients will never move at all.
Versioning is the cost we accept for having promised an interface, rather than the way we change one.
