SDKs, like most downloaded software, quickly end up with client-server version skew. A great SDK gracefully handles skew by being forwards compatible with potential API changes.
This is especially important for Java SDKs, which could to be used in Android apps that users take a long time to update (if they update at all). An app shouldn’t start crashing just because it’s not using the latest SDK version!
It turns out Java enums are not trivially forwards compatible in this way.
An example API
Suppose we’re designing a Java SDK for a Pet Store API. We might end up with an enum for representing order status:
Users of our SDK would find this type more convenient than a String
because it indicates exactly which statuses are possible. Users can even use “switch expressions” to ensure they handle all possible statuses:
But how do we convert a string from the API response into an enum constant?
The most common way to parse a string into an enum constant is using the built-in Enum.valueOf
method, which is automatically generated for every enum:
This works, but there are problems lurking.
An API change
Suppose one day our product manager pings us and says, “Customers want to know when their fluffball is on the way. Is that possible?”
“Sounds reasonable,” we think to ourselves. So we implement the feature, add "in_transit"
as a possible order status in the API, and update our SDK and app like so:
The feature works fine in our local environment so we decide to ship the changes.
Crashes galore
Almost immediately, we start getting crash reports for the Pet Store app. They all look like this:
But we did update the SDK enum with an IN_TRANSIT
constant! What gives?
It turns out that all of the crashes are coming from customers who haven’t updated their app yet. In that previous version, Enum.valueOf
throws an IllegalArgumentException
when the input doesn’t match any known enum constant.
The API started including "in_transit"
in responses, but almost all customers are still using the previous SDK version!
The solution
So what’s an SDK engineer to do? How could we have avoided this incident?
One option is to make the type Optional<PetOrderStatus>
instead of just PetOrderStatus
, which would allow us to return Optional.empty()
when the value is not a known constant. This isn’t ideal for a few reasons:
- We can’t access the raw API value in the
Optional.empty()
case. Optional.empty()
looks like it means there’s no order status, but that’s not what we’re trying to convey.- What if we want to represent the concept of “no order status” in the future? We would no longer be able to use
Optional.empty()
for that!1
A more robust solution would be to store the raw API value and provide access to both the raw value and an enum representation of it.
We might end up with a class that looks like this:
And our SDK users could use the class like so:
This design has several benefits:
- We still get all the benefits of enums, including exhaustiveness checks that now require us to handle the unknown status case.
- We can still use the raw API value in the unknown status case.
An API change: part 2
With this new SDK design, our app wouldn’t have started crashing after the API change. Instead, previous app versions would have displayed the following message:
And if we had made the following change to our SDK and app:
Then customers would get better display text the next time they update.
Footnotes