Backward compatibility, even inside a single project
Today we're talking about backwards compatibility, in the context of the applications you developed during your daily work.
To avoid growing to an unmanageable size, applications usually gain dependencies oriented towards:
- internal libraries or frameworks of your company.
- published open source (or closed source) software.
In any case, the application needs code that lives in a different version control repository.
Backwards compatibility needs to be taken into account whenever you're not acting on the leaf of the dependency graph but on all the other intermediate nodes. I'm both an open source developer as a proprietary one, so I have some tips that can be applied to both internal libraries and open source one.
Adding stuff without impacting compatibility is usually easy, with a potential issues for the internal consistency of the code. However, since the new API methods were not in place before, you have much freedom to add them as long as you do not clash with the existing ones. Just adding functions, methods, or URL is one of the safest strategies.
Even discounting the cost of maintaining all the old and new public facets, there are a few concerns however:
- good names: if they are in use, they cannot be recycled for the newer and better version of methods. Most programming languages mitigate this via method overloading or dynamic checks on the type of input parameters.
- consistency: if old methods all have that get*() prefix and you want to add a new one, you're compelled to name it getSomething() instead of something() or exportSomething(receiver), examples of other styles and structure.
The lifecycle of methods and functions is longer when exported publicly, since other projects cannot atomically switch to new names or versions but need an interim period in which they can use both.
Furthermore, if you're working with Feature Branching or Pull Requests, this applies even inside a single project (albeit with fortunately shorter cycle periods). If you remove methods from a master branch along with all references, you're still missing branches that were started from master before the removal, and that upon merge may break totally for the missing functionality they were relying on.
So I produced a little checklist to remove public methods and URLs that takes little effort but several days of elapsed time while you wait between steps.
1. Deprecate the functionality.
Mark the method with a @deprecate. If your language does not support the annotation, write an error_log('deprecated feature X') as the first line of the method implementation. I'm also considering an X-Deprecated HTTP header to signal an API that has started the removal process.
2. Wait for all branches to reenter in master via merging.
The waiting period for this should be reasonable, and it benefits from keeping user stories small and branches short-lived.
While you wait, new branches depart from the current master without the feature, and as such can be ignored in the waiting list above.
3. Actually remove the feature.
This process can take a lot, even several weeks. However, the time you actually spend is not much; you just need to wait and to remember to take the next steps a week from now.
Modifying the behavior of methods can often be done with compatibility, and this is part of good factoring: isolate a change inside an implementation while the rest of the world continue to send the same events to it. REST in its Hypermedia API One True incarnation tries do do this by hiding all URLs except the starting one.
However, when the contract between the client and the server breaks, it's much easier to avoid modifying the functionality and to remove it and add it with a different name by following the methods described above.
Usually you can also find more descriptive and fitting names for the new entities to add, since a lot of background processes in your designer's head have come to conclusion during the time you contemplate the original functionality.
The exception, of course, is the possibility of distinguishing between old and new clients by a version field. This however enlarges the surface area of the APIs, since the version must be passed in with other inputs (or it must be deduced from the current inputs with heuristics.)