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:
name
andage
- 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
final
and 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.Record
implicitly.
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", "john@example.com"); } 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.