Let's build a contrived example to illustrate breaking changes.
We are managing a JSON API that has a user endpoint.
This is a valid call to the API:
http://danielallendeutsch.com/api/users/1.json
.
It will return this response:
{ "email": "danielallendeutsch@gmail.com", "name": "Daniel Deutsch", "profile": "danielallendeutsch.com" }
All is well in the world. Until, for whatever reason, we need to update our database schema.
Instead of storing email
, name
, and profile
,
we need to store email
, first_name
, last_name
and profile
1.
Okay! We update the database schema—and we are even clever enough to migrate all of the existing users2. But how do we handle our API? There are 3 options:
1. Backward Compatible: have the API return first_name, last_name, and name.
2. Versioning: create two APIs, one that supports the prior schema and one that supports the new schema.
3. Breaking Change: have the API return just first_name and last_name.
There is nothing the speaks against delivering the new and the old at the same time.
Because the names do not conflict, it is possible to serve everything.
Sure, name
no longer exists in the database.
But I can write a function that joins
first_name
and last_name
with a space.
{ "email": "danielallendeutsch@gmail.com", "name": "Daniel Deutsch", "first_name": "Daniel", "last_name": "Deutsch", "profile": "danielallendeutsch.com" }
Code that depends on the is API can access
user['name']
and/or user['first_name']
.
In real life, you could remove name
from the documentation
and encourage the use of first_name
and last_name
.
But any old code that depends on name
would not break.
Pros: for people consuming the API, everything just works. Everything existing is still good, plus there are new features.
Cons: as the developer, you need to write more code. You are responsible for writing a function that wrangles the new schema into something it is not. Also—you are carrying around legacy code.
It is possible to create two versions of the API.
http://danielallendeutsch.com/api/v1/users/1.json
and
http://danielallendeutsch.com/api/v2/users/1.json
are
both valid endpoints.
This is what they return, v1 and v2 respectively:
{ "email": "danielallendeutsch@gmail.com", "name": "Daniel Deutsch", "profile": "danielallendeutsch.com" }
{ "email": "danielallendeutsch@gmail.com", "first_name": "Daniel", "last_name": "Deutsch", "profile": "danielallendeutsch.com" }
In this situation, existing code that depends on your API is already using v1. There are no changes to make for them. New things that are created that use this API will be built against v2.
The pros and cons are similar to above. Except consumers of the API can no longer mix-and-match features. Pros: it still works. Cons: maintaining legacy code.
What if first_name
and last_name
is just better than name
?
And what if no one depends on the API except for you.
Then you can say fuck it, and return the new stuff:
{ "email": "danielallendeutsch@gmail.com", "first_name": "Daniel", "last_name": "Deutsch", "profile": "danielallendeutsch.com" }
This will break anything that expects name
to be returned.
But it allows you to shed your legacy code.
It is not fun to maintain old code for 5+ years.
You can get away with this if you can also change everything that depends on the API. Or if you don't care if you break those things.
I think when we're younger, a true breaking change seems impossible. With every decision that is made, we only add to the options available. Everything can be fixed, and mistakes are reversible.
But as we get older, the stakes get higher. It becomes clear that breaking changes are possible—sometimes we break dependencies and there is no going back.
Worst of all, we learn that the choices we make have the real possibility of being bad/wrong. We tell each other that "everything happens for a reason" and "it wasn't meant to be" and "30 is the new 40". I think we say these things because if we didn't, we'd be paralysed by fear, indecision, and regret.
But here is what is true:
30 != 40
,
some decisions we make in life will make us less happy,
worse off, than if we'd chosen differently.
I listened to a beautiful story recently. In it, a woman decides to give up her son for adoption. This is not backward compatible. She made what she thought was the best decision at the time, with the information she had. It is impossible for anyone to say, but in the simplest sense—it may have been the wrong decision.
There are some things in life that cannot be hurried3. Time and distance often afford perspective that would have been nice to have sooner.
So what is the right thing to do when we realize we made a mistake and would prefer to go back? Do we fight to revert the breaking change? Or do we learn to live with our new reality?
I have no answers. I do not know how to optimize for happiness.
Recently, I experienced a significant breaking change. I gave up on legacy. It was a mistake. I want my prior schema back. I love her and I miss her. She is my partner. I think my life will be worse.
With each passing year, the stakes get higher; the cost of indecision and reverting increases aggressively. I am nervous.
So what did we learn? 1. Use deprecation warnings. Before implementing a breaking change, do everything possible to make sure it's the right decision. 2. Life is tough, and moves only in one direction. Do whatever it takes to minimize mistakes.
Have anything to say? Questions or feedback? Tweet at me @cmmn_nighthawk!