1. Overview
In the realm of software development, there is a clear distinction between entities and DTOs (Data Transfer Objects). Understanding their precise roles and differences can help us build more efficient and maintainable software.
In this article, we’ll explore the differences between entities and DTOs and try to offer a clear understanding of their purpose, and when to employ them in our software projects. While going through each concept, we’ll sketch a trivial application of user management, using Spring Boot and JPA.
2. Entities
Entities are fundamental components that represent real-world objects or concepts within the domain of our application. They often correspond directly to database tables or domain objects. Therefore, their primary purpose is to encapsulate and manage the state and behavior of these objects.
2.1. Entity Example
Let’s create some entities for our project, representing a user that has multiple books. We’ll start by creating the Book entity:
@Entity
@Table(name = "books")
public class Book {
@Id
private String name;
private String author;
// standard constructors / getters / setters
}
Now, we need to define our User entity:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String address;
@OneToMany(cascade=CascadeType.ALL)
private List<Book> books;
public String getNameOfMostOwnedBook() {
Map<String, Long> bookOwnershipCount = books.stream()
.collect(Collectors.groupingBy(Book::getName, Collectors.counting()));
return bookOwnershipCount.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
}
// standard constructors / getters / setters
}
2.2. Entity Characteristics
In our entities, we can identify some distinctive characteristics. In the first place, entities commonly incorporate Object-Relational Mapping (ORM) annotations. For instance, the @Entity annotation marks the class as an entity, creating a direct link between a Java class and a database table.
The @Table annotation is used to specify the name of the database table associated with the entity. Additionally, the @Id annotation defines a field as the primary key. These ORM annotations simplify the process of database mapping.
Moreover, entities often need to establish relationships with other entities, reflecting associations between real-world concepts. A common example is the @OneToMany annotation we’ve used to define a one-to-many relationship between a user and the books he owns.
Furthermore, entities don’t have to serve solely as passive data objects but can also contain domain-specific business logic. For instance, let’s consider a method such as getNameOfMostOwnedBook(). This method, residing within the entity, encapsulates domain-specific logic to find the name of the book the user owns the most. This approach aligns with OOP principles and the DDD approach by keeping domain-specific operations within entities, fostering code organization and encapsulation.
Additionally, entities may incorporate other particularities, such as validation constraints or lifecycle methods.
3. DTOs
DTOs primarily act as pure data carriers, without having any business logic. They’re used to transmit data between different applications or parts of the same application.
In simple applications, it’s common to use the domain objects directly as DTOs. However, as applications grow in complexity, exposing the entire domain model to external clients may become less desirable from a security and encapsulation perspective.
3.1. DTO Example
To keep our application as simple as possible, we will implement only the functionalities of creating a new user and retrieving the current users. To do so, let’s start by creating a DTO to represent a book:
public class BookDto {
@JsonProperty("NAME")
private final String name;
@JsonProperty("AUTHOR")
private final String author;
// standard constructors / getters
}
For the user, let’s define two DTOs. One is designed for the creation of a user, while the second one is tailored for response purposes:
public class UserCreationDto {
@JsonProperty("FIRST_NAME")
private final String firstName;
@JsonProperty("LAST_NAME")
private final String lastName;
@JsonProperty("ADDRESS")
private final String address;
@JsonProperty("BOOKS")
private final List<BookDto> books;
// standard constructors / getters
}
public class UserResponseDto {
@JsonProperty("ID")
private final Long id;
@JsonProperty("FIRST_NAME")
private final String firstName;
@JsonProperty("LAST_NAME")
private final String lastName;
@JsonProperty("BOOKS")
private final List<BookDto> books;
// standard constructors / getters
}
3.2. DTO Characteristics
Based on our examples, we can identify a few particularities: immutability, validation annotations, and JSON mapping annotations.
Making DTOs immutable is a best practice. Immutability ensures that the data being transported is not accidentally altered during its journey. One way to achieve this is by declaring all properties as final and not implementing setters. Alternatively, the @Value annotation from Lombok or Java records, introduced in Java 14, offers a concise way to create immutable DTOs.
Moving on, DTOs can also benefit from validation, to ensure that the data transferred via the DTOs meets specific criteria. This way, we can detect and reject invalid data early in the data transfer process, preventing the pollution of the domain with unreliable information.
Moreover, we may usually find JSON mapping annotations in DTOs, to map JSON properties to the fields of our DTOs. For example, the @JsonProperty annotation allows us to specify the JSON names of our DTOs.
4. Repository, Mapper, and Controller
To demonstrate the utility of having both entities and DTOs represent data within our application, we need to complete our code. We’ll start by creating a repository for our User entity:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
Next, we’ll proceed with creating a mapper to be able to convert from one to another:
public class UserMapper {
public static UserResponseDto toDto(User entity) {
return new UserResponseDto(
entity.getId(),
entity.getFirstName(),
entity.getLastName(),
entity.getBooks().stream().map(UserMapper::toDto).collect(Collectors.toList())
);
}
public static User toEntity(UserCreationDto dto) {
return new User(
dto.getFirstName(),
dto.getLastName(),
dto.getAddress(),
dto.getBooks().stream().map(UserMapper::toEntity).collect(Collectors.toList())
);
}
public static BookDto toDto(Book entity) {
return new BookDto(entity.getName(), entity.getAuthor());
}
public static Book toEntity(BookDto dto) {
return new Book(dto.getName(), dto.getAuthor());
}
}
In our example, we’ve done the mapping manually between entities and DTOs. For more complex models, to avoid boilerplate code, we could’ve used tools like MapStruct.
Now, we only need to create the controller:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping
public List<UserResponseDto> getUsers() {
return userRepository.findAll().stream().map(UserMapper::toDto).collect(Collectors.toList());
}
@PostMapping
public UserResponseDto createUser(@RequestBody UserCreationDto userCreationDto) {
return UserMapper.toDto(userRepository.save(UserMapper.toEntity(userCreationDto)));
}
}
Note that using findAll() can impact performance with large collections. Including something like pagination can help in these cases.
5. Why Do We Need Both Entities and DTOs?
5.1. Separation of Concerns
In our example, the entities are closely tied to the database schema and domain-specific operations. On the other hand, DTOs are designed only for data transfer purposes.
In some architectural paradigms, such as hexagonal architecture, we may find an additional layer, commonly referred to as the Model or Domain Model. This layer serves the crucial purpose of totally decoupling the domain from any intrusive technology. This way, the core business logic remains independent of the implementation details of databases, frameworks, or external systems.
5.2. Hiding Sensitive Data
When dealing with external clients or systems, controlling what data is exposed to the outside world is essential**.** Entities may contain sensitive information or business logic that should remain hidden from external consumers. DTOs act as a barrier that helps us expose only safe and relevant data to the clients.
5.3. Performance
The DTO pattern, as introduced by Martin Fowler, involves batching up multiple parameters in a single call. Instead of making multiple calls to fetch individual pieces of data, we can bundle related data into a DTO and transmit it in a single request. This approach reduces the overhead associated with multiple network calls.
One way of implementing the DTO pattern is through GraphQL, which allows the client to specify the data it desires, allowing multiple queries in a single request.
6. Conclusion
As we’ve learned throughout this article, entities and DTOs have different roles and can be very distinct. The combination of both entities and DTOs ensures data security, separation of concerns, and efficient data management in complex software systems. This approach leads to more robust and maintainable software solutions.
As always, the source code is available over on GitHub.