How I Made My Spring Boot DTOs Immutable Using Java Records and ModelMapper


The Journey from Mutable DTOs to Immutable Java Records
As a Java developer, I'm always looking for ways to improve my code quality. Recently, while working on a Spring Boot REST API, I came across an intriguing blog post: DTOs must be immutable.
This got me thinking—why should DTOs be immutable?
The Problem with Mutable DTOs
Initially, I was using traditional Java POJOs for my DTOs, with getters and setters, and mapping them to my JPA entities using ModelMapper.
A typical DTO looked something like this:
public class UserDTO {
private String name;
private String email;
public UserDTO() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
And I used ModelMapper to map it to my JPA entity:
ModelMapper modelMapper = new ModelMapper();
User user = modelMapper.map(userDTO, User.class);
But here's the issue: DTOs should be immutable to make them thread-safe, predictable, and maintainable. Mutable DTOs can be modified unexpectedly, leading to bugs.
So, I decided to replace my DTOs with Java Records.
The Power of Java Records
Since Java 14, we have records, and since I'm using Java 21, it made perfect sense to adopt them.
A record is a final, immutable class with automatic getters, a constructor, equals, hashCode, and toString methods.
So, I refactored my DTO to:
public record UserDTO(String name, String email) {}
No setters. No boilerplate. Fully immutable.
Now, whenever I need a new DTO, I can just create a new instance, without worrying about accidental modifications.
But then, I ran into a new problem…
ModelMapper Doesn't Support Java Records
When I tried using ModelMapper to convert my UserDTO
to a User
entity, it threw an error because ModelMapper doesn't support Java records out of the box.
ModelMapper modelMapper = new ModelMapper();
User user = modelMapper.map(new UserDTO("John Doe", "john@example.com"), User.class);
Boom! It failed!
The Solution: modelmapper-module-record
After searching for a solution, I stumbled upon a StackOverflow answer (source) that mentioned a new ModelMapper module for records.
Steps to Fix the Issue
Add Dependency to
build.gradle
To enable record mapping, I added this dependency:
implementation 'org.modelmapper:modelmapper-module-record:1.0.0'
Register the Record Module in Spring Boot
In my Spring Boot configuration class, I registered the RecordModule with ModelMapper:
import org.modelmapper.ModelMapper;
import org.modelmapper.module.record.RecordModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ModelMapperConfig {
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.registerModule(new RecordModule()); // Enable Record Support
return modelMapper;
}
}
Now, ModelMapper magically supports Java records! 🎉
Final Working Code
Here’s the complete Spring Boot setup:
DTO Using Java Record
public record UserDTO(String name, String email) {}
JPA Entity
import jakarta.persistence.*;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and Setters
}
ModelMapper Configuration
@Configuration
public class ModelMapperConfig {
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.registerModule(new RecordModule()); // Enable Record Support
return modelMapper;
}
}
Converting DTO to Entity
@Service
public class UserService {
private final ModelMapper modelMapper;
public UserService(ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
public User convertToEntity(UserDTO userDTO) {
return modelMapper.map(userDTO, User.class);
}
}
The Result
Now, when I call:
UserDTO dto = new UserDTO("John Doe", "john@example.com");
User user = modelMapper.map(dto, User.class);
It works flawlessly.
Switching to Java records for DTOs made my code cleaner, safer, and more maintainable. Thanks to ModelMapper's new record module, mapping DTOs to JPA entities is now effortless.
If you’re using Java 17+ and Spring Boot, I highly recommend adopting Java records for your DTOs. It’s a small change with big benefits!
Have thoughts on this? Let’s discuss in the comments!
Follow me for more Spring Boot & Java insights!
Subscribe to my newsletter
Read articles from Niaz Bin Siraj directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
