Skip to Content
Course content

140: Common Kotlin Interview Questions on Sealed Classes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior developer who was building a complex payment processing screen. They had used a standard open class to represent the different states of a transaction—Pending, Success, and Failure. It looked fine at first. But a month later, a new requirement came in for a RequiresAction state (for 3D Secure authentication). The dev added the new class, but they missed one single when expression buried in a helper utility. The app didn't crash, but it just... stopped doing anything when that state hit. The user was stuck on a spinning loader forever. If they had used a sealed class, the compiler would have screamed at them the moment they added that fourth state, forcing them to handle it everywhere. That's the primary reason sealed classes exist: they turn runtime bugs into compile-time errors.

The Exhaustiveness Guarantee

When you're in an interview, the most important word you can use when talking about sealed classes is "exhaustiveness." Interviewers want to hear that you understand how the compiler treats a sealed hierarchy. In a normal class hierarchy, the compiler has no idea how many subclasses exist. You're forced to add an else branch to your when expressions, which is essentially a "catch-all" that hides future bugs.

With a sealed class, you're telling Kotlin: "This is a closed set of types." Because the compiler knows every possible subclass, it can verify that your when block covers every single case. If you omit one, it won't compile. I always recommend mentioning that this eliminates the need for the else branch, making your code safer and more intentional. Here is how that looks in practice with a payment scenario:

sealed class PaymentStatus {
    object Pending : PaymentStatus()
    data class Success(val transactionId: String) : PaymentStatus()
    data class Failure(val error: Throwable) : PaymentStatus()
    object Refunded : PaymentStatus()
}

fun handlePayment(status: PaymentStatus) {
    // No 'else' branch needed here! 
    // If I add 'PaymentStatus.Cancelled' tomorrow, this line breaks immediately.
    val message = when (status) {
        is PaymentStatus.Pending -> "Payment is processing..."
        is PaymentStatus.Success -> "Done! ID: ${status.transactionId}"
        is PaymentStatus.Failure -> "Error: ${status.error.message}"
        is PaymentStatus.Refunded -> "Money returned to account."
    }
    println(message)
}

Sealed Classes vs. Enums: The Classic Trap

You will almost certainly be asked: "Why not just use an Enum?" This is a bit of a trick question because it's not about if you can, but how the data is stored. Enums are for constant values. Every instance of an Enum constant is a singleton; you can't have two different PaymentStatus.Failure objects with different error messages.

Sealed classes, however, allow you to have multiple instances of a subclass, each holding its own unique state. In my example above, Success is a data class. This means every successful payment can carry its own specific transactionId. You simply can't do that with an Enum. I usually tell interviewers that Enums are for types of things, while sealed classes are for states of things.

Expanding with Sealed Interfaces

Since Kotlin 1.5, we've had sealed interfaces. You might wonder why we need both. The key is that a class can only extend one sealed class (because Kotlin doesn't support multiple inheritance), but it can implement multiple interfaces. This allows you to create "cross-cutting" sealed hierarchies.

Imagine you have a UIEvent sealed interface and a Loggable sealed interface. A specific event, like UserClickedPurchase, could implement both. This gives you the same exhaustiveness guarantees but with much more flexibility in how you organize your domain logic. It's a subtle distinction, but mentioning sealed interfaces shows you're up-to-date with the language evolution and aren't just reciting a textbook from 2017.




📋 Practical Task

Build a Robust File Upload State Machine

You are tasked with implementing the state management for a file upload system. The system needs to handle various states, some of which carry data and some of which are simple markers. Create a sealed hierarchy called UploadState that implements the following requirements:

  • Idle: A state indicating no upload is currently happening (no data needed).
  • Uploading: A state that tracks the current progress as a Double (e.g., 0.75 for 75%).
  • Paused: A state that stores the lastByteUploaded as a Long.
  • Finished: A state that stores the fileUrl as a String.
  • Error: A state that stores an exception of type Throwable.

Once you've defined the hierarchy, write a function called getUploadMessage(state: UploadState): String. This function must use a when expression to return a specific string for each state (e.g., "Uploading: 75%", "Error: Connection timed out"). Crucially, do not use an else branch; rely on the sealed class's exhaustiveness to ensure all states are handled.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.