Back to Blog
Java

Java Sealed Interface Implementation

Learn how to implement sealed interfaces in Java: the permits clause, restrictions, records, exhaustive pattern matching, and API design trade-offs.

sealed interfacesJava 17type hierarchypattern matchingAPI design
A sealed interface with a finite set of permitted implementations, shown as a closed box with labeled compartments.

A sealed interface in Java restricts which types can implement it, giving the compiler enough information to verify exhaustive pattern matching. Sealed interfaces became a standard feature in Java 17 as part of the sealed types feature. They let you declare a finite set of permitted implementations, which is useful when modeling domain hierarchies where all possible subtypes are known at compile time.

Declaring a Sealed Interface

The core syntax for a sealed interface uses the sealed modifier and a permits clause that lists every direct permitted subtype. For example:

public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); }

The permits list must contain every direct permitted class that implements the interface. If an interface extends the sealed interface, that direct subinterface must also be listed. Each permitted class must be in the same module or, if the interface is in an unnamed module, in the same package. Each permitted class must be final, sealed, or non-sealed; a permitted interface must be sealed or non-sealed. Records and enums are implicitly final in the usual case and do not need an explicit modifier.

public final class Circle implements Shape { private final double radius; public Circle(double radius) { this.radius = radius; } public double radius() { return radius; } @Override public double area() { return Math.PI * radius * radius; } } public sealed class Rectangle implements Shape permits Square { // ... } public non-sealed class Triangle implements Shape { // ... }

A final class ends the hierarchy for that branch. A sealed class can itself have permitted subclasses. A non-sealed class reopens the hierarchy, allowing any subclass without further restrictions. This gives you fine control over how much of the type hierarchy is closed.

Rules and Restrictions for Sealed Implementations

Sealed interfaces enforce several rules at compile time. The permits list must include every direct permitted subtype; the compiler rejects any missing or extra entry. Also, each permitted subtype must be accessible from the interface's location, meaning it must be in the same module or package. If you try to implement a sealed interface without listing the class in permits, compilation fails with an error like class is not allowed to extend sealed interface.

Another restriction is that the permitted subtypes must be direct. You cannot have a class that implements a sealed interface indirectly through another class. For a class, the relationship starts with a direct implements at the sealed interface. You can extend a branch further only by marking a permitted class sealed or non-sealed.

Sealed Interfaces vs Sealed Classes

Sealed interfaces and sealed classes share the same rules for permits and subtype modifiers. The difference is conceptual: interfaces define contracts without state, while classes can carry implementation and fields. In practice, sealed interfaces are often preferred for pure abstractions, especially when you want to support multiple inheritance of types. Sealed classes are useful when the hierarchy needs shared state or default method implementations.

AspectSealed InterfaceSealed Class
StateNo instance fieldsCan have instance fields
Multiple typesA class can implement several sealed interfacesA class can extend only one sealed class
Default or concrete methodsYes, via default methodsYes, via inherited concrete methods
Typical useContracts, data modeling, DTOsBase classes with shared logic

Choose a sealed interface when the hierarchy is about capability, not implementation. Choose a sealed class when subclasses should inherit concrete behavior or fields.

Exhaustive Pattern Matching with Sealed Interfaces

One of the main benefits of a sealed interface is that the compiler can prove that a switch expression covers all possible subtypes. With Java 21's pattern matching for switch, you can write:

public String describe(Shape shape) { return switch (shape) { case Circle c -> "Circle with radius " + c.radius(); case Rectangle r -> "Rectangle with area " + r.area(); case Triangle t -> "Triangle"; }; }

Because Shape is sealed, the compiler knows these three cases are exhaustive. You do not need a default branch. If you later add a new permitted type, the switch will fail to compile until you handle it. This shifts errors from runtime to compile time, which is a significant maintainability advantage.

An if/else if chain of instanceof tests is not exhaustiveness-checked, so use a switch expression when you want the compiler to prove that every permitted subtype is covered.

Common Mistakes and Edge Cases

A frequent mistake is forgetting to mark a permitted subclass as final, sealed, or non-sealed. The compiler rejects the class with an error like sealed, non-sealed or final modifiers expected. Another mistake is listing a subclass in permits that is not directly implementing the interface. For example, if Square extends Rectangle and Rectangle is sealed, you cannot list Square in Shape's permits; it must be listed in Rectangle's permits.

Records are another common case. A record is implicitly final, so it can be a permitted subtype without an explicit modifier if it is listed in the interface's permits clause. For example, in a separate sealed hierarchy:

public sealed interface Figure permits Point, Circle { double area(); } public record Point(double x, double y) implements Figure { @Override public double area() { return 0; } } public final class Circle implements Figure { @Override public double area() { return Math.PI; } }

An enum can also be a permitted subtype if the sealed interface lists it, though this is less common.

Maintainability and API Design Considerations

Sealed interfaces are a design tool for controlling extensibility. Use them when you own the complete set of implementations and want to prevent external code from adding new ones. This is common in domain modeling, such as representing financial instruments, AST nodes, or UI elements. The sealed hierarchy makes the code easier to reason about because the compiler enforces a closed set.

However, sealing an interface is a commitment. Adding a new permitted type later is a breaking change for any switch that relies on exhaustiveness. If your API is intended to be extended by clients, a sealed interface may be too restrictive. In that case, a regular interface or an abstract class with a public constructor is more appropriate. The decision hinges on whether you can foresee all implementations at design time.

Sealed interfaces also improve documentation. The permits clause acts as a declarative list of known implementations, making the intended hierarchy explicit. This is especially valuable in large codebases where discoverability matters.

Runtime Behavior and Reflection

The sealed information is stored in the class file as part of the PermittedSubclasses attribute. At runtime, you can inspect it via reflection using Class.getPermittedSubclasses(). This method returns an array of Class<?> objects representing the direct permitted subtypes, or null if the class is not sealed. This is useful for frameworks that need to discover implementations dynamically, such as serialization libraries or dependency injection containers.

There is no runtime performance overhead from sealing. The compiler enforces all rules statically; the JVM does not check sealed relationships at runtime. The PermittedSubclasses attribute is only metadata. This means you can use sealed interfaces freely in performance-sensitive code without worrying about reflection costs unless you explicitly call getPermittedSubclasses().

The permits entries appear in the class file's PermittedSubclasses attribute. Application code normally should treat their order as informational rather than as a contract.

When a sealed interface is used with records, the record's components are also available via reflection, but the sealed relationship itself does not change how records behave. Records are final, so they can be direct permitted implementations without extra modifiers.

For developers working with libraries that generate code or proxies, sealed interfaces can be restrictive. A dynamic proxy cannot implement a sealed interface unless it is explicitly listed in permits, which is impossible for runtime-generated code. If you need proxying or runtime subclassing, avoid sealing the interface. This is a practical constraint to weigh when designing an API that may be used with frameworks like Spring or Hibernate.

Sealed interfaces are a mature feature in modern Java. They integrate cleanly with pattern matching and records, making them a solid choice for closed hierarchies. The key is to use them deliberately, understanding that they trade flexibility for compile-time safety.

Java Sealed Interface Implementation: Syntax, Rules, and Pattern Matching | RYUSLOG DEV