Sealed Classes (Java 17+) — Closed Type Hierarchies and Exhaustive Matching
Fast recall
Quick revision
Core answer
sealed class Shape permits Circle, Square, Triangle and the compiler enforces that only those listed types may directly extend Shape. Each permitted subtype must declare itself `final` (no further extension), `sealed` (continues the closed hierarchy with its own permits list), or `non-sealed` (opens itself back up to arbitrary extension). The big payoff is exhaustive pattern matching: when you switch over a sealed type, the compiler verifies you've handled every permitted subtype — no default needed. Adding a new permitted subtype later forces every switch in the codebase to be updated (compile error until you add the new case), which is a maintenance superpower for evolving domain models. Sealed types model algebraic data types (ADTs) the way functional languages do: combine sealed interface Result with record Success(T value) and record Failure(String error) for a Rust-style Result enum, fully type-safe with no nulls. Constraints: permitted subtypes must be in the same module (or same package for unnamed modules), and the compiler must see them at compile time. Use sealed types for closed business domains (event types, payment methods, command/result types, AST nodes) — anywhere the set of variants is known and finite. Don't use them when extension is genuinely open (plugins, libraries) — use a regular interface.Whether the candidate knows the `sealed`/`non-sealed`/`final` triad, the `permits` clause, exhaustive switch with sealed types, the same-module/package restriction, and the ADT modeling use case
Forgetting that permitted subtypes must declare final/sealed/non-sealed. Not knowing about the same-module restriction. Treating sealed as just 'final but inheritable' instead of an exhaustiveness tool
Mention sealed + records + pattern matching as the Java ADT story, compare to Scala sealed traits and Kotlin sealed classes, discuss when to use vs `final`, vs interface, and reference JEP 409
Interview-ready guide
Interview answer
Sealed classes (Java 17, JEP 409) let you explicitly restrict which classes can extend or implement them. Instead of any class anywhere being free to extend your base type, you declare the closed set with permits.
Syntax
public sealed interface Shape permits Circle, Square, Triangle {}- Each permitted subtype must declare
final,sealed, ornon-sealed - All permitted subtypes must be in the same module (or same package for unnamed modules)
- Same-file shorthand: omit
permitsif all subtypes are nested
Three modifiers for permitted subtypes
- `final` — closes the hierarchy here (most common)
- `sealed` — continues with its own permits list
- `non-sealed` — opens this branch back up to arbitrary extension
Records are implicitly `final` — perfect leaves
sealed interface Shape permits Circle, Square+record Circle(double r) implements Shape {}- Most real-world sealed hierarchies use records as leaves
The big payoff: exhaustive pattern matching (Java 21)
- Switch over a sealed type with no
defaultneeded - Compiler verifies all permitted subtypes are covered
- Adding a new subtype → compile error in every switch in the codebase
- Maintenance superpower for evolving domain models
With record deconstruction (JEP 440)
case Circle(double r) -> Math.PI * r * r- Destructure inline in the case
With `when` guards
case Circle c when c.radius() > 100 -> "big circle"
Sealed + records + pattern switch = ADTs in Java
- Same expressive power as Scala case classes or Rust enums
- Compile-time type safety, no nulls, exhaustive matching
Real-world domain modeling
Result type for functional error handling
sealed interface Resultpermits Success, Failure record Successand(T value) record Failure(String error) - Pattern match for safe handling
Payment methods
sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet, Crypto- Adding ApplePay forces every processor to handle it
Domain events
sealed interface OrderEvent permits Placed, Confirmed, Shipped, Delivered, Cancelled
AST nodes for interpreters
sealed interface Expr permits Lit, Var, Add, Mul, Let- Recursive evaluator via pattern match
Module/package rules
- Permitted subtypes must be in the same module (or package for unnamed modules)
- Compiler must see them at compile time
- JVM enforces via PermittedSubclasses attribute in class file
- Loading an unauthorized extension throws IncompatibleClassChangeError
No external extension
- Library users cannot add subtypes to your sealed hierarchy
- This is the whole point — it's truly closed
- For extensible domains (plugins), use regular interfaces
Reflection support
Class.getPermittedSubclasses()returns the arrayClass.isSealed()returns true- Useful for frameworks doing introspection
Sealed vs final
final— nothing extendssealed— a known set extends- Use sealed when you want polymorphism over a closed set
Sealed vs open interface
- Open — anyone can implement
- Sealed — compiler-enforced closed set
- Use sealed when the closed set is intentional
Sealed vs enum
- Enum — fixed set of constants (limited per-instance data)
- Sealed — fixed set of subtypes, each can carry distinct data and have distinct methods
- Use sealed when variants carry different shapes of data
When to use sealed types
- Closed business domains (payment methods, event types, command/result)
- AST nodes
- State machines (each state as permitted subtype)
- Result/Option/Either functional types
- Anywhere you'd use Scala sealed trait or Kotlin sealed class
- Combine with records for pure data ADTs
When NOT to use sealed types
- Open extension points (plugins, library hooks) — use regular interfaces
- Single-implementation types — just use
final - JPA entity hierarchies (sealed conflicts with persistence flexibility)
- Cases without exhaustive switch needs (sealed adds ceremony for no benefit)
Best practices
- Combine with records for pure data variants
- Use exhaustive switch expressions — don't add
default(defeats the maintenance benefit) - Same-file shorthand for small hierarchies
- Use
non-sealedsparingly (it opens up the branch) - Keep the hierarchy shallow
- Document why it's sealed
Replaces Visitor pattern for most uses
- One switch method instead of separate visitor class per operation
- New variants added in one place
- Compile-time exhaustiveness for free
- Visitor still useful when you can't modify the data classes
Recommendation: Use sealed classes/interfaces whenever you have a closed business domain with a known, finite set of variants — payment methods, event types, command/result types, AST nodes, state machines. Combine with records for pure data variants and pattern matching switch (Java 21) for exhaustive type dispatch with no `default` needed. The key win is the maintenance superpower: adding a new permitted subtype forces every switch in the codebase to be updated (compile error), so you can't silently miss a case. This is what functional languages (Scala, Kotlin, Rust, Haskell) have had for years — Java now has it too. For truly extensible types (plugins, library extension hooks), keep using a regular interface — sealed is for closed sets. For single-implementation types, just use final. For enum-like flags with no per-instance data, use an enum. Reach for sealed when each variant carries different data shapes and you need type-safe exhaustive dispatch. Use `non-sealed` sparingly — it opens up a branch and breaks the exhaustiveness guarantee. Don't add `default` to switches over sealed types — it defeats the compile-time check that's the whole point. Sealed + records + pattern matching is the Java answer to ADTs, and it's one of the most powerful additions to the language in years.
Sealed Classes (Java 17+) — Closed Type Hierarchies and Exhaustive Matching
Direct Answer
Sealed classes and interfaces (Java 17, JEP 409) let you explicitly restrict which classes can extend or implement them. Instead of any class anywhere being free to extend your base type, you declare sealed class Shape permits Circle, Square, Triangle and the compiler enforces that only those listed types may directly extend Shape. Each permitted subtype must declare itself final (no further extension), sealed (continues the closed hierarchy with its own permits list), or non-sealed (opens itself back up to arbitrary extension). The big payoff is exhaustive pattern matching: when you switch over a sealed type, the compiler verifies you've handled every permitted subtype — no default needed. Adding a new permitted subtype later forces every switch in the codebase to be updated (compile error until you add the new case), which is a maintenance superpower for evolving domain models. Sealed types model algebraic data types (ADTs) the way functional languages do: combine sealed interface Result<T> with record Success<T>(T value) and record Failure<T>(String error) for a Rust-style Result enum, fully type-safe with no nulls. Constraints: permitted subtypes must be in the same module (or same package for unnamed modules), and the compiler must see them at compile time. Use sealed types for closed business domains (event types, payment methods, command/result types, AST nodes) — anywhere the set of variants is known and finite. Don't use them when extension is genuinely open (plugins, libraries) — use a regular interface.
Common Mistakes & Pitfalls
Forgetting that permitted subtypes must declare final/sealed/non-sealed. Not knowing about the same-module restriction. Treating sealed as just 'final but inheritable' instead of an exhaustiveness tool
Check your understanding
Answer these without looking back at the article.
- What's the difference between `final`, `sealed`, and `non-sealed`?
- Why must permitted subtypes be in the same module?
- How do sealed classes enable exhaustive switch matching?
- When would you use a sealed type vs a regular interface?
- How do sealed types replace the Visitor pattern?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.