Sealed classes in Kotlin

Florian Garcia
betclic-tech
4 min readSep 26, 2022

--

In this article, we will talk about sealed classes and interfaces. What they are, what problem they solve and how we can leverage this in real life.

The problem

Let’s take the following code:

Our code only handles two types of animals (cats and dogs). But the compiled code does not know that only those two types exist. This means we have to add this ugly else block in our when.

This can be a real issue because you start to think about what to do in a situation that shouldn’t exist but that will potentially exist in the future. For instance, say that tomorrow we add cows to the list of supported animals, the code will compile just fine because we have put the else block. We could change the else part to throw an exception but Kotlin does not have checked exceptions (so it’s easy to forget to catch one), and thus we end up with runtime exceptions. That’s a bit sad because we probably want to know at compile time that we need to handle this new case.

Solution

As you probably have guessed, the solution is sealed classes or interfaces. But what are they? From the Kotlin documentation:

Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance. All direct subclasses of a sealed class are known at compile time. No other subclasses may appear after a module with the sealed class is compiled. For example, third-party clients can’t extend your sealed class in their code. Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled.

If rephrased, this means that we can define a bounded hierarchy. One of the important things to know when working with sealed classes is that you cannot instantiate the sealed class; thus only the leaves of the structure exist at runtime (but you can use the interface as a type). When we develop applications that are not libraries, it is pretty rare to allow an abstract class / interface to have an infinite number of implementations, usually we know what we need to handle at compile time. Sealed classes allow us just that.

With sealed classes

So let’s use the sealed keyword ! That gives us the following code:

Boom, we are now covering all cases! Even if this code was embedded in a library, it would not be possible to extend the interface, and we are completely sure that we didn’t forget to implement a case. Pretty handy, right?

Going further

But we can go further, say that we want a complex representation of the animal kingdom and treat cattle and pets differently (just like with infrastructure). We can improve a bit our hierarchy to add those notions like this:

What’s interesting with hierarchy like this is that we can stop the pattern matching on intermediate levels. Here, we can handle the Cattle and Pet parts in the interfaceLevelDescribe method. But if we want, we can handle all the hierarchy leaf classes and we are still exhaustive. Pretty nice!

In real life

Now is the time you start to wonder, “Ok, great, we can handle dumb examples. Can we see some real use case?”. Sure we can! We can use sealed hierarchy in our services to improve error handling. Let’s take the following method:

The findPlayers can fail if the database call fails with an IO exception or if the rows mapping fails. As previously stated, In Kotlin we don’t have checked exceptions; thus we need to think about what can fail and handle errors with a catch (we have something better but this will be in another article). This is especially true if we use Java libraries that rely on Exceptions to handle unexpected cases (most of them ?).

This is not ideal to handle (the boring else block), but once again, the sealed keyword is available. We can define an error hierarchy to ensure we only handle the cases we know.

By wrapping our exceptional code in a custom sealed hierarchy and forwarding the context to it, we can add meaning to the error and simplify the final error handling (usually in a controller / job …). Here we don’t do much with the exception but you get the idea 🙂 Moreover, if we add a new exception, we will be forced by the compiler to handle the new branch and thus make us think about what to do with it. The compiler assist us and we don’t have to search the code to know what needs to be handled, this gives us some time to think about what to do without worrying about the where block!

We will not dive into the way to wrap those exceptions into custom exceptions (yet), but a naïve way to do it would be :

This is simple to do but a bit tiresome and also, If I can, I prefer not to rely on exceptions at all in our Kotlin code.

Conclusion

Sealed classes are a great tool in Kotlin. It allows us to create a bounded hierarchy and remove the burden of handling cases that can never happen. It also improves readability and expressiveness as we can create structures with multiple levels that match our domain. This is also true for exception handling, where we can have an exception hierarchy bound to a particular use case. We use them intensively at Betclic.

As always, be aware that sealed hierarchies are not always what you want. For a library, this can make sense to let an interface open to let users define specific behaviour that will interact with the rest of the library (for SPI, for interface level programming …).

The part on the exception handling was a bit light, but in a following article we will dive into how we handle exceptions at Betclic and we will reuse the sealed classes described above because it is awesome!

--

--

Florian Garcia
betclic-tech

Enthusiast software engineer, learner and speaker. He/Him 👾 https://twitch.tv/ImFlog 💻 https://github.com/ImFlog Working @Betclic