DTO Pattern
What Is a DTO?
A DTO (Data Transfer Object) is an object that carries data between layers without any business behavior. A DTO separates the data representation from the business entity.
In Spring Boot, DTOs are very important because:
- Entities often have circular (bidirectional) relationships
- Entities have internal fields that should not be exposed
- API consumers need a different data format than the database
- Security: don't expose sensitive fields such as the password
Why Not Return the Entity Directly?
// ❌ Don't do this
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String password; // LEAKED to the API!
private String salt; // LEAKED to the API!
@OneToMany(mappedBy = "user")
private List<Order> orders; // Circular reference, infinite loop!
}
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // Password & salt are exposed!
}
}
Main problems:
| Problem | Impact |
|---|---|
| Password is exposed | Security is at risk |
| Circular reference | JSON serialization error |
| Not flexible | API consumers can't request specific fields |
| Tight coupling | Entity changes affect the API |
Request DTO vs Response DTO
Separate the DTOs for requests and responses:
// UserRequestDTO - for Create/Update
public class UserRequestDTO {
private String name;
private String email;
private String password;
// getters and setters
}
// UserResponseDTO - for the API response
public class UserResponseDTO {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
// getters and setters
}
// UserController using DTOs
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<UserResponseDTO> createUser(
@RequestBody @Valid UserRequestDTO request) {
UserResponseDTO response = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponseDTO> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
@GetMapping
public ResponseEntity<List<UserResponseDTO>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
}
Manual Mapping
The simplest approach, with no extra libraries:
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserResponseDTO createUser(UserRequestDTO request) {
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setPassword(request.getPassword());
User saved = userRepository.save(user);
return mapToResponse(saved);
}
@Override
public UserResponseDTO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
return mapToResponse(user);
}
private UserResponseDTO mapToResponse(User user) {
UserResponseDTO dto = new UserResponseDTO();
dto.setId(user.getId());
dto.setName(user.getName());
dto.setEmail(user.getEmail());
dto.setCreatedAt(user.getCreatedAt());
return dto;
}
}
Pros: no extra dependencies.
Cons: lots of boilerplate, easy to forget a field.
Mapping with MapStruct
MapStruct generates mapper implementations at compile time:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserResponseDTO toResponseDTO(User user);
User toEntity(UserRequestDTO requestDTO);
List<UserResponseDTO> toResponseDTOList(List<User> users);
}
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public UserServiceImpl(UserRepository userRepository,
UserMapper userMapper) {
this.userRepository = userRepository;
this.userMapper = userMapper;
}
@Override
public UserResponseDTO createUser(UserRequestDTO request) {
User user = userMapper.toEntity(request);
User saved = userRepository.save(user);
return userMapper.toResponseDTO(saved);
}
@Override
public List<UserResponseDTO> getAllUsers() {
List<User> users = userRepository.findAll();
return userMapper.toResponseDTOList(users);
}
}
Add the dependency in pom.xml:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
Complete Example: UserDTO
// DTO for updating the profile
public class UserUpdateDTO {
private String name;
private String email;
// No password - the password is updated separately
}
// Service
@Override
@Transactional
public UserResponseDTO updateUser(Long id, UserUpdateDTO request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
user.setName(request.getName());
user.setEmail(request.getEmail());
User updated = userRepository.save(user);
return userMapper.toResponseDTO(updated);
}
Best Practices
- One DTO per operation — not one DTO for everything
- Validate in the DTO — use
@Validin the controller - Don't return entities — always return a response DTO
- Use MapStruct — for larger projects
- Consistent naming —
XxxRequestDTO,XxxResponseDTO
With a proper DTO pattern, your API is safe and well structured. In the next chapter, we'll get to know the H2 Database for development and testing.