All posts
This is some text inside of a div block.
Engineering

Making Java enums forwards compatible

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:

enum PetOrderStatus {
    PLACED,
    APPROVED,
    DELIVERED,
}

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:

String displayText = switch (petOrderStatus) {
    case PLACED -> "Your order was placed and is being reviewed.";
    case APPROVED -> "Your order was approved.";
    case DELIVERED -> "Your order was delivered!";
};

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:

PetOrderStatus petOrderStatus =
    PetOrderStatus.valueOf(statusString.toUpperCase());

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:

 enum PetOrderStatus {
     PLACED,
     APPROVED,
+    IN_TRANSIT,
     DELIVERED,
 }
 String displayText = switch (petOrderStatus) {
     case PLACED -> "Your order was placed and is being reviewed.";
     case APPROVED -> "Your order was approved.";
+    case IN_TRANSIT -> "Your order is on the way!";
     case DELIVERED -> "Your order was delivered!";
 };

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:

Exception in thread "main" java.lang.IllegalArgumentException: No enum constant PetOrderStatus.IN_TRANSIT
	at java.base/java.lang.Enum.valueOf(Enum.java)
	at PetOrderStatus.valueOf(PetStoreApp.java)

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:

public final class PetOrderStatus {
    private final String value;

    private PetOrderStatus(String value) {
        this.value = value;
    }

    /** Used to "parse" the API value. */
    public static PetOrderStatus of(String value) {
        return new PetOrderStatus(value);
    }

    /** Returns the raw API value. */
    public String _value() {
        return value;
    }
		
    /** Returns an enum containing known order statuses. */
    public Value value() {
        return switch (value) {
            case "placed" -> Value.PLACED;
            case "approved" -> Value.APPROVED;
            case "delivered" -> Value.DELIVERED;
            default -> Value._UNKNOWN;
        };
    }

    public enum Value {
        PLACED,
        APPROVED,
        DELIVERED,
        /** The order status is not known to this SDK version. */
        _UNKNOWN,
    }
}

And our SDK users could use the class like so:

String displayText = switch (petOrderStatus.value()) {
    case PLACED -> "Your order was placed and is being reviewed.";
    case APPROVED -> "Your order was approved.";
    case DELIVERED -> "Your order was delivered!";
    // Fallback for new order statuses not known to this SDK version.
    case _UNKNOWN -> "Your order status is: " + petOrderStatus._value();
};

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:

Your order status is: in_transit

And if we had made the following change to our SDK and app:

 public final class PetOrderStatus {
     // ...

     public Value value() {
         return switch (value) {
             case "placed" -> Value.PLACED;
             case "approved" -> Value.APPROVED;
+            case "in_transit" -> Value.IN_TRANSIT; 
             case "delivered" -> Value.DELIVERED;
             default -> Value._UNKNOWN;
         };
     }

     public enum Value {
         PLACED,
         APPROVED,
+        IN_TRANSIT,
         DELIVERED,
         /** The order status is not known to this SDK version. */
         _UNKNOWN,
     }
 }
 String displayText = switch (petOrderStatus.value()) {
     case PLACED -> "Your order was placed and is being reviewed.";
     case APPROVED -> "Your order was approved.";
+    case IN_TRANSIT -> "Your order is on the way!";
     case DELIVERED -> "Your order was delivered!";
     // Fallback for new order statuses not known to this SDK version.
     case _UNKNOWN -> "Your order status is: " + petOrderStatus._value();
 };

Then customers would get better display text the next time they update.

Footnotes

  1. Yes, we could do Optional<Optional<PetOrderStatus>>, but good luck getting that through design review.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Posted by
Tomer Aberbach
Tomer Aberbach
Software Engineer