Sealed classes opened my mind
How we use Kotlin to tame state at Etsy
Bubbly the Baby Seal by SmartappleCreations
Etsy’s apps have a lot of state. Listings, tags, attributes, materials, variations, production partners, shipping profiles, payment methods, conversations, messages, snippets, lions, tigers, and even stuffed bears.
All of these data points form a matrix of possibilities that we need to track. If we get things wrong, then our buyers might fail to find a cat bed they wanted for Whiskers. And that’s a world I don’t want to live in.
There are numerous techniques out there that help manage state. Some are quite good, but none of them fundamentally changed our world quite like Kotlin’s sealed classes. As a bonus, most any state management architecture can leverage the safety of Kotlin’s sealed classes.
What are sealed classes?
The simplest definition for sealed class is that they are “like enums with actual types.” Inspired by Scala’s sealed types, Kotlin’s official documentation describes them thus:
Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state.
A precise, if a bit wordy, definition. Most importantly: they are restricted hierarchies, they represent a set, they are types, and they can contain values.
Let’s say we have a class
Result to represent the value returned from a network call to an API. In our simplified example, we have two possibilities: a list of strings, or an error.
Now we can use Kotlin’s when expression to handle the result. The
when expression is a bit like pattern matching. While not as powerful as pattern matching in some other languages, Kotlin’s
when expression is one of the language’s most important features.
Let’s try parsing the result based on whether it is was a success or an error.
We’ve done a lot here in a just a few lines so let’s go over each aspect. First we were able to check the type of
result using Kotlin’s
is operator, which is equivalent to Java’s
instanceof operator. By checking the type, Kotlin was able to smartcast the value of
result for us for each case. So if
result is a success, we can access the value as if it is typed
Result.Success. Now we can can pass
items without any type casting to
showItems(items: List<String>). If the result was an error, just print the stack trace to the console.
The next thing we did with the
when expression is to exhaust all the possibilities for the
Result sealed class type. Typically a
when expression must have an
else clause. However, in the above example, there are no other possible types for
Result, so the compiler, and IDE, know we don’t need an
else clause. This is important as it is exactly the kind of safety we’ve been longing for. Look at what happens when we add a
Cancelled type to the sealed class:
Now the IDE would show an error on our when statement because we haven’t handled the
Cancelled branch of
‘when’ expression must be exhaustive, add necessary ‘is Cancelled’ branch or ‘else’ branch instead.
The IDE knows we didn’t cover all our bases here. It even knows which possible types
result could be based on the
Result sealed class. Helpfully, the IDE offers us a quickfix to add the missing branches.
There is one subtle difference here though. Notice we assigned the
when expression to a
val. The requirement that a
when expression must be exhaustive only kicks in if you use
when as a value, a return type, or call a function on it. This is important to watch out for as it can be a bit of a gotcha. If you want to utilize the power of sealed classes and
when, be sure to use the
when expression in some way.
So that’s the basic power of sealed classes, but let’s dig a little deeper and see what else they can do.
Let’s take a new example of a sealed class
Then let’s create a new class call
Merino and have it extend from
What would be the effect of this on our
when expression if we were branching on
Yarn? Have we actually created a new possibility that we must have a branch to accommodate? In this case we have not, because a
Merino is still considered part of
Wool. There are still only two branches for
So that didn’t really help us represent the hierarchy of wool properly. But there is something else we can do instead. Let’s expand the example of
Wool to include
Alpaca types of wool and make
Wool into a sealed class.
when expression will see each
Wool subclass as unique branches that must be exhausted.
Only wool socks please
As our state grows in complexity however, there are times in which we may only be concerned about a subset of that state. In Android it is common to have a custom view that perhaps only cares about the loading state. What can we do about that? If we use the when expression on
Yarn, don’t we need to handle all cases? Luckily Kotlin’s stdlib comes to the rescue with a helpful extension function.
Say we are processing a sequence of events. Let’s create a fake sequence of all our possible states.
Now let’s say that we’re getting ready to knit a really warm sweater. Maybe our
KnitView is only interested in the
Wool.Alpaca states. How do we handle only those branches in our
when expression? Kotlin’s
Iterable extension function
filterIsInstance to the rescue! Thankfully we can filter the tree with a simple single line of code.
And now, like magic, our
when expression only needs to handle
Wool states. So now if we want to iterate through the sequence we can simply write the following:
Meanwhile at Etsy
Like a lot of apps these days we support letting our buyers and sellers login via Facebook or Google in addition to email and password. To help protect the security of our buyers and sellers, we also offer the option of Two Factor Authentication. Adding additional complexity, a lot of these steps have to pause and wait for user input. Let’s look at the diagram of the flow:
So how do we model this with sealed classes? There are 3 places where we make a call out to the network/API and await a result. This is a logical place to start. We can model the responses as sealed classes.
First, we reach out to the server for the social sign in request itself:
Next, is the request to actually sign in:
Finally, we have the two factor authentication for those that have enabled it:
This is a good start but let’s look at what we have here. The first thing we notice is that some of the states are the same. This is when we must resist our urges and instincts to combine them. While they look the same, they represent discrete states that a user can be in at different times. It’s safer, and more correct to think of each state as different. We also want to prefer duplication over the wrong abstraction.
The next thing we notice here is that these states are actually all connected. In some ways we’re starting to approach a Finite State Machine (FSM). While an FSM here might make sense, what we’ve really done is to define a very readable, safe way of modeling the state and that model could be used with any type of architecture we want.
The above sealed classes are logical and match our 3 distinct API requests — ultimately however, this is an imperfect representation. In reality, there should be multiple instances of
SignInResult for each branch of
SocialSignInResult, and multiple instances of
TwoFactorResult for each branch of
For us three steps proved to be the right level of abstraction for the refactoring effort we were undertaking at the time. In the future we may very well connect each and every branch in a single sealed class hierarchy. Instead of three separate classes, we’d use a single class that represented the complete set of possible states.
Let’s take a look at what that would have looked like:
Note: to keep things simple I omitted the parameters and used only regular subclasses
Our sealed class is now beginning to look a bit more busy, but we have successfully modeled all the possible states for an Etsy buyer or seller during login. And now we have a discrete set of types to represent each state, and a type safe way to filter them using
This is pretty powerful stuff. As you can imagine we could connect a custom view to a stream of events that filtered only on 2FA states. Or we could connect a progress “spinner” that would hide itself on some other subset of the states.
These ways of representing state opened up a clean way to react with business logic or changes on the UI. Moreover, we’ve done it in a type-safe, expressive, and fluent way.
So with a little luck, now you can login and get that cat bed for Whiskers!
Special thanks to Cameron Ketcham who wrote most of this code!