10 Technical Practices for Microservices Implementation

10 Technical Practices for Microservices Implementation #

Nowadays, it is becoming increasingly difficult to be an excellent programmer. The fierce market competition, rapid iteration of the Internet, and the large-scale development of software systems have undoubtedly greatly increased the difficulty of software design. Therefore, the requirements for the abilities of architects are getting higher and higher, as stated in my book:

As a top-level architect, you should have two core abilities: (1) the ability to convert business into technology; (2) the ability to leverage technology to support business.

“Just as a soldier who does not want to become a general is not a good soldier,” as an ordinary developer, you should also set the goal of becoming a top-level architect and strive constantly.

The ability to convert business into technology means that you need to focus more on understanding the business. Technology itself cannot generate value. You must have strong business implementation capabilities and be able to transform user’s business requirements into technical solutions, developing products and features that users are willing to use, so that they will pay for it and the company can make money. Only with this ability can you effectively help the company generate benefits and reflect your value. Learning DDD will enable you to master the ability to quickly learn about the business domain.

The ability to leverage technology to support business means that you must have extensive knowledge and a broad perspective, so that you can quickly implement reasonable, and even optimal, technical solutions based on user’s business pain points. By creating the features that users need and making them pay for it, the business can generate benefits. However, it is now a time of rapid technological change, with various high-tech emerging constantly. Developing a new product each time is not just using existing technology, but applying more new technologies to solve new problems, making the product more competitive and vital. Therefore, you must have broad technical knowledge and strong technical implementation capabilities.

In the previous lesson, we discussed the analysis and design process of implementing microservices through DDD, and then implemented these designs in the development of each microservice. The implementation of microservices is not that simple. It requires solving many technical challenges in design and implementation. In this lesson, let’s explore this topic.

How to Leverage the Advantages of Microservices #

Microservices are not a silver bullet, and they have many “pitfalls.” As mentioned at the beginning, when we break down a large business system into simple microservices, we hope to use reasonable microservice designs to make each requirement change handled by a small team independently, and let the requirement change fall on a specific microservice. Only in this way, modifying this microservice independently, packaging and upgrading it independently, the new requirement can be implemented, and the advantages of microservices can be harnessed.

image.png

However, many past systems were designed like this (as shown in the above figure). Multiple modules need to read the product information table, so they all directly read it through JDBC. Now we want to transition to microservices. Initially, we adopted the microservice design of data sharing, which means that the database remains unchanged, and then the microservices are simply split according to the functional modules. At this time, multiple microservices need to read the product information table, and they all access it directly through SQL. With such a design, once the product information table changes, multiple microservices need to be modified. This design makes microservices complex in terms of changes and deployment and unable to leverage their advantages.

image

Through the guidance of DDD mentioned earlier, we hope to design microservices that are “small and specialized.” According to this idea, when other microservices need to read or write product information, they cannot directly read the product information table. Instead, they should call the “product maintenance” microservice through API interfaces. In this way, the code that needs to be modified in the future due to changes in product information is only limited to the “product maintenance” microservice. As long as the API interfaces of the “product maintenance” microservice remain unchanged, this change is unrelated to other microservices. Only with such a design can the advantages of microservices truly be realized. In order to standardize the design of “small and specialized” microservices, during the initial transformation to microservices, the database tables should be divided according to user permissions based on Domain-Driven Design (DDD) principles. Each microservice can only access its own tables using its own account. When accessing other tables, the corresponding microservice can only be accessed through its API. This division prepares for future database splitting, making the microservices transformation smoother.

How to Provide Microservice Interfaces #

Therefore, the design of microservices is not isolated from each other; they need to call each other’s interfaces to achieve high cohesion. However, when one microservice team requests an interface call from another microservice team, how should the other microservice team design it?

The first question is, when multiple teams are requesting API interfaces from you, how do you provide the interfaces? If each team provides you with requirements and you have to create a new interface for each, your microservice will become very unstable. Therefore, when multiple teams make requests, these interfaces must be planned so that their requirements can be met by reusing as few interfaces as possible. When a new interface is proposed, try to solve the problem using existing interfaces. By doing this, you can maintain your microservice better with lower maintenance costs.

Next, what should be done when the caller needs to change the interface? The changes to existing interfaces should be made as backward-compatible as possible, meaning that the interface name and parameters should remain unchanged, and only new functionality should be added internally. This is done to avoid affecting the calls from other microservices. If it is really necessary to change an existing interface, it is better to add a new interface instead of modifying the original one.

DDD 10–Golden Sentence.png

Furthermore, do the value objects passed between the caller and callee need to be exactly the same? No, they don’t have to be. If the callee adds fields to value objects due to certain changes, but the caller doesn’t use these fields, then the caller doesn’t need to change the value objects accordingly. This is because the calls between microservices are made through RESTful interfaces, passing data in the form of JSON, which is a loosely coupled call. Therefore, the value objects passed between the caller and callee can be inconsistent, reducing the scope of microservice updates when requirements change.

Finally, how does the caller call the interface? This can be done through synchronous or asynchronous calls.

  • In Lecture 09, it was mentioned that the “Order Acceptance Service” notifies the “Restaurant Acceptance Service” through a message queue after an order is placed, which is an asynchronous call.
  • Furthermore, the “Order Acceptance Service” often needs to query the user table, but as mentioned earlier, it does not have permission to access the user table because it belongs to the “User Registration” microservice. In this case, the “Order Acceptance Service” calls the relevant interfaces of the “User Registration Service” synchronously.

In terms of specific design and implementation, a Feign interface for the “User Registration Service” should be added to the local “Order Acceptance” microservice. This allows the “Order Acceptance Service” to call the “User Registration Service” as if it were a local call, achieving remote invocation through this Feign interface. This design is called the “antifungal layer” design, as shown in the following diagram:

Drawing 2.png

Microservices Splitting and Designing Anti-Corruption Layers #

For example, let’s imagine this scenario. In the past, the “User Registration Service” was part of the “User Order” microservice. However, as the microservice design progressed, it became necessary to split the “User Registration Service” into another microservice. In this case, both the “User Order Service” and the “Cancel Order Service”, as well as other services that call the “User Registration Service”, will encounter errors and require modifications. The maintenance cost becomes high. To solve this problem, we can create a Feign interface for the “User Registration Service” in each microservice. This way, other services do not need modification, reducing maintenance costs. This is the role of the “anti-corruption layer”, which aims to reduce maintenance costs when interface changes occur.

Decentralized Data Management #

Following the DDD design mentioned earlier, the database is divided into user databases, order databases, order receiving databases, delivery databases, and restaurant databases based on microservices. How can we implement these database designs? The biggest challenge in microservices design is to deal with high concurrency and big data on the internet. Therefore, we can adopt the concept of “decentralized data management” and select different data storage solutions based on the data volume and user access characteristics:

  • For microservices such as “User Registration” and “Restaurant Management” with small data volume but frequent reads, we can use a small MySQL database and deploy Redis in front of it to improve query performance.
  • For microservices such as “User Order”, “Restaurant Order”, and “Courier Delivery” with large data volume and high concurrency writes, a single database can’t handle the pressure. In this case, a distributed storage solution like TiDB, a NewSQL database, can be used to distribute data across multiple nodes and solve I/O bottlenecks.
  • For query analysis services like “Business Analysis” and “Order Query”, NoSQL databases or big data platforms can be used. Data from production databases can be synchronized to a distributed storage solution through read-write separation and then undergo a series of preprocessing, allowing for decision analysis and sub-second queries on massive historical data.

Based on these designs, we can effectively handle high concurrency and big data in internet applications, improving system performance. The design is shown in the following diagram:

Drawing 3.png

Decentralized data management diagram for online food ordering system

Challenges in Data Association Querying #

In addition, due to the database split, various queries performed by different microservices during the business process can no longer perform join operations as before. Instead, data filling is done through interface calls. For microservices like “User Order”, “Restaurant Order”, and “Courier Delivery”, they no longer have access to user and restaurant tables due to database split. Therefore, the query process needs to be refactored. The process is divided into two steps, as shown in the diagram below:

image

The query process involves two steps:

  1. Querying order data without performing join operations. The result of such a query may be 10,000 rows. However, only 20 rows are returned to the microservice through pagination.
  2. Calling the relevant interfaces of the “User Registration” and “Restaurant Management” microservices to fill in the user and restaurant data.

This approach solves the problem of cross-database association query and improves query efficiency with massive data. Traditional database designs tend to slow down query speed as data volume increases because join operations are involved. Therefore, to improve query performance with massive data, it is necessary to eliminate join operations and implement data filling by pagination.