15 How to Design a Technical Backend That Supports Microservices

15 How to design a technical backend that supports Microservices #

With the support of a technical platform for domain-driven design (DDD), how can it be applied to a microservices architecture? A technical platform that supports DDD and microservices should possess the following capabilities:

  • Solving the technological uncertainty in the current microservices architecture, allowing microservice projects to adapt to future changes in architectural technologies at a lower cost.
  • Facilitating the application of domain-driven design to microservices, including domain modeling, microservice decomposition based on bounded contexts, event notification mechanisms, etc.
  • Resolving the problem of remote data assembly between microservices, especially when it comes to replacing local queries with remote API calls during the assembly of domain objects in repositories and factories.

Addressing Technological Uncertainty #

In today’s microservices architecture, Spring Cloud has become the dominant framework. However, there are still many uncertainties in various technical components of the Spring Cloud framework, such as whether to use Eureka as the registration center or whether to choose Zuul or Gateway as the service gateway, and so on. Additionally, the emergence of service meshes, like Service Mesh, suggests the possibility that all microservices will switch to service meshes in the future. Under such circumstances, how should we make architectural decisions for microservices? We should strive to decouple the code from Spring Cloud in order to make future transitions to a service mesh easier. How can we achieve this?

Drawing 0.png

Design with a single controller and a single DAO in microservices architecture

As shown in the above diagram, when the frontend accesses microservices through the service gateway, it first needs to access the microservices in the aggregation layer. Thus, a single controller in the aggregation layer receives frontend requests. In this way, only this controller is coupled with the MVC framework, while all the services that follow are decoupled. This achieves the separation of business code and technical framework.

Similarly, when a service performs various operations and calls microservices in the atomic service layer, instead of using Ribbon for remote calls, we expose the interfaces of the atomic service layer microservices and create a Feign interface in the aggregation layer microservices. Then, the aggregation layer microservices make calls to the atomic microservices via this locally defined interface. This way, the services in the aggregation layer microservices do not couple with Spring Cloud components, and only the Feign interfaces couple with Spring Cloud to achieve remote calling. This decouples the business code from the technical framework.

This same principle applies to the exposed interfaces of the atomic service layer microservices. Instead of directly exposing API interfaces, which would require writing related annotations and coupling the service with Spring Cloud, we have a unified controller to expose the interfaces and call the internal services. This way, all the services remain pure and decoupled from the Spring Cloud technical framework, with only the controller coupled to it.

With these designs in place, when transitioning from the Spring Cloud framework to a service mesh in the future, it only requires migrating the pure, decoupled services and the business code in the domain objects to the new framework, resulting in a very low-cost switch between different technical frameworks and the ability to keep pace with technological developments. This approach effectively addresses the issue of technological uncertainty and facilitates architectural evolution.

Architectural Design for Remote Microservice Calls #

In addition, the greatest challenge in microservice architecture design lies in properly decomposing microservices while ensuring single responsibility, meaning low coupling between microservices and high cohesion within each microservice. How can we achieve this in software projects? The best industry practice is undoubtedly domain-driven design, where we first model the domain business and then perform microservice decomposition based on bounded contexts. This design ensures that each change is confined to a specific microservice. Once a microservice completes its changes, it can be independently upgraded, resulting in reduced maintenance costs and faster delivery. Based on this idea, each microservice is designed using a domain-driven technology platform. In this way, each microservice is based on domain-driven modeling and design, and then implemented in the technology platform, which not only improves development speed but also reduces maintenance costs.

However, after the transition to microservices, there is a technical challenge that needs to be addressed: cross-database data operations. When a monolithic application is split into multiple microservices, not only does the application need to be split, but the databases also need to be split. For example, after the microservices are split, there is an order database for orders and a user database for users. In this case, when querying orders and needing to retrieve the corresponding user information, the query cannot be performed directly from the local database. Instead, the remote interface of the “user” microservice is called to query the user database, and then return the result to the “order” microservice. In this situation, the existing technology platform needs to be adjusted.

How can it be adjusted? When executing queries or loading operations, the generic DDD repository no longer queries the local database using DAO to retrieve the order and fill in the user information. Instead, it changes to calling the remote interface of the user microservice. A User Service Feign interface can be written in the local order microservice, and the order repository and factory can call this interface. The remote call to the user microservice is then implemented through the Feign interface.

Remote Calls Using Feign Interfaces #

Each microservice runs as an independent process on separate JVMs, possibly on different physical nodes, and can only be accessed through networking. Therefore, the calls between microservices are inevitably remote calls. In the past, we used Ribbon to handle the calls between microservices, where a restTemplate could be injected into any position in the code for remote calls.

However, this kind of code is too casual and becomes increasingly difficult to read and maintain. For example, assume that there are two modules, A and B, in one microservice, both of which need to call module C. As the business becomes more complex, the microservice needs to be split, and module C is separated into another microservice. In this case, modules A and B cannot call module C in the same way as before, otherwise an error occurs.

Drawing 1.png

Remote calls using Ribbon

How can we solve this problem? We need to modify both module A and B, and add restTemplate for remote calls to module C. In other words, all programs that call module C need to be modified, which incurs higher costs and risks.

Therefore, when implementing calls between microservices, we usually use another solution: Feign. Feign is not a new solution, but a encapsulation of Ribbon. Its purpose is to make the code more standardized and easier to maintain. The approach is that we don’t modify any code in module A and B, but create a local interface C′ for module C in the microservice. This interface is exactly the same as module C, containing all the methods of module C. Therefore, modules A and B can still call the local interface C′ just like before. However, interface C′ is just an interface and cannot do anything itself, so we need to add Feign annotations to implement the remote calls and call module C. This solution does not modify modules A and B, nor module C, but just adds an interface C′, minimizing the maintenance costs.

Drawing 2.png

Remote calls using Feign

How to implement remote calls between microservices using Feign?

First, when creating a project, change the pom.xml file to add dependencies like Eureka Client, Hystrix, and Actuator, and replace Ribbon with Feign:

<dependency>
<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-openfeign</artifactId>

</dependency>

<!-- Circuit Breaker - Hystrix -->

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>

</dependency>

<!-- Circuit Breaker Monitoring -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>


Next, in the FeignApplication main class, not only do we need to add the Discovery Client, but also the Feign Client:

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.netflix.hystrix.EnableHystrix;

import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication

@EnableDiscoveryClient

@EnableFeignClients

@EnableHystrix

public class FeignApplication {

public static void main(String[] args) {
@SpringBootApplication
public class FeignApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(FeignApplication.class, args);
    }
}

When using Feign to make calls, first write an interface on the consumer side that is exactly the same as the API exposed by the producer, and then add the Feign annotation:

/**
 * The service of suppliers.
 * @author fangang
 */
@FeignClient(value="service-supplier", fallback=SupplierHystrixImpl.class)
public interface SupplierService {

    /**
     * @param id
     * @return the supplier
     */
    @RequestMapping(value = "orm/supplier/loadSupplier", method = RequestMethod.GET)
    public Supplier loadSupplier(@RequestParam("id")Long id);

    /**
     * @param ids
     * @return the list of suppliers
     */
    @PostMapping("orm/supplier/loadSuppliers")
    public List<Supplier> loadSuppliers(@RequestParam("ids")List<Long> ids);

    /**
     * @return the list of suppliers
     */
    @GetMapping("orm/supplier/listOfSuppliers")
    public List<Supplier> listOfSuppliers();

}

In this example, the specific process is as follows:

  • There is a SupplierService class on the producer side, so first write an interface called SupplierService on the consumer side that is exactly the same.
  • Then, add the @FeignClient annotation in front of the interface.
  • The value here is the name the producer registered in the Eureka registry.
  • Add the annotation before each method, use @GetMapping for GET requests and @PostMapping for POST requests. The name should be the name of the interface exposed by the producer.
  • If you need to include parameters in the URL, add the @RequestParam annotation before the parameter.
  • With the above annotations, Feign can retrieve the corresponding data from the interface, assemble it into a URL, and finally execute the Ribbon to remote call microservices.

Calling Feign Interface using Ref Tag #

After using Feign for remote calls between microservices, changes need to be made in the vObj.xml file when modeling. The join tag needs to be changed to ref. The configuration is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
  <vo class="com.demo2.product.entity.Product" tableName="Product">
    <property name="id" column="id" isPrimaryKey="true"></property>
    <property name="name" column="name"></property>
    <property name="price" column="price"></property>
    <property name="unit" column="unit"></property>
    <property name="classify" column="classify"></property>
    <property name="supplier_id" column="supplier_id"></property>
    <ref name="supplier" refKey="supplier_id" refType="manyToOne" bean="com.demo2.product.service.SupplierService" method="loadSupplier" listMethod="loadSuppliers"></ref>
  </vo>
  <vo class="com.demo2.product.entity.Supplier" tableName="Supplier">
    <property name="id" column="id" isPrimaryKey="true"></property>
    <property name="name" column="name"></property>
  </vo>
</vobjs>

In this configuration, the join tag for supplier is changed to ref, where:

  • bean represents the Feign interface for the consumer to make local calls to the supplier microservice.
  • method specifies the method to call on this Feign interface.
  • listMethod is an optimization measure for batch querying “products” datasets.

With this configuration, when querying products, the generic repository will make remote calls to the “supplier” microservice using the SupplierService Feign interface, rather than calling the local DAO, thus achieving data assembly across microservices.

Summary #

This article introduces the design idea of a technical middleware that supports DDD + microservices architecture. With the design mentioned above, the clean architecture thinking of decoupling business code from technical frameworks is realized, making it easier for the system to evolve in the future. It also achieves data assembly between domain models in microservices to solve the key technical problems of DDD transitioning to microservices architecture. With this technical middleware, development teams can apply DDD and implement it in real projects.

In the next article, the code for this technical middleware will be shared on GitHub, and further explanations on the code design and project practice will be provided.

Click here to view the source code on GitHub.