In Spring Boot applications, especially those using REST APIs, you often don’t want to expose your Entity classes directly to the client. That’s where DTOs (Data Transfer Objects) come in. DTOs help control what data is sent or received via the API, making your code cleaner, safer, and easier to maintain. Lets see some best practices in DTO Mapping in Spring Boot.

🎯 What You’ll Learn in This Post
- What is a DTO?
- Why not return entities directly?
- How to map between entities and DTOs
- Manual vs automatic mapping (MapStruct & ModelMapper)
- Best practices for DTO usage
- Full example with code
📦 What is a DTO?
A DTO (Data Transfer Object) is a simple Java class used to carry data between processes (e.g., between the client and server). It contains only fields you want to expose—nothing more, no JPA annotations, no business logic.
Example:
1 2 3 4 5 6 7 8 |
public class UserDTO { private String name; private String email; // Getters and setters } |
🤔 Why Not Expose Entity Directly?
Here’s why returning entities in APIs is a bad idea:
Problem | Description |
---|---|
Exposes internal structure | Entities may contain sensitive/internal fields |
Tight coupling | Changes in DB model break API |
Serialization issues | Circular references (@OneToMany , @ManyToOne ) can cause infinite loops |
Over-fetching data | You send too much unnecessary data |
🔁 Mapping Between Entity and DTO
Let’s use a simple User
entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Entity public class User { @Id @GeneratedValue private Long id; private String name; private String email; private String password; // should not be sent in API // Getters and setters } |
And a DTO version:
1 2 3 4 5 6 7 |
public class UserDTO { private String name; private String email; } |
1️⃣ Manual Mapping (Best for Simple Cases)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class UserMapper { public static UserDTO toDTO(User user) { UserDTO dto = new UserDTO(); dto.setName(user.getName()); dto.setEmail(user.getEmail()); return dto; } public static User toEntity(UserDTO dto) { User user = new User(); user.setName(dto.getName()); user.setEmail(dto.getEmail()); return user; } } |
✅ Pros:
- Full control
- No third-party library
❌ Cons:
- Tedious for large models
- Lots of repetitive code
2️⃣ Using ModelMapper (For Auto Mapping)
Add dependency:
1 2 3 4 5 6 7 8 9 |
<dependency> <groupid>org.modelmapper</groupid> <artifactid>modelmapper</artifactid> <version>3.1.1</version> </dependency> |
Configuration
1 2 3 4 5 6 7 8 9 10 |
@Configuration public class MapperConfig { @Bean public ModelMapper modelMapper() { return new ModelMapper(); } } |
Usage
1 2 3 4 5 6 7 8 9 |
@Autowired private ModelMapper modelMapper; public UserDTO convertToDTO(User user) { return modelMapper.map(user, UserDTO.class); } |
✅ Pros:
- Quick setup
- Reduces boilerplate
❌ Cons:
- Reflection-based (slower)
- Harder to debug
- Needs customization for nested mappings
3️⃣ Using MapStruct (Best for Performance + Clean Code)
Add Maven dependency:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<dependency> <groupid>org.mapstruct</groupid> <artifactid>mapstruct</artifactid> <version>1.5.5.Final</version> </dependency> <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> |
Mapper Interface
1 2 3 4 5 6 7 8 |
@Mapper(componentModel = "spring") public interface UserMapper { UserDTO toDTO(User user); User toEntity(UserDTO dto); } |
Usage
1 2 3 4 5 6 7 |
@Autowired private UserMapper userMapper; UserDTO dto = userMapper.toDTO(user); |
✅ Pros:
- Compile-time generation (fast & type-safe)
- Clean and maintainable code
❌ Cons:
- Learning curve
- Requires build configuration
📌 Best Practices for DTO Mapping
Best Practice | Why It Matters |
---|---|
Never expose entities directly | Avoid security, performance, and coupling issues |
Use separate DTOs for request and response | More flexibility (UserRequestDTO , UserResponseDTO ) |
Validate DTOs, not entities | Clean separation of concerns |
Avoid bi-directional relationships in DTOs | Prevents infinite recursion |
Use MapStruct for large apps | Reduces manual effort and improves performance |
Prefix nested DTOs clearly | Example: OrderDTO → OrderItemDTO , not just ItemDTO |
🧪 Real-World Example: User API
Entity
1 2 3 4 5 6 7 8 9 10 11 |
@Entity public class User { @Id @GeneratedValue private Long id; private String name; private String email; private String password; } |
DTOs
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class UserRequestDTO { private String name; private String email; private String password; } public class UserResponseDTO { private String name; private String email; } |
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @PostMapping public UserResponseDTO createUser(@RequestBody UserRequestDTO dto) { return userService.createUser(dto); } @GetMapping("/{id}") public UserResponseDTO getUser(@PathVariable Long id) { return userService.getUser(id); } } |
Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Service public class UserService { @Autowired private UserRepository userRepository; @Autowired private UserMapper userMapper; public UserResponseDTO createUser(UserRequestDTO dto) { User user = userMapper.toEntity(dto); user = userRepository.save(user); return userMapper.toDTO(user); } public UserResponseDTO getUser(Long id) { return userRepository.findById(id) .map(userMapper::toDTO) .orElseThrow(() -> new RuntimeException("User not found")); } } |
🧹 Summary
Approach | Best For |
---|---|
Manual Mapping | Small projects or specific transformations |
ModelMapper | Quick start for simple apps |
MapStruct | Production-grade, high-performance apps |
📎 Additional Tips
- ✅ Annotate DTO fields with
@NotNull
,@Email
,@Size
, etc. for validation - ✅ Use
@JsonIgnore
on entities for fields you don’t want to serialize - 🚫 Don’t use entities as both request and response objects
- 🧪 Add unit tests for mapping logic if complex