I think the key insight here is that whether or not a change is "breaking" has nothing to do with whether or not the change seems, to you, to be a minor or major change, and has nothing to do with how broad the scope or complexity of change is.
Instead, the only thing that determines if a change is breaking is: does code that use to work before the change no longer work after the change?
It is up to you how strict to be. For instance, is it OK to break code that explicitly should not have worked, like originally a list was not listed as allowing nulls but due to a bug you weren't properly checking for them? That's up to you in your change management policy - some consider bugs OK if they break code, some say that if any conceivable code existed that would have worked before (even if you have no proof that such code actually exists) then the change should be considered breaking. Again, that's up to your policy to decide how to handle.
If you have a policy that says that major versions can be breaking but minor versions cannot, then yes - any change that is defined as a breaking change means you cannot include it in a minor version. If that is the only thing that changes, then yes that means you would have to do a major version change to include it.
This problem is created because you have combined two orthogonal things: whether or not a change is minor or major, and whether or not it is breaking. Because one depends upon the other, it means that something is minor in scope yet breaking, it is promoted to a major change. This is not a bug, though, it is an intentional feature of this style of semantic versioning.
However, there is a way to avoid some of this in the future, which is this: if a class is generic or fuzzy in what it allows and supports (and List is about as fuzzy an example as you can find), if you want to be more picky than that you are often best served by creating a wrapper or inheriting class, i.e. a PickyList
with all sorts of rules about what it accepts and what it doesn't. You can even include a warning that add()
rules can change over time due to changing external rules (common in regulatory settings or when external constraints are not defined in the same code - imagine having a class that adds to a financial market order book, for example), and so all consumers of this function should be sure to not assume add will succeed and handle errors/failures appropriately.
In the end, remember the wisdom behind why breaking changes should be handled with care and additional burden: code rot and hard to update libraries are a massive problem in IT today. The level of difficulty significant projects have in just trying to keep versions updated every few years is disgusting and, quite frankly, insane. "Managing software version hell" has joined "ability to Google" in my list of skills that every software developer must have and yet never show up on a job description.
Anything you do to fight the good fight against code rot and version hell from the infinite march of "breaking change with questionable benefits or necessity" is a sacrifice towards the greater good, and I thank you for your service in this worthy cause.
List<T>
, then it's not a breaking change.List<T>
but you won't be able to add to the List anymore, thats why I am asking.List<T>
anymore, does it?Collection.emptyList
,Arrays.asList
and Java11List.of
which each return an immutable List which is explicility allowed in the JavaDoc of theList
interface. Thats the whole struggle