Skillversitas

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

  1. One DTO per operation — not one DTO for everything
  2. Validate in the DTO — use @Valid in the controller
  3. Don't return entities — always return a response DTO
  4. Use MapStruct — for larger projects
  5. 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.