Breaking changes is a result of a code changes, that how the API behaves. It can be low level code API, REST API, any microservice API or even user interface behavior.
Breaking changes can be of two types: structural and semantic.
Structural Breaking Changes
Structural changes appears when the structure (contract) of an API changed. Changes can be in any code structural element. This is the list of structural breaking changes groups:
- remove parameter
- add parameter
- change parameter type
- change return type
- add required constructor
- make attribute required
- add type required restriction (also called generic)
Let's explore a lodash
popular utils library in JavaScript ecosystem. In a changelog
we can find breaking changes in a version 3.0.0. When semver major version changes, it indicates, that API contains breaking changes.
var wrapped = _([1, 2, 3]); // in 2.4.1 wrapped.forEach(function(n) { console.log(n); }); // ➜ logs each value from left to right and returns the lodash wrapper // in 3.0.0 wrapped.forEach(function(n) { console.log(n); }); // ➜ returns the lodash wrapper without logging until `value` is called wrapped.forEach(function(n) { console.log(n); }).value(); // ➜ logs each value from left to right and returns the array
This example depicts the structural breaking changes, where the return type of function changed. In version 2.x this function was calling forEach
eagerly. In version 3.x lodash
made this function lazy, and returns wrapper instead of void
.
Semantic Breaking Changes
Semantic changes appears when output or side effect of API changed/added/removed. This is the list of semantic breaking changes groups:
- adding/removing/changing sorting return type. Consumer may rely on the way array is sorted or not sorted in the return of API.
- evaluation side effect. Chat GPT stores and "remembers" history by a chat id. Breaking change may appear if the storing algorithm is changed or how the history is red.
- additional constraints/validation on input
- adding/changing/removing implicit modification logic. As an example: submit the form and some default values are applied. Changes in the implicit date formatting, changes in the timezone, setting date to an attribute changes.
Let's explore a lodash
again. In a changelog
we can find breaking changes in a version 3.0.0. When semver major version changes, it indicates, that API contains semantic breaking changes.
var array = [1], wrapped = _(array); // in 2.4.1 var a = wrapped.push(2), // pushes `2` to `array` b = wrapped.push(3); // pushes `3` to `array` a.value(); // ➜ returns `array`; [1, 2, 3] b.value(); // ➜ returns `array`; [1, 2, 3] // in 3.0.0 var a = wrapped.push(2), // creates a lazy sequence to push `2` to `array` b = wrapped.push(3); // creates a lazy sequence to push `3` to `array` a.value(); // ➜ pushes `2` to `array` and returns `array`; [1, 2] b.value(); // ➜ pushes `3` to `array` and returns `array`; [1, 2, 3] a.value(); // ➜ pushes `2` to `array` and returns `array`; [1, 2, 3, 2] b.value(); // ➜ pushes `3` to `array` and returns `array`; [1, 2, 3, 2, 3] // use `_#commit` to commit a sequence and continue chaining var a = wrapped.push(2).commit(), // pushes `2` to `array` b = wrapped.push(3).commit(); // pushes `3` to `array` a.value(); // ➜ returns `array`; [1, 2, 3] b.value(); // ➜ returns `array`; [1, 2, 3]
This example depicts none of the structural changes. But calling the same function in different major versions (2.x and 3.x) results in different inner state of closure.
How to avoid breaking changes?
Structural breaking changes can be avoided by providing additional method/function/constructor/class. Consider the case with the lodash
and forEach
. lodash
could avoid the breaking changes by adding another method/function instead of forEach
, they could add forEachLazy
or lazyForEach
which will provide the same eager API as in 2.x version. Another option how to avoid breaking changes is create new lazy API, by adding an additional parameter to the default export function as _.({lazy: true})
. In this case only lazy wrapper with explicitly called value()
method will be created.
Why we are doing breaking changes?
An example with forEach
has two options how to avoid breaking changes.
- add another method/function
forEachLazy
- add function overload for a default
lodash
function with additional optional parameters.
Both of these cases will complicate lodash
API, will make it less consistent with other functions.
Also it will increase maintenance cost of two different approaches to be handled for array methods.
These two reasons are not the only reasons to make breaking changes. In my opinion those are the main reasons. To make decision you need to balance between API complexity / maintenance cost and consumer migration cost / benefit.