What is it? ๐ง
DTO is short for Data Transfer Object.
Why do we need it? ๐ค
It allows us to decouple domain objects from objects we present to the client.
It helps us present customizable views of our domain objects to the client.
It helps us deliver only the necessary data to the client, thus making our responses smaller and easier to deliver.
- As an example of this use case, there was a page in our front-end application that was taking a long time to load.
- After investigation, we noticed that the backend was sending a response with a lot of data inside of it, thus making its processing time longer.
- We then, applied the DTO principle and sent only the needed data thus resulting in 5x faster response time.
How to do it? ๐
- Get the business object from the persistence layer (database for example)
- Create a new DTO object using values from the business object
- Return the DTO to the client.
Ok, let's code a bit โ๏ธโ๏ธ
Let's say we have a domain class called Shipment๐
import lombok.*;
import java.util.Date;
@Getter @Setter @Builder @AllArgsConstructor
public class Shipment {
private Integer id;
private Date shipmentDate;
private String productCode;
private String owner;
private String serialNumber;
}
A mock persistence layer ๐ (database for example) to get Shipment records
public class ShipmentRepository {
// methods to get Shipment from database
}
A DTO for admins' ๐ท๐ฟโโ๏ธ view of the Shipment
import lombok.*;
@Getter @Setter @Builder @AllArgsConstructor
public class Shipment_AdminDTO {
private Integer id;
private String productCode;
private String serialNumber;
}
Another DTO for users' ๐ท๐ฟโโ๏ธ view of the Shipment
import lombok.*;
@Getter @Setter @Builder @AllArgsConstructor
public class Shipment_UserDTO {
private Date shipmentDate;
private String productCode;
private String owner;
}
A service to get the Shipment from the repository, convert it to a DTO, and return it to the client
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class ShipmentService {
private final ShipmentRepository shipmentRepository;
public Shipment_UserDTO getShipment_ForUser(Integer id){
return shipmentRepository.
getShipmentById(id).
map(this::getShipment_UserDTO_FromShipment).
orElseThrow(() -> new RuntimeException("Shipment Not Found"));
}
public Shipment_AdminDTO getShipment_ForAdmin(Integer id){
return shipmentRepository.
getShipmentById(id).
map(this::getShipment_AdminDTO_FromShipment).
orElseThrow(() -> new RuntimeException("Shipment Not Found"));
}
private Shipment_UserDTO getShipment_UserDTO_FromShipment(Shipment s){
return new Shipment_UserDTO(s.getShipmentDate(), s.getProductCode(), s.getOwner());
}
private Shipment_AdminDTO getShipment_AdminDTO_FromShipment(Shipment s){
return new Shipment_AdminDTO(s.getId(), s.getProductCode(), s.getOwner());
}
}
An example response for an admin DTO could be:
{
"id" : "1",
"productCode" : "VVV",
"serialNumber" : "3XX"
}
An example response for a user DTO could be:
{
"shipmentDate" : "04/16/2021",
"productCode" : "ZZZ",
"owner" : "Jalil"
}
Important Note ๐๐ผ ๐๐ผ
it's preferable to not return a list Of DTOs as a response.
Why?
Let's say in the future we would like to return another field alongside the list of DTOs, like their total count, or some other metadata.
In that case, we might break other people's code that is using our API and expecting a list of objects to find an object with inner fields.
If it had an object from the beginning, we could add fields to it without breaking code that uses our API.
A code example,
import lombok.*;
@Getter @Setter @AllArgsConstructor
public class Shipments_AdminDTO {
private final List<Shipment_AdminDTO> shipment_adminDTOs;
}
public Shipments_AdminDTO getShipments_ForAdmin(){
List<Shipment_AdminDTO> shipment_adminDTOS =
shipmentRepository.
getAllShipments().
stream().
map(this::getShipment_AdminDTO_FromShipment).
collect(Collectors.toList());
return new Shipments_AdminDTO(shipment_adminDTOS);
}
This tutorial utilized Lombok and Java streams, to learn more about them check out my other posts.
Top comments (0)