When you're working on a product that is constantly evolving, adding, deprecating, and deleting features not only has to be communicated with your customers in advance, but often you have to maintain multiple versions.
While you should never have to maintain more than a handful of versions of a specific feature at a given point in time, it's very common to introduce a breaking change to a feature, deprecate the old version, and give your users the time to check it out and upgrade their system if need be, and at a well-known time in the future, remove the old version altogether.
Especially when dealing with software your customers are running in production, you simply cannot introduce breaking changes without prior notice and a clear path forward, if you don't want to risk churn.
Before we get to the different ways to think about your versions, let's think about how we version the actual product. In theory, every feature that is expected to change should have an assigned version the customer is running on. In multi-tenant deployments which host your product for more than one customer, this is required as different customers may run different versions of a feature if they have not upgraded yet. If you are exclusively operating dedicated infrastructure per customer, every deployment could be running on a specific version.
As long as you carefully version all features, be it APIs, routes, or other parts of your system that might change in future iterations and think about strategies to upgrade users to a new version if possible, you should be prepared for the long run. Having processes in place and communicating changes and planned deprecations or removals of used features clearly will make a huge difference, and your customers will notice the effort.
Let's continue with conventions for versioning itself.
In addition to semantic versioning (using a major, minor, and patch version such as 2.1.0), a recent trend has been to use time-based versions, such as 2021.5, which can be directly linked to the product release in May, when following a monthly release schedule. This makes it less artificial and closely related to product development and your roadmap.
A new version should be declared for a feature only when it is the initial version or you add breaking changes to how it works. Simply extending the feature with non-breaking additions is not considered breaking in contrast to renaming options or removing functionality.
This will leave you with clear procedures to follow when performing a change, force you to think whether it will break the experience of your customers, and help you to continue developing your product within boundaries both product management and customer relations can accept.
As an example, Stripe documented and wrote about how they manage their APIs versions, how they introduce new and backward-compatible changes, and how they manage incompatible changes without breaking their customers' experience.
Dated versions are only released if a change is considered breaking, and existing users are not immediately forced to use the new version, instead, they can check for changes they might have to adapt their systems to, and can perform an upgrade at their discretion, also being able to roll back within 72 hours, which retries failed webhooks with the old structure.
Instead of versioning each feature, the complete API is fixed to a specific version for each user. When someone has not upgraded to the newest version, requests are first computed with the newest possible version and then transformed to match the older version. Sometimes, a new version cannot be converted to the old representation.
Shopify does it similarly: New stable versions are released every three months at the beginning of the quarter and removes support for each version a year after releasing it. Users thus have a grace period to update their applications, and will otherwise receive an API response using the closest-available version. Deprecations added in new versions are applied to previous ones retroactively, but users still have the same period to upgrade their applications.
As you can see, there's a lot to watch out for when designing your services to be future-proof but investing in proper versioning makes it much easier to build for the future as early on as possible.