Skip to content
thesarfo

Concept

DTOs, Controllers, Services & Mappers

How DTOs, controllers, services, and mappers relate in a Spring app, worked through a request DTO and a separate response DTO for the same entity.

views 0

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:

  1. Controllers receive requests in an API-friendly format (JSON/XML), often mapped to a DTO.
  2. Controllers pass DTOs to services.
  3. Services use DTOs, or pull data from repositories based on DTO information.
  4. Services may use mappers to convert DTOs to entities before domain operations.
  5. Services apply business logic, possibly calling other services/repositories.
  6. To return data, services may convert entities back to DTOs via mappers.
  7. Controllers receive DTOs (possibly converted from entities) from services and build the response body.
  8. 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);
}