06 Advantages of Breaking Microservice Design Dilemmas With Bounded Context

06 Advantages of Breaking Microservice Design Dilemmas with Bounded Context #

In the previous lecture, we discussed the process of modeling, analysis, and design in domain-driven design using the scenario of user placing an order. However, from the perspective of a larger e-commerce website, user placing an order is just a small part of it.

So how should we approach domain-driven design for the entire e-commerce website? It contains so many scenarios, each scenario involving numerous domain objects, which in turn will result in many domain objects and complex associations between them. How can we use domain-driven design to design such a system? How do we draw the domain model? Do we create one large, intricate diagram or multiple smaller ones? After completing this lecture, you will be able to address these questions.

Problem Domain and Context Boundaries #

If we were to draw all the scenarios and the numerous domain objects involved in the entire system on one big diagram, we can imagine the diagram being filled with densely packed domain objects and their intricate relationships. The person drawing this diagram would find it challenging, and so would the person trying to understand it. Such a diagram is also not conducive to clarifying our thoughts, exchanging ideas, and improving design quality.

The correct approach is to divide the entire system into relatively independent business scenarios and perform domain analysis and modeling in each of these business scenarios, which are called “problem subdomains”, or simply “subdomains”.

The core design principle of domain-driven design is to analyze and design software in the context of the real world, which first requires analyzing and understanding the business and problems of the real world. The business and problems of the real world are referred to as the “problem domain”, and the business rules and knowledge within it are called “business domain knowledge”, for example:

  • The problem domain of an e-commerce website involves how people shop online and what the shopping process looks like.
  • The problem domain of an online ordering system involves how people place orders online, how restaurants receive orders online, and how the system dispatches delivery riders.

However, whether it’s an e-commerce website or an online ordering system, both have a very large and complex problem domain. Analyzing this problem domain all at once is difficult, so we need to adopt a “divide and conquer” strategy and divide the problem domain into multiple problem subdomains. For example:

  • The e-commerce website includes multiple subdomains such as user selection, ordering, payment, logistics, etc.
  • The online ordering system includes subdomains such as user ordering, restaurant order receiving, rider dispatching, etc.

If a subdomain is particularly complex, it can be further divided into subdomains based on it. Therefore, the domain-driven design of a complex system is centered around subdomains, creating one domain model design after another, and using it as a guide for program design. These individual domain model designs are called “Context Bounds” (CB).

The design of context bounds in DDD reflects the requirement of the “Single Responsibility Principle” in high-quality software design, meaning that each context bounds implements the business logic that changes for the same reason. For example, the context bounds for “user placing an order” implement the business logic related to placing an order. Therefore, when there is a change in the business logic related to placing an order, it only affects the “user placing an order” context bounds, and only requires modification to that context bounds, without affecting other context bounds. This reduces the scope of code modification when there are changes in requirements, thus reducing maintenance costs.

Should the reading of user information during the process of placing an order also be implemented in the “user placing an order” context bounds? The answer is no because reading user information is not the responsibility of placing an order. When there is a change in the placing an order business logic, it doesn’t necessarily mean that the user information has changed, and vice versa. These are two separate reasons for software changes.

Therefore, the operation of reading “user information” should be assigned to the “user information management” context bounds, and the “user placing an order” context bounds should only call its interface. Through this division, high cohesion within context bounds and low coupling between context bounds are achieved, which can effectively reduce the cost of future code changes and improve the quality of software design. The relationship between context bounds is referred to as “Context Map”.

Context Bounds and Microservices #

The concept of “high cohesion within context bounds” means that the functionality implemented within each context bounds is the code that changes for the same reasons. Only changes related to these reasons require modification to the context bounds, and changes unrelated to these reasons do not require modification to the context bounds. It is precisely because of the great characteristics of context bounds that many microservices teams now use them as a principle for splitting microservices, with each context bounds corresponding to one microservice.

Drawing 0.png

When a microservice system is split according to this principle, it becomes possible to quickly apply each requirement change to a specific microservice during future changes and maintenance. This way, a requirement can be implemented by modifying the corresponding microservice, and after upgrading the service, it can be delivered for user use. Such a design allows more and more planning and development teams to achieve low-cost maintenance and fast delivery in the future, thereby enhancing enterprise competitiveness and the ability to adapt to market changes.

For example, in the shopping process of an e-commerce website, shopping, placing an order, payment, and logistics are all reasons for software changes. Therefore, by dividing them into different context bounds based on different business scenarios and splitting them into microservices accordingly. Then, when there is a change in shopping, the shopping microservice is modified, when there is a change in placing an order, the placing an order microservice is modified. However, they both require reading product information during the business processing, so they call the “product management” microservice to obtain product information. This way, when there is a change in product information, it only affects the “product management” microservice, without affecting other microservices, resulting in reduced maintenance costs and increased delivery speed.

The concept of “low coupling between context bounds” means that when context bounds call each other via a context map, they do so through interfaces. As shown in the figure below, if module A needs to call module B, then it becomes coupled with module B:

  • If module A needs to be reused, then module B must be present wherever module A is used, otherwise module A will produce errors.
  • If module B also relies on module C, and module C relies on module D, then module B, C, and D must be present wherever module A is used, resulting in high costs for using module A.

However, if module A does not rely on module B, but relies on interface B’, then it is not necessary to have module B everywhere that module A is used. If module F implements interface B’, then module A can simply call module F. This way, the coupling between the caller and the callee is loosened. Drawing 2.png

When implementing the code, “low coupling” between “boundaries” can be achieved through microservices. For example, the “order” microservice needs to call the “payment” microservice. In the design:

  • First, add a “payment” interface to the “order” microservice, so that all calls to payment in the “order” microservice are calls to this interface;
  • Next, implement payment in other “payment” microservices. For example, assume that we have designed two “payment” microservices, A and B. If the configured service at runtime is A, then the “order” microservice calls A; if it is B, then it calls B.

This way, the coupling between the “order” microservice and the “payment” microservice is resolved, and the system can handle various user environments and requirements by modifying the configuration.

With boundary context design, the quality of the system’s design improves and the cost of changes decreases.

  • In the past, each module directly read the user information table in the database when reading user information. Once the user information table changes, all modules need to be modified, resulting in an increasing cost of changes.
  • Now, with domain-driven design, the responsibility of reading user information is delegated to the “user management” boundary context. Other modules call its interface to access user information. Therefore, when the user information table changes, only the “user management” boundary context is affected, and other modules remain unaffected, reducing the cost of maintenance. The entire system is divided logically according to boundary contexts, but physically they are still part of the same project and run in the same JVM. This type of boundary context is only a “logical boundary”.
  • In the future, when transforming a monolithic application into a microservices architecture, each boundary context will run in a different microservice, which is a different project with a different JVM. Moreover, as microservices are split, the databases are also split, and each microservice uses a different database. Therefore, when various microservices need to access user information, they do not have permission to access the user database directly and can only call the relevant interfaces exposed by the “user” microservice through remote interfaces. This is when this boundary context truly becomes a “physical boundary”, as shown in the following diagram:

Drawing 3.png

Challenges in Microservice Splitting #

Today, many software teams are joining the microservices transformation trend, breaking down increasingly complex monolithic applications into simple and understandable microservices to reduce the complexity of the system. However, the biggest challenge now is how to split microservices.

Drawing 5.png

As shown in the above diagram, many systems have been designed this way in the past. Now, if we simply and crudely split them into multiple microservices according to this design concept, it will be a disaster for future maintenance.

  • When multiple modules need to read the product information table, they directly access the table using JDBC (Java Database Connectivity).
  • Following this design concept, multiple microservices need to read the product information table.
  • As a result, once the product information table changes, multiple microservices need to be modified. Not only do multiple teams need to modify the code to maintain this requirement, but their microservices also need to be modified, released, and upgraded simultaneously.

If every maintenance task is carried out in this way, not only can microservices not leverage their advantages, but the cost of maintenance will also increase. If microservices are designed this way, using microservices would be worse than not using them.

The key issue here is that when multiple microservices need to read the same table, it means that the code responsible for the same software change reason (due to changes in product information) is scattered among multiple microservices. In this case, when the system changes due to the same reason, the code modification will naturally be scattered among multiple microservices. In other words, the root cause of the design problem mentioned above violates the “single responsibility principle” and makes the design of microservices no longer highly cohesive.How should microservices be designed and split? The key lies in “being small and specialized”. In other words, “specialization” means high cohesion.

Therefore, microservice design is not merely about splitting, but also requires higher design requirements, namely achieving “high cohesion”. Only in this way can future changes be primarily maintained in a specific microservice, thus reducing the cost of maintenance. Only in this way can the advantages of microservices be leveraged, and only then is it the correct way to approach microservices.

To achieve high cohesion in microservice design, the best practice is DDD:

  • Start with DDD to analyze requirements and build domain models, gradually establishing multiple problem subdomains;
  • Then map the problem subdomains to boundary contexts, forming a context map of their relationships;
  • Finally, implement the subdomains in microservices with anemic models or rich models, and form interfaces between microservices based on the context map.

Only with this design can “low coupling between microservices and high cohesion within microservices” be achieved.

Conclusion #

In summary, the challenge in microservice design is splitting, and the core of splitting lies in being “small and specialized” and achieving “high cohesion”. Therefore, the key to solving the challenge of microservices lies in DDD. With DDD, microservice teams can analyze and understand the business requirements clearly, think through the design deeply, and thereby improve the quality of microservice design when dealing with increasingly complex business scenarios.