In application development, you can use many design patterns, including the DTO (Data Transfer Object) pattern. A DTO is a simple object that usually has only fields with getters and setters and doesn't hold any business logic. The fundamental purpose of a DTO is to provide the necessary data for transfer to a (remote) client.
Why use special objects to transfer data?
Using data transfer objects has the following advantages. They can:
Decouple business logic from the communication layer.
Hide unnecessary data or protect sensitive information.
Avoid multiple calls to the remote server.
Prevent breaking changes in the API during updates of the application model.
Let's elaborate on each of the points!
Decoupling is achieved using different objects — an entity for the business logic and a DTO for the communication layer. Using decoupling, you help yourself and make the application architecture more flexible.
The second advantage of DTOs is no less critical for you as a developer. You may want to hide some database data from the client application for security reasons or redundancy. To that end, you can use a DTO as a filter that will pass only the selected data. In the example below, the domain object has multiple fields. We "filter" them to only four fields with a DTO and limit client application access to sensitive fields.
At the same time, you can use a DTO to combine fields from different domain objects. This way, the DTO helps reduce client calls and passes all data in one shot. The example below shows two separate entities for blogs and authors. We create a BlogDTO object with all the data required for the client application.
As you may have guessed, the DTO pattern also allows us to keep the API contract unchanged when pushing internal changes to the domain model.
The API contract is a document that describes how the API works. The contract should reflect any changes to the API to avoid unexpected behavior from clients that consume it. As an API provider, if you change the connection protocol, the client app won't work. For this reason, any API changes should be communicated and documented. For more details about designing APIs in practice, you can explore Swagger tools.
Implementation: mappers
The DTO pattern implementation has three core items:
A domain object.
A DTO.
A mapper.
We have already discussed the domain and DTO objects. When it comes to a mapper, it is a function that takes a DTO as input and produces a domain object or vice versa. You can write a mapper manually or take an auto-mapper from a library.
Let's learn how to map objects with an example. Suppose you have the following domain object for users:
Java
public class User {
private int id;
private String name;
private String email;
private LocalDate accountCreatedAt;
// constructors, getters and setters
}Kotlin
data class User(
var id: Int,
var name: String,
var email: String,
var accountCreatedAt: LocalDate
)You will find the account creation date on the server side, and the client application doesn't need this information. So our DTO will look like this:
Java
public class UserDTO {
private int id;
private String name;
private String email;
// constructors, getters, and setters
}Kotlin
class UserDTO(
var id: Int,
var name: String,
var email: String
) First, we will learn how to implement the mapping methods manually. It's pretty straightforward: we create a new object and set its fields in compliance with the domain object:
Java
UserDTO convertUserToDTO(User user) {
return new UserDTO(user.getId(), user.getName(), user.getEmail());
}Kotlin
fun convertUserToDTO(user: User): UserDTO {
return UserDTO(user.id, user.name, user.email)
}It makes sense to implement the conversion methods in the business layer of your application because they may contain some logic. To convert a UserDTO object to a User, you need additional information:
Java
User convertDTOToUser(UserDTO dto) {
User user = new User(dto.getId(), dto.getName(), dto.getEmail(), null);
user.setAccountCreatedAt(LocalDate.now());
return user;
}Kotlin
fun convertDTOToUser(dto: UserDTO): User {
val user = User(id = dto.id, name = dto.name, email = dto.email, accountCreatedAt = LocalDate.now())
return user
}Now that we have the conversion methods, let's create a Spring-managed component, that we can use later on. We can do this as follows:
Java
@Component
public class MyMapper {
UserDTO convertUserToDTO(User user) {
return new UserDTO(user.getId(), user.getName(), user.getEmail());
}
User convertDTOToUser(UserDTO dto) {
User user = new User(dto.getId(), dto.getName(), dto.getEmail(), null);
user.setAccountCreatedAt(LocalDate.now());
return user;
}
}Kotlin
@Component
class MyMapper {
fun convertUserToDTO(user: User): UserDTO {
return UserDTO(id = user.id, name = user.name, email = user.email)
}
fun convertDTOToUser(dto: UserDTO): User {
return User(id = dto.id, name = dto.name, email = dto.email, accountCreatedAt = LocalDate.now())
}
}We can now add the mapper to the service constructor and make use of it.
Java
public UserService(UserRepository userRepository, MyMapper myMapper) {
this.userRepository = userRepository;
this.myMapper = myMapper;
}Kotlin
class UserService(
private val userRepository: UserRepository,
private val myMapper: MyMapper
) Let's assume we have a repository with users, and we want to fetch all of them. The following service code implements this:
Java
public List<UserDTO> getAllUsers() {
return userRepository.findAll().stream()
// here we use the mapper to transform a User into a UserDTO
.map(myMapper::convertUserToDTO)
.collect(Collectors.toList());
}Kotlin
fun getAllUsers(): List<UserDTO> {
return userRepository.findAll().map(myMapper::convertUserToDTO)
}So far so good, but we can do better. Rather than creating mappers manually, you can also use a library for mapping objects. For example, it's convenient to use ModelMapper, which can be added to the project as follows:
Gradle
dependencies {
implementation 'org.modelmapper:modelmapper:3.1.0'
} Maven
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.0</version>
</dependency> After including the dependency, you can declare a mapper from the library as a Spring Bean to inject it into our service. We could add the following to the main class annotated with @SpringBootApplication, for instance.
Java
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}Kotlin
@Bean
fun modelMapper(): ModelMapper {
return ModelMapper()
}We will use the ModelMapper bean in the custom mapper, MyMapper, from above. This way, we can easily replace the custom mapping code we wrote with code that makes use of this library.
Java
@Component
public class MyMapper {
private final ModelMapper modelMapper;
public MyMapper(ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
UserDTO convertUserToDTO(User user) {
// here we make use of the 3rd party library to transform a User into a UserDTO
return modelMapper.map(user, UserDTO.class);
}
// convertDTOToUser follows a similar implementation! Give it a try!
}Kotlin
@Component
class MyMapper(private val modelMapper: ModelMapper) {
fun convertUserToDTO(user: User): UserDTO {
// here we make use of the 3rd party library to transform a User into a UserDTO
return modelMapper.map(user, UserDTO::class.java)
}
// convertDTOToUser follows a similar implementation! Give it a try!
}With this approach the service using the mapper doesn't have to be touched, and everything works as before. Cool, right?
Your DTO should have an empty constructor, and each field should have getters and setters to make the mapping work. In the above example, all conversions happen automatically without configuration because the User class has fields with the same names as the UserDTO class. In more complex cases, you can configure the ModelMapper instance.
Lombok vs. Java records
Since Java is a very verbose language, especially for simple classes like DTOs, Project Lombok has become very popular. This library provides annotations for auto-generating constructors, getters, setters, etc., so you don't have to write them yourself. Lombok reduces boilerplate code, but you need to be cautious when working with some parts of it. For example, you should be careful with JPA entities because the @EqualsAndHashCode annotation of Lombok can break HashSet and HashMap usage. So, using Lombok is controversial in the context of persistence. It usually depends on the team conventions if Lombok is used or not. The DTO from the above example looks like this with Lombok:
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class UserDTO {
private int id;
private String username;
private LocalDate dateOfBirth;
}Records were introduced as a preview feature in JDK 14 and finalized in JDK 16. Records provide a convenient way to declare immutable classes. The UserDTO class from before looks as follows as a record:
public record UserDTO(int id, String username, LocalDate dateOfBirth) {
}These two lines of code provide the following:
A private final field for each property.
A getter for each field.
A public constructor with all arguments.
Implementations of the
equalsandhashCodemethods.An implementation of the
toStringmethod.
There is no straightforward answer to which is better, Java records or Lombok. Lombok has some features of Java records. With Lombok, you can configure specific access levels of constructors or fields. It can implement a builder pattern, which might be preferable for classes with many fields. Nevertheless, you might want to use Java records for simple and immutable objects.
It's essential to notice that the DTO pattern implementation doesn't require using Java records, Lombok, ModelMapper, or any other library. These tools only reduce the amount of manual work done by the programmer. A simple Java POJO with manual mapping of the necessary fields will also do.
Conclusion
Data transfer objects are a convenient way of organizing data for the client. If you expect considerable and fast codebase growth, they will help protect the client application from breaking changes in your code. DTOs can also be very useful if you have a complex domain model. Data transfer objects can hide unnecessary or sensitive fields and aggregate multiple entities into one object. You can use Lombok to reduce boilerplate code in your DTO classes or use records with newer versions of Java.