Back to Stream: map & Transform
Q14Question 14 of 34

Create a list of EmployeeDTO(name, department) from the employee list using map()

Key Concepts
  • map() can transform one object type to another, constructing a new DTO per element
  • DTO (Data Transfer Object) carries only the fields a particular layer needs, decoupling internal models from APIs
  • The mapping function constructs the DTO: e -> new EmployeeDTO(e.getName(), e.getDepartment())
  • Sensitive fields like salary are excluded by design because the mapping function never reads them
  • In Spring applications, the map step is where entity-to-DTO conversion happens before the API response
  • For large projects, mapping libraries like MapStruct generate the boilerplate, but understanding manual map is essential

Definition

`map` can build a new, slimmer object per element, exposing only the fields a layer should see.
  • 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?

JAVA
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);
    }
}
TEXT
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.

JAVA
// 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.

JAVA
// 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.

JAVA
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.

JAVA
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate