Sealed Interfaces with Records
Sealed Interfaces with Records
In functional programming there is a concept called an algebraic data type (ADT) — a type that is exactly one of a fixed, known set of variants. Think of it as a closed family of shapes: a Shape is either a Circle, a Rectangle, or a Triangle — never something else, and never anonymous. Java 17 gives you first-class support for this pattern by combining sealed interfaces and records.
Why the Combination Matters
A sealed interface says: "only these types may implement me." A record says: "I am a transparent, immutable data carrier." Together they let you model a domain variant as a named, self-describing value with zero boilerplate. The compiler knows every possible case — which is what makes exhaustive switch expressions (covered in the next lesson) compile-safe.
sealed class, Haskell data, or Rust enum with fields.
Modeling a Payment Result
Imagine a payment system where processing a charge can end in one of three outcomes: success, a soft decline (retryable), or a hard failure. Here is the ADT:
Notice what we got for free from the record declarations: constructors, equals, hashCode, toString, and accessors — all based on the declared components. The sealed interface contributes nothing but the constraint: only these three types exist.
The Permits Clause vs. Same-File Inference
If all permitted types are declared in the same compilation unit (same .java file or same package, depending on nesting), you can omit the permits clause and the compiler infers it. But being explicit is usually better for readability in production code:
permits for any ADT that will grow or that lives across multiple files. Implicit inference is convenient for small, co-located families — like in a test or a small utility package.
Records May Implement Multiple Interfaces
A record can implement more than one interface, including a mix of sealed and regular interfaces. This is useful when a variant also needs to participate in another abstraction:
Adding Compact Constructors for Validation
Records inside a sealed hierarchy still support compact constructors for validation. If a Declined result must always carry a non-blank reason, enforce it at construction time:
The compact constructor runs before the fields are assigned — exactly the right place for guard clauses.
Nesting: Sealed Interfaces Inside Records
ADTs can go deeper. Suppose Failed needs to distinguish between a network error and a fraud block. You can nest another sealed interface inside:
Now Failed is itself a transparent record, but its kind field is a typed, exhaustively-matchable inner ADT.
Consuming the ADT (Preview of Pattern Matching)
Even before exhaustive pattern-matching switch (Lesson 9), you can already use instanceof patterns to consume a sealed-record hierarchy cleanly:
The throw new AssertionError line is a defensive guard. In Lesson 9 you will replace this entire method with a single switch expression, and the compiler will verify that every variant is handled — no guard needed.
Summary
Combining sealed interfaces with records gives Java a concise, type-safe way to model closed variant types (ADTs). The sealed interface names and locks the set of variants; each record provides the variant's data transparently. The compiler tracks every possible case, enabling exhaustive analysis and eliminating the need for defensive fallback branches. This pattern is the foundation for the pattern-matching switch you will write in the next lesson.