Sealed Hierarchies & Exhaustive Pattern Matching in Java 21
Master modern algebraic data modeling. Restrict inheritance with sealed classes, eliminate visitor patterns, and leverage exhaustive pattern matching for switch.
By the end, you’ll be able to…
- Declare Sealed Classes and Interfaces with explicit 'permits' clauses.
- Understand final, sealed, and non-sealed subclass modifiers.
- Write exhaustive Pattern Matching switch expressions with type patterns and record deconstruction.
01Sealed Classes: Restricting Inheritance Hierarchies
Historically, Java only provided two extremes for inheritance: open inheritance (any class can extend, unless restricted by package-private visibility) or complete closure (final classes cannot be extended at all). There was no way to declare: 'Class A can be extended, but ONLY by classes B, C, and D.'
Sealed Classes and Interfaces (sealed interface Shape permits Circle, Rectangle) solve this. By explicitly declaring which subclasses are permitted to extend them, sealed types allow developers to model bounded domain hierarchies (Algebraic Data Types) with mathematical precision.
02Subclass Modifiers: final, sealed, and non-sealed
Every permitted subclass of a sealed class must explicitly declare one of three modifiers to define its own inheritance posture:
1. final: The subclass cannot be extended any further (e.g. records are implicitly final). 2. sealed: The subclass continues to restrict inheritance to its own permitted subtypes. 3. non-sealed: The subclass opens itself up for unrestricted inheritance by any external class. This gives architects complete, granular control over the API boundary.
03Exhaustive Pattern Matching for switch (Java 21)
The combination of Sealed Types and Java 21 Pattern Matching for switch revolutionizes business logic. Because the compiler knows every possible subtype permitted by the sealed interface, switch expressions can be statically proven to be Exhaustive.
You no longer need a dummy default: case that throws an exception! If a developer later adds a new permitted subtype (e.g. PayPalPayment), the compiler immediately raises a compile error on every switch expression that has not handled the new variant.
04Why This Matters in Production
Sealed classes completely eliminate the classic Gang of Four Visitor Pattern, which required hundreds of lines of awkward double-dispatch boilerplate to traverse domain objects.
By pairing sealed hierarchies with pattern-matching switch expressions, domain logic is clean, co-located, and compiler-verified. Adding a new domain variant becomes a safe, guided refactoring where the compiler guides you to every single place in the codebase that needs updating.
05Production Architecture & Engineering Trade-Offs
In large-scale enterprise codebases, rigorous domain modeling relies on enforcing structural invariants at the type level rather than relying on defensive runtime validation. Modern Java leverages sealed types and records to turn runtime type errors into compile-time exhaustiveness guarantees. When designing domain transfer objects or persistent entities, ensuring immutability protects against unintended side effects across concurrent worker threads.
Furthermore, avoiding deep inheritance hierarchies in favor of composition and interface contracts eliminates fragile base class anti-patterns. By encapsulating state mutations within well-defined service boundaries and exposing pure functional transformers, software architects build maintainable, audit-compliant systems that scale smoothly across distributed cloud infrastructure.
The ideas worth keeping.
- Sealed classes and interfaces restrict inheritance to an explicit permitted set of subtypes.
- Subclasses must declare final, sealed, or non-sealed to maintain architectural boundaries.
- Pattern matching switch expressions provide exhaustive, compiler-verified type handling without visitor boilerplate.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Modeling Result<T, E> Algebraic Type
Implement a sealed interface Result<T, E> permitted to be Success(T data) or Failure(E error) using records.
Explore a worked solution
public sealed interface Result<T, E> permits Result.Success, Result.Failure { record Success<T, E>(T data) implements Result<T, E> {} record Failure<T, E>(E error) implements Result<T, E> {}}Sealed Result provides a functional error handling type that can be exhaustively matched with pattern matching switch.
Exercise 2: Exhaustive Evaluation Switch
Write a function evaluate(Result<String, Exception> res) that extracts data or formats the error message using a switch expression.
Explore a worked solution
public static String evaluate(Result<String, Exception> res) { return switch (res) { case Result.Success(String data) -> "OK: " + data; case Result.Failure(Exception err) -> "ERR: " + err.getMessage(); };}Record deconstruction patterns extract component variables directly inside the case label without casting.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Modeling Result<T, E> Algebraic Type
Implement a sealed interface Result<T, E> permitted to be Success(T data) or Failure(E error) using records.
Build successful. (Java 15) [Process exited normally]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.