Definition
- Sealed generic type – a sealed interface or class with a type parameter, e.g.,
sealed interface Order.permits BuyOrder, SellOrder - Permits names classes – the
permitsclause lists class names, not parameterized types, because generics are erased. - Exhaustiveness by class – a
switchis exhaustive when every permitted class is covered, regardless of type arguments. - Wildcard patterns – match with
BuyOrder>because the exact type argument is not recoverable at runtime. - Type-safe modeling –
ResultwithSuccessandFailuregives exhaustive handling with typed payloads.
Think of National Bank of India having a sealed set of order tickets: BuyOrder and SellOrder. Each ticket is stamped with the instrument it trades: equity, bond, or currency. The sealed rule fixes that there are exactly two ticket shapes. The generic parameter is the instrument stamp. A wildcard means "any ticket regardless of instrument." The closed set of two shapes stays closed no matter which instrument stamp is on them.
Understanding the Concepts
How do you declare a generic sealed type?
A sealed interface can declare a type parameter. Each permitted subtype decides how to handle the parameter: keep it generic, bind it to a concrete type, or add bounds.
package com.bank.model;
import java.math.BigDecimal;
public sealed interface Order<T extends Instrument>
permits BuyOrder, SellOrder {}
public record BuyOrder<T extends Instrument>(
T instrument, BigDecimal quantity, BigDecimal price
) implements Order<T> {}
public record SellOrder<T extends Instrument>(
T instrument, BigDecimal quantity, BigDecimal price
) implements Order<T> {}
public interface Instrument {
String ticker();
}
public record Equity(String ticker, String exchange) implements Instrument {}
public record Bond(String ticker, String issuer) implements Instrument {}
Why does the permits clause name classes, not types?
The permits clause lists class names like BuyOrder and SellOrder, not parameterized types like BuyOrder. This is because generics are erased at runtime. The PermittedSubclasses attribute in the class file stores raw class names. The closed set is defined at the class level, independent of type arguments.
// permits names classes:
public sealed interface Order<T extends Instrument>
permits BuyOrder, SellOrder {}
// NOT parameterized types:
// permits BuyOrder<Equity>, SellOrder<Bond> ← invalid syntax
How does exhaustiveness work with generics?
An exhaustive switch over Order extends Instrument> only needs to cover BuyOrder and SellOrder. Exhaustiveness is checked against the closed set of permitted classes, not against the possible type arguments.
package com.bank.service;
import com.bank.model.*;
import java.math.BigDecimal;
public class OrderProcessor {
public static <T extends Instrument> BigDecimal notional(Order<T> order) {
return switch (order) {
case BuyOrder<T> b -> b.quantity().multiply(b.price());
case SellOrder<T> s -> s.quantity().multiply(s.price());
// No default: exhaustive over the two permitted classes
};
}
public static void main(String[] args) {
var equity = new Equity("RELIANCE", "NSE");
var buy = new BuyOrder<>(equity,
new BigDecimal("100"), new BigDecimal("2450.75"));
System.out.println("Notional: " + notional(buy));
}
}
Notional: 245075.00
How do wildcards work with sealed generics?
Wildcards work exactly as they do for any generic type. You can accept Order extends Instrument> to read any order regardless of instrument.
package com.bank.service;
import com.bank.model.*;
import java.math.BigDecimal;
public class RiskEngine {
// Accepts any order of any instrument
public static BigDecimal grossValue(Order<? extends Instrument> order) {
return switch (order) {
case BuyOrder<?> b -> b.quantity().multiply(b.price());
case SellOrder<?> s -> s.quantity().multiply(s.price());
};
}
}
Note the BuyOrder> pattern: you use a wildcard because the type argument is erased at runtime.
How do you model Result with sealed generics?
Sealing plus generics is a clean way to build type-safe heterogeneous results.
package com.bank.model;
public sealed interface Result<T> permits Success, Failure {}
public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(String error, int code) implements Result<T> {}
package com.bank.service;
import com.bank.model.*;
import java.math.BigDecimal;
public class ResultDemo {
public static Result<BigDecimal> getBalance(String accountId) {
if (accountId == null || accountId.isBlank()) {
return new Failure<>("Account ID required", 400);
}
return new Success<>(new BigDecimal("50000.00"));
}
public static void main(String[] args) {
Result<BigDecimal> result = getBalance("RAJESH-001");
String output = switch (result) {
case Success<BigDecimal>(var balance)
-> "Balance: " + balance;
case Failure<BigDecimal>(var error, var code)
-> "Error " + code + ": " + error;
};
System.out.println(output);
}
}
Balance: 50000.00
How does a subtype fix the type parameter?
A permitted subtype can bind the type parameter to a concrete type, creating a specialized variant.
package com.bank.model;
import java.math.BigDecimal;
// General order interface
public sealed interface Order<T extends Instrument>
permits BuyOrder, SellOrder, EquityMarketOrder {}
// Keeps generic
public record BuyOrder<T extends Instrument>(
T instrument, BigDecimal quantity, BigDecimal price
) implements Order<T> {}
public record SellOrder<T extends Instrument>(
T instrument, BigDecimal quantity, BigDecimal price
) implements Order<T> {}
// Fixes T to Equity
public record EquityMarketOrder(
Equity instrument, BigDecimal quantity
) implements Order<Equity> {}
How does bounded type propagation work?
Bounded type parameters on the sealed type propagate to permitted subtypes, keeping the domain consistent.
// The bound T extends Instrument flows to all subtypes
// BuyOrder<String> would not compile because String
// does not extend Instrument
What should a senior engineer know?
Sealing closes the family of classes; generics type the payload. Wildcards let you consume the whole family at the use site. The closed set and exhaustive switch hold regardless of type arguments. Variance choices still belong to the use site via wildcards; sealing does not change variance. Generic sealed hierarchies like Result and Order are powerful modeling tools because they combine type safety with compiler-checked exhaustiveness.
Visual Overview
Sealed Generic Hierarchy:
sealed Order<T extends Instrument>
│
├── BuyOrder<T> (keeps T generic)
├── SellOrder<T> (keeps T generic)
└── EquityMarketOrder (fixes T = Equity)
Exhaustiveness:
switch (order) {
case BuyOrder<?> → handled
case SellOrder<?> → handled
case EquityMarketOrder → handled
// No default: all 3 classes covered
}
Type arguments erased at runtime:
BuyOrder<Equity> → BuyOrder (raw class)
BuyOrder<Bond> → BuyOrder (same raw class)
Exhaustiveness checks classes, not type args
Key Takeaway
Sealing and generics constrain different things and work together cleanly. Sealing closes the set of subtype classes. Generics parameterize the data each subtype carries. The permits clause names classes, not parameterized types, because generics are erased. The PermittedSubclasses attribute stores raw class names, so the closed set is at the class level.
The key interaction for pattern matching is that an exhaustive switch over a wildcard-typed sealed generic only needs to cover the permitted classes, not the possible type arguments. You match with BuyOrder> because the type argument is erased at runtime. This gives compiler-checked handling of every order action while keeping the instrument data type-safe at the API boundary.
At National Bank of India, Rajesh modeled trading orders as a generic sealed interface Order with BuyOrder and SellOrder. The risk engine accepted Order extends Instrument> to process any order, and the exhaustive switch covered both classes with no default. For the Result pattern, Success and Failure gave exhaustive handling with typed payloads.
Remember one sentence: sealing closes the family of classes, generics type the payload, and the exhaustive switch holds regardless of type arguments because the closed set is at the class level.
---