Hello! Welcome to the next lesson in your journey to becoming a backend developer.
In our last lesson on API versioning, we saw how creating different versions of an API often involves creating different Java objects to represent the data for each version (like UserDtoV1 and UserDtoV2). These objects are a practical application of a fundamental design pattern called the Data Transfer Object (DTO).
Today, we'll dive deep into this concept. This lesson focuses on how to manually design and implement Data Transfer Objects (DTOs) to decouple your API from your domain models. By the end of this session, you'll understand why this decoupling is one of the most important practices for building clean, secure, and maintainable REST APIs.
1. The Problem: Exposing Your Internal Models
In a Spring Boot application that uses JPA, we define Entities. An entity is a Java class that maps directly to a database table. It represents the core data and business logic of your application's domain.
For example, a Customer entity might look like this:
@Entity
public class Customer {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
private String password; // Hashed password
private boolean isAccountLocked;
// ...getters and setters
}
A common mistake is to return this Customer entity directly from a REST controller.
@GetMapping("/customers/{id}")
public Customer getCustomer(@PathVariable Long id) {
return customerRepository.findById(id).orElseThrow();
}
Can you spot the problem? The API response would look like this:
{
"id": 1,
"name": "Alex",
"email": "alex@example.com",
"password": "bcrypt_hashed_password_string...",
"isAccountLocked": false
}
By exposing the entity directly, we are leaking sensitive (password) and internal (isAccountLocked) data to the client. This is a security risk and creates a tight coupling between our internal database structure and our public API. Any change to the Customer entity could break our API.
2. The Solution: Data Transfer Objects (DTOs)
A DTO is a simple object whose only purpose is to carry data between different parts of an application (like from your service layer to a client via a REST API). It is a "dumb" data holder with no business logic.
By using DTOs, we create a protective layer, or a "facade," for our API.

Let's watch a video that clearly explains the motivation behind DTOs and shows how they solve the problem of data exposure.
JAVA DTO Pattern Tutorial | Simplify Your Code
This video from Amigoscode provides an excellent introduction to the DTO pattern. It starts by highlighting the exact problem we just discussed—exposing sensitive entity data—and then demonstrates the solution.
Please watch from the beginning until 05:26. Pay close attention to: The concept of a DTO and its role as a data carrier and mapper (02:01 - 04:30). The real-world problem shown with an API call that exposes a hashed password and other internal fields (04:30 - 05:26).
As the video explained, DTOs give us several key benefits:
- Security: We have full control over which fields are exposed, preventing leaks of sensitive data like passwords or internal state flags.
- Decoupling: The API contract (the DTO) is independent of the database schema (the entity). You can refactor your database entities without breaking your public API.
- Flexibility: You can create different DTOs for different use cases. For example,
CustomerSummaryDTOfor a list view and a more detailedCustomerDetailsDTOfor a single-item view.
For a more detailed textual breakdown of the differences between entities and DTOs, the following article is a great reference.
Differences Between Entities and DTOs
This Baeldung article, "Differences Between Entities and DTOs", provides a clear, structured comparison that reinforces the concepts we've just discussed.
Please read Sections 2 ('Entities'), 3 ('DTOs'), and 5 ('Why Do We Need Both Entities and DTOs?'). Focus on understanding the distinct characteristics and responsibilities of each type of object.
3. Manual Implementation: DTOs and Mappers
Now that we understand the "what" and "why," let's focus on the "how." The process involves two main steps:
- Creating the DTO class.
- Creating a "Mapper" to convert the entity object to the DTO object.
Step 1: Creating the DTO
Since Java 14, records have become the ideal choice for implementing DTOs. A record is a concise, immutable data carrier, which is exactly what a DTO should be.
Here's how we'd create a CustomerDTO for our Customer entity:
public record CustomerDTO(
Long id,
String name,
String email,
List<String> roles // Assuming we want to show roles, but not the password
) {}
Notice how password and isAccountLocked are intentionally left out. We only include the data we want the client to see.
Step 2: Creating the Manual Mapper
A mapper is responsible for the conversion logic. You write the code that takes an instance of Customer and produces an instance of CustomerDTO.
Let's continue with the Amigoscode video to see a complete, hands-on implementation of both the DTO and a dedicated mapper class.
JAVA DTO Pattern Tutorial | Simplify Your Code
This segment of the video walks through the entire manual implementation process, from creating the DTO record to writing the mapping logic and refactoring it into a separate class.
Watch from 06:09 to 13:16. Observe how: The CustomerDTO is created as a Java record. The controller and service are updated to return the new CustomerDTO. The mapping logic is first written inside the service using a Java Stream's map operation. This logic is then extracted into a dedicated CustomerDTOMapper class for better organization and reusability.
The video demonstrated a great pattern: creating a mapper class that implements Java's Function interface. This makes it a clean, reusable component that can be easily injected into your services.
@Service // Or @Component
public class CustomerDTOMapper implements Function<Customer, CustomerDTO> {
@Override
public CustomerDTO apply(Customer customer) {
return new CustomerDTO(
customer.getId(),
customer.getName(),
customer.getEmail(),
customer.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())
);
}
}
This mapper can then be injected and used in the service layer, keeping the service's logic clean and focused on business operations.
@Service
public class CustomerService {
private final CustomerRepository customerRepository;
private final CustomerDTOMapper customerDTOMapper;
// ... constructor injection
public List<CustomerDTO> getAllCustomers() {
return customerRepository.findAll()
.stream()
.map(customerDTOMapper) // Use the injected mapper
.collect(Collectors.toList());
}
}
To see the final result of this implementation, you can watch the last part of the video.
JAVA DTO Pattern Tutorial | Simplify Your Code
This final clip shows the API response after the DTO pattern has been correctly implemented.
Watch from 14:41 to 15:56 to see the 'after' state. Notice that the JSON response now contains only the fields defined in the CustomerDTO and the sensitive data is gone.
4. DTOs for Requests
DTOs are not just for responses. They are equally important for handling incoming data from POST or PUT requests. A Request DTO defines the exact shape of the data you expect from the client.
For example, when a user registers, you don't want them to be able to set their own id or roles. A CustomerRegistrationRequest DTO enforces this:
public record CustomerRegistrationRequest(
@NotNull String name,
@NotNull String email,
@NotNull String password
) {}
Your controller would then accept this DTO in the request body. In the service, you would map this request DTO to a new Customer entity before saving it.
@PostMapping("/customers")
public void registerCustomer(@RequestBody @Valid CustomerRegistrationRequest request) {
customerService.addCustomer(request);
}
This prevents clients from passing in malicious or unwanted data and makes your API contracts explicit for both reading and writing data.
Test your understanding!
You have the following JPA Product entity:
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private String description;
private BigDecimal price;
private String internalSku; // Internal Stock Keeping Unit
private int stockQuantity;
}
- Design a
ProductResponseDTOusing a Java record. It should only expose the fields relevant to a customer browsing your store (ID, name, description, and price). - Write a static method in a
ProductMapperclass to convert aProductentity to your newProductResponseDTO.
Show answer
-
ProductResponseDTORecord:import java.math.BigDecimal; public record ProductResponseDTO( Long id, String name, String description, BigDecimal price ) {}This DTO correctly omits the
internalSkuandstockQuantityfields, which are internal concerns. -
ProductMapperClass with Static Method:public class ProductMapper { public static ProductResponseDTO toDto(Product product) { if (product == null) { return null; } return new ProductResponseDTO( product.getId(), product.getName(), product.getDescription(), product.getPrice() ); } }This simple, stateless mapper effectively transforms the entity into the DTO. In a service, you would call it like this:
ProductMapper.toDto(product).
Conclusion
You have now learned one of the most critical patterns in modern backend development. By separating your API's data contract from your internal domain model, you build applications that are more secure, robust, and easier to maintain.
Key Takeaways:
- Entities represent your internal domain model and are tied to your database schema. They should not be directly exposed via your API.
- DTOs (Data Transfer Objects) are simple data carriers that define the public contract of your API for both requests and responses.
- Decoupling with DTOs enhances security by preventing data leaks and provides flexibility to evolve your API and domain model independently.
- Java Records are an excellent, modern choice for creating immutable DTOs.
- Mappers contain the logic to convert between entities and DTOs. This logic can be organized in dedicated, reusable components.
What's Next?
As you've seen, writing manual mapping code is straightforward but can become repetitive and tedious, especially for applications with many entities and complex object structures. What if you could automate this boilerplate code?
In the next lesson, we will do just that. You will learn how to automate entity-to-DTO conversion using a mapping library like ModelMapper, which will significantly speed up your development process while keeping your code clean.