Definition
- Object-to-object map: projects a rich domain entity into a slimmer Data Transfer Object.
- DTO purpose: carries only the fields a particular layer needs, no business logic.
- Decoupling: changing a database column does not break the API when DTOs sit between them.
- Security: sensitive fields like salary are excluded because the mapping function never reads them.
- Spring pattern: controllers return DTOs, never entities, to prevent lazy-loading issues and data leaks.
The HR system at the National Bank of India holds a full employee file with salary, address, and bank details, but the public staff webpage should only show name and department. A clerk copies just those two fields onto a simpler summary card for each person. That summary card is the DTO.
Understanding the Concepts
What is a DTO and why use map to create one?
A Data Transfer Object, or DTO, is a plain object whose only job is to carry data across a boundary, from the service layer to an API response, for example. It holds a chosen subset of fields and no business logic. Projecting an entity to a DTO is important because it decouples your internal database model from what you expose externally.
How do you map Employee to EmployeeDTO?
package com.bank.hr;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
public class EmployeeToDTO {
record EmployeeDTO(String name, String department) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Rajesh", "Loans", new BigDecimal("95000")),
new Employee("Priya", "IT", new BigDecimal("120000")),
new Employee("Amit", "Finance", new BigDecimal("85000")));
List<EmployeeDTO> dtos = staff.stream()
.map(e -> new EmployeeDTO(e.getName(), e.getDepartment()))
.collect(Collectors.toList());
dtos.forEach(System.out::println);
}
}
EmployeeDTO[name=Rajesh, department=Loans]
EmployeeDTO[name=Priya, department=IT]
EmployeeDTO[name=Amit, department=Finance]
The salary field is simply never read by the mapping function, so it cannot appear in the output.
How is this different from returning the entity directly?
| Approach | Pros | Cons |
|---|---|---|
| Return entity | Simple, no mapping code | Leaks internal fields, tight API-schema coupling |
| Return DTO via map | Decoupled, secure, shaped response | Requires mapping code |
| MapStruct / ModelMapper | Generated boilerplate | Library dependency, learning curve |
What are common mistakes?
Mistake: returning the entity directly from the controller, leaking sensitive fields.
// WRONG: returning entity exposes salary and internal IDs
@GetMapping("/staff")
public List<Employee> getStaff() {
return employeeRepository.findAll(); // salary visible in JSON!
}
// CORRECT: map to DTO first
@GetMapping("/staff")
public List<EmployeeDTO> getStaff() {
return employeeRepository.findAll().stream()
.map(e -> new EmployeeDTO(e.getName(), e.getDepartment()))
.collect(Collectors.toList());
}
What about using Java records for DTOs?
Java 16+ records are ideal for DTOs because they are immutable, have auto-generated equals, hashCode, and toString, and require minimal boilerplate.
// A record is a compact DTO
public record EmployeeDTO(String name, String department) {}
// Usage is the same
List<EmployeeDTO> dtos = staff.stream()
.map(e -> new EmployeeDTO(e.getName(), e.getDepartment()))
.toList();
What about extracting the mapper to a reusable function?
When the mapping logic is used in multiple places, extract it into a method and use a method reference.
public class EmployeeMapper {
public static EmployeeDTO toDTO(Employee e) {
return new EmployeeDTO(e.getName(), e.getDepartment());
}
}
// Clean method reference
List<EmployeeDTO> dtos = staff.stream()
.map(EmployeeMapper::toDTO)
.collect(Collectors.toList());
What about null fields in the entity?
If the department could be null, the DTO will carry that null, which may cause issues in JSON serialisation. Guard it in the mapper.
List<EmployeeDTO> safe = staff.stream()
.map(e -> new EmployeeDTO(
e.getName(),
e.getDepartment() != null ? e.getDepartment() : "Unassigned"))
.collect(Collectors.toList());
What should a senior engineer know?
- In Spring, keep controllers returning DTOs, never entities, to prevent lazy-loading triggers, circular references, and data leaks.
- For large projects, MapStruct generates mapper implementations at compile time with zero runtime overhead.
- Understanding the manual map is essential because it shows you grasp why the DTO exists: security, decoupling, and response shaping.
- The map step is also the right place to apply formatting, such as masking account numbers.
What are common interview follow-up questions?
- "Why not return the entity directly?" It leaks internal fields and tightly couples the API to the schema.
- "When would you use MapStruct?" When the project has many entities and the manual mapping becomes repetitive.
- "Can you use a constructor reference?" Only if the DTO constructor matches the entity's fields exactly, which is rare.
Visual Overview
Employee (rich entity)
┌──────────────────────────────────┐
│ name: "Rajesh" │
│ department: "Loans" │
│ salary: 95000 ← NOT copied │
│ address: "..." ← NOT copied │
└──────────────────────────────────┘
│
│ map(e -> new EmployeeDTO(name, dept))
▼
EmployeeDTO (slim transfer object)
┌──────────────────────────────────┐
│ name: "Rajesh" │
│ department: "Loans" │
└──────────────────────────────────┘
Salary never read → never exposed
Key Takeaway
To project a rich entity into a slimmer transfer object I use map, where the mapping function constructs a new DTO from only the fields I want to expose. For each employee I built an EmployeeDTO carrying just name and department, so every employee became exactly one DTO, one-to-one, and I collected the result into a list. The salary was simply never read, so it could not appear in the output.
In my banking project at the National Bank of India this was the public staff directory on the intranet. The internal employee record contains salary and other sensitive fields that must never reach that page, so the map step was the deliberate point where I narrowed the full entity down to the two safe fields.
The DTO pattern matters because it decouples the internal model from the external contract, keeps sensitive data from leaking, and shapes the response to exactly what the view needs. In Spring applications I keep controllers returning DTOs and never entities, and on larger projects I use MapStruct, but I always understand the manual map first.
Remember one sentence: map(e -> new EmployeeDTO(name, dept)) projects each entity to a slim DTO, exposing only the fields the API should see.
---