1. Overview
In this tutorial, we'll discuss the DTO pattern, what it is, how, and when to use them. At the end of it, let's hope we'll know how to use it properly.
2. The Pattern
DTOs or Data Transfer Objects are objects that carry data between processes in order to reduce the number of methods calls. The pattern was first introduced by Martin Fowler in his book EAA. He explained that the pattern's main purpose is to reduce roundtrips to the server by batching up multiple parameters in a single call. Therefore reducing the network overhead in such remote operations. Another benefit of this practice is the encapsulation of the serialization's logic (the mechanism that translates the object structure and data to a specific format that can be stored and transferred). It provides a single point of change in the serialization nuances. It also decouples the domain models from the presentation layer, allowing both to change independently.
3. How to Use It?
DTOs normally are created as POJOs. They are flat data structures that contain no business logic, only storage, accessors, and eventually methods related to serialization or parsing. The data is mapped from the domain models to the DTOs, normally through a mapper component in the presentation or facade layer. The image below illustrates the interaction between the components:
4. When to Use It?
As mentioned in its definition, DTOs come in handy in systems with remote calls, as they help to reduce the number of them. They also help when the domain model is composed of many different objects, and the presentation model needs all their data at once or even reduces roundtrip between client and server. With DTOs, we can build different views from our domain models, allowing us to create other representations of the same domain but optimizing them to the clients' needs without affecting our domain design. Such flexibility is a powerful tool to solve complex problems.
5. Use Case
To demonstrate the implementation of the pattern, we'll use a simple application with two main domain models. In this case, User and Role, to focus on the pattern, let's look at 2 examples of functionality, user retrieval, and creation of new ones.
5.1. DTO vs. Domain
Below is the definition of both models:
public class User {
private String id;
private String name;
private String password;
private List<Role> roles;
public User(String name, String password, List<Role> roles) {
this.name = Objects.requireNonNull(name);
this.password = this.encrypt(password);
this.roles = Objects.requireNonNull(roles);
}
// Getters and Setters
String encrypt(String password) {
// encryption logic
}
}
public class Role {
private String id;
private String name;
// Constructors, getters and setters
}
Now let's look at the DTOs so that we can compare them with the Domain models. At this moment is important to notice that the DTO represents the model sent from or to the API client. Therefore, the small differences are either to pack together the request sent to the server or optimize the response of the client:
public class UserDTO {
private String name;
private List<String> roles;
// standard getters and setters
}
The DTO above provides only the relevant information to the client, hiding the password, for example, for security reasons. The next one groups all the data necessary to create a user and sends it to the server in a single request. And mentioned before, this optimizes the interactions with the API. See the code below:
public class UserCreationDTO {
private String name;
private String password;
private List<String> roles;
// standard getters and setters
}
5.2. Connecting Both Sides
Next, the layer that ties both classes uses a mapper component to pass the data from one side to the other and vice-versa. This normally happens in the presentation layer, as shown below:
@RestController
@RequestMapping("/users")
class UserController {
private UserService userService;
private RoleService roleService;
private Mapper mapper;
// Constructor
@GetMapping
@ResponseBody
public List<UserDTO> getUsers() {
return userService.getAll()
.stream()
.map(mapper::toDto)
.collect(toList());
}
@PostMapping
@ResponseBody
public UserIdDTO create(@RequestBody UserCreationDTO userDTO) {
User user = mapper.toUser(userDTO);
userDTO.getRoles()
.stream()
.map(role -> roleService.getOrCreate(role))
.forEach(user::addRole);
userService.save(user);
return new UserIdDTO(user.getId());
}
}
Last, we have the Mapper component that transfers the data making sure that both DTO and domain model doesn't need to know about each other:
@Component
class Mapper {
public UserDTO toDto(User user) {
String name = user.getName();
List<String> roles = user
.getRoles()
.stream()
.map(Role::getName)
.collect(toList());
return new UserDTO(name, roles);
}
public User toUser(UserCreationDTO userDTO) {
return new User(userDTO.getName(), userDTO.getPassword(), new ArrayList<>());
}
}
6. Common Mistakes
Although the DTO pattern is quite a simple design pattern, few mistakes are frequently found in applications implementing this technique. The first one is to create different DTOs for every occasion. That will increase the number of classes and mappers we need to maintain. Try to keep them concise and evaluate the trade-offs of adding one or reusing an existing one. The opposite is also valid. Avoid trying to use a single class for many scenarios. This practice may lead to big contracts where many attributes are frequently not used. Another common mistake is to add business logic to those classes. That should not happen. The purpose of the pattern is to optimize the data transfer and the structure of the contracts. Therefore, all business logic should live in the domain layer. Last but not least, we have the so-called LocalDTOs, where DTOs pass data across domains. The problem once again is the cost of maintenance of all the mapping. One of the most commons arguments in favor of this approach is the encapsulation of the domain model. But actually, the problem here is to have our domain model coupled with the persistence model. By decoupling them, the risk to expose the domain model almost disappears. However, other patterns reach a similar outcome, but they usually are used in more complex scenarios, such as CQRS, Data Mappers, CommandQuerySeparation, etc.
7. Conclusion
In this article, we saw the definition of the DTO Pattern and its reason for existing, and how to implement it. We also saw some of the commons mistakes related to its implementation and way to avoid them. As usual, the source code of the example is available over on GitHub.