Articles

Sealed Classes vs Enums – When to use each, real-world examples (API states, UI state handling)

Sealed Classes vs Enums – When to use each, real-world examples

Every software system, whether small or enterprise-scale, needs a way to represent states. Think about a simple traffic light, an API call that may succeed or fail, or a mobile app screen that could be loading data, showing results, or displaying an error. These are all examples of finite states that need to be modeled in code. Choosing the right construct to represent these states not only impacts readability but also maintainability and correctness of the application.

In most modern languages, especially Kotlin and Java, developers often reach for Enums or Sealed Classes when modeling such stateful scenarios. Both are designed to handle sets of known, predefined possibilities, but they do so in very different ways. Enums are concise and great for representing a fixed set of constants, like days of the week or app themes. Sealed classes, on the other hand, allow richer, more descriptive representations, where each state can carry its own data or even define its own behavior.

However, knowing when to choose enums and when sealed classes are the better fit can be confusing. Pick the wrong abstraction, and you may end up writing code that is either unnecessarily rigid or overly complex.

This article aims to clear that confusion. We’ll not only compare enums and sealed classes but also demonstrate their practical use through real-world examples, such as handling API responses and managing UI states. By the end, you’ll have a clear decision-making framework for choosing the right tool for the job.

Understanding the Basics

When developers begin modeling states in their applications, they usually encounter two popular constructs in languages like Kotlin and Java: Enums and Sealed Classes. Both tools provide a way to define a set of possible values, but the way they operate and the kind of problems they solve differ significantly. Before diving into comparisons and real-world use cases, it is important to build a solid understanding of what each construct actually is and how it behaves.

Enums are the simpler of the two, offering a straightforward way to declare a fixed set of constants. Sealed classes, on the other hand, go beyond mere constants and allow states to carry data or behavior with them. Let’s examine each one more closely.

What are Enums?

Enums, short for enumerations, represent a fixed set of constants under a single type. They are extremely useful when you need to define values that do not change and belong to the same category. For instance, directions in navigation (north, south, east, west) or days of the week are perfect candidates for enums.

Example in Kotlin:

enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

Characteristics of enums include:

  • Type safety — you cannot assign values outside the defined constants.
  • Readability and simplicity.
  • Support for built-in functions such as name (string representation) and ordinal (position in declaration).
  • Can contain methods and properties if extended functionality is required.

What are Sealed Classes?

Sealed classes are more expressive. They let you define a restricted class hierarchy, where a class can only have a limited number of subclasses. Unlike enums, each subclass of a sealed class can hold its own data and behavior, making them ideal for representing states that carry context.

Example in Kotlin:

sealed class ApiResponse
data class Success(val data: String) : ApiResponse()
data class Error(val message: String) : ApiResponse()
object Loading : ApiResponse()

Key points about sealed classes:

  • They allow exhaustive when checks, ensuring all states are handled.
  • Subclasses can carry data, making them suitable for complex state modeling.
  • They provide a balance between flexibility and control, ensuring hierarchy is closed within the same file.

Strengths and Limitations of Enums

Enums are often the go-to choice when developers need to model fixed sets of values in their applications. They have been around for decades in programming languages, and their simplicity makes them both powerful and approachable. However, like every tool, enums have their strengths and weaknesses. Understanding both sides helps developers decide whether enums are the best fit for a given problem or if another construct, like sealed classes, is more appropriate.

Strengths

The biggest strength of enums lies in their simplicity. They are concise and easy to read, which makes them perfect for finite sets of constants that do not require additional information. A classic example is representing traffic light states:

enum class TrafficLight {
    RED, YELLOW, GREEN
}

Some key strengths include:

  • Excellent for finite, constant categories such as days of the week or directions.
  • Enums are type-safe, ensuring only defined constants can be used.
  • They can hold methods and properties, giving flexibility when you need functionality attached to a constant.
  • Serialization and deserialization of enums are widely supported in frameworks, making them convenient in APIs or configuration files.
  • Their compact syntax improves readability and reduces boilerplate code.

Limitations

Despite their advantages, enums fall short in more complex scenarios. They are rigid by design and cannot easily represent states that require contextual data. Consider an API call result: you may want to model not only success and failure but also attach data to those outcomes. With enums, this quickly becomes awkward.

enum class ApiState {
    LOADING,
    SUCCESS,   // But where do we keep the actual data?
    ERROR      // And how do we store error messages?
}

Other limitations include:

  • Lack of hierarchy or subtyping, meaning all values remain flat and peer-level.
  • They cannot represent data-rich states without hacks, such as using additional variables elsewhere in the code.
  • Enums can become inflexible if requirements grow, forcing developers to refactor into sealed classes or more advanced constructs.

Strengths and Limitations of Sealed Classes

While enums are often sufficient for representing simple sets of constants, sealed classes provide a much more expressive way of modeling states. They are especially popular in modern Kotlin applications because they give developers the power to represent hierarchies of states, where each state may carry its own unique data. This makes sealed classes a strong choice for handling complex application logic, such as API results or UI workflows. However, their power comes with trade-offs, and knowing both the strengths and limitations is crucial for making the right choice.

Strengths

One of the biggest advantages of sealed classes is their flexibility. Each subclass of a sealed class can contain data, methods, or unique logic. This makes them particularly well-suited for scenarios where states need to represent more than just names.

Example in Kotlin:

sealed class ScreenState {
    object Loading : ScreenState()
    data class Success(val data: List<String>) : ScreenState()
    data class Error(val message: String) : ScreenState()
}

Important strengths include:

  • They support exhaustive when expressions, ensuring that no state is forgotten during handling.
  • Subclasses can carry their own data, making them excellent for modeling real-world events or responses.
  • They allow hierarchy and polymorphism, enabling shared behavior between related states.
  • By being closed within the same file, they provide controlled extensibility while keeping code predictable.

Limitations

Despite their power, sealed classes are not without drawbacks. They can feel verbose compared to enums, especially for simple categories. For instance, defining multiple subclasses requires more boilerplate code than declaring a few enum constants.

Other limitations include:

  • Serialization is not straightforward and often requires additional libraries or custom adapters.
  • Sealed classes were traditionally restricted to a single file, limiting organization (though newer Kotlin versions introduced sealed interfaces to ease this).
  • They can be an over-engineered choice when dealing with simple states such as themes or toggles.
  • Performance differences are usually negligible but can matter in extremely lightweight models compared to enums.

When to Use Enums vs Sealed Classes

By now, it should be clear that enums and sealed classes serve different purposes, even though they often overlap when modeling states. The real challenge for developers is not understanding how each one works, but knowing when to use enums and when sealed classes are a better fit. Choosing the right construct can drastically improve code readability, reduce bugs, and make systems easier to extend over time.

General Rule of Thumb

The simplest way to think about the distinction is this:

  • Use Enums when you only need to represent a simple, constant, and finite set of states. These are scenarios where values do not change and carry no additional data.
  • Use Sealed Classes when your states are complex, hierarchical, or require associated data. This allows each state to hold information that provides context for the program’s behavior.

For example, a simple theme toggle between Light and Dark mode should use enums because the states are fixed and independent. On the other hand, an API response with varying success or failure messages should use sealed classes because each state must represent more than just a name.

Practical Example Matrix

The following table provides a quick reference guide for deciding between enums and sealed classes in common real-world scenarios:

Use CaseBest ChoiceReason
Days of the weekEnumA fixed, finite set of seven constants with no extra data needed.
API responseSealed ClassSuccess or error states require carrying payloads or error messages.
UI theme (light/dark)EnumA simple toggle with no extra context makes enums more efficient.
UI screen statesSealed ClassScreens often carry data, such as a list or error details.
Payment statusDependsEnum works for basic statuses, but sealed class fits if metadata is required.

Real-World Examples

So far, we have seen the theoretical strengths and weaknesses of enums and sealed classes. But theory alone is not enough—developers need to understand how these constructs behave in real-world applications. Enums and sealed classes often appear in scenarios like API response modeling, UI state handling, payment workflows, and even simple app settings such as theme switching.

These examples will make the decision-making process clearer by showing how each construct fits into actual projects.

API Response Handling

One of the most common cases in application development is representing the state of an API call. When fetching data from a server, the request may be in one of several states: loading, successful, or failed.

With enums, this might look like:

enum class ApiState {
    LOADING,
    SUCCESS,
    ERROR
}

This is compact and easy to read. However, there is a major limitation: you cannot attach data to the states. For example, when a request fails, you usually want to pass along the error message, or when it succeeds, you need to include the result data. Enums cannot handle this directly.

Using sealed classes, the representation becomes more powerful:

sealed class ApiState {
    object Loading : ApiState()
    data class Success(val data: List<User>) : ApiState()
    data class Error(val message: String, val code: Int) : ApiState()
}

Here, Success carries the list of users, while Error provides both a message and an error code. When combined with a when expression, the compiler ensures every possible case is covered, reducing the chances of missing an error scenario.

UI State Management

Modern apps often need to display different states of the UI: a loading spinner, a populated list, an empty screen, or an error message. Enums can provide a minimal solution:

enum class ScreenState {
    EMPTY,
    LOADING,
    POPULATED,
    ERROR
}

This works when you only need to switch between views. But as soon as you want to pass data—such as a list of items to display in the populated state or an error string—the enum approach becomes insufficient.

Sealed classes shine here:

sealed class ScreenState {
    object Empty : ScreenState()
    object Loading : ScreenState()
    data class Populated(val items: List<Item>) : ScreenState()
    data class Error(val message: String) : ScreenState()
}

With this model, the UI logic becomes clearer. Each state carries its own information, so the UI layer does not need to fetch data from elsewhere. This reduces bugs and avoids spreading state-related logic across multiple parts of the code.

Payment Status and E-commerce Orders

Another good example is an e-commerce application where orders go through multiple stages. An enum can represent the basic lifecycle of an order:

enum class OrderStatus {
    PENDING,
    SHIPPED,
    DELIVERED,
    CANCELED
}

This is perfectly fine if you only care about the labels. But what if you want to store additional details, such as a tracking number when the item is shipped, or a refund reason when it is canceled? That is where sealed classes become necessary:

sealed class OrderStatus {
    object Pending : OrderStatus()
    data class Shipped(val trackingId: String) : OrderStatus()
    object Delivered : OrderStatus()
    data class Canceled(val reason: String) : OrderStatus()
}

By embedding metadata directly in the states, sealed classes make the order lifecycle much richer and more descriptive. Developers can immediately see what data belongs to which status, keeping logic consistent.

Theme or Mode Switching

Not every scenario requires sealed classes. A common example is switching between themes in an application. Since this involves only a small set of constants and no associated data, enums are a perfect fit:

enum class Theme {
    LIGHT,
    DARK,
    SYSTEM
}

Here, introducing sealed classes would only add unnecessary boilerplate. This is a perfect case where enums are simpler, cleaner, and more efficient.

Takeaway from Real-World Use

From these examples, a clear pattern emerges. Enums are excellent for representing simple, fixed states with no extra context. Sealed classes, however, excel when states must carry data and behavior. In practice, most real-world applications will use a mix of both. The trick is knowing which tool fits which problem: enums for clarity and simplicity, sealed classes for flexibility and expressiveness.

Performance Considerations

When choosing between enums and sealed classes, developers sometimes worry about performance differences. While performance is always an important factor, in most modern applications the difference between enums and sealed classes is negligible compared to the clarity and maintainability they provide. Still, it is worth understanding how each construct behaves under the hood.

Enums and Memory Efficiency

Enums are extremely lightweight for representing small, constant sets. They are implemented as singletons for each constant, meaning only one instance of each value exists at runtime. This makes them memory efficient and fast to compare. For instance:

enum class Direction { NORTH, SOUTH, EAST, WEST }

Every enum value here is instantiated only once, and checks like if (direction == Direction.NORTH) are highly optimized at the JVM level.

Sealed Classes and Overhead

Sealed classes, while slightly heavier, provide the flexibility of carrying data. Each subclass can be a full object or data class, which naturally consumes more memory than an enum constant. For example:

sealed class ApiState
data class Success(val data: List<User>) : ApiState()

Every new Success state means a new object allocation. This introduces more overhead compared to enums, but in real-world apps, this cost is rarely significant.

Practical Consideration

The golden rule is to avoid premature optimization. Unless your system is handling millions of state objects per second, the performance difference will not matter. Instead, prioritize readability, maintainability, and correctness. If performance ever becomes an issue, profiling will reveal the actual bottlenecks. In most cases, whether you choose enums or sealed classes will have almost no impact on runtime performance.

Best Practices & Guidelines

Choosing between enums and sealed classes is not always straightforward. While both constructs help model finite states, their use cases differ. Following a few best practices can help ensure that your code remains clean, maintainable, and easy to extend over time.

Start Simple with Enums

If your problem domain only involves a finite, constant set of values, enums are the cleanest solution. They are concise and easy to maintain. For example, a feature toggle or theme switcher should almost always be modeled with enums rather than sealed classes.

Switch to Sealed Classes When Data is Needed

When states require contextual information, sealed classes are a natural choice. For example, an error state that must carry an error message or an API success state that needs to deliver a data payload is best expressed with a sealed class rather than forcing enums to do extra work with external variables.

Keep Hierarchies Manageable

While sealed classes allow subclassing, avoid creating overly complex hierarchies. Too many subclasses can confuse readers instead of clarifying intent. Group related states together and place them in the same file for better readability.

Use Exhaustive when Expressions

Take advantage of the compiler’s ability to enforce exhaustive handling with sealed classes. This ensures that new states added in the future do not go unhandled, making your code safer and less error-prone.

Combine When Necessary

In some cases, a hybrid approach works well. For example, a sealed class might define a set of high-level states, while enums represent smaller subcategories within those states. This balances simplicity with flexibility.


Conclusion

Throughout this discussion, we explored the differences between enums and sealed classes in depth. Both constructs aim to solve the same underlying problem: modeling a finite set of states in a safe, predictable way. Yet, the way they approach this goal is fundamentally different, and this difference makes them suited for very different scenarios.

Enums shine when the set of states is small, fixed, and requires no additional data. Think of days of the week, traffic lights, or themes in an application. Their elegance lies in simplicity: a single keyword and a handful of constants are often all that is needed. The fact that enums are compact, lightweight, and natively supported across many frameworks makes them an excellent first choice when dealing with truly constant states.

On the other hand, sealed classes provide the power of expressiveness. Each state in a sealed class hierarchy can carry its own properties, behavior, or even complex data structures. This makes them indispensable in scenarios such as API response modeling, where a “success” state must deliver data while an “error” state needs to carry a message or status code. Similarly, in UI state management, sealed classes let you attach actual lists, error messages, or empty placeholders to the states themselves, keeping the logic self-contained and less error-prone.

The comparison shows that enums and sealed classes are not competitors but complementary tools. In many applications, both will coexist. Enums will handle the lightweight, unchanging aspects of the system, while sealed classes will manage complex, evolving logic where additional context is required. The decision should not be guided by personal preference alone but by the nature of the problem being solved.

It is also important to remember that maintainability and correctness often outweigh micro-performance differences. While enums are marginally more efficient in terms of memory and instantiation, sealed classes bring safety through exhaustive when expressions and structured hierarchies. In real-world projects, these qualities help prevent bugs, reduce duplicated logic, and make systems easier to extend.

Ultimately, the best developers are not those who stick to one tool but those who know when to use which tool. If the problem is simple, reach for an enum. If the problem requires context and flexibility, reach for a sealed class. And if your system needs both, do not hesitate to combine them. This pragmatic approach ensures that your codebase remains clean, scalable, and aligned with real-world requirements.


.

You may also like...