Java has consistently evolved to provide developers with more expressive, concise, and efficient ways to write code. One of the major additions in Java 14 was the introduction of records — a new type of class that simplifies the creation of immutable data carriers. This article thoroughly explores Java records, including their syntax, features, use cases, limitations, and best practices.
What is a Java Record?
A record in Java is a special class designed to be a simple data carrier. It reduces boilerplate code by automatically generating commonly used methods like equals(), hashCode(), and toString().
Java records were introduced as a preview feature in Java 14, finalized in Java 16, and are now a permanent part of the Java language.
Why Were Records Introduced?
Before records, creating a simple data class in Java involved writing a lot of repetitive code:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Person person)) return false;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}This is a lot of code. Java records simplify this class.
Syntax of Java Record
Using a record, the Person class can be written as:
public record Person(String name, int age) {}Explanation:
- Declares a record named
Person - Defines two fields:
nameandage - Automatically generates:
- A constructor
- Getters (but no “get” prefix, e.g.,
name()instead ofgetName()) equals(),hashCode(), andtoString()methods
Syntax of Lombok Value annotation
Using a Lombok, the Person class can be written as:
import lombok.Value;
@Value
public class Person {
String name;
int age;
public Person(String name, int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.name = name;
this.age = age;
}
}Explanation:
Value annotation automatically:
- Makes the class final
- Makes all fields private and final
- Generates getter methods (named exactly like the fields, just like records)
- Generates toString(), equals(), and hashCode()
- No setters (immutability by default)
Features of Java Records
1. Immutable by Default
- All fields in a record are
finaland cannot be modified after object creation.
2. Concise Syntax
- Removes boilerplate code like getters, constructors, and
toString().
3. Auto-generated Methods
- Records automatically implement
equals(),hashCode(), andtoString().
4. Can Implement Interfaces
- A record can implement an interface but cannot extend another class.
5. Compact Constructors
- Records allow constructors to be customized while still maintaining immutability.
Examples of Java Records
1. Using a Record
@Slf4j
public class RecordExample {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
log.info(person.getName()); // Alice
log.info(String.valueOf(person.getAge())); // 30
log.info(String.valueOf(person)); // Person[name=Alice, age=30]
}
}2. Custom Constructor
By default, records provide a constructor matching the defined fields. However, developers can define a custom compact constructor:
public record Person(String name, int age) {
public Person {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
}
}Here, we add validation to ensure the age is non-negative.
Exception in thread "main" java.lang.IllegalArgumentException: Age cannot be negative at com.example.programming.model.Person.<init>(Person.java:11) at com.example.programming.utils.example.RecordExample.main(RecordExample.java:12)
3. Implementing an Interface
Records can implement interfaces:
public interface Describable {
String describe();
}
public record Person(String name, int age) implements Describable {
@Override
public String describe() {
return name + " is " + age + " years old.";
}
}Limitations of Java Records
While records are helpful, they come with some restrictions:
1. Cannot Extend Classes
- A record cannot be inherited from another class. It only extends
java.lang.Recordimplicitly.
2. Immutable Fields
- Fields in a record cannot be changed after initialization.
3. No Additional Instance Variables
- All fields must be declared in the record header; extra instance variables are not allowed.
4. Cannot Modify Default Methods
- Records automatically provide
equals(),hashCode(), andtoString(), and overriding them requires careful implementation.
When to Use Java Records

Example: Using Java Record in a REST API
Records are great for defining API response models:
@RestController
public class UserController {
@GetMapping("/user")
public User getUser() {
return new User("John Doe", "[email protected]");
}
public record User(String name, String email) {}
}Here, User is a simple DTO with minimal code.

Comparison: Records vs Lombok
Many Java developers use Lombok to reduce boilerplate code. Here’s a comparison:

If you’re using Java 16 or newer, records are preferable since they are built into the language.
Conclusion
Java records provide a powerful and concise way to create immutable data structures while eliminating repetitive boilerplate code. They are best suited for data-centric classes such as DTOs, API responses, and configuration models. However, they do not replace regular classes, especially if mutability or inheritance is required.
This article was originally published on Medium.



