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)
}
💡 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")
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'sFragment
orViewModel
. 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:
- Do I just need a simple, fixed set of constants?
- Yes? -> Use an
enum class
.
- Yes? -> Use an
- Do I need a fixed set of concepts, but where each one might carry different data?
- Yes? -> Use a
sealed class
orsealed interface
.
- Yes? -> Use a
- Do I need a base class for other developers (or my future self) to extend in unpredictable ways?
- Yes? -> Use an
abstract class
.
- Yes? -> Use an
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)