Definition
non-sealedreopens – the hierarchy at that point returns to normal unrestricted extension rules.- Unlisted classes – any class may extend the
non-sealedtype without appearing in anypermitsclause. - Transitive subtype – the unlisted class is a subtype of the sealed interface through the
non-sealedbranch. - First-level enforcement – sealing only restricts who may directly implement the sealed interface; below a
non-sealedbranch, extension is free. - Exhaustiveness impact – the compiler cannot enumerate unknown subtypes of a
non-sealedbranch, but matching thenon-sealedbase in a switch covers them all.
Imagine National Bank of India seals the list of who may directly be an Account: only Savings, Current, and a special Partner product. For Partner, head office stamps "open policy" (non-sealed), meaning any third-party fintech partner may build their own product on top of Partner without head office approving each one. So while nobody new can be a direct Account, anyone can build under the open Partner branch, and those partner-built products are still Accounts through Partner.
Understanding the Concepts
How does non-sealed reopen the hierarchy?
Sealing restricts direct subtypes: the permits clause names exactly which types may directly implement the sealed interface. Choosing non-sealed on one of those permitted types is a deliberate statement that this branch is reopened.
package com.bank.model;
import java.math.BigDecimal;
public sealed interface Account
permits SavingsAccount, CurrentAccount, PartnerProduct {
BigDecimal balance();
}
final class SavingsAccount implements Account {
@Override public BigDecimal balance() { return new BigDecimal("500000"); }
}
final class CurrentAccount implements Account {
@Override public BigDecimal balance() { return new BigDecimal("250000"); }
}
// non-sealed: this branch is open for anyone
non-sealed class PartnerProduct implements Account {
@Override public BigDecimal balance() { return BigDecimal.ZERO; }
}
Can unlisted classes extend the non-sealed branch?
Yes. Any class may extend PartnerProduct without being listed in any permits clause. The unlisted class becomes a subtype of Account transitively.
package com.bank.partner;
import com.bank.model.PartnerProduct;
import java.math.BigDecimal;
// Not listed in any permits clause
public class WalletProduct extends PartnerProduct {
@Override
public BigDecimal balance() {
return new BigDecimal("15000");
}
}
public class InsuranceProduct extends PartnerProduct {
@Override
public BigDecimal balance() {
return new BigDecimal("200000");
}
}
package com.bank.service;
import com.bank.model.Account;
import com.bank.partner.WalletProduct;
public class PartnerDemo {
public static void main(String[] args) {
Account wallet = new WalletProduct();
System.out.println("Wallet is Account: " + (wallet instanceof Account));
System.out.println("Balance: " + wallet.balance() + " INR");
}
}
Wallet is Account: true
Balance: 15000 INR
How does the sealing guarantee still hold?
The sealing guarantee is that only the permitted types directly implement the sealed interface. The unlisted class does not directly implement Account; it extends PartnerProduct, which is permitted. The first-level closure is intact.
// Direct implementors of Account (sealed):
// ✓ SavingsAccount (final)
// ✓ CurrentAccount (final)
// ✓ PartnerProduct (non-sealed)
// WalletProduct extends PartnerProduct, NOT Account directly
// The sealed contract is not broken
What is the impact on exhaustive switch?
Because the non-sealed branch is open, the compiler cannot enumerate the classes beneath it. But matching the non-sealed base as one case covers it and all its unknown subtypes.
package com.bank.service;
import com.bank.model.*;
import java.math.BigDecimal;
public class FeeCalculator {
public BigDecimal calculateFee(Account account) {
return switch (account) {
case SavingsAccount s -> new BigDecimal("150.00");
case CurrentAccount c -> new BigDecimal("300.00");
case PartnerProduct p -> new BigDecimal("100.00");
// PartnerProduct case covers WalletProduct,
// InsuranceProduct, and any future partner products
// No default needed: all permitted types are covered
};
}
public static void main(String[] args) {
FeeCalculator calc = new FeeCalculator();
System.out.println("Savings fee: " + calc.calculateFee(
new SavingsAccount()));
System.out.println("Wallet fee: " + calc.calculateFee(
new com.bank.partner.WalletProduct()));
}
}
Savings fee: 150.00
Wallet fee: 100.00
When should you use non-sealed?
Use non-sealed for a plugin architecture: a closed core with one deliberately open extension point. The vendor controls the sealed set but opens exactly one branch for third-party implementations.
package com.bank.model;
import java.math.BigDecimal;
// Closed core: final leaves
// Open extension point: non-sealed PartnerProduct
public sealed interface Account
permits SavingsAccount, CurrentAccount, PartnerProduct {
BigDecimal balance();
}
// Core products: closed, cannot be extended
final class SavingsAccount implements Account {
@Override public BigDecimal balance() { return new BigDecimal("500000"); }
}
final class CurrentAccount implements Account {
@Override public BigDecimal balance() { return new BigDecimal("250000"); }
}
// Extension point: open for partners
non-sealed class PartnerProduct implements Account {
@Override public BigDecimal balance() { return BigDecimal.ZERO; }
}
What are the trade-offs of non-sealed?
| Aspect | final/sealed branch | non-sealed branch |
|---|---|---|
| Extension | Restricted | Open |
| Exhaustive switch | Full leaf enumeration | Match base type only |
| Compiler guarantees | All subtypes known | Unknown subtypes possible |
| Use case | Core domain types | Plugin/extension points |
What about testing with non-sealed branches?
Because the non-sealed branch is open, tests can create mock or stub implementations without modifying the sealed hierarchy.
package com.bank.test;
import com.bank.model.PartnerProduct;
import java.math.BigDecimal;
public class MockPartnerProduct extends PartnerProduct {
private final BigDecimal mockBalance;
public MockPartnerProduct(BigDecimal mockBalance) {
this.mockBalance = mockBalance;
}
@Override
public BigDecimal balance() {
return mockBalance;
}
}
What should a senior engineer know?
Use non-sealed sparingly and deliberately. In a banking domain, keep the regulated core products final so the closed-set guarantee holds, and open only one branch for partners. This isolates the open extension point and makes it auditable rather than leaving a hole in the whole model. In exhaustive switches, always match the non-sealed base to handle unknown partner subtypes as a group.
Visual Overview
┌─────────────────────────────────────────────┐ │ non-sealed Branch: Open Extension Point │ ├─────────────────────────────────────────────┤ │ │ │ sealed interface Account │ │ permits Savings, Current, Partner │ │ │ │ │ ├── final Savings (closed leaf) │ │ │ │ │ ├── final Current (closed leaf) │ │ │ │ │ └── non-sealed Partner (OPEN) │ │ ├── WalletProduct (unlisted) │ │ ├── InsuranceProduct (unlisted) │ │ └── ... (anyone can extend) │ │ │ │ switch (account): │ │ case Savings → ... │ │ case Current → ... │ │ case Partner → ... (covers all below) │ │ // Exhaustive at permitted level ✓ │ └─────────────────────────────────────────────┘
Key Takeaway
Yes, if a permitted subtype of a sealed interface is declared non-sealed, any unlisted class can extend it freely. Sealing only restricts the direct implementors through the permits clause. Choosing non-sealed is a deliberate statement that this branch is reopened, and below it the normal unrestricted extension rules apply.
The unlisted class extends the non-sealed type and becomes a subtype of the sealed interface transitively. The sealing guarantee still holds because the unlisted class does not directly implement the sealed interface; it goes through the permitted non-sealed type. In a banking project at National Bank of India, Pankaj seals Account so only savings, current, and partner may directly be accounts. Savings and current are final, and partner is non-sealed. A wallet product from a third-party fintech extends partner without being listed anywhere, and it is still a valid Account.
The important consequence is about exhaustiveness. Because the non-sealed branch is open, the compiler cannot enumerate all classes beneath it. But matching the non-sealed base as one case in a switch covers it and all its unknown subtypes, so the switch stays exhaustive at the permitted-type level. This trades some leaf-level exhaustiveness for extensibility, by design.
The practical rule is to use non-sealed sparingly and deliberately. Keep the regulated core products final, and open exactly one branch for third-party extensions. This isolates the open point and makes it auditable rather than leaving a hole in the whole model.
Remember one sentence: a non-sealed subtype reopens its branch so unlisted classes can extend it freely, and they become indirect subtypes of the sealed interface through the open branch.
---