What Are Data Transfer Objects (DTOs), and How Do You Use Them in Java Spring?
In modern software development, especially in layered architectures, the way data moves between layers matters. Data Transfer Objects (DTOs) are simple objects dedicated to carrying that data. They can improve an application’s performance, security, and maintainability.
This article explains what DTOs are, why they matter, and how to create and use them in Java Spring Boot, from manual mapping to dedicated libraries.
What Is a DTO?
A DTO is a design pattern for transferring data between parts of an application, such as the presentation and business-logic layers, or between services. Its original purpose is to group meaningful data into a single remote call instead of requiring many separate calls.
A DTO typically:
- Contains fields, accessors such as getters and setters, and constructors.
- Has no business logic; its job is to carry data.
- Is implemented as a Plain Old Java Object, or POJO.
Why Use DTOs?
The benefits go beyond keeping code tidy.
1. Data Privacy and Security
Database entities often include internal fields that clients should not see. A User entity might contain password, createdAt, or internalNotes. Returning that entity directly from an API can expose these fields.
A DTO lets you select only what the client needs, such as id, username, and profileImageUrl. This separates the API from the internal data model and helps keep sensitive data out of responses.
2. Performance and Network Overhead
DTOs are useful when a response combines information from several entities. Suppose a page needs a blog post and its author. Instead of sending separate Post and Author objects, you can return one PostWithAuthorDTO containing the required fields. The client receives the data in one API call without assembling it itself.
3. A Stable API Contract
Entities change as business logic and database schemas evolve. If an API exposes them directly, an internal change—such as renaming a field—can break the contract and force clients to update.
DTOs provide a boundary between the API contract and the database model. You can change the internal model while keeping the public structure stable, reducing coupling between layers.
4. Client-Specific Data Structures
The shape a client needs may differ considerably from the database schema. DTOs make it possible to present information in a convenient format without redesigning persistence around each client.
Creating and Using DTOs in Spring Boot
There are several ways to map entities to DTOs. Consider a Product entity in an e-commerce application.
Example Product entity:
This entity includes public fields and internal fields such as cost. Accessors are omitted from these class sketches for brevity.
@Entitypublic class Product { @Id @GeneratedValue private Long id;
private String name; private String description; private double price; private double cost; private int stock; private boolean isActive; private LocalDateTime createdAt;}Target ProductDTO:
Only the necessary, client-visible information is included.
public class ProductDTO { private Long id; private String name; private String description; private double price; private int stock;}Method 1: Manual Mapping
The simplest approach is to write the conversion yourself. This works well for small projects or mappings with specialized logic.
public class ProductMapper { public static ProductDTO toDTO(Product product) { if (product == null) { return null; } ProductDTO dto = new ProductDTO(); dto.setId(product.getId()); dto.setName(product.getName()); dto.setDescription(product.getDescription()); dto.setPrice(product.getPrice()); dto.setStock(product.getStock()); return dto; }
public static Product toEntity(ProductDTO dto) { if (dto == null) { return null; } Product product = new Product(); product.setName(dto.getName()); product.setDescription(dto.getDescription()); product.setPrice(dto.getPrice()); product.setStock(dto.getStock()); return product; }}Mapping back to an entity does not necessarily set every field. Fields such as id and createdAt are often managed elsewhere.
Use in a service, assuming the repository has been injected:
public ProductDTO getProductById(Long id) { Product product = productRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found")); return ProductMapper.toDTO(product);}Advantages:
- No extra library is required.
- You have complete control over the mapping.
Disadvantages:
- Large objects create repetitive, error-prone boilerplate.
- Adding or removing fields requires manual mapper updates.
Method 2: Automatic Mapping with MapStruct
MapStruct is a compile-time code generator. It produces concrete mapper implementations from interfaces described with annotations.
1. Add the dependencies to pom.xml:
<dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.5.Final</version> </dependency></dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.10.1</version> <configuration> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.5.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins></build>2. Define the mapper interface:
@Mapper(componentModel = "spring")public interface ProductMapper { ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);
ProductDTO toDTO(Product product);
Product toEntity(ProductDTO productDTO);}MapStruct generates an implementation during compilation. Fields with matching names are mapped automatically. With componentModel = "spring", the generated mapper can be injected as a Spring bean.
Use in a service:
@Servicepublic class ProductService { private final ProductRepository productRepository; private final ProductMapper productMapper;
public ProductService(ProductRepository productRepository, ProductMapper productMapper) { this.productRepository = productRepository; this.productMapper = productMapper; }
public ProductDTO getProductById(Long id) { Product product = productRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found")); return productMapper.toDTO(product); }}Advantages:
- Eliminates repetitive mapping code.
- Generates fast code at compile time without reflection.
- Provides type safety, with mapping errors caught during compilation.
Disadvantage:
- Initial setup is a little more involved than manual mapping.
Advanced DTO Patterns: Composite DTOs
Sometimes one DTO needs to combine data from several sources. An order-details view might need the order itself, customer information, and a list of products.
public class CustomerDTO { private Long id; private String fullName;}
public class OrderDetailsDTO { private Long orderId; private String orderStatus; private LocalDateTime orderDate; private CustomerDTO customer; private List<ProductDTO> products;}OrderDetailsDTO combines data from Order, Customer, and Product entities into one structure. Nested DTOs let the client obtain everything it needs in one request.
Conclusion
DTOs are an important part of modern software architecture. By controlling which data crosses a boundary, shaping responses, and reducing coupling, they help make applications more secure and maintainable. Start with manual mapping where it fits, then consider a tool such as MapStruct as the project grows. A well-designed DTO layer is a valuable investment in the long-term health of an application.
Have a question or something to add? Send me a note ↗ .