Record
What Is a Java Record
A record is a special Java class designed to store immutable data. It is useful when you need a simple data carrier, such as:
- DTO
- API response object
- Configuration object
- Query result object
- Value object
- Test data object
The record automatically gives:
- Private final fields
- Constructor
- Getter-like accessor methods
equals(): compares records by field valueshashCode(): hash based on field value
Syntax
// class definition
public record UserResponse( Long id, String username, String email ) {}
// object creation
UserResponse user = new UserResponse(1L, "david", "david@example.com");
// access fields
user.id();Record vs. Normal Class
| Normal class | Record | |
|---|---|---|
| Boilerplate | More code | Much less code |
| Fields | Manually declared | Declared in record header |
| Constructor | Manually written | Generated automatically |
| Getters | Usually getName() | Accessors like name() |
| Immutability | Manual | Built in for fields |
| equals() / hashCode() | Manual or generated by IDE/Lombok | Generated automatically |
| Best for | Objects with behavior or mutable state | Simple immutable data carriers |
Constructor
A record automatically has a constructor.
- Compact constructor
A compact constructor lets you add validation or normalization.
public record UserRequest(
String username,
String email
) {
public UserRequest {
if (Objects.isNull(username) || username.isBlank()) {
throw new IllegalArgumentException("Username is required");
}
if (Objects.isNull(email) || email.isBlank()) {
throw new IllegalArgumentException("Email is required");
}
username = username.trim();
email = email.trim().toLowerCase();
}
}- Canonical constructor
You can also write the full constructor. Usually, prefer the compact constructor unless you need full control.
Adding Methods to Records
- Object methods
Records can contain simple helper methods, but avoid putting heavy business logic inside records. Business logic belongs in service layer.
public record UserResponse(
Long id,
String username,
String email
) {
// this is okay
public String displayName() {
return username + " <" + email + ">";
}
// this is not good
public boolean canRegister(UserRepository userRepository) {
return !userRepository.existsByEmail(email);
}
}- Static methods and constants
Records can have static methods and constants. It is usually useful for factory methods or test data.
Inheritance
Records cannot extend another class, because records already implicitly extend java.lang.Record.
However, records can implement interfaces, which is useful when several response types share common behaviors.
public interface ApiResponse {
String type();
}
public record UserResponse(
Long id,
String username,
String email
) implements ApiResponse {
@Override
public String type() {
return "USER";
}
}Defensive Copying
Records fields are automatically private final, so there are not setters. This means records are good when the object should not change after creation. However, records are shallowly immutable for collection fields, so best practice:
public record TeamResponse(
String name,
List<String> members
) {
public TeamResponse {
members = List.copyOf(members);
}
}