Etsy Icon>

Code as Craft

Sealed classes opened my mind main image
Bubbly the Baby Seal by SmartappleCreations

Sealed classes opened my mind

  image

How we use Kotlin to tame state at Etsy.

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). 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 Result.

‘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.  

Weaving state

Let’s take a new example of a sealed class Yarn.

Then let’s create a new class call Merino and have it extend from Wool

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 Yarn:

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 Merino and Alpaca types of wool and make Wool into a sealed class.

Now our 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.Merino and 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 SocialSignInResult.

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 Iterable.filterIsInstance<Type>(iterable).

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!

[1] Lead Image: Bubbly the Baby Seal by SmartappleCreations