02 Practice Ddd With the Example of E Commerce Payment Function

02 Practice DDD with the Example of E-commerce Payment Function #

In the previous lesson, we spent a lot of time explaining the root cause of software decay and how DDD solves the problem of software decay. Now, let’s use the payment function of an e-commerce website as an example to practice the process of software design and its changes based on DDD.

Applying DDD for Software Design #

The initial requirement description about the user payment function received by developers is as follows:

  • After the user places an order, they enter the payment function through the ordering process;
  • Obtain user information such as user name and address through the user profile;
  • Record the products and their quantities, and summarize the payment amount;
  • Save the order;
  • Perform payment through remote calls to the payment interface.

In the past, when receiving this requirement, developers would often hastily design and start coding, resulting in low design quality.

However, by adopting a domain-driven approach, developers should first analyze the requirements and design domain models. Based on the above business scenario, we can analyze:

  • In this scenario, there is an “order,” and each order corresponds to a user;
  • A user can have multiple user addresses, but each order can only have one user address;
  • Additionally, an order corresponds to multiple order details, each order detail corresponds to a product, and each product corresponds to a supplier.

Drawing 0.png

Finally, we can perform actions on orders such as “placing an order,” “making a payment,” and “checking the order status.” Therefore, the following domain model diagram is formed:

Drawing 2.png

With this domain model, we can design the program using the model as follows:

Drawing 4.png By guiding the domain model, the “order” is divided into order service and value object, the “user” is divided into user service and value object, and the “product” is divided into product service and value object… then, implement their respective methods on this basis.

Requirement Change for Product Discounts #

After completing the first version of the payment function of an ecommerce website based on the domain model, the first requirement change quickly arrives, which is to add a discount function. This discount function includes timed discounts, limited quantity discounts, discounts for certain product categories, discounts for specific products, and no discount. How should we design when we receive this requirement? Obviously, it is not okay to insert if statements into the payoff() method. At this time, according to the principles of domain-driven design, the requirement change should be analyzed and then modified based on the real world behind the domain model.

Drawing 6.png

This is the domain model of the previous version. Now we need to add the discount function based on this model, and it needs to be divided into different types such as timed discounts, limited quantity discounts, and discounts for certain product categories. How should we analyze and design in this case?

Firstly, we need to analyze the relationship between payment and discounts.

What is the relationship between payment and discounts? You may think that the discount is applied during the payment process, so it should be included in the payment. Is this way of thinking correct? What principles and concepts should we base our design on? This is when another heavyweight design principle should be introduced, which is the “Single Responsibility Principle”.

Single Responsibility Principle: Each element in a software system should only be responsible for its own scope of responsibilities, and delegate other tasks to others, and I just invoke them.

The Single Responsibility Principle is a very important principle in software design, but understanding it correctly is a key issue. In this sentence, the key to understanding it accurately lies in the “responsibility”, that is, where is the scope of its own responsibilities. In the past, we mistakenly understood this “responsibility” as doing a certain thing, and all things related to this thing are its responsibilities. It is because of this wrong understanding that many incorrect designs were made, such as putting the discount in the payment function. So, how can we correctly understand this “responsibility”?

“A responsibility is a reason for software to change” is a statement from the famous software master Uncle Bob in his book “Agile Software Development: Principles, Patterns, and Practices”. However, this statement is too concise and it is difficult to understand its connotation deeply, thus unable to effectively improve our design quality. Let me explain this sentence.

First, let’s think about what is high-quality code. You may immediately think of “low coupling, high cohesion”, and various design principles, but these evaluation standards are too abstract. The most direct and practical evaluation standard is that when a user proposes a requirement change, the lower the cost of modifying the software to implement this change, the higher the design quality of the software. When a requirement change arrives, how to minimize the cost of modifying the software? Is it to modify the code of 3 modules for implementing the requirement, and then test all these 3 modules afterwards? The maintenance cost of this scenario is definitely “high”. So how to minimize it? To modify the code of only 1 module, with the lowest maintenance cost.

So, how can we modify only one module every time a change occurs to implement a new requirement? It requires us to constantly organize the code during the maintenance of the software, put the code changed due to the same reason together, and separate the code changed due to different reasons, and place them in different modules or classes. In this way, when modifying the code due to a certain reason, the code to be modified is in the same module or class, the change range is narrowed, the maintenance cost is reduced, and naturally the design quality is improved.

In summary, the Single Responsibility Principle requires us to constantly organize the code during the maintenance of the software, put the code changed due to the same reason together, and separate the code changed due to different reasons. Based on this principle, let’s go back to the previous case. How should we analyze the relationship between “payment” and “discount”? We just need to answer two questions:

  • When “payment” changes, is it necessary for “discount” to change?
  • When “discount” changes, is it necessary for “payment” to change?

When the answers to these two questions are negative, it means that “payment” and “discount” are different reasons for software changes. So, is it appropriate to put them together in the same class or method? No, it is not appropriate. In this case, the “discount” should be extracted from the “payment” and placed in a separate class.

The same reasoning applies to:

  • When “timed discount” changes, is it necessary for “limited quantity discount” to change?
  • When “limited quantity discount” changes, is it necessary for “discount for certain product categories” to change?
  • ……

Finally, it was found that different types of discounts were also a reason for different changes in the software. Is it appropriate to put them in the same class and method? Based on the above analysis, we made the following design:

Drawing 8.png

In this design, the discount function was separated from the payment function and an interface was created. Based on this, various types of discount implementation classes were designed. With this design, changes in payment will not affect the discount, and changes in the discount will not affect the payment. Similarly, when the “limited time discount” changes, it only affects the “limited time discount”, and when the “limited quantity discount” changes, it only affects the “limited quantity discount” and is unrelated to other discount types. The scope of changes has been narrowed, maintenance costs have been reduced, and the design quality has improved. This design is the essence of the “Single Responsibility Principle”.

Then, based on this version of the domain model, program design is conducted, and design patterns can also be added. Therefore, we made the following design:

Drawing 10.png

Obviously, the “Strategy Pattern” was added to this design, where the discount function was turned into a discount strategy interface and various discount strategy implementation classes. When a discount type changes, only the corresponding discount strategy implementation class is modified. When adding a new type of discount, a new discount strategy implementation class is written, improving the design quality.

Changes in VIP Member Requirements #

Based on the first change, the second change soon arrived, which is to add VIP membership. The business requirements are as follows:

  • Different types of VIP members (gold card members, silver card members) have different discounts;
  • VIP members receive benefits (points, coupons, etc.) when making payments;
  • VIP members can enjoy certain privileges.

How should we design these requirements? Similarly, let’s start with the domain model and analyze the relationship between “User” and “VIP Member”, and the relationship between “Payment” and “VIP Member”. When analyzing, let’s answer those two questions again.

  • When there is a change in “User”, should “VIP Member” also change?
  • When there is a change in “VIP Member”, should “User” also change?

Through analysis, it is found that “User” and “VIP Member” are two completely different things.

  • What “User” needs to do is user registration, modification, cancellation, etc.;
  • What “VIP Member” needs to do is member discount, member benefits, and member privileges;
  • The relationship between “Payment” and “VIP Member” is to call member discount, member benefits, and member privileges during the payment process.

Through the above analysis, we made the following version of the domain model:

Drawing 12.png

With these changes to the domain model, we can use them as a basis to guide the subsequent changes in the program code.

Changes in Payment Methods #

Similarly, the third change is to add more payment methods. We analyze the relationship between “Payment” and “Payment Method” in the domain model and find that they are also reasons for different changes in the software. Therefore, we boldly made the following design:

Drawing 14.png

In the design and implementation, because we need to integrate with various third-party payment systems, we need to integrate with external systems. To minimize the impact of changes in third-party external systems on us, we decisively added the “Adapter Pattern” in between, as shown below:

Drawing 16.png

By adding the adapter pattern, the payment service no longer calls the external payment interface during payment, but the “Payment Method” interface, decoupling it from external systems. As long as the “Payment Method” interface is stable, the payment service remains stable. For example:

  • When the Alipay payment interface changes, it only affects the Alipay adapter;
  • When the WeChat payment interface changes, it only affects the WeChat payment adapter;
  • When adding a new payment method, only a new adapter needs to be written.

No matter what changes there are in the future, the scope of code modification has been narrowed, reducing maintenance costs and improving code quality.

Conclusion #

In this talk, we have practiced how to use DDD for software design and changes, as well as how to analyze, evaluate, and implement high-quality code during the design and change process. Next, we will practice how to further implement domain model design into microservice design and database design in the software system.