What are DTOs (Data Transfer Objects)
In enterprise-level Java development, structuring data exchange between different application layers is a critical concern. Applications typically consist of several layers, such as controllers, services, repositories, and occasionally external systems, and it is essential to transfer data between these boundaries in a clean and secure manner.
One of the most popular design patterns for achieving this separation is the Data Transfer Object (DTO) pattern. DTOs provide a clean, efficient, and decoupled way to transfer data without exposing domain models unnecessarily or cluttering the internal architecture. In this article, we will explore what DTOs are, why they are useful, and how to implement them in our applications.
1. Introduction to Data Transfer Objects
A Data Transfer Object (DTO) is an object that carries data between processes or application layers. Unlike domain models (which often contain business logic, validation, or persistence annotations), DTOs are focused solely on carrying data. They typically contain only fields, constructors, getters, setters, and sometimes mapping methods, but no business logic.
In modern applications, DTOs are frequently used in REST APIs. Instead of directly exposing entity classes (such as JPA entities) to clients, you define DTOs to shape the data being exchanged. This ensures that your internal representation of data remains decoupled from the API contract and offers flexibility when requirements evolve.
1.1 Why Use DTOs
Using Data Transfer Objects (DTOs) in Java applications provides several important benefits, especially in layered architectures. DTOs act as intermediaries between different layers such as controllers, services, repositories, and even external systems. They ensure that data passed between these layers remains clean, secure, and well-structured. Without DTOs, applications might expose internal domain models directly, which can create tight coupling, security risks, and unnecessary complexity when evolving the codebase.
DTOs are valuable when working with APIs. For instance, we may not want to expose sensitive information from our entities, such as passwords, internal IDs, or system-specific configurations, directly to API consumers. A DTO allows us to selectively include only the relevant fields, tailoring responses to the needs of the client while keeping sensitive details hidden. This leads to a more controlled and secure data exchange.
Another reason to use DTOs is maintainability. As projects grow, entities often become cluttered with additional fields and relationships that may not be required in all use cases. DTOs let us create lightweight, purpose-specific representations of data, reducing payload sizes in API responses and improving performance. They also help decouple our domain model from the API contract, so we can evolve the internal structure of our entities without breaking external clients.
DTOs also improve clarity in complex applications. When integrating multiple systems, such as combining data from a database and a third-party API, DTOs serve as a unified format for communication, making code easier to read, test, and maintain. Combined with mapping strategies, whether manual or automated using libraries like MapStruct, they provide a clean, reusable way to handle object transformations.
In summary, DTOs improve security, performance, maintainability, and clarity while enabling cleaner separation of concerns across different layers of an application.
2. Defining Domain Models (Entities)
To demonstrate DTO usage, we’ll define two JPA entities: User and Order. These represent domain objects with persistence annotations.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders;
// Constructors, getters and setters
}
@Table(name = "orders")
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String product;
private Double price;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
// Constructors, getters and setters
}
Here, we define two simple entities: User and Order. A User can have many Order objects, establishing a one-to-many relationship. These entities contain fields relevant to persistence and are not designed for direct exposure through APIs. Instead, we will use DTOs to represent them externally.
3. How to Create a DTO
When creating DTOs, the goal is to define lightweight objects that expose only the necessary information to clients. For example, exposing the entire User entity with its orders may be overkill for most API calls. Instead, we can define UserDTO and OrderDTO classes.
public class UserDTO {
private Long id;
private String username;
private String email;
private List<OrderDTO> orders;
public UserDTO() {
}
public UserDTO(Long id, String username, String email, List<OrderDTO> orders) {
this.id = id;
this.username = username;
this.email = email;
this.orders = orders;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public List<OrderDTO> getOrders() {
return orders;
}
public void setOrders(List<OrderDTO> orders) {
this.orders = orders;
}
}
public class OrderDTO {
private Long id;
private String product;
private Double price;
public OrderDTO() {
}
public OrderDTO(Long id, String product, Double price) {
this.id = id;
this.product = product;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getProduct() {
return product;
}
public void setProduct(String product) {
this.product = product;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}
These DTO classes are stripped-down versions of our entities. Notice that the UserDTO references OrderDTO instead of the Order entity. This design ensures that only essential data is shared with clients while keeping persistence concerns separate.
4. Mapping Entities to DTOs
To use DTOs effectively, we need to map between entities and DTOs. There are multiple approaches, including manual mapping, libraries such as ModelMapper, and using MapStruct. For simplicity, we’ll demonstrate manual mapping.
public class UserMapper {
public static UserDTO toDTO(User user) {
if (user == null) {
return null;
}
UserDTO userDto = new UserDTO();
userDto.setId(user.getId());
userDto.setUsername(user.getUsername());
userDto.setEmail(user.getEmail());
userDto.setOrders(toOrderDTOList(user.getOrders()));
return userDto;
}
private static List<OrderDTO> toOrderDTOList(List<Order> orders) {
if (orders == null) {
return null;
}
return orders.stream()
.map(order -> new OrderDTO(
order.getId(),
order.getProduct(),
order.getPrice()
))
.collect(Collectors.toList());
}
public static User toEntity(UserDTO dto) {
if (dto == null) {
return null;
}
User user = new User();
user.setId(dto.getId());
user.setUsername(dto.getUsername());
user.setEmail(dto.getEmail());
if (dto.getOrders() != null) {
List<Order> orders = dto.getOrders().stream()
.map(o -> {
Order order = new Order();
order.setId(o.getId());
order.setProduct(o.getProduct());
order.setPrice(o.getPrice());
order.setUser(user); // maintain relationship
return order;
})
.collect(Collectors.toList());
user.setOrders(orders);
}
return user;
}
}
The above UserMapper class handles conversion between the User entity and the UserDTO. This ensures that persistence objects are not exposed directly to API consumers and keeps the application layers cleanly separated.
The toDTO(User user) method transforms the User entity into a UserDTO. It copies the basic fields (id, username, email) and converts the list of orders into OrderDTO objects using the helper method toOrderDTOList. This way, the client only receives the relevant data in a simplified structure.
The toEntity(UserDTO dto) method performs the reverse mapping, creating a User entity from a UserDTO. It maps the basic fields and reconstructs the orders, making sure to set the parent reference (order.setUser(user)) so that JPA relationships remain consistent.
In summary, the UserMapper acts as a bridge between entities and DTOs, providing bidirectional transformations that simplify data transfer and maintain separation of concerns.
Converting Entities and DTOs
The service layer is where business logic is applied and where conversions between entities and DTOs typically take place. In this example, the UserService class acts as the bridge between the repository (data access) layer and the controller layer. It retrieves data from the database, converts entities into DTOs before returning them, and also accepts DTOs as input, converting them back into entities for persistence.
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<UserDTO> getAllUsers() {
return userRepository.findAll()
.stream()
.map(UserMapper::toDTO)
.collect(Collectors.toList());
}
public UserDTO getUserById(Long id) {
return userRepository.findById(id)
.map(UserMapper::toDTO)
.orElseThrow(() -> new RuntimeException("User not found"));
}
public UserDTO createUser(UserDTO dto) {
User user = UserMapper.toEntity(dto);
User saved = userRepository.save(user);
return UserMapper.toDTO(saved);
}
}
This service provides three main methods. getAllUsers() fetches all users from the repository, converts each entity into a UserDTO, and returns the list. getUserById(Long id) looks up a specific user, maps it to a DTO if found, or throws an exception if not.
Finally, createUser(UserDTO dto) takes a DTO provided by the client, converts it into a User entity, saves it to the database, and then returns the saved record as a UserDTO. This ensures that the application works only with DTOs at its boundaries, while entities remain confined to the persistence layer.
5. Automating DTO Mapping with MapStruct
For larger projects, writing manual mapping code quickly becomes repetitive and error-prone. To simplify this process, libraries such as MapStruct or ModelMapper can be used to automate the conversion between entities and DTOs. This section focuses on MapStruct, a widely used annotation-based library that generates type-safe mapping implementations at compile time, significantly reducing boilerplate and improving maintainability.
To use MapStruct in a Maven-based project, add the following dependencies and plugins:
<dependencies>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler plugin to enable annotation processing -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
With this configuration, MapStruct generates mapper implementations during compilation, eliminating the need for manual conversion logic.
Creating a MapStruct Mapper
Let’s replace the manual UserMapper we wrote earlier with a MapStruct interface.
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDTO toDTO(User user);
User toEntity(UserDTO dto);
List<UserDTO> toDTOList(List<User> users);
List<User> toEntityList(List<UserDTO> userDTOs);
OrderDTO toOrderDTO(Order order);
Order toOrderEntity(OrderDTO orderDTO);
}
MapStruct automatically generates the mapper implementation, eliminating the need for manual coding. When we specify componentModel = "spring" in the @Mapper annotation, the mapper is registered as a Spring bean, allowing it to be injected directly into services. Similarly, if we are working with CDI, we can use componentModel = "cdi" to achieve the same integration within a CDI context.
6. Conclusion
In this article, we explored Data Transfer Objects (DTOs) and their role in keeping application layers clean and maintainable. We built a Spring Boot example showing how to convert entities into DTOs for responses and map DTOs back into entities for persistence. By applying DTOs, we prevent exposing database models directly, simplify APIs, and ensure a clear separation of concerns across the application.
7. Download the Source Code
This article explored what DTOs are in Java and why they matter.
You can download the full source code of this example here: what are DTOs java


Shouldn’t the DTOs be records rather than classes in modern Java?
Hi Nils Weinander,
That’s an excellent point. Records are indeed a strong fit for many DTO use cases in modern Java, as they provide concise and immutable data carriers. However, classes still offer more flexibility when mutability is required.
I appreciate your input. The article needs to be updated in the future to use records.
I really don’t understand why would anyone convert entities to DTO immediately when fetching data and work with DTO instead of entities directly. I understand that DTO as re handy for API and as projections but when I load data, I change entities directly without the need of doing double mapping between DTO and hibernate. I find it as anti pattern.