09 How Ddd Is Implemented in Microservices Design and Realization

09 How DDD is implemented in Microservices Design and Realization #

Since the launch of this column, many readers have reached out to me to discuss various DDD-related topics. I have noticed that when people come across concepts like anemic models, rich models, strategy patterns, and decorator patterns, they often exclaim, “Is this what DDD is all about? It’s not much different from our usual development process.” Little do they know that they haven’t truly grasped the essence of DDD.

The Essence of DDD #

So, what is the essence of DDD? It is domain modeling, which changes our perception of software development. As shown in Figure 1, the essence of DDD includes:

  • Gaining a deep understanding of the business domain
  • Translating this understanding into a domain model
  • Using the domain model to guide the design of the database and the software

Drawing 0.png

Figure 1: The essence of Domain-Driven Design

In the past, we believed that software development was based on fulfilling user requirements. This approach often led to shallow understanding of the requirements and resulted in repetitive changes as the requirements evolved, making the development process exhausting and exposing the software development project to significant risks.

DDD has changed all of this. It requires us to proactively understand the business domain and acquire domain knowledge. The deeper our understanding of the business, the more professional the product we can develop. This, in turn, will make our customers more satisfied with our products and more likely to purchase and use them.

However, the real world is complex, and it is impossible for us to fully understand the business domain from the beginning. Initially, our understanding of the business will be shallow, and the domain models we create based on this understanding will also be shallow. As a result, although the software we develop may be usable, it may not necessarily meet the user’s expectations. Nevertheless, if we continuously communicate with our customers, deepen our understanding of the business, and listen to their feedback, our understanding of the business will become deeper and more accurate. Combined with our professional knowledge, we will be able to understand what problems our software needs to solve for our customers, how to achieve optimal solutions, and how to ensure a user-friendly experience. At this point, it is no longer the customer that is demanding requirements, but rather we proactively propose requirements, proactively improve functionality, and solve the pain points of the customer. The effect of doing this is that the customer will feel “I don’t know why, but I feel that your software is easy to use and comfortable”. At this point, not only will the customer no longer make constant changes, but our software will also become more professional and competitive in the market. This is the true essence of DDD.

There is a problem here. If we don’t have a deep understanding of the business, it will affect the product. So, can we have a very deep understanding of the business from the beginning? This is almost impossible. We often say that doing things cannot be solely based on enthusiasm, but must comply with natural laws. In fact, the design and development process of software is the same.

  • In the early stages when your understanding of the business is relatively rough, start domain modeling from the main processes.
  • Then, continuously add things to the domain model. As each functionality is added one by one, the domain model becomes increasingly rich and complete. Every time a new feature is added, use the “two hat” approach, first refactor and then add new features, constantly improving each design.
  • In this way, the domain model grows bit by bit like a small tree, eventually completing all the functions.

This design process is called “Iterative and Incremental Development”. By using iterative and incremental development, you don’t have to consider too many problems at the beginning, and you can gradually delve into them from simple ones, which reduces the difficulty of the design. At the same time, the system is always in a state of change, making the design more adaptable to change.

Domain Modeling Based on Bounded Contexts #

Going back to the microservice design in Lesson 08, after the event storming analysis of the online ordering system, how should we proceed with the design? By dividing it into bounded contexts, the system has been divided into several bounded contexts such as “User Registration,” “User Ordering,” “Restaurant Order Acceptance,” “Delivery by Driver,” and “Restaurant Management.” This division also determines the division of backend microservices. Next, start domain modeling for each bounded context.

First, start with the “User Ordering” context. Through business domain analysis, a domain model as shown in Figure 2 is drawn. The core of this model is the “Order,” which associates the user with user addresses. An order has multiple dish details, and each dish detail corresponds to a menu, with each menu belonging to a restaurant. In addition, an order is also associated with its payment and invoice. Initially, their attributes and methods may not be so comprehensive, but as the design progresses, the model is continuously refined and improved.

1.png

Based on this, start dividing the bounded contexts. The user and user address belong to the “User Registration” context, and the restaurant and menu belong to the “Restaurant Management” context. They are all support domains for the “User Ordering” context, meaning that they provide interfaces for the “User Ordering” context. The only classes that truly belong to the “User Ordering” context are “Order,” “Dish Detail,” “Payment,” and “Invoice.” They ultimately form the “User Ordering” microservice and its database design. Since user names, addresses, phone numbers, and other information are in the “User Registration” context and need to be obtained through remote interface calls each time, it is necessary from a system optimization perspective to appropriately duplicate them into the “Order” domain object to improve query efficiency. Similarly, the “Dish Name” has also been duplicated. The updated design is shown in Figure 3:

2.png

After completing the “User Ordering” context, start designing the “Restaurant Order Acceptance” context, as shown in Figure 4. In the previous lesson, it was mentioned that the “User Ordering” microservice sends orders to the “Restaurant Order Acceptance” microservice using event notification mechanisms. Specifically, it sends orders and dish details to the “Restaurant Order Acceptance” context. The “Restaurant Order Acceptance” context will store them in its own database and add the “Restaurant Order Acceptance” class, which has a one-to-one relationship with the order. 3.png

With the same train of thought, the domain modeling of “Knight Delivery” can be completed through the notification of domain events.

With the above design, the microservices that were discussed in the previous lecture are further divided into each microservice’s design. Then, each microservice’s design is implemented for database design based on the ideas from lecture 03, and the anemic model and rich model design is implemented based on the ideas from lecture 04.

It is worth noting that “Order” and “Dish Details” are an aggregate. In the past, designing with the anemic model, separate value objects, services, and DAOs were designed for them. Now, designed with the rich model, only the order domain object, services, repository, factory, and dish details are included in the order object, while the order DAO is included in the order repository. The anemic model and rich model have obvious differences in design. The implementation of aggregates will be discussed in more detail in the next lecture.

In-depth Understanding of Business and Model Refactoring #

As mentioned earlier, it is not possible to deeply understand the business in one go; it is a gradually deepening process. For example, in the design of “User Address,” at first, there was no “Contact” and “Phone Number” because they could be obtained through the association with the user. However, as the business goes deeper, it is discovered that when a user places an order, it may not necessarily be delivered to them personally; it could be delivered to someone else. This is a real business scenario that was not initially considered. To solve this, the “Contact” and “Phone Number” were promptly added to the “User Address,” and the problem was resolved.

In addition, how should such a business scenario be designed if the user needs to cancel an order after placing it? Through communication with the customer, the requirements for this business scenario were determined:

  • If the restaurant has not accepted the order yet, it can be directly canceled.
  • If the restaurant has already accepted the order, it needs to be confirmed by the restaurant before it can be canceled.
  • If the restaurant is ready, it cannot be canceled.

Firstly, the “Restaurant Accepts Order” needs to provide an interface for status inquiries and an interface for confirming cancellations. After canceling the order, a cancellation time needs to be recorded, and an “Order Cancellation” domain event is formed to notify the “Restaurant Accepts Order” context. For this reason, the “User Places Order” context needs to add a “Cancellation Time” to the order.

However, after the “User Places Order” context updates the “Order” object, should the “Restaurant Accepts Order” and “Knight Delivery” contexts also be updated accordingly? As mentioned before, the design of microservices aims to:

  • Minimize the number of microservices updated each time to reduce maintenance costs.
  • Even if it is not possible, try to narrow down the range of updates.

Adding the “Cancellation Time” field is meaningful for the “Restaurant Accepts Order” context, and its corresponding change is justifiable. However, for the “Knight Delivery” context, the “Cancellation Time” has no relevance, so there is no need to update it. The calls between microservices are based on RESTful interface calls, and parameters are passed through JSON objects, which is a form of loose coupling invocation. Therefore, even if the data structure of the “Order” object is inconsistent between the “Restaurant Accepts Order” and “Knight Delivery” contexts, it does not affect their calls. Consequently, there is no need to update the “Knight Delivery” context, and the scope of updates is reduced, thus reducing maintenance costs.

After completing the above design, there is still a difficult problem to solve, which is the tracking of order status.

Tracking Order Status

After a user places an order, they often continuously track the status of the order, whether it is “Order Placed”, “Order Accepted”, “Order Ready”, or “Order Delivered”. However, these status information is scattered among various microservices, and it is not possible to implement it in the “User Places Order” context. How can the status information of orders be collected from these microservices while maintaining loose coupling between them? The solution is still notification of domain events.

Each microservice encapsulates the domain event as a message and sends it to the message queue after completing the operation of a domain event. For example, the “User Places Order” microservice puts the order event into the message queue after completing the user’s order. In this way, not only can the “Restaurant Accepts Order” microservice receive this message and complete subsequent acceptance operations, but the “Order Query” microservice can also receive this message and track the order. See Figure 5.

Drawing 4.png

Figure 5 Order tracking diagram

Through the notification of domain events and the design of the message queue, the design for the calls between microservices becomes loosely coupled. The “Order Query” microservice can collect various order status information like an external attachment, without affecting the original design of the microservices. This achieves decoupling between microservices and reduces the maintenance costs of the system. The “Order Query” microservice uses redundancy to save information such as “Order Placed Time,” “Cancellation Time,” “Order Accepted Time,” “Order Ready Time,” etc., in different order statuses into the order table, and even adds an “Order Status” to record the current status, and adds the functionality of Redis caching. This design guarantees efficient tracking of orders. It is worth noting that efficient queries for big data are usually achieved through redundancy.

Summary #

The essence of DDD is domain modeling, which means deeply understanding the business. Only by deeply understanding the business and designing it into the domain model can the software be more professional and satisfy the users’ usage. Therefore, based on domain modeling for each bounded context, each functionality is added to the model and implemented in the design of each microservice. As the business becomes more complex and the understanding deepens, the original model needs to be adjusted in a timely manner to adapt to new functionalities and ensure that the design is always of high quality.