Definition
sealedmodifier – restricts which classes or interfaces may extend or implement the type, standardised in Java 17.permitsclause – lists the exact set of allowed direct subtypes by name; can be omitted when all subtypes are in the same source file.- Subtype modifiers – every permitted subtype must be
final(ends the branch),sealed(continues restriction with its own permits), ornon-sealed(reopens for unrestricted extension). - Records – implicitly
final, so they satisfy the rule automatically without an extra modifier. - Same-module rule – all permitted subtypes must be in the same module as the sealed type (or same package on the classpath).
- Class-file attribute – the compiler writes the permitted set into
PermittedSubclasses, and the JVM validates at load time.
Imagine National Bank of India defines the concept of an "Account" and declares by policy that only three specific products may ever be a kind of Account: Savings, Current, and Loan. Head office writes this list on an official circular. Each listed product must then state its own policy: it is either a final product that cannot be sub-divided, a further sealed product that permits only its own named variants, or an open product that any branch may extend freely.
Understanding the Concepts
What is a sealed class or interface?
A sealed type restricts which other types may directly extend or implement it. You declare it with the sealed modifier and a permits clause listing the allowed subtypes. Any other type trying to extend it is a compile error.
package com.bank.model;
import java.math.BigDecimal;
public sealed interface Account
permits SavingsAccount, CurrentAccount, LoanAccount {
String id();
BigDecimal balance();
}
Only SavingsAccount, CurrentAccount, and LoanAccount may implement Account. The compiler and JVM both enforce this.
What are the permits clause rules?
The rules are strict. Permitted subtypes must be accessible at compile time, must directly extend or implement the sealed type (not grandchildren), and must actually exist. In a named module, they may be in different packages of the same module. On the classpath, they must be in the same package.
package com.bank.model;
import java.math.BigDecimal;
// All three subtypes in the same package (classpath rule)
public sealed interface Account
permits SavingsAccount, CurrentAccount, LoanAccount {
String id();
BigDecimal balance();
}
final class SavingsAccount implements Account {
private final String id;
private final BigDecimal balance;
SavingsAccount(String id, BigDecimal balance) {
this.id = id;
this.balance = balance;
}
@Override public String id() { return id; }
@Override public BigDecimal balance() { return balance; }
}
final class CurrentAccount implements Account {
private final String id;
private final BigDecimal balance;
CurrentAccount(String id, BigDecimal balance) {
this.id = id;
this.balance = balance;
}
@Override public String id() { return id; }
@Override public BigDecimal balance() { return balance; }
}
final class LoanAccount implements Account {
private final String id;
private final BigDecimal balance;
LoanAccount(String id, BigDecimal balance) {
this.id = id;
this.balance = balance;
}
@Override public String id() { return id; }
@Override public BigDecimal balance() { return balance; }
}
What must each permitted subclass be?
Every permitted subclass must carry exactly one of three modifiers.
| Modifier | Meaning | Use case |
|---|---|---|
| final | Cannot be extended further | Concrete leaf product |
| sealed | Continues the restriction with its own permits | Multi-level closed hierarchy |
| non-sealed | Reopens for unrestricted extension | Plugin base or third-party extension point |
package com.bank.model;
import java.math.BigDecimal;
public sealed interface Account
permits SavingsAccount, LoanAccount, PartnerProduct {
BigDecimal balance();
}
// final: no further subclassing
final class SavingsAccount implements Account {
@Override public BigDecimal balance() { return new BigDecimal("500000"); }
}
// sealed: continues the restriction
sealed class LoanAccount implements Account
permits HomeLoan, CarLoan {
@Override public BigDecimal balance() { return new BigDecimal("1000000"); }
}
final class HomeLoan extends LoanAccount {}
final class CarLoan extends LoanAccount {}
// non-sealed: reopens the hierarchy
non-sealed class PartnerProduct implements Account {
@Override public BigDecimal balance() { return BigDecimal.ZERO; }
}
// Anyone can extend PartnerProduct freely
class WalletProduct extends PartnerProduct {}
Can you omit the permits clause?
Yes, when all permitted subtypes are declared in the same source file, the compiler infers the permitted set. This is convenient for small, closed hierarchies.
package com.bank.model;
import java.math.BigDecimal;
// No permits clause: compiler infers from same file
public sealed interface PaymentResult {
record Success(BigDecimal amount) implements PaymentResult {}
record Failure(String reason) implements PaymentResult {}
record Pending(String expectedDate) implements PaymentResult {}
}
Records are implicitly final, so they satisfy the permitted-subtype rule automatically without writing final.
How does sealing enable exhaustive switch?
Because the compiler knows the complete set of subtypes, it can verify that a switch covers every case without a default branch. If someone adds a new permitted subtype, every switch that was exhaustive fails to compile.
package com.bank.service;
import com.bank.model.PaymentResult;
import com.bank.model.PaymentResult.*;
import java.math.BigDecimal;
public class PaymentProcessor {
public String describe(PaymentResult result) {
return switch (result) {
case Success s -> "Transferred " + s.amount() + " INR";
case Failure f -> "Failed: " + f.reason();
case Pending p -> "Pending until " + p.expectedDate();
// No default needed: compiler knows the set is complete
};
}
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();
System.out.println(processor.describe(new Success(new BigDecimal("75000"))));
System.out.println(processor.describe(new Failure("Insufficient funds")));
}
}
Transferred 75000 INR
Failed: Insufficient funds
What are common mistakes with sealed types?
The most common mistake is forgetting the modifier on a permitted subclass.
// WRONG: missing modifier on permitted subclass
// class SavingsAccount implements Account { } // COMPILE ERROR
// Error: sealed, non-sealed or final modifiers expected
// CORRECT: add final
final class SavingsAccount implements Account {
@Override public BigDecimal balance() { return BigDecimal.ZERO; }
}
How is sealing recorded in the class file?
The compiler writes the permitted set into a PermittedSubclasses attribute. The JVM validates at load time that each class claiming to extend a sealed type is actually in the permitted list. This makes the restriction robust even against bytecode manipulation.
package com.bank.service;
import java.util.Arrays;
public class SealedReflection {
sealed interface Account permits Savings, Current {}
final record Savings() implements Account {}
final record Current() implements Account {}
public static void main(String[] args) {
System.out.println("isSealed: " + Account.class.isSealed());
Class<?>[] permitted = Account.class.getPermittedSubclasses();
Arrays.stream(permitted)
.forEach(c -> System.out.println(" Permitted: " + c.getSimpleName()));
}
}
isSealed: true
Permitted: Savings
Permitted: Current
What should a senior engineer know?
Sealed types combine with records and pattern matching to form algebraic data types. A sealed interface is the sum type (one of these alternatives), each record is a product type (bundled fields), and exhaustive switch is the safe consumer. In a bank's codebase this models a fixed set of account products or transaction states with compile-time guarantees that every variant is handled.
Visual Overview
┌─────────────────────────────────────────────┐
│ Sealed Hierarchy Structure │
├─────────────────────────────────────────────┤
│ │
│ sealed interface Account │
│ permits Savings, Loan, Partner │
│ │ │
│ ├── final Savings │
│ │ └── (no further extension) │
│ │ │
│ ├── sealed Loan │
│ │ permits HomeLoan, CarLoan │
│ │ ├── final HomeLoan │
│ │ └── final CarLoan │
│ │ │
│ └── non-sealed Partner │
│ └── (anyone can extend) │
│ ├── WalletProduct │
│ └── InsuranceProduct │
│ │
├─────────────────────────────────────────────┤
│ Exhaustive switch: │
│ switch (account) { │
│ case Savings s -> ... │
│ case Loan l -> ... // covers │
│ case Partner p -> ... // HomeLoan │
│ } // No default needed │
└─────────────────────────────────────────────┘
Key Takeaway
A sealed class or interface, standardised in Java 17, restricts which other types may extend or implement it. You declare it with the sealed modifier and a permits clause that names the exact set of allowed direct subtypes. Any other type trying to extend it is a compile error. This gives the author precise, compiler-enforced control over the inheritance hierarchy.
The permits rules are strict. Listed subtypes must be accessible, must directly extend the sealed type, and must be in the same module (or same package on the classpath). You can omit the permits clause when all subtypes are in the same source file, and the compiler infers the set.
The most important rule is what each permitted subclass must be. final means the branch ends with no further extension. sealed means it continues the restriction with its own permits list. non-sealed means it deliberately reopens the hierarchy for unrestricted extension. Records satisfy the rule automatically because they are implicitly final. Omitting all three modifiers is a compile error.
In a banking project at National Bank of India, Pankaj models account products as a sealed Account interface permitting savings, current, and loan. Savings and current are final leaves, loan is itself sealed with home loan and car loan, and a partner product is non-sealed for third-party extensions. The real payoff is exhaustiveness: the fee calculator switches over the account type with no default branch, and the compiler guarantees every case is covered. If someone adds a new product but forgets to handle it, the build fails immediately.
Remember one sentence: a sealed type uses permits to name its allowed subtypes, each of which must be final, sealed, or non-sealed, giving the compiler a closed set for exhaustive pattern matching.
---