What are Records in Java and when should you use them?
Table of Contents
1. Short Answer
A Java Record (stable since Java 16, JEP 395) is a concise, immutable data carrier class. Declaring:
record Point(int x, int y) {}
automatically provides a canonical constructor, accessor methods x() and y(), and correct implementations of equals(), hashCode(), and toString() — eliminating the boilerplate you would otherwise write manually or via Lombok.
2. What is a Java Record?
Before Java 16, creating a simple immutable value object (e.g., a 2D point) required significant boilerplate:
// Pre-Record Java — lots of boilerplate
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
With Records, all of the above collapses to a single line:
record Point(int x, int y) {}
Key Concept
Records are not just syntactic sugar. They carry semantic meaning: a record declares that its primary purpose is holding data, and its representation IS its API. The compiler enforces this by making record components final.
3. Auto-Generated Members
For a record declared as record Person(String name, int age) {}, Java automatically generates:
| Member | What is generated |
|---|---|
| Canonical constructor | Person(String name, int age) |
| Accessor methods | name() and age() — NOT getName() |
equals() |
Compares all components by value |
hashCode() |
Based on all components |
toString() |
Person[name=Alice, age=30] |
Accessor Naming
Record accessors use the component name directly: person.name(), not person.getName(). This can break compatibility with frameworks expecting JavaBean conventions. Keep this in mind when using records with older libraries.
4. Complete Code Example
// Java 16+ — Record declaration
record Person(String name, int age) {}
// Java 17+ — Record with custom method
record Product(String id, String name, double price) {
// Custom instance method
public String displayLabel() {
return name + " ($" + String.format("%.2f", price) + ")";
}
// Static factory method
public static Product free(String id, String name) {
return new Product(id, name, 0.0);
}
}
public class RecordDemo {
public static void main(String[] args) {
Person alice = new Person("Alice", 30);
Person alice2 = new Person("Alice", 30);
// Accessor methods
System.out.println(alice.name()); // Alice
System.out.println(alice.age()); // 30
// equals() by value
System.out.println(alice.equals(alice2)); // true
// toString()
System.out.println(alice); // Person[name=Alice, age=30]
// Custom method
Product p = new Product("P001", "Java Book", 49.99);
System.out.println(p.displayLabel()); // Java Book ($49.99)
// Static factory
Product freeItem = Product.free("P002", "Trial License");
System.out.println(freeItem.price()); // 0.0
}
}
5. Compact Constructors — Adding Validation
A compact constructor lets you add validation or normalization without repeating the parameter list. The assignment to fields happens automatically after the compact constructor body runs.
record Range(int min, int max) {
// Compact constructor — no parameter list, no this.field = field
Range {
if (min > max) {
throw new IllegalArgumentException(
"min (%d) must be <= max (%d)".formatted(min, max)
);
}
// Can also normalize: min = Math.min(min, max);
}
}
// Usage
Range valid = new Range(1, 10); // OK
Range bad = new Range(10, 1); // throws IllegalArgumentException
Pro Tip
Use compact constructors for input validation and normalization. You can even reassign the parameters inside the compact constructor body — the modified values are what get stored in the final fields.
6. Records vs Lombok @Value
Both produce immutable value objects. Here is how they compare:
| Feature | Java Record | Lombok @Value |
|---|---|---|
| Dependency | None — built into JDK 16+ | Requires Lombok on classpath |
| Accessor style | name() |
getName() (JavaBean) |
| Inheritance | Cannot extend/be extended | Can extend classes |
| Validation | Compact constructor | Custom constructor body |
| Pattern matching | Native support (Java 21) | Not supported |
| Serialization | Native JVM support | Works via annotation |
| JPA entity | Not suitable | Not suitable either |
// Lombok @Value equivalent
@Value
public class PersonLombok {
String name;
int age;
// generates: getName(), getAge(), equals, hashCode, toString
}
// Java Record equivalent
record PersonRecord(String name, int age) {}
// generates: name(), age(), equals, hashCode, toString
7. When NOT to Use Records
Records are not a universal replacement for classes. Avoid them when:
- Mutable state required — record components are implicitly
final. You cannot write a setter. - Inheritance needed — records implicitly extend
java.lang.Recordand arefinal. They cannot extend any other class. - JPA / Hibernate entities — these require a no-arg constructor, mutable fields, and JavaBean accessor naming.
- Framework compatibility — Jackson (older versions), MapStruct, and some Spring components expect
getX()naming. Configure the library or use a different approach. - Optional fields — all record components are required. You cannot have optional components without custom constructor overloads.
Jackson Compatibility
Jackson 2.12+ supports records natively. Older versions require @JsonProperty annotations or explicit constructor configuration. Spring Boot 2.5+ ships with Jackson 2.12+, so most Spring projects handle records automatically.
8. Interview Context
Interviewers ask about Records to gauge your knowledge of modern Java. Strong answers will:
- Mention Java 16 (stable) and the problem Records solve (boilerplate in value objects)
- List the five auto-generated members
- Explain the
x()vsgetX()naming difference - Note that records are implicitly
finaland extendjava.lang.Record - Discuss compact constructors for validation
- Know when NOT to use them (JPA entities, mutable objects)
Bonus Points
Mention that records are a natural companion to sealed classes and pattern matching switch (Java 21), enabling algebraic data type modeling similar to Kotlin data classes or Scala case classes.
9. Common Pitfalls
- Expecting getters to be named
getX()— they are namedx(). Code or frameworks relying on JavaBean convention will break. - Trying to add instance fields — records only allow static fields beyond their components. Attempting to add
private int z;is a compile error. - Forgetting records are final — you cannot subclass a record, nor can a record extend another class.
- Using records as JPA entities — JPA requires a no-arg constructor and mutable state. Use a regular class for entities.
- Implementing
equals()manually — the generatedequals()compares all components. If you override it, make sure you overridehashCode()too.
10. Conclusion
Java Records are one of the most developer-friendly additions to modern Java. They eliminate boilerplate for immutable data carriers, integrate with the type system, and compose naturally with sealed classes and pattern matching. Use them confidently for DTOs, domain value objects, API response types, and any place you previously reached for Lombok @Value — provided the project runs Java 16+ and the accessors' naming convention is acceptable to your ecosystem.