DTO (Data Transfer Object) — a lightweight object for moving data between layers (controller ↔ service, service ↔ external API). Holds a subset of a domain entity’s properties, focused on data exchange rather than business logic — improving separation of concerns and encapsulation.
Controller — the entry point for requests. Receives them, delegates to services, returns
responses. Uses @GetMapping, @PostMapping, etc. for different HTTP verbs. Typically
receives/returns DTOs, keeping the API layer decoupled from the internal domain model.
Service — encapsulates business logic. Talks to repositories, performs calculations/validation, orchestrates functionality. No direct dependency on the web layer. May receive/return DTOs (converted to/from entities) for communicating with other layers.
Mapper — converts between formats (DTO ↔ entity), keeping each layer’s data representation clean and separate.
How they relate:
- Controllers receive requests in an API-friendly format (JSON/XML), often mapped to a DTO.
- Controllers pass DTOs to services.
- Services use DTOs, or pull data from repositories based on DTO information.
- Services may use mappers to convert DTOs to entities before domain operations.
- Services apply business logic, possibly calling other services/repositories.
- To return data, services may convert entities back to DTOs via mappers.
- Controllers receive DTOs (possibly converted from entities) from services and build the response body.
- Controllers return the HTTP response, DTO data serialized to JSON/XML.
Implementing a DTO
Say we have a Student entity holding all of a user’s information. When a request needs a
student as its body, we don’t want to expose the whole entity — a DTO lets us specify only the
fields we actually need.
public record StudentDto( String firstName, String lastName, String email) {}@PostMapping("/students")public Student post( @RequestBody StudentDto student) { return repository.save(student);}The repository expects an actual Student, not a DTO, so we need a conversion method:
private Student toStudent(StudentDto dto) { var student = new Student(); student.setFirstName(dto.firstName()); student.setLastName(dto.lastName()); student.setEmail(dto.email()); return student;}And use it in the controller:
@PostMapping("/students")public Student post( @RequestBody StudentDto studentDto) { var student = toStudent(studentDto); return repository.save(student);}But this still returns the entire Student entity in the response — including anything
sensitive. So we need a second DTO, this time for the response shape:
public record StudentResponseDto( String firstName, String lastName, String email) {}And a mapper the other direction, entity to response DTO:
private StudentResponseDto toStudentResponseDto(Student student) { return new StudentResponseDto( student.getFirstName(), student.getLastName(), student.getEmail() );}Putting it together — request DTO in, entity through the repository, response DTO out:
@PostMapping("/students")public StudentResponseDto post( @RequestBody StudentDto studentDto) { var student = toStudent(studentDto); var savedStudent = repository.save(student); return toStudentResponseDto(savedStudent);}