1. Overview
Microservices have become popular in recent years. One of the essential characteristics of microservices is that they are modular, isolated, and easy to scale. The microservices need to work together and exchange data. To achieve this, we create shared data transfer objects called DTOs.
In this article, we will present ways in which DTOs are shared between microservices.
2. Exposing Domain Objects as DTO
Models that represent the application domain are managed using microservices. Domain models are different concerns, and we separate them from data models in the DAO layer.
The main reason for this is that we don’t want to expose the complexity of our domain through the services to the clients. Instead, we expose DTOs between our services that serve application clients through REST APIs. While DTOs pass between these services, we convert them to domain objects.
The service-oriented architecture above schematically shows the components and flow of DTO to Domain objects.
3. DTO Sharing Between Microservices
Take, as an example, the process of a customer ordering a product. This process is based on the Customer-Order model. Let’s look at the process from the side of the service architecture.
Let’s say that the Customer service sends request data to the Order service as:
"order": {
"customerId": 1,
"itemId": "A152"
}
The Customer and Order services communicate with each other using contracts. The contract, which is otherwise a service request, is displayed in JSON format. As a Java model, the OrderDTO class represents a contract between the Customer service and the Order service:
public class OrderDTO {
private int customerId;
private String itemId;
// constructor, getters, setters
}
We could share contracts between services also using the Protocol Buffer language instead of JSON. In this case, we need to share a “protobuf” file that will be used for the generation of the Client Stubs.
3.1. Sharing DTO Using Client Modules (Libraries)
A microservice requires certain information from other services to process any request. Let’s say there is a third microservice that receives order payment requests. Unlike the Order service, this service requires different customer information:
public class CustomerDTO {
private String firstName;
private String lastName;
private String cardNumber;
// constructor, getters, setters
}
If we also add a delivery service, customer information would have:
public class CustomerDTO {
private String firstName;
private String lastName;
private String homeAddress;
private String contactNumber;
// constructor, getters, setters
}
So, placing the CustomerDTO class in a shared module no longer serves the intended purpose. To solve this, we approach a different method.
Within each microservice module, let’s create a client module (library) and next to it a server module:
order-service
|__ order-client
|__ order-server
The order-client module contains a DTO shared with Customer service. Therefore, the order-client module has the following structure:
order-service
└──order-client
OrderClient.java
OrderClientImpl.java
OrderDTO.java
The OrderClient is an interface that defines an order method for processing order requests:
public interface OrderClient {
OrderResponse order(OrderDTO orderDTO);
}
To implement the order method, we use the RestTemplate object to send a POST request to the Order service:
String serviceUrl = "http://localhost:8002/order-service";
OrderResponse orderResponse = restTemplate.postForObject(serviceUrl + "/create",
request, OrderResponse.class);
Besides, the order-client module is ready for use. It now becomes a dependent library of the customer-service module:
[INFO] --- maven-dependency-plugin:3.1.2:list (default-cli) @ customer-service ---
[INFO] The following files have been resolved:
[INFO] com.baeldung.orderservice:order-client:jar:1.0-SNAPSHOT:compile
Of course, this has no purpose without the order-server module to expose the “/create” service endpoint to the order client:
@PostMapping("/create")
public OrderResponse createOrder(@RequestBody OrderDTO request)
Thanks to this service endpoint, the Customer service can send an order request through its order client. By using the client module, microservices communicate with each other in a more isolated way. Attributes in the DTO are updated within the client module. Therefore, contract breaking is limited to services that use the same client module.
4. Conclusion
In this article, we explained a way to share DTO objects between microservices. At best, we achieve this by making special contracts as parts of microservice client modules (libraries). In this way, we separate the service client from the server part that contains the API resource. As a result, there are some benefits:
- There is no redundancy in the DTO code between services
- Contract breaking is limited to services that use the same client library
A code sample of a Spring Boot application is available over on GitHub.