DEV Community

kavearhasi v
kavearhasi v

Posted on

Kotlin's Toolbox: Sealed Class vs. Enum vs. Abstract Class

So far in this series, we've seen how sealed classes help us escape the "Junk Drawer" anti-pattern and how their exhaustive when checks can make our code virtually crash-proof. The response has been amazing, but one question keeps popping up in my DMs and the comments:

"This is great, but it feels a lot like an enum. And isn't it just a restricted abstract class? When should I actually use one over the other?"

This is the right question to ask. It’s the difference between knowing what a tool is and knowing how to build with it. Using the wrong tool for the job leads to clunky, frustrating code. Honestly, learning to make this choice with confidence was a major level-up moment in my career.


## The First Crossroad: Sealed Class vs. enum

This is the most common point of confusion. Both create a restricted set of options. But they operate on fundamentally different levels.

Let me put it as simply as I can:

  • An enum is a fixed set of values.
  • A sealed class is a fixed set of types.

Think of an enum like a set of pre-printed name tags for a conference: MONDAY, TUESDAY, WEDNESDAY. The values are constant, simple, and singleton—there is only one MONDAY.

A sealed class is like a set of name tag templates. We have a template for "Attendee" and a template for "Speaker." You can create many unique instances from the "Attendee" template (John Doe, Attendee, Jane Smith, Attendee), and each can have different data.

When the enum Breaks

Let's model some HTTP errors. An enum seems like a good start:

// Simple, but too rigid for the real world
enum class HttpError(val code: Int) {
    NOT_FOUND(404),
    UNAUTHORIZED(401)
}
Enter fullscreen mode Exit fullscreen mode

💡 Note: Kotlin enums can have properties and methods, but those are shared per constant. What they cannot do is let you create new instances with unique, per-use data. That’s where sealed classes step in.

This works until your product manager says, "For 'Unauthorized' errors, we need to know why the user was unauthorized so we can show a specific message."

Uh oh. How do you attach a unique reason String to just the UNAUTHORIZED value? You can't. Not cleanly. You'd have to add a nullable reason property to the enum itself, and we're right back in the junk drawer.

Where the Sealed Class Shines

A sealed class models this perfectly because it's a set of types, and each type can have its own unique properties—its own data "shape."

// Each type has the exact data it needs. No more, no less.
sealed class HttpError(val code: Int) {
    // A simple, stateless object for 404
    object NotFound : HttpError(404)

    // A data class that carries unique state for 401
    data class Unauthorized(val reason: String) : HttpError(401)
}

// Now you can create unique instances
val error1 = HttpError.Unauthorized("Session expired")
val error2 = HttpError.Unauthorized("Invalid credentials")
Enter fullscreen mode Exit fullscreen mode

The Guideline:

  • Use an enum for a finite set of simple, constant values that don't need to hold unique data per instance. (Days of the week, Directions, simple on/off switches).
  • Use a sealed class when you have a finite set of related concepts, but each concept needs to carry different or unique data. It's perfect for modeling the various "shapes" a result or state can take.

## The Second Crossroad: Sealed Class vs. abstract class

This distinction is all about intent and the future. It’s about building for a known world versus an unknown one.

  • An abstract class defines an open world. It's a template designed to be extended by anyone, anywhere, at any time in the future.
  • A sealed class defines a closed world. It's a template whose implementations are all known and finalized at compile time.

Analogy Time: An abstract class is like giving someone a key-cutting machine. They can make an infinite number of different keys, many of which you'll never see or predict. A sealed class is like giving someone a specific keyring with a fixed set of keys. You know exactly which keys are on it, and no more can be added.

This "closed world" guarantee is the entire reason the compiler can give us that magical, exhaustive when check we saw in the last post. Because the compiler is holding the keyring, it knows all the keys. With the key-cutting machine of an abstract class, it can never be sure, so it must force you to add an else branch to handle unknown future keys.

The Guideline:

  • Use an abstract class when you're creating a base for an unknown or unrestricted number of future classes. This is common in libraries or frameworks. Think of Android's Fragment or ViewModel. The Android team doesn't know what kind of ViewModels you'll create, so the hierarchy must be open.
  • Use a sealed class when you are modeling a domain where you know all possible variations upfront. Think UI states (Loading, Success, Error), network results, or any finite state machine. You close the hierarchy on purpose to gain compile-time safety.

## Your Quick Decision Guide

Let's boil it down. When you're at a crossroads, ask yourself these questions:

  1. Do I just need a simple, fixed set of constants?
    • Yes? -> Use an enum class.
  2. Do I need a fixed set of concepts, but where each one might carry different data?
    • Yes? -> Use a sealed class or sealed interface.
  3. Do I need a base class for other developers (or my future self) to extend in unpredictable ways?
    • Yes? -> Use an abstract class.

Choosing the right tool is about being precise with your intent. And in software, intent is everything.

What's a real-world scenario where this guide would have helped you choose a different tool than you did? Share your stories in the comments!

Top comments (0)