Records and Sealed Classes
Records and sealed types remove boilerplate and let the compiler check that every case is handled.
On this page · 5 min · Advanced
On this page
- In simple words
- Learning objectives
- Explanation
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Advanced
- Updated
- Oct 30, 2026
Related tutorials
In simple words#
A record is a printed form with fixed fields — fill it once, it never changes. A sealed type is a menu with a fixed list of dishes; the compiler knows every option and reminds you if you forget one.
- record
- Immutable data class with generated methods.
- sealed
- Restricts which classes may extend a type.
- Exhaustive switch
- Compiler checks all subtypes are handled.
Learning objectives#
- Create records with validation
- Define sealed hierarchies
- Use pattern matching in switch
- Know when to use each
Explanation#
record Point(int x, int y) generates constructor, accessors (x(), y()), equals, hashCode and toString. Fields are final.
A compact constructor validates input: record Email(String value) { Email { if (!value.contains("@")) throw …; } }.
sealed interface Shape permits Circle, Rectangle limits implementations to known types.
switch over a sealed type with record patterns is checked for completeness — no default needed.
Code example: Payment results as a sealed hierarchy#
sealed interface PaymentResult permits Success, Declined, Pending {}
record Success(String txnId, double amount) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
record Pending(int retryAfterSeconds) implements PaymentResult {}
record Money(double amount) {
Money { // compact constructor
if (amount < 0) throw new IllegalArgumentException("Amount cannot be negative");
}
}
public class Payments {
static String message(PaymentResult r) {
return switch (r) {
case Success(String id, double amt) -> "Paid ₹" + amt + " (txn " + id + ")";
case Declined(String reason) -> "Declined: " + reason;
case Pending(int secs) -> "Pending, retry in " + secs + "s";
}; // no default: compiler checks all cases
}
public static void main(String[] args) {
System.out.println(message(new Success("TX-81", 1499.0)));
System.out.println(message(new Declined("Insufficient funds")));
System.out.println(new Money(250));
}
}
Paid ₹1499.0 (txn TX-81) Declined: Insufficient funds Money[amount=250.0]
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The sealed interface permits exactly three records.
- Records carry the data for each outcome.
- The switch destructures each record pattern.
- Adding a new result type forces updating every switch.
Real-world use case#
API responses, domain events and payment outcomes become safe, self-documenting types.
Common mistakes#
Records are immutable. Create a new record with changed values.
It hides missing cases. Let the compiler check exhaustiveness.
Best practices#
- Records for DTOs and value objects
- Validate in compact constructors
- Sealed types for fixed sets of cases
- Exhaustive switches without default
Remember this#
record = immutable data, no boilerplate.
sealed = fixed list of subtypes.
Pattern switch + sealed = compiler-checked cases.
Practice exercise#
Your task: Create a record Student(String name, int marks) that rejects marks above 100.
Show solution
record Student(String name, int marks) {
Student {
if (marks < 0 || marks > 100) throw new IllegalArgumentException("Marks 0-100");
}
}
Mini quiz#
What does a record generate automatically?
Want more? Practise all Java MCQs · Java interview questions
My notes#
Saved in this browser.