Core Java

Sealed Interfaces and Exhaustive Switch: Making Illegal States Unrepresentable

A compact, runnable example showing how sealed hierarchies plus pattern-matching switches turn a whole category of bugs into compile errors.

“Make illegal states unrepresentable” is a phrase that traces back to Yaron Minsky’s Effective ML lectures at Jane Street, and for a long time it described a benefit Java simply couldn’t offer. You could model a closed set of cases with an interface and a pile of implementations, but nothing stopped someone from adding a rogue implementation elsewhere, and nothing forced a switch to handle every case. Since sealed interfaces and pattern matching for switch both became stable in Java 17 and Java 21, respectively , that gap has closed. This piece walks through a small example, end to end, showing exactly where the compiler now catches what used to be a runtime surprise.

The Bug Illegal States Enable

Picture a payment result modeled the old way: one class with a status enum and a grab bag of nullable fields — a transaction ID that’s only set when approved, a reason string that’s only set when declined. Every piece of code that touches this object has to remember which fields are valid for which status, and nothing enforces that. A new status value added six months later can slip past an old if/else chain entirely, silently falling into a branch that assumed only two outcomes existed. The bug isn’t a typo — it’s a state the type system was never asked to rule out.

Sealing the Hierarchy

A sealed interface fixes the first half of the problem. Declaring an interface sealed means only a specific, closed set of types may implement it — either listed explicitly with permits, or, when every implementation lives in the same source file, inferred automatically by the compiler. Nobody outside that set can add a new implementation without touching the sealed declaration itself. This became a stable, permanent Java feature in JEP 409, part of Java 17.

The Exhaustive Switch

Sealing the hierarchy only pays off once something checks it exhaustively, and that’s what pattern matching for switch does. When a switch expression’s selector is a sealed type, the compiler knows the complete list of possible subtypes — so it can verify every one of them is handled, and refuse to compile if any are missing, with no default branch needed to paper over the gap. This capability was finalized in JEP 441, part of Java 21, and it’s available unchanged in Java 25, the current LTS release.

The Complete, Runnable Example

Save this as Main.java and run it directly with java Main.java. Java’s single-file source launcher compiles and executes it in one step, no build tool required, on Java 21 or newer.

import java.math.BigDecimal;

public class Main {

    sealed interface PaymentResult {}

    record Approved(String transactionId, BigDecimal amount) implements PaymentResult {}
    record Declined(String reason) implements PaymentResult {}
    record Error(String message) implements PaymentResult {}

    static String describe(PaymentResult result) {
        return switch (result) {
            case Approved a -> "Approved " + a.transactionId() + " for $" + a.amount();
            case Declined d -> "Declined: " + d.reason();
            case Error e -> "Error: " + e.message();
        };
    }

    public static void main(String[] args) {
        PaymentResult approved = new Approved("TX-42", new BigDecimal("19.99"));
        PaymentResult declined = new Declined("insufficient funds");

        System.out.println(describe(approved));
        System.out.println(describe(declined));
    }
}

Nothing here is exotic — records for the three outcomes, a sealed interface tying them together, and a switch expression with no default. That last part is the point: the absence of a default isn’t a lucky omission, it’s the compiler confirming there’s nothing left to handle.

What Happens When You Add a New Case

Now suppose a new outcome shows up — refunds. Add one more record to the sealed interface:

record Refunded(String transactionId, BigDecimal amount) implements PaymentResult {}

Nothing else changes yet, but the project no longer compiles. The existing describe method — untouched, in a file you may not even remember writing — now fails to build, with a message close to this:

error: the switch expression does not cover all possible input values
        return switch (result) {
               ^

That error is the entire value proposition. With a traditional interface and an instanceof chain, the same change compiles fine and simply produces wrong output the first time a refund flows through, probably in production, probably reported by a confused customer rather than a compiler.

ApproachNew Variant AddedTypical Failure Mode
Plain interface + instanceof chainCompiles unchangedFalls through to a default or an unrelated branch at runtime
Enum + switch with defaultCompiles unchangedNew case silently handled by whatever the default does
Sealed interface + exhaustive switchFails to compileNone — the build stops until every call site is updated

When this pattern pays off

  • The set of cases is genuinely closed and known in advance — API responses, parser results, state machine states
  • Different cases carry genuinely different data, not just a shared shape with some fields left null
  • The same closed type gets handled in more than one place in the codebase, where a missed case is easy to lose track of
  • You’d rather find out about a new case at build time than from a bug report

What We Learned

Sealed interfaces close off who’s allowed to implement a type; exhaustive switch expressions use that closed list to guarantee every case is handled. Neither one does much alone, a sealed interface with a switch that still has a default gives the compiler an escape hatch, and an exhaustive switch over a non-sealed type has no closed list to check against in the first place. Together, they take a category of bug that used to surface as a runtime surprise, the case nobody remembered to handle, and turn it into a build failure, right where it’s cheapest to fix.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button