Sealed Classes in Java

Oracle Certified Java Developer with 7+ years of experience specializing in backend development, microservices architecture, and cloud-based solutions. Proven expertise in designing scalable systems, optimizing performance, and mentoring teams to enhance productivity. Passionate about building high-performance applications using Java, Spring Boot, Kafka, and cloud technologies (AWS/GCP)
Sealed classes are one of the biggest “modern Java” features that came after Java 8, finalized in Java 17 (LTS). They give you more control over inheritance and make class hierarchies safer and easier to reason about.
1. The problem they solve
In Java 8, if you wrote an abstract class or interface, any class in the world could extend/implement it (unless you made the constructor/package-private). That meant:
You couldn’t always predict or restrict the hierarchy.
Exhaustive
switchorinstanceofchecks weren’t possible (because subclasses could be added anywhere).This made reasoning, pattern matching, and security harder.
2. What sealed classes do
With a sealed class (or interface), you explicitly list which classes are allowed to extend/implement it.
Syntax:
public sealed class Shape
permits Circle, Rectangle, Square { }
public final class Circle extends Shape { }
public non-sealed class Rectangle extends Shape { }
public sealed class Square extends Shape permits ColoredSquare { }
Keywords:
sealed → restricts which classes can extend it (must be in same package or module).
permits → lists the allowed subclasses.
final → subclass can’t be extended further.
sealed → subclass itself continues the sealing, listing its own permitted subclasses.
non-sealed → subclass lifts the restriction, allowing free extension again.
3. Non-Sealed Classes in Java
When you define a sealed class or interface (Java 17+), you explicitly list which classes are allowed to extend/implement it.
But not every subclass needs to remain sealed or final.
That’s where non-sealed comes in:
It removes the restriction and allows any other class to extend it.
Useful when you want to control the top-level hierarchy but leave some branches open for extensibility.
Example:-
Sealed interface
sealed interface Shape permits Circle, Rectangle, Polygon { }
Sealed and final implementations
final class Circle implements Shape { }
final class Rectangle implements Shape { }
Non-sealed implementation
non-sealed class Polygon extends Shape { }
class Triangle extends Polygon { }
class Hexagon extends Polygon { }
Here:
Shapeis sealed → onlyCircle,Rectangle, andPolygonare allowed to implement it.CircleandRectangleare final → no further subclassing.Polygonis non-sealed → other classes are free to extend it, even outside the original permits list.
4. Do subclasses of a sealed class need final / sealed / non-sealed?
Yes, it’s mandatory.
Every direct subclass of a sealed class (or sealed interface) must explicitly declare one of the following modifiers:
final→ no further subclassing allowed.sealed→ subclass is also sealed, and must declare its ownpermitslist.non-sealed→ subclass removes restrictions, allowing free subclassing again.
Why this rule?
Ensures clarity of hierarchy → the compiler knows if the subclass hierarchy is closed, open, or final.
Makes exhaustive pattern matching possible in
switch.Prevents ambiguity (you always know whether further extension is possible).
If you omit final, sealed, or non-sealed in a permitted subclass, the compiler throws an error.
5. Class is not allowed to extend sealed class from another package why is it so?
This is actually a Java language rule, not something specific to your project.
🔹 Why you see this error
In Java, when you declare a sealed interface or class, you must list its permitted subclasses with permits. But there’s a scope restriction:
If your code is not using modules (
module-info.java):
➝ All permitted subclasses must be in the same package as the sealed type.If you are using modules (Java 9+ with
module-info.java):
➝ All permitted subclasses must be in the same module (not necessarily the same package).
🔹 How to fix
You have two options:
Keep everything in the same package
MoveRestEasyProviderintocom.demo.identity.project.servicesso it’s in the same package asJaxRsService.Use modules
If you have amodule-info.java, you can relax the rule by making both the interface and its implementations part of the same module. Then the implementations can live in different packages.Example in
module-info.java:module com.demo.identity.project { exports com.demo.identity.project.services; exports com.demo.identity.project.providers; }This way, both
JaxRsServiceandRestEasyProviderare in the same module, and sealing works across packages.🔹 Why Java enforces this
It’s about enforceability and maintainability:
The compiler must know all permitted subclasses at compile time.
Without modules, package-level sealing ensures all permitted types are in the same compilation unit.
With modules, sealing extends to the module boundary.
Note:-
I have create a blank module-info.java class. And the error is gone. Why is it so?
That makes sense — the reason the error disappeared once you introduced a module-info.java is tied to how the Java Platform Module System (JPMS) treats sealed classes.
Why the error existed before
When your code was on the classpath (no modules), the JLS rule says:
👉 Asealedclass can only be extended by classes in the same package.Since your base class and subclass were in different packages (
servicesvsproviders), the compiler rejected it.
What changed after adding module-info.java
By introducing module-info.java, you converted your code into a named module.
Once classes belong to a named module:
The rule for
sealedclasses changes:
👉 Asealedclass can be extended by permitted subclasses that are in the same module (they no longer need to be in the same package).In your
sealedclass, you explicitly listed the subclass in thepermitsclause, and both classes are now in the same module (com.demo.identity.project).The compiler now recognizes this relationship as valid → the error is gone.
Summary
Classpath mode (no modules):
Sealed base + subclass must be in the same package.Module mode (with
module-info.java):
Sealed base + subclass may be in different packages, as long as they are in the same module.
That’s exactly why declaring module-info.java fixed your issue: it lifted the restriction from “same package” → “same module.” ✅
5. Why it matters
Safer modeling: You can encode business rules in the type system. Example: A
PaymentMethodsealed to onlyCard,UPI,Wallet.Exhaustive checks: With pattern matching for
switch(Java 21), the compiler can verify you’ve covered all cases of a sealed hierarchy.Better readability & maintainability: Readers instantly know the closed set of implementations.
6. Example with pattern matching
sealed interface PaymentMethod permits Card, UPI, Wallet { }
record Card(String number) implements PaymentMethod { }
record UPI(String id) implements PaymentMethod { }
record Wallet(String provider) implements PaymentMethod { }
public String process(PaymentMethod p) {
return switch (p) {
case Card c -> "Processing card " + c.number();
case UPI u -> "Processing UPI " + u.id();
case Wallet w -> "Processing wallet " + w.provider();
}; // compiler ensures this switch is exhaustive
}
7. Where you’ll use them
Modeling fixed domain hierarchies (payment types, shapes, states in a workflow, commands/events).
Working with pattern matching (switch + records).
Replacing “enums with data”: instead of an enum plus multiple unrelated data holders, you can model each as a sealed subtype.


