Breaking changes are always work for your users. Work you are forcing them to do when they upgrade to your new version. They took a dependency on your library or software because it saved them time but now it's costing them time.
Every breaking change is a reason for them to stop and reconsider their options. If your library is paid-for or the preferred way for paying users to access your services then lost users can come with a real financial cost.
Use breaking changes sparingly.
Good reasons for breaking changes
- A feature is being removed for business reasons
- Standards are changing how something works
- The feature did not work as intended and it's impossible to fix without a break
- Service or library dependencies you rely upon are forcing a change
- A small breaking change now prevents actual significant breaking changes later
Even when presented with these you should think not only about whether you can avoid a break but also take the opportunity to think about what you can do now to avoid similar breaks in the future.
Poor reasons for breaking changes
- It makes the internals of the library tidier
- Parameter order, method naming or property naming would be ”clearer”
- Consistency with other platforms or products
- Personal subjective interpretations of “better”
- Compatibility with a different library to attract new users
While many of these are admirable goals in of themselves they are not a reason to break your existing customers.
Managing breaking changes
It goes without saying that intentional breaking changes should only occur in major versions with the exception of security fixes that require users of your library take some action.
Here are some thoughts to ease the pain of breaking changes:
- List each breaking change in a migration guide with a before and after code fragment
- Summarize breaking changes in the README with a link to the migration guide for more information
- Keep breaking change count low even on major releases
Users should ideally also be able to find/replace or follow compiler errors. Consider:
- Platform-specific mechanisms for dealing with breaking changes. e.g. in C# you can use the
[Obsolete]attribute to help guide to the replacement API, Java has the
- Leaving a stub for the old method in place for one whole major release that calls the new method with the right arguments and produces a log warning pointing to the migration guide.
If a package is drastically different users will need to rewrite code. This is always a bigger deal for them than you expect, because:
- It is unscheduled and was not on their radar (no they are not monitoring your GitHub issue discussions)
- They use your library in ways you likely don't expect or anticipate
- The more they depend on your product then the more work your rewrite involves
Really consider whether your new API is actually better (ideally before you ship it). One way to do this is to produce a set of example usage code for using the old library vs the new library. Put them side-by-side and open them up to feedback. Is the new API genuinely better? Some indicators it might be are:
- Less code for users to write
- Plenty of sensible and safe defaults
- Less specialized terminology and concepts
- Easier to test and abstract
- Existing customers prefer the new syntax and think it's worth changing
Some indicators that it isn't really any better is: internal staff prefer it, it aligns better with some other platform or just being different.
Sometimes it's worth doing because it targets a different crowd or comes at the problem from a simpler direction or abstraction. If so, then seriously consider giving it a new package name and put it out for early access.
Make sure users are led to the right library of the two and if there is a lot of code duplication and users on the "old" library then consider making the next version of the old library a compatibility wrapper around the new library.