How do records, sealed types and pattern matching change the way you model a domain?
Together they let you write closed data hierarchies the compiler can check. A record declares that a type is its components, a sealed interface enumerates the permitted variants, and an exhaustive switch over record patterns turns adding a variant into a compile error at every place that handles them.
What the interviewer is scoring
- Does the candidate get the JDK versions right, including which features are still preview
- Whether exhaustiveness is presented as the payoff rather than brevity
- That they can say what a record does not give you, not only what it does
- Whether they recognise when a sealed hierarchy is the wrong tool and polymorphism is the right one
- Can they explain what a compact constructor is for
Answer
What landed when
Version accuracy is part of the answer here, because these features arrived across five releases and candidates routinely attribute all of them to JDK 21.
| Feature | Finalised in |
|---|---|
Pattern matching for instanceof | JDK 16 |
| Records | JDK 16 |
| Sealed classes and interfaces | JDK 17 |
Pattern matching for switch | JDK 21 |
| Record patterns (deconstruction) | JDK 21 |
Unnamed variables and patterns (_) | JDK 22 |
Primitive types in patterns and instanceof remain a preview feature as of JDK 25, so it is not something to build on. JDK 21 is the release where the set became coherent, which is why it gets the credit, but records and sealed types were usable years earlier and saying so signals you have followed the releases rather than read one summary.
A record is a claim about identity
A record declares that the type is nothing but its components. You get a canonical constructor, accessors named after the components, and equals, hashCode and toString derived from every component. The important part is the semantic commitment: two records with equal components are the same value, and the language enforces that by generating the methods rather than trusting you to keep them in sync with the fields.
That commitment is also the constraint. A record cannot declare instance fields beyond its components, cannot extend another class, and its component fields are final. If your type has identity independent of its state — an entity with a database-assigned ID, something with a lifecycle, anything you mutate — a record is the wrong shape and forcing it produces worse code than a plain class.
Validation goes in a compact constructor, which is the piece most candidates have not used:
public record DateRange(java.time.LocalDate from, java.time.LocalDate to) {
// Compact constructor: no parameter list, no field assignments. It runs
// before the implicit assignment to the fields, so you validate here and
// may also normalise by reassigning the parameters.
public DateRange {
if (to.isBefore(from)) {
throw new IllegalArgumentException("to precedes from");
}
}
}
Sealed types make the compiler your reviewer
sealed lets a type enumerate its permitted implementations. On its own that is mildly useful documentation. Combined with pattern matching in switch it becomes an exhaustiveness check: a switch that covers every permitted subtype needs no default branch, and if someone adds a variant later, every such switch stops compiling until it is handled.
sealed interface Shape permits Circle, Rectangle, Triangle { }
record Circle(double radius) implements Shape { }
record Rectangle(double width, double height) implements Shape { }
record Triangle(double base, double height) implements Shape { }
final class Areas {
static double area(Shape shape) {
// Record patterns bind the components directly; no accessor calls, no
// casts. No default branch, so a fourth Shape breaks this line at
// compile time rather than at run time.
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
case Triangle(double b, double h) -> b * h / 2;
};
}
}
That is the whole argument for the combination. Before it, an unhandled variant was a default branch throwing IllegalStateException — a run-time failure discovered by a user. Now it is a build failure discovered by the person making the change. The class of bug this removes is specifically "someone added a case and missed a place that handles cases", which in a codebase of any age is a frequent one.
How idiomatic modelling shifts
The practical change is that data and behaviour separate more often, and that is a deliberate reversal of the advice most Java developers absorbed.
Classical object orientation says: put area() on Shape as an abstract method, let each subtype implement it, and never switch on type. That is still right when the set of variants is open and the set of operations is small and stable — adding a new shape should not require editing existing files. A sealed hierarchy inverts the trade: the variants are fixed and known, and the operations are many and defined by the consumers. Once Shape is sealed, adding an operation is a new switch somewhere convenient, and adding a variant is a compile error everywhere it matters.
This is why sealed hierarchies fit result and command types so well. A parse result is Success | Failure and that will not grow. An event stream has a known set of event types, and each consumer cares about a different subset. Putting handleForBilling() and handleForAudit() as abstract methods on the event interface couples the event to every consumer; a sealed interface with switches in the consumers does not.
The choice between them is a real design decision with a name — variants versus operations — and being able to state it in those terms is what separates a senior answer from a recital of syntax.
Where the enthusiasm goes wrong
The mistake is treating a record as a guarantee of immutability. A record's fields are final; the objects they point to are not. A record holding a List hands every caller a reference to a list they can mutate, and the record's own hashCode changes with it, which breaks it as a map key. A record holding an array is worse: the generated equals compares that component with Object.equals, so two records with identical array contents are unequal.
If you want an immutable record you still do the work manually — copy mutable arguments in the compact constructor with List.copyOf, and return copies from accessors you override. The record gives you correct equality over the components you declared; it does not give you value semantics for components that do not have them.
Records and sealed types are not shorthand for the classes you were already writing. They let you tell the compiler that a hierarchy is closed and that a type is its data, and the return on that is a build failure instead of a production incident whenever the shape of the domain changes.
Likely follow-ups
- Why can a record not have instance fields beyond its components?
- When is a sealed hierarchy plus a switch worse than an abstract method on the interface?
- What happens to an exhaustive switch when the sealed interface gains a permitted subtype and you do not recompile the consumer?
- Why do records make awkward JPA entities?
Related questions
- When would you make a type a record, a struct, or a class? What does each choose for you?mediumAlso on records4 min
- How does your order placement change on a venue that allocates pro rata within a price level instead of first in, first out?hardSame kind of round: concept6 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- How do you test a Node service, and what do you refuse to mock?mediumSame kind of round: concept5 min
- How does Node load your modules, and how would you make a Node service use more than one core?mediumSame kind of round: concept6 min
- Angular, Vue and React are answers to the same problems. Compare how each handles change detection, reactivity and dependency injection.hardSame kind of round: concept5 min
- How do you turn what the business tells you into a domain model that holds up once the exceptions arrive?hardSame kind of round: design7 min
- Walk me through what happens between a customer tapping a card and the merchant getting paid.hardSame kind of round: concept6 min