Sometimes you return a JPA entity straight from a Spring Boot endpoint and everything seems fine — until later, when unexpected data leaks or performance issues make you regret it.
Let’s fix that with DTO.
DTO
s (Data Transfer Objects) are the clean, maintainable way to move data between your backend and clients. And ModelMapper makes it painless. Here's how to do it right — without the hidden pitfalls.
Why Skipping DTOs is a Trap
Returning JPA entities directly might seem harmless in small projects, but it backfires fast:
- Data leaks → Your API exposes fields like salary, password, or internal IDs.
- Tight coupling → Changing your database schema breaks your API (and vice versa).
- Performance surprises → Lazy-loaded relationships trigger unexpected SQL queries.
The fix? Always use DTOs to control exactly what leaves your backend.
What’s a DTO?
A DTO is a plain Java object — just fields, getters, and setters. No JPA annotations, no business logic. Its only job: carry data safely between layers.
Example: Entity vs. DTOs
JPA Entity (Database Layer):
@Entity
public class Employee {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
private double salary; // ⚠️ Sensitive field!
// getters & setters
}
DTOs (API Layer)
We create two separate DTOs — one for requests (input) and one for responses (output):
// For API requests (e.g., POST /employees)
public class EmployeeRequestDTO {
private String name;
private String email;
// getters & setters
}
// For API responses (e.g., GET /employees/1)
public class EmployeeResponseDTO {
private Long id;
private String name;
private String email;
// getters & setters
}
Key Point:
-
EmployeeRequestDTO
omitsid
(auto-generated) andsalary
(internal). -
EmployeeResponseDTO
omitssalary
but includesid
(needed by clients).
Setting Up ModelMapper
ModelMapper automates mapping between DTOs and entities. Add it to your pom.xml
:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>LAST VERSION</version>
</dependency>
Then configure it as a Spring Bean:
@Configuration
public class ModelMapperConfig {
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
Watch Out:
ModelMapper matches fields by name. If your DTO and entity fields differ, add explicit mappings (or use @Mapping
annotations).
Service Layer: Where Mapping Happens
Best Practice: Keep mapping logic in the service layer, not the controller.
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Autowired
private ModelMapper modelMapper;
// Convert Entity → Response DTO
private EmployeeResponseDTO toResponseDto(Employee employee) {
return modelMapper.map(employee, EmployeeResponseDTO.class);
}
// Convert Request DTO → Entity
private Employee toEntity(EmployeeRequestDTO dto) {
return modelMapper.map(dto, Employee.class);
}
public EmployeeResponseDTO getEmployee(Long id) {
Employee employee = employeeRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Employee not found"));
return toResponseDto(employee); // ✅ Always return a DTO
}
public EmployeeResponseDTO createEmployee(EmployeeRequestDTO dto) {
Employee employee = toEntity(dto);
Employee saved = employeeRepository.save(employee);
return toResponseDto(saved);
}
}
Controller: DTOs Only
Your controller should never see JPA entities — only DTOs:
@RestController
@RequestMapping("/employees")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/{id}")
public ResponseEntity<EmployeeResponseDTO> getEmployee(@PathVariable Long id) {
EmployeeResponseDTO response = employeeService.getEmployee(id);
return ResponseEntity.ok(response);
}
@PostMapping
public ResponseEntity<EmployeeResponseDTO> createEmployee(@RequestBody EmployeeRequestDTO dto) {
EmployeeResponseDTO response = employeeService.createEmployee(dto);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(response);
}
}
Why This Structure Wins
- No accidental data leaks (thanks to separate request/response DTOs).
- Clean separation of concerns (controllers handle HTTP, services handle business logic).
- Easy to maintain (mapping logic is centralized). Less boilerplate (ModelMapper beats manual field copying).
Final Tips
- For high-performance apps, try MapStruct (compile-time mapping).
- Validate DTOs (use
@Valid
and Jakarta Bean Validation). - Keep DTOs immutable (use
record
in Java 17+).
Top comments (0)