How I use sealed classes in Kotlin

In a previous article I gave a look into how I tend to structure building server side software in Kotlin. Within that article I showed a few cases where I opted to use sealed classes for return types. For me sealed classes are one of the killer features ok Kotlin that I sorely miss whenever I am using another language. I would like to explore why that is further below.

Multiple Return Types

Many newer languages have the concept of multiple return types. These allow you as the developer to encode extra information in your return statements. Lets take a look at an example of a method signature that you may see when trying to authenticate a user when not utilizing sealed classes.

//Returns a boolean if the user is authenticated - note you dont have a reference to the user or error if one occured
fun authenticate(email: String, password: String): Boolean

This method signature is simple and easy to use but there is a fair bit of information the consumer might be wondering about. The signature says nothing about the potential errors that could arise and it likely leaves us wanting the User object if the user authenticated successfully.

In the Go programming language you can utilize multiple return types. In this case we can return a user object if authenticated or return an error object that contains useful information about problems if they arise.

func Authenticate(email string, password string) (*EmailIdentity, error)

For me the extra information in the method signature is huge. It means I have to read less documentation and can get up and running quicker just by looking at the interface.

In Kotlin we are not able to return multiples types however we can utilize sealed classes to help encode additional information in our return types similar to how multiple return types work.

Sealed classes

A sealed class is a way to represent a specific class hierarchy. In many ways you can think of them as enums on steroids. They allow you to define a set of classes that are of the same class type but contain a wider variety of information. Lets take a look at a method signature and the sealed class definition from a recent project.

fun authenticate(email: String, password: String): EmailIdentityResult

sealed class EmailIdentityResult {
    data class Success(val identity: EmailIdentity) : EmailIdentityResult()
    object InvalidCredentials : EmailIdentityResult()
    object NotFound : EmailIdentityResult()
}

The method signature and return type here have lots of useful embedded information. The results, edge cases, and errors are expressed in a type safe manner within separate types. As you look at this sealed class you might be wondering if handling this will be a branching nightmare as a consumer - fortunately kotlin has another trick up its sleeve.

When statement

The when statement in kotlin replaces the switch operator from other languages and can be used as both and expression or a statement. Lets take a look at how we might consume the authenticate method from above.

when (val result = emailIdentityApi.authenticate(email, password)) {
    is EmailIdentityResult.Success -> {
        val token = jwtTokenFactory.createToken(result.identity.userId)
        HttpResponse.Ok(mapOf("token" to token))
    }
    is EmailIdentityResult.InvalidCredentials -> {
        HttpResponse.Error(401, "Invalid credentials")
    }
    is EmailIdentityResult.NotFound -> {
        // Handle user not found case
    }
    else -> {
        HttpResponse.Error(500, "Unable to authenticate")
    }
}

In the above statement we have the ability to handle all the potential return types in a single statement. Kotlin also has a deep understanding of the class hierarchy so we we can cleanly access result.identity or any other embedded information in the return types without casting.

The ability to encode this level of information in a single return type helps me write more expressive APIs that can be better understood with less branching. It helps me communicate edge cases and errors which may have only been found in documentation previously. If you are looking at your method signatures and having trouble communicating a wider variety of potential results try utilizing sealed classes to model the outcomes and see how it works for you.