Mastering Sealed Classes in Java: Model Hierarchies with Confidence


Introduction
"Simplicity is the ultimate sophistication." – Leonardo da Vinci
Imagine you're working on a payment processing system. You’ve modeled different payment statuses using a class hierarchy: Success, Failed, and Pending. Weeks later, a new developer joins and accidentally adds a new subclass called UnknownStatus without discussing it with the team. Bugs start to appear because the logic elsewhere never accounted for this new type. Wouldn’t it be nice if Java could restrict what types can extend or implement a base type? That’s exactly what sealed classes bring to the table.
Java's type system has evolved steadily, and one of the standout features in recent versions is the sealed class. Introduced as a preview in Java 15 and stabilized in Java 17, sealed classes give developers precise control over class hierarchies. This feature enhances code readability, maintainability, and safety by making inheritance explicit and restricted.
In this post, we’ll explore what sealed classes are, how to use them, and why they’re important when it comes to modeling domain-specific logic.
What Are Sealed Classes?
A sealed class or sealed interface restricts which other classes or interfaces may extend or implement it. This restriction allows the compiler to understand the entire hierarchy at compile-time, enabling stronger type checks and more robust pattern matching.
Basic Syntax:
public sealed class Shape permits Circle, Rectangle, Square {
// common methods
}
public final class Circle extends Shape { }
public final class Rectangle extends Shape { }
public final class Square extends Shape { }
Here, only Circle, Rectangle, and Square are allowed to extend Shape.
Rules and Requirements
When you define a sealed class:
You must specify the permitted subclasses using the permits clause.
Each permitted subclass must explicitly declare itself as final, sealed, or non-sealed.
Example:
public sealed class Vehicle permits Car, Bike, Truck { }
public final class Car extends Vehicle { }
public final class Bike extends Vehicle { }
public non-sealed class Truck extends Vehicle { }
Car and Bike cannot be extended further, whereas Truck removes the restriction and can be extended freely.
Benefits of Sealed Classes
Exhaustiveness in switch
With sealed hierarchies, the compiler knows all the subtypes. This makes switch expressions safer and helps prevent bugs from unhandled cases.Domain Modeling
Sealed classes are ideal for modeling domain concepts like PaymentStatus, Result<T>, or Shape, where only a few valid states or types are allowed.Better Refactoring and Tooling Support
IDEs and compilers can provide better suggestions and refactoring tools when they know the entire hierarchy.
Real-World Example
Let’s model a PaymentStatus sealed interface:
public sealed interface PaymentStatus permits Success, Failed, Pending { }
public final class Success implements PaymentStatus { }
public final class Failed implements PaymentStatus { }
public final class Pending implements PaymentStatus { }
You can now use a switch to safely branch logic:
public String handleStatus(PaymentStatus status) {
return switch (status) {
case Success s -> "Payment completed successfully.";
case Failed f -> "Payment failed. Please retry.";
case Pending p -> "Payment is still pending.";
};
}
If you forget a case, the compiler will remind you!
Integration with Pattern Matching
From Java 21 onwards, pattern matching and record patterns pair beautifully with sealed classes, enabling de-structuring of objects within switch expressions.
sealed interface Result permits Success, Error { }
record Success(String message) implements Result { }
record Error(String reason) implements Result { }
String handleResult(Result result) {
return switch (result) {
case Success(String msg) -> "Success: " + msg;
case Error(String reason) -> "Error: " + reason;
};
}
This results in concise, expressive, and type-safe branching logic.
When to Use Sealed Classes?
You have a fixed set of known subtypes.
You want safe pattern matching with exhaustiveness checking.
You’re modeling a domain with clear, limited states.
You want to enforce strong invariants in your API.
Conclusion
Sealed classes bring a much-needed enhancement to Java’s object-oriented paradigm. By controlling inheritance explicitly, you make your code safer and easier to maintain. Combined with pattern matching and records, sealed classes empower you to write cleaner, more expressive Java code.
Subscribe to my newsletter
Read articles from Abhinav K directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abhinav K
Abhinav K
A software engineer with a knack for technology