Introduction to Modern Java Data Modeling
Java has evolved significantly over recent versions, and two of the most impactful additions for everyday developers are Records (finalized in Java 16) and Sealed Classes (finalized in Java 17). Together, they allow you to model data more expressively and safely than ever before — with far less boilerplate.
What Are Java Records?
A Record is a special kind of class designed purely to carry data. Before Records, creating an immutable data class meant writing constructors, getters, equals(), hashCode(), and toString() by hand — often dozens of lines for a simple object.
With Records, you declare everything in one line:
public record Point(int x, int y) {}
The compiler automatically generates:
- A canonical constructor
- Accessor methods (
x()andy()) - A meaningful
toString() - Correct
equals()andhashCode()implementations
Compact Constructors for Validation
Records support compact constructors — a concise way to add validation logic without repeating parameter assignments:
public record Range(int min, int max) {
Range {
if (min > max) throw new IllegalArgumentException("min must be <= max");
}
}
What Are Sealed Classes?
A Sealed Class restricts which other classes or interfaces may extend or implement it. This gives you a closed hierarchy — useful when you want to model a fixed set of possibilities, like the result of an operation or a set of shape types.
public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
Pattern Matching with Switch (Java 21+)
Sealed classes shine brightest when combined with pattern matching in switch expressions, introduced in Java 21:
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
};
Because Shape is sealed, the compiler knows the switch is exhaustive — no default branch needed. If you add a new permitted subtype later, the compiler will flag every switch that needs updating.
When to Use Records vs. Regular Classes
| Use Case | Record | Regular Class |
|---|---|---|
| Immutable data carrier (DTO, value object) | ✅ Ideal | Verbose |
| Mutable state | ❌ Not suitable | ✅ Use this |
| Inheritance required | ❌ Records are final | ✅ Use this |
| API response/request body | ✅ Great fit | Works, but verbose |
Practical Tips
- Use Records for DTOs: Replace your Jackson/Lombok data classes with Records in Spring Boot — they serialize/deserialize cleanly.
- Combine with Sealed Interfaces: Model domain events or result types (
Success,Failure) with sealed hierarchies for exhaustive handling. - Don't overuse: Records are not suitable for JPA entities since JPA requires mutable state and a no-arg constructor.
Conclusion
Java Records and Sealed Classes are not just syntax sugar — they represent a shift toward more algebraic, expressive data modeling in Java. By adopting these features, you write less boilerplate, reduce bugs from incomplete switch handling, and make your domain model intentions crystal clear to other developers.