Exhaustive Pattern Matching with Sealed Interfaces and Switch Expressions in Java 21
Introduction
When the domain model grows, if‑else chains and classic switch statements become fragile. Adding a new subtype often means hunting down every place that inspects the type, and the compiler cannot warn you about a missed case. Java 21 gives us a clean solution: sealed interfaces combined with switch expressions that are exhaustive by design.
Why sealed interfaces matter
A sealed interface lets you declare exactly which classes may implement it. The compiler then knows the complete hierarchy, so a switch expression over the sealed type can be verified for completeness. If you later add a new permitted subclass, every switch that isn’t updated will produce a compile‑time error — no more silent bugs in production.
Key point: Sealed types turn an open hierarchy into a closed, compile‑time‑checked one, enabling safe pattern matching.
Defining the domain
Imagine a payment‑processing service that handles several event types. We model each event as a record implementing a sealed interface.
public sealed interface PaymentEvent permits Authorization, Capture, Refund, Void {}
public record Authorization(String transactionId, BigDecimal amount) implements PaymentEvent {}
public record Capture(String transactionId, BigDecimal amount) implements PaymentEvent {}
public record Refund(String transactionId, BigDecimal amount, String reason) implements PaymentEvent {}
public record Void(String transactionId, String reason) implements PaymentEvent {}Each record carries only the data it needs. The permits clause makes the hierarchy explicit and closed.
Processing with switch expressions
Now we can write a single method that handles every event without a single instanceof check.
public CompletableFuture handle(PaymentEvent event) {
return switch (event) {
case Authorization auth -> authorize(auth.transactionId(), auth.amount());
case Capture cap -> capture(cap.transactionId(), cap.amount());
case Refund ref -> refund(ref.transactionId(), ref.amount(), ref.reason());
case Void v -> voidTransaction(v.transactionId(), v.reason());
// No default needed — the compiler guarantees exhaustiveness
};
}
private CompletableFuture authorize(String id, BigDecimal amt) { /* … */ }
private CompletableFuture capture(String id, BigDecimal amt) { /* … */ }
private CompletableFuture refund(String id, BigDecimal amt, String reason) { /* … */ }
private CompletableFuture voidTransaction(String id, String reason) { /* … */ } Notice the pattern variables (auth, cap, …) — they give you typed access to the record components instantly. The switch expression returns a CompletableFuture, so the whole pipeline stays asynchronous.
Handling new cases safely
Six months later the business adds a Chargeback event. You simply add a new record and update the permits list:
public sealed interface PaymentEvent permits Authorization, Capture, Refund, Void, Chargeback {}
public record Chargeback(String transactionId, BigDecimal amount, String code) implements PaymentEvent {}Recompiling the project now flags the handle method with an error: the switch expression does not cover all permitted subtypes. You add the missing arm, run the tests, and deploy — zero risk of an unhandled event slipping through.
Performance note
Switch expressions compile to an efficient tableswitch or lookupswitch bytecode when the target type is a sealed interface with a modest number of implementations. There’s no reflection, no visitor boilerplate, and the JIT can inline the small handler methods. In micro‑benchmarks the pattern‑matching version is on par with a hand‑written if‑else chain, while being far easier to maintain.
Putting it together
Sealed interfaces plus switch expressions give you a domain‑driven way to model finite state spaces — payment events, order statuses, command types, you name it. The compiler becomes your safety net, and the code reads like a specification rather than a maze of conditionals. Next time you reach for a sprawling if‑else block, ask yourself whether a sealed hierarchy could make the intent explicit and the maintenance burden disappear.