12 How a Set of Services Should Adapt to Different End User Requirements

12 How a Set of Services Should Adapt to Different End-user Requirements #

After practicing in the previous few chapters, members are now able to bind their mobile phone numbers, update their personal information, bind their personal vehicle information, open monthly cards, and check-in, among other functions. Next, let’s start with viewing the user’s own data from the client and talk about the issue of service invocation.

Simple handling #

We have already vertically split the user data and distributed it in different databases. When the client needs to display the data, it needs to call the APIs of different services separately. The front-end then reassembles the data and presents it to the user.

Member personal information, vehicle information, and monthly card information are maintained in the member database, while points information is maintained in the points database. If we want to display both of these data on the same page, the client needs to make two API calls to retrieve the complete set of data, as shown in the following diagram:

img

This approach essentially gives control to the front-end, as it is responsible for organizing the data while the backend only provides fine-grained services. While the microservice architecture increases business flexibility, it also complicates the front-end calls, resulting in two prominent issues:

  1. The front-end needs to initiate multiple API requests, which increases network overhead and may negatively impact the user experience in extreme cases.
  2. The workload of front-end development increases.

Service Aggregation #

The problems exposed by the previous data call process become amplified when the functionality becomes complex and the services become finely divided, thereby affecting the use of the product. In such cases, it is necessary to optimize the call process. We can make some adjustments at the architectural level by adding an adaptation layer between the client and the microservice layer. The purpose is also simple: the client only needs to make one request to the adaptation layer service, which will aggregate multiple child services. The data from each child database is then reassembled into the data required by the front-end based on business rules. When this data is returned to the front-end, the front-end only needs to handle the presentation. As a result, the call chain looks like the following diagram:

img

The original two requests initiated by the client (in reality, there may be more requests depending on the data distribution) have now been reduced to one request. The service can also provide APIs at different levels of granularity. The most granular APIs can be built on the basis of fine-grained APIs, while also providing preliminary aggregation interfaces. Depending on the data, a higher-level data aggregation can be performed in the adaptation layer.

Service Adaptation #

While service aggregation has already optimized the call process to some extent, there are still shortcomings. In the era of mobile internet, the form of terminals is becoming increasingly diverse, including WeChat Official Accounts, mini-programs, native applications, as well as tablets and desktops. When faced with different clients, a single adaptation layer will inevitably struggle to meet the different requirements of multiple terminals, making coordination extremely difficult. When the requirements of a terminal change, the API interfaces for different terminals also need to be changed, resulting in high development, testing, and operation costs. Therefore, a further change in structure is required to achieve optimization, as shown in the following diagram:

img

For each client, a corresponding adaptation layer is built in the backend. When one party’s requirements change, only the corresponding adaptation layer needs to be modified, without the need to change the underlying backend services.

If a client needs to call fine-grained services, it can directly call the underlying microservices without going through the adaptation layer service. This is not absolute.

BFF Architecture #

Regarding the problems we mentioned above with service aggregation and service adaptation, the industry has long had a term called BFF architecture, which stands for Backend For Frontend. This means that the backend serves the front-end. In this layer, adjustments can be made to the services based on the different requirements of the front-end, without changing the basic backend services. It is language-independent and can be implemented using Java, Node.js, PHP, or other languages. As for whether this layer is maintained by front-end developers or backend developers, the industry does not have a unified agreement, but it tends to be maintained by front-end developers because the BFF layer is closely integrated with the front-end.

Project Implementation #

Since we are developing on the Java platform, we will continue to use Java for this adaptation layer. Of course, if there are other languages that you are proficient in, such as Node.js, you can also use them.

Create two adaptation layer service projects, parking-bff-minprogram-serv and parking-bff-native-serv, to correspond to the mini-program and native application terminals. Add these two basic functions and configure them according to the previous project configuration so that they can be applied normally. For example, provide an interface management interface, service invocation, circuit breaker configuration, service registration and discovery, and so on.

There is a difference between mini-programs and native applications when obtaining member information - mini-programs do not require vehicle information, while native applications need to display vehicle information.

Write the feignClient class for calling the member and points APIs:

@FeignClient(value = "member-service", fallback = MemberServiceFallback.class)
public interface MemberServiceClient {

    @RequestMapping(value = "/member/getMember", method = RequestMethod.POST)
    public CommonResult<Member> getMemberInfo(@RequestParam(value = "memberId") String memberId);

    // The parking-bff-minprogram-serv adaptation layer does not have this interface
    @RequestMapping(value = "/vehicle/get", method = RequestMethod.POST)
public CommonResult<Vehicle> getVehicle(@RequestParam(value = "memberId") String memberId);
}

@FeignClient(value = "card-service", fallback = MemberCardServiceFallback.class)
public interface MemberCardClient {

    @RequestMapping(value = "/card/get", method = RequestMethod.POST)
    public CommonResult<MemberCard> get(@RequestParam(value = "memberId") String memberId) throws BusinessException;

}



Write the business logic processing class:



@RestController
@RequestMapping("bff/nativeapp/member")
public class APIMemberController {

    @Autowired
    MemberServiceClient memberServiceClient;

    @Autowired
    MemberCardClient memberCardClient;

    @PostMapping("/get")
    public CommonResult<MemberInfoVO> getMemberInfo(String memberId) throws BusinessException {
        CommonResult<MemberInfoVO> commonResult = new CommonResult<>();

        // service aggregation
        CommonResult<Member> member = memberServiceClient.getMemberInfo(memberId);
        CommonResult<Vehicle> vehicle = memberServiceClient.getVehicle(memberId);
        CommonResult<MemberCard> card = memberCardClient.get(memberId);

        MemberInfoVO vo = new MemberInfoVO();
        if (null != member && null != member.getRespData()) {
            vo.setId(member.getRespData().getId());
            vo.setPhone(member.getRespData().getPhone());
            vo.setFullName(member.getRespData().getFullName());
            vo.setBirth(member.getRespData().getBirth());
        }

        if (null != card && null != card.getRespData()) {
            vo.setCurQty(card.getRespData().getCurQty());
        }
        // The adapter layer of parking-bff-minprogram-serv does not have this data aggregation
        if (null != vehicle && null != vehicle.getRespData()) {
            vo.setPlateNo(vehicle.getRespData().getPlateNo());
        }
        commonResult.setRespData(vo);
        return commonResult;
    }
}



From the code, it can be seen that the two interfaces that originally needed to be called by the client are directly completed by the adapter layer. The results are aggregated and returned to the client at once, reducing one interaction. For different terminals, the data response is also different, reducing data transmission costs and the possible exposure of sensitive data.

So far, by introducing the BFF adapter layer, our architecture has been further optimized, reducing the complexity of front-end calls and network overhead. In addition to service aggregation and service adaptation, can you think of any other functions of the BFF layer?