Java Records, Sealed Types & Pattern Matching
Between Java 14 and Java 21, Java gained a set of features that transform how data is modeled. Records give concise, immutable data carriers with generated constructors, accessors, equals, hashCode, and toString. Sealed classes and interfaces declare closed hierarchies, meaning exactly these subtypes and no others. Pattern matching for instanceof and switch, plus record patterns, lets you test and deconstruct those types safely and exhaustively.
Together they enable data-oriented programming: modeling a domain as plain data with algebraic data types, then processing it with exhaustive switch expressions, the style of Kotlin sealed classes, Rust enums, and TypeScript discriminated unions (see type narrowing). The result is less boilerplate, and the compiler catches missing cases.
TL;DR
record Point(int x, int y) {}creates an immutable data class with constructor, accessors (x()),equals,hashCode, andtoString.- Validate or normalize in a compact constructor; make defensive copies of mutable components.
sealed interface Shape permits Circle, Squarerestricts subtypes, so the compiler knows them all.if (obj instanceof String s)tests and binds in one step.switchexpressions with type patterns, record patterns (case Circle(var r) ->), and guards (when) replace visitor-pattern boilerplate.- Switches over sealed types are exhaustive: adding a subtype makes non-updated switches fail to compile.
Quick Example
A payment domain as sealed records, processed with an exhaustive switch:
Core Concepts
Records
A record declares its state in its header:
- Fields are
private final, and accessors are named after components (amount()). equalsandhashCodecompare all components, so records have value semantics.- Records can implement interfaces, have static members, nested types, and extra methods and constructors (delegating to the canonical one).
- They can't extend other classes (they implicitly extend
Record), and can't declare extra instance fields. - Immutability is shallow: a
Listcomponent can still be mutated unless you copy it (items = List.copyOf(items)).
Records are ideal for DTOs, value objects (domain-driven design), method return tuples, map keys, and events. JPA entities generally can't be records (they need mutability and no-arg constructors), but records work well as projections. See Spring Data JPA.
Sealed Classes and Interfaces
Permitted subclasses must be final, sealed, or non-sealed. Sealing documents the domain ("a payment result is one of exactly three things"), prevents unexpected subtypes, and enables exhaustiveness checking.
Pattern Matching for instanceof
It removes the cast-after-check boilerplate, and the binding is only in scope where the match is guaranteed.
Switch Expressions and Patterns
Modern switch:
- Is an expression (returns a value) with arrow cases, and no fall-through.
- Supports type patterns (
case Circle c ->), record patterns (case Circle(double r) ->, nested patterns included), andcase null. - Supports guards:
case Declined d when d.retryable() ->. - Is exhaustive over sealed hierarchies and enums, with no
defaultneeded, so the compiler flags missing cases when the hierarchy grows. - Uses unnamed patterns (
_, Java 22+) for components you don't need:case Wallet(var provider, _) ->.
Order matters: more specific cases (with guards) must come before general ones, and the compiler rejects dominated cases.
Data-Oriented Programming
The pattern: model data as immutable records, group alternatives into sealed interfaces (algebraic data types), keep operations in functions that switch over the data, and let the compiler enforce exhaustiveness. It complements object-oriented design. Use it when the set of variants is closed and operations vary, while classic polymorphism suits open hierarchies where new types are added often.
Best Practices
Validate in Compact Constructors
Make invalid records unrepresentable: check invariants and normalize values in the compact constructor, so every instance is valid. See error handling.
Copy Mutable Components
Use List.copyOf, Map.copyOf, or unmodifiable wrappers for collection components, to preserve true immutability.
Prefer Exhaustive Switches Over default
Omitting default in switches over sealed types means adding a new subtype produces compile errors at every switch that must handle it, which is far safer than silently falling into a default branch.
Use Records for Boundaries
API request and response DTOs, messages, events, and configuration properties are natural records: concise, immutable, and self-documenting. Jackson and Spring support them well.
Common Mistakes
Assuming Records Are Deeply Immutable
Adding default to Sealed Switches
A default branch defeats exhaustiveness checking: new subtypes silently fall into it. Omit it for sealed hierarchies, unless there's a genuine catch-all behavior.
Using Records for JPA Entities
Hibernate entities need mutable state, a no-arg constructor, and proxies, none of which records support. Use classes for entities and records for projections and DTOs.
FAQ
What's the difference between a record and a Lombok @Value class?
Both produce immutable data classes with accessors, equals, hashCode, and toString. Records are a language feature: no annotation processor, understood by the compiler for pattern matching and deconstruction, with a standard accessor naming style (name() rather than getName()). Records can't extend classes or add instance fields; Lombok offers more configuration.
Why use sealed interfaces?
To model closed sets of alternatives explicitly (payment results, commands, AST nodes, states), prevent unknown subtypes, and let the compiler verify that switches handle every case. They're Java's equivalent of algebraic data types.
What are record patterns?
Patterns that deconstruct a record into its components directly in instanceof or switch: case Point(int x, int y) -> …. They can nest (case Line(Point(var x1, var y1), Point p2) -> …), and combine with guards and unnamed patterns.
Which Java version do I need?
Records and sealed types are standard from Java 16 and 17, pattern matching for switch and record patterns are final in Java 21, and unnamed variables and patterns in Java 22. Java 21 LTS or later gives you the complete, stable feature set.
Related Topics
- Java — The language overview
- Java Streams — Processing collections of records
- Kotlin — Data classes and sealed classes on the JVM
- TypeScript Type Narrowing — Discriminated unions in TypeScript
- Rust Traits — Enums and pattern matching in Rust
- Domain-Driven Design — Value objects and modeling