Introduction
If Java developers were asked one question for the last twenty years, it would probably be:
“Why do I have to write so much boilerplate code?”
Consider a simple DTO.
Before Java Records, creating this class required writing:
- Fields
- Constructor
- Getters
equals()hashCode()toString()
Even though the class often contained no business logic whatsoever.
Example:
public class CustomerDto {
private final Long id;
private final String name;
private final String email;
public CustomerDto(Long id,
String name,
String email){
this.id = id;
this.name = name;
this.email = email;
}
public Long getId(){
return id;
}
public String getName(){
return name;
}
public String getEmail(){
return email;
}
@Override
public boolean equals(...)
@Override
public int hashCode()
@Override
public String toString()
}
Nearly 70 lines of code.
Only three fields.
Most enterprise applications contain hundreds or even thousands of such classes.
Java Records eliminate this boilerplate while preserving immutability and strong typing.
They allow developers to express what data is, instead of spending time writing repetitive infrastructure code.
Learning Objectives
By the end of this article, you will be able to:
- Understand why Records were introduced.
- Learn how Records work.
- Understand immutability.
- Create constructors and validation.
- Use Records with Spring Boot.
- Use Records with Jackson.
- Design REST APIs using Records.
- Understand why Records should not replace JPA entities.
Before Java Records
Typical DTO:
public class EmployeeDto {
private final Long id;
private final String name;
private final String department;
...
}
Most of the code exists only because Java requires it.
Java Records
The same class becomes:
public record EmployeeDto(
Long id,
String name,
String department){
}
Three lines.
Exactly the same meaning.
Far less code.
What Does the Compiler Generate?
The compiler automatically creates:
- Private final fields
- Canonical constructor
- Accessor methods
equals()hashCode()toString()
For example:
EmployeeDto dto =
new EmployeeDto(1L,
"Rahul",
"Engineering");
System.out.println(dto.name());
Notice:
dto.name()
not
dto.getName()
Accessor methods have the same name as the component.
Records Are Immutable
This is one of their biggest strengths.
EmployeeDto dto =
new EmployeeDto(...);
dto.name = "John";
Compilation error.
State cannot be modified after construction.
This makes Records naturally thread-safe when their components are also immutable.
Compact Constructors
Validation can still be performed.
public record Customer(
Long id,
String name){
public Customer{
if(name == null ||
name.isBlank()){
throw new IllegalArgumentException(
"Name cannot be blank");
}
}
}
No field assignments are necessary.
The compiler performs them automatically after validation.
Additional Methods
Records are not limited to data.
Business helper methods are allowed.
public record Money(
BigDecimal amount,
String currency){
public boolean isPositive(){
return amount.signum() > 0;
}
}
Records vs POJOs
| Feature | POJO | Record |
|---|---|---|
| Constructor | Manual | Generated |
| Getters | Manual | Generated |
| equals/hashCode | Manual | Generated |
| toString | Manual | Generated |
| Mutable | Usually | No |
| Boilerplate | High | Minimal |
Enterprise Example – REST Request
public record CreateCustomerRequest(
String firstName,
String lastName,
String email){
}
Spring Boot automatically binds JSON into Records.
Example request:
{
"firstName":"Rahul",
"lastName":"Mittal",
"email":"rahul@example.com"
}
No setters required.
REST Response
public record CustomerResponse(
Long id,
String fullName,
String email){
}
Records make excellent response models.
Kafka Events
public record OrderCreatedEvent(
UUID orderId,
Instant createdAt,
BigDecimal amount){
}
Events should be immutable.
Records are a natural fit.
Configuration Objects
Spring Boot configuration:
public record StorageProperties(
String bucket,
String region){
}
Configuration rarely changes after startup, making immutability desirable.
Value Objects
Example:
public record EmailAddress(
String value){
}
Validation:
public EmailAddress{
if(!value.contains("@")){
throw new IllegalArgumentException();
}
}
A Record can model a domain value with validation while remaining immutable.
Records with Jackson
Modern versions of Jackson support Records out of the box.
Serialization:
Record
↓
Jackson
↓
JSON
Deserialization:
JSON
↓
Jackson
↓
Record
No additional getters or setters are required.
Records in Spring Boot
Records work well for:
- Request DTOs
- Response DTOs
- Configuration properties
- Events
- Projections
They reduce boilerplate while making intent explicit.
Records and JPA
This is where many developers become confused.
Consider:
@Entity
public record Customer(...){
}
Although some providers have experimental support for limited scenarios, Records are generally not suitable as JPA entities.
Why?
JPA entities typically require:
- Mutable state
- Lifecycle management
- Lazy loading
- Proxy generation
- No-argument constructors
Records intentionally provide:
- Final components
- Immutability
- Canonical constructors
These design goals conflict with traditional JPA entity requirements.
Instead:
Entity
↓
Mapper
↓
Record DTO
↓
REST API
This separation is considered a best practice.
Records vs Lombok
Many projects use Lombok.
Example:
@Data
@AllArgsConstructor
@NoArgsConstructor
Records eliminate much of this boilerplate without requiring annotation processing.
However, Lombok still provides features that Records do not, such as builders and mutable data classes.
Enterprise Architecture
Database
↓
JPA Entity
↓
Mapper
↓
Record DTO
↓
REST API
↓
Client
This is becoming a common architecture in modern Spring Boot applications.
Common Mistakes
Using Records for Entities
Entities represent mutable persistence state.
Records represent immutable data.
Choose the right abstraction.
Putting Business Services Inside Records
Records may contain helper methods, but complex business logic belongs in domain services.
Assuming Records Replace Every Class
Records are ideal for immutable data carriers.
Regular classes remain appropriate for mutable domain models and services.
Best Practices
✔ Use Records for DTOs.
✔ Use Records for Kafka events.
✔ Use Records for REST APIs.
✔ Use Records for immutable value objects.
✔ Validate data in compact constructors.
✔ Keep JPA entities as regular classes.
Interview Questions
Why were Records introduced?
To reduce boilerplate for immutable data carriers while preserving strong typing.
Are Records immutable?
Yes.
Their components are final and cannot be reassigned after construction.
Can Records contain methods?
Yes.
They can define constructors, helper methods, and implement interfaces.
Should Records replace JPA entities?
Generally no.
Records are best suited for immutable DTOs and value objects, while JPA entities are typically mutable.
What accessor method does a Record generate?
For a component named name, the generated accessor is:
name()
not
getName()
Summary
Records represent one of the most significant improvements to Java’s object model. By eliminating repetitive boilerplate, they allow developers to focus on modeling data rather than writing infrastructure code.
In enterprise applications, Records are an excellent choice for DTOs, REST contracts, messaging events, configuration objects, and immutable value types. They improve readability, reduce maintenance, and integrate seamlessly with modern frameworks such as Spring Boot and Jackson.
Understanding where Records fit—and where they do not—is essential for building clean, maintainable Java applications.
Coming Up Next
Part 34 – Java 15: Sealed Classes – Controlling Inheritance for Better Domain Modeling
We’ll explore another transformative feature that reshapes object-oriented design:
- Why unrestricted inheritance can be problematic
sealed,non-sealed, andfinal- Domain-driven design
- Payment and workflow hierarchies
- Pattern Matching synergy
- Spring Boot considerations
- Sealed Classes vs Interfaces
- Enterprise architecture best practices
This article will lay the foundation for the Pattern Matching features introduced in Java 17–21, where modern Java’s type system becomes significantly more expressive.