08 How Ddd Resolves Microservices Split Difficulties

08 How DDD resolves Microservices split difficulties #

The technology architecture of microservices is not difficult in itself. Many development teams, in the early stages of their microservice transformation, focus primarily on learning the technology architecture of microservices. However, when they actually start implementing microservices into specific business scenarios, they realize that the real challenge lies in how to decompose microservices, what principles to follow in the decomposition, and what potential risks they may face. Let’s address these issues one by one.

Principles of Microservice Decomposition #

In the previous content, we have mentioned the principles of microservice decomposition multiple times. Now, I will explain them to you in detail.

The principle of microservice decomposition is to be “small and focused”, meaning that microservices should have high cohesion within themselves and low coupling between different microservices.

“High cohesion within microservices” means adhering to the Single Responsibility Principle. In other words, the code within each microservice should be a reason for software changes. If a code needs to be modified due to a specific reason, it should be located within that microservice and have no relevance to other microservices. By narrowing down the scope of code modifications to a specific microservice, it can be modified and released independently, thus achieving the required functionality. This is how microservices can leverage their advantages.

“Low coupling between microservices” means that during the implementation of their own business processes, if a microservice needs to perform certain processes that are not its responsibility, it should delegate those processes to other microservices by using their interfaces. For example, in the “User Ordering” microservice, when placing an order, it needs to query user information. However, querying user information is not its responsibility; it belongs to the “User Registration” microservice. Therefore, the “User Ordering” microservice does not need to directly execute the user information query. Instead, it should invoke the interface of the “User Registration” microservice. How should it make the invocation? Direct invocation may lead to coupling. Instead, by using a registration center, the “User Ordering” microservice only needs to call the microservice named “User Registration” registered in the registration center. In the software design, there can be multiple implementations of the “User Registration” microservice, and the one that is registered in the registration center will be called. This way, the decoupling of calls between microservices can be achieved.

By using DDD to model the business and then dividing the system based on bounded contexts, we can ensure a high level of cohesion within bounded contexts and low coupling between bounded contexts. Therefore, decomposing microservices based on bounded contexts can ensure high-quality microservice design. Furthermore, by analyzing the context map, we can understand the interface calling relationships between microservices, enabling multiple development teams to collaborate effectively.

Subdomain Division and Bounded Context #

As mentioned in Lesson 06, when drawing a domain model, it is not a good idea to draw all the domain objects of the entire system on a single big diagram. It would be difficult and time-consuming to draw, and also not conducive to communication. Therefore, domain modeling involves dividing a system into multiple subdomains, with each subdomain representing an independent business scenario. Analysis and modeling are conducted around these business scenarios, which involve many domain objects that may be related to objects in other subdomains. In this way, the implementation of each subdomain becomes a “bounded context”, and the relationships between them form a “context map”.

In this case, the analysis is conducted around the domain event “Order Placed”. It belongs to the bounded context of “User Ordering”. However, the “User” and “Address” related to it come from the bounded context of “User Registration”, and the “Restaurant” and “Menu” related to it come from the bounded context of “Restaurant Management”. Therefore, in this business scenario, the bounded context of “User Ordering” belongs to the “Core Domain”, while the bounded contexts of “User Registration” and “Restaurant Management” belong to “Supporting Domains”. Similarly, the design is conducted around various domain events in this case, resulting in the following:

Drawing 0.png Analysis diagram of the “Order Placed” bounded context

By designing in this way, the scope of the “Order Placed” bounded context, the related context map, and the interfaces are all clearly defined. With these designs, microservices can be separated according to the bounded context. With the microservices split according to this design, all the requirements related to the user placing an order are implemented in the “Order Placed” microservice. However, when reading user information for the order, instead of directly joining the user information table, the microservice calls the interface of the “User Registration” microservice. This way, when the user information changes, it is independent of the “Order Placed” microservice, and only requires independent development and upgrading in the “User Registration” microservice, thus reducing the maintenance cost of the system.

Drawing 1.png

Analysis diagram of the “Order Received” and “Order Ready” bounded contexts

Similarly, as shown in the above diagram, we have analyzed the “Order Received” and “Order Ready” bounded contexts, and have classified them into the “Restaurant Order Received” bounded context, which will be designed as the “Restaurant Order Received” microservice. The main domain of these scenarios is the “Restaurant Order Received” bounded context, and the related supporting domains are the “User Registration” and “Order Placed” bounded contexts. Through these designs, not only is the scope of the microservices properly divided, but the interfaces between microservices are also clarified, achieving high cohesion within microservices and low coupling between them.

Domain Event Notification Mechanism #

Following the domain model design discussed in Lecture 07, as well as the bounded context divisions based on this model, the entire system is divided into microservices such as “Order Placed,” “Restaurant Order Received,” and “Delivery Rider Dispatch.” However, in the process of designing and implementing, there is still a design challenge: how to notify domain events. For example, when a user places an order in the “Order Placed” microservice, an order is created in that microservice. However, “Restaurant Order Received” is another microservice that needs to promptly obtain information about the placed order in order to process the order. So, how can the “Restaurant Order Received” microservice be notified of new orders? Indeed, one approach is to let the “Restaurant Order Received” microservice periodically query the “Order Placed” microservice for placed order information. However, this design not only increases the system load of the “Order Placed” and “Restaurant Order Received” microservices, resulting in wasted resources, but also introduces coupling between these two microservices, which is not conducive to future maintenance. Therefore, the most effective approach is to use a message queue to implement the notification of domain events between microservices.

Drawing 2.png

Drawing 3.png

Domain event notification in the online ordering system

As shown in the above diagram, in the specific design, after the “Order Placed” microservice completes the order placement and saves the order, it sends the order as a message to a message queue. At this point, the “Restaurant Order Received” microservice will have a daemon process constantly listening to the message queue. Once a message is received, it triggers the reception of the message and sends a “Order Received” notification to the restaurant. In this design:

  • The “User Ordering” microservice is only responsible for sending messages, and it is not relevant to who receives and processes these messages.
  • The “Restaurant Receiving Order” microservice is only responsible for receiving messages, and it is not relevant to who sends these messages.

This design achieves decoupling between microservices, reducing the cost of future changes. Similarly, after the restaurant’s food is ready, it is notified to the “Courier Accepting Order” through a message queue. In the entire microservice system, domain event notifications between microservices will often exist, so it is best to sink this mechanism into the technical middleware in the architecture design.

Microservice Design with Domain-Driven Design (DDD) #

Through a series of domain-driven design discussed in Lesson 07:

  • First, domain modeling is carried out through event storming sessions.
  • Then, based on the domain modeling, boundary context design is performed.

All these designs are intended to guide the final microservice design.

In the process of applying DDD to guide microservice design:

  • First, split the microservices according to the boundary contexts, and define the interfaces and invocation relationships between the microservices based on the context map.
  • Based on the boundary context division, divide the domain models into multiple problem subdomains, each of which has a domain model design.
  • Based on the domain models of each subdomain, design the business domain layer of each microservice according to the rich model and anemic model, including Service, Entity, and Value Object.
  • At the same time, design the databases of each microservice according to the domain models.

Finally, implement the above design in the calls between microservices, domain event notifications, and the design of front-end microservices, as shown in the following diagram:

Drawing 4.png

Microservice Design of Online Ordering System

Here, it can be seen that the design of the front-end microservices is different from that of the back-end microservices. The previous content mainly discussed the design of back-end microservices, while the design of front-end microservices is closely related to the user interface (UI). Therefore, by planning different roles, the front-end microservices are divided into User App, Restaurant Web, and Courier App. In the User App, all user-facing functions such as “User Registration,” “User Ordering,” and “User Selection” are designed. It acts as an aggregation service for receiving user requests:

  • When “User Registration” is performed, the “User Registration” microservice is called.
  • When “User Selection” is performed, the “Restaurant Management” microservice is queried.
  • When “User Ordering” is performed, the “User Ordering” microservice is called.

Conclusion #

Using DDD for requirement analysis and modeling can help improve the quality of microservice design and realize “loose coupling and high cohesion,” fully leveraging the advantages of microservices. However, there are still many challenges to be addressed in the design and implementation of microservices. This lesson dissected these challenges and their solutions. However, instead of having each microservice come up with its own solutions, a unified technical middleware for microservices should be established to address these issues. These design aspects will be explained in the relevant chapters on the construction of the microservices technology middleware in the following lessons.

In the next lesson, we will demonstrate how to implement the design of each microservice based on the domain model and microservice design, as well as the potential design challenges that may be encountered.