Tight Coupling Between Service and DTO in Spring Boot

In Spring Boot, it's tempting to directly pass data from a service to a controller using DTOs (Data Transfer Objects). However, this approach leads to tight coupling between the service and a specific DTO. The service "knows" about the DTO's structure and is responsible for creating it. This limits code flexibility, reusability, and testability. Modifying the DTO requires changes to the service, and the need to provide different data representations (different DTOs) leads to duplication of logic within the service.

This is particularly relevant when DTOs are generated automatically, for example, from an OpenAPI (Swagger) specification. In this case, the service should *not* directly depend on the generated DTO classes, as they may change when the API is modified. The service should operate on internal domain models (Entities), and the transformation into DTOs should occur at the service boundary, for example, in the controller or a dedicated mapper class.

The Solution: A Generic Service with Transformation via Functional Interfaces

We propose an approach based on using a generic method in the service and functional interfaces for data transformation. The service retrieves a User from the database and passes it to a function that transforms the User into the desired DTO (e.g., UserDto).

Code Example:

@Service public class UserService {

public Optional getUser(Long userId, Function mapper) { return userRepository.findByIdWithAddresses(userId) .map(mapper); } }

In the Controller:

@RestController public class UserController {

@GetMapping("/users/{userId}") public ResponseEntity getUser(@PathVariable Long userId) { return userService.getUser(userId, UserDto::fromUser) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } }

Benefits:

* Separation of Concerns: The service is not responsible for DTO creation, only for providing the data. * Independence from External DTOs: The service is independent of DTOs generated from OpenAPI or other external sources. This allows the API to evolve without requiring changes to the service logic. * Flexibility: You can easily add new DTOs and data transformation strategies. * Reusability: The service can be used to obtain different data representations. * Testability: The service is easily testable because it does not depend on a specific DTO.

Using the method reference UserDto::fromUser allows you to elegantly pass the data transformation method to the service. This significantly improves the application architecture and simplifies further maintenance and development of the project.