When designing modern scalable server applications, we most often use a multi-layered architecture relying on best patterns and practices.
Designing effective multi-layered architecture is a common way to avoid unnecessary problems and write high-quality code as this simplifies the way the software infrastructure can be controlled. Dividing our codebase into different levels reduces the effort required for maintenance since each level can be managed independently and the required changes are localized, simpler and less extensive than they would otherwise be.
However, this approach may introduce new problems when it comes to creating Data Transfer Object (DTO) most commonly used to transfer data between Service layer and the UI layer.
DTO
Main benefits in using DTO is that it reduces the amount of data needed to be sent across the wire in distributed applications and removes the coherence of the data models of different layers of the application or other microservice.
Another use for DTOs can be to encapsulate parameters for method calls.
As Martin Fowler explains, DTO should be represent by a class that contains some or all of the parameters from one or more original domain classes.
“DTOs can help us to keep the integrity of data on Java applications.” — Martin Fowler
DTO classes should not include any business logic nor fields containing sensitive data (user passwords, details about credit cards, etc) therefore preventing potential leakage on the client side. Without DTO, the presentation and the domain layers are tightly coupled.
DTO says, since each call to any remote interface is expensive, response to each call should bring as much data as possible. So, if multiple requests are required to bring data for a particular task, data can be combined in a DTO so that only one request can bring all the required data.
Mapping with DtoMapper
When using the DTO pattern, you would also make use of DTO builders/mappers. Builders are used to create DTOs from Domain Objects, and vice versa. The conversion from Domain Object to DTO and back again can be a costly process, an additional cost is the boilerplate code of mapping. This would have some performance impact which you can optimize, but has the benefit of structural isolation between layers.
In the following article, we will see how DtoMapper helps us solving these issues and how we can use it in combination with lazy load in order to preserve the performance of our server application.
Example
We will create an application for assigning and tracking user tasks. We will use a Spring Boot that exposes a REST API and we will read the data from the H2 database.
And add maven dependency in pom.xml file:
<dependency>
<groupId>com.github.igoricelic</groupId>
<artifactId>dtomapper</artifactId>
<version>1.0.2</version>
</dependency>
To simplify the example as much as possible, we will have only two entities — the user and the task.
The task is created by the manager and assigned to one or more users for processing. The system will not distinguish the manager from the other users, so there are no roles defined.
Now, we can create an entities and corresponding DTO classes:
public class User {
@Id
private Long id;
private String fullName;
@Column(unique = true)
private String username;
@ManyToMany
private List<Task> tasks;
// constructor, get and set methods
}
public class UserDto {
private Long id;
private String fullName;
private String username;
@Property(depth = 1)
private List<TaskDto> tasks;
// constructor, get and set methods
}
and the task:
public class Task {
@Id
private Long id;
private String title;
private String description;
private LocalDateTime deadline;
@ManyToOne
@JoinColumn(name = "manager_id")
private User manager;
@ManyToMany(mappedBy = "tasks")
private List<User> employees;
// constructor, get and set methods
}
public class TaskDto {
private Long id;
private String title;
private String description;
private LocalDateTime deadline;
@Property(path = "manager.fullName")
private String manager;
@Property(depth = 1)
private List<UserDto> employees;
// constructor, get and set methods
}
We create endpoints that return paginated users with basic details (without tasks), but also an individual user from the database with all details.
@GetMapping
public Page<UserDto> findAll(Pageable pageable) {
return userService.findAll(pageable);
}
@GetMapping("/{id}")
public UserDto findById(@PathVariable Long id) {
return userService.findById(id);
}
As you can see same DTO wrapper, but with a different data set, it will return both endpoints. Let’s now look at the service layer and the mapping process itself:
// service dependencies
private final Mapper mapper;
private final UserDao userDao;
@Override
public Page<UserDto> findAll(Pageable pageable) {
// fetch results from database
Page<User> results = userDao.findAll(pageable);
// map results to desired type and return results as page
return results.map(user -> mapper.map(user, UserDto.class));
}
@Override
public UserDto findById(Long id) {
// fetch user if exists, if doesn't exist -> throw exception
User result = userDao.findById(id)
.orElseThrow(RuntimeException::new);
// map result to desired type with depth
UserDto mappedResult = mapper.map(result, 1, UserDto.class);
// return result
return mappedResult;
}
The key is the ‘depth’ parameter which we use to regulate which properties we want to map.
All project code for this example can be found on the github repo:
igoricelic/webapp_dtomapper
github.com
Conclusion
In this article, we reviewed the Data Transfer Object Design Pattern with its pros & cons and learned how to use DtoMapper library.
DtoMapper was created about a year ago inspired by the shortcomings of existing libraries.
Basic DtoMapper features such as response customization with reusing the DTO classes and keeping lazy load performance of ORM, or the ability to manipulate with properties by relative path for the respective root, make DtoMapper better than similar open source tools.
Source: Medium - by Igor Icelic
The Tech Platform
Comments