×
Community Blog How to Code Complex Applications: Core Java Technology and Architecture

How to Code Complex Applications: Core Java Technology and Architecture

In this blog, we will introduce a set of methodologies to code complex applications, focusing on the retail industry.

11.11 Big Sale for Cloud. Get unbeatable offers with up to 90% off on cloud servers and up to $300 rebate for all products! Click here to learn more.

Everyone who follows my work knows that I have been committed to the governance of application architecture and code complexity. Recently, I have been studying the code of the Ling Shou Tong product domain. The complex business scenario of Ling Shou Tong poses a new challenge at the architecture and code levels. To address the challenge, I have conducted a carefully thought out study. On the basis of the actual business scenario, I have developed a set of methodologies on how to code complex applications, and today, I would like to share these methodologies with you.

Processing of Complex Application: Background

Let's begin with a brief background about Ling Shou Tong. It is a B2B model for offline stores that is developed to reconstruct traditional supply chain channels through digitization for improving supply chain efficiency and boosting New Retail. In this process, Alibaba acts as the platform that provides the service functions of Bsbc.

1

Firstly, in the product domain, a "launch" action is performed. Once, the product is launched, it can be sold to various mom-and-pop stores through Ling Shou Tong. Launching a product is one of the key business operations in Ling Shou Tong. Therefore, it involves several verification and association operations. A simplified business process for product launching is illustrated below:

2

Process Decomposition

For addressing a complex business scenario, writing code by using a service method is not feasible. So, if there is no way to address it by using one class, it is recommended to use decomposition instead.

Actually, if engineers can recall the "divide and conquer" method, it is regarded as a good job. At least, it is better to have the consideration for decomposition than none. I have also encountered business scenarios of similar complexity, which are processed by using a number of methods and classes without decomposition.

However, there is a challenge with decomposition as well. Many engineers rely too much on tools or auxiliary means to implement decomposition. For example, in our product domain, we have a minimum of three similar decomposition methods, such as self-made process engines and database-based process handling.

3

To put it simply, all these methods are only auxiliary to the pipeline processing and do not add anything considerable. Therefore, I recommend that we follow the Keep It Simple and Stupid (KISS) approach by not using any tools with a simple pipeline mode as the suboptimal choice and methods such as process engines as the last choice. Unless your application has a strong demand for process visualization and orchestration, it is recommended not to use any tools such as process engines. This is suggested, primarily because it introduces additional complexity, especially when the process engines require persistence, and secondly, as it splits the code, which results in poor readability of the code. To be bold, it is estimated that 80% of the use of process engines is not worthwhile.

Coming back to the main topic of product launching, there are few essential questions that need to be addressed:

  • Do the adopted tools form the core of the topic?
  • Does the code flexibility introduced by the design mode form the core of the topic?

Apparently, answers to both these questions is a clear "no". The core point should be how to break down the problem and abstract it. If you know the pyramid principle, you can use structured decomposition to deconstruct the problem into a hierarchical pyramid structure as shown below:

4

The code written as per the decomposition method is like a book with clear directories and content. Taking the example of product launching, the program entry is an OnSale command that consists of three phases.

@Command
public class OnSaleNormalItemCmdExe {

    @Resource
    private OnSaleContextInitPhase onSaleContextInitPhase;
    @Resource
    private OnSaleDataCheckPhase onSaleDataCheckPhase;
    @Resource
    private OnSaleProcessPhase onSaleProcessPhase;

    @Override
    public Response execute(OnSaleNormalItemCmd cmd) {
        
        OnSaleContext onSaleContext = init(cmd);
        
        checkData(onSaleContext);

        process(onSaleContext);

        return Response.buildSuccess();
    }

    private OnSaleContext init(OnSaleNormalItemCmd cmd) {
        return onSaleContextInitPhase.init(cmd);
    }

    private void checkData(OnSaleContext onSaleContext) {
        onSaleDataCheckPhase.check(onSaleContext);
    }

    private void process(OnSaleContext onSaleContext) {
        onSaleProcessPhase.process(onSaleContext);
    }
}

Each of these phases can be split into multiple steps. Using the OnSaleProcessPhase as an example, it contains a series of steps as mentioned below:

@Phase
public class OnSaleProcessPhase {

    @Resource
    private PublishOfferStep publishOfferStep;
    @Resource
    private BackOfferBindStep backOfferBindStep;
    //omit other steps

    public void process(OnSaleContext onSaleContext){
        SupplierItem supplierItem = onSaleContext.getSupplierItem();

        // generate OfferGroupNo
        generateOfferGroupNo(supplierItem);
       
       // publish offer
        publishOffer(supplierItem);

        // bind back offer stock
        bindBackOfferStock(supplierItem);

        // synchroize sotck
        syncStockRoute(supplierItem);

        // set virtual product tag
        setVirtualProductExtension(supplierItem);

        // set procteciton label
        markSendProtection(supplierItem);

        // record Change Details
        recordChangeDetail(supplierItem);

        // synchronize price
        syncSupplyPriceToBackOffer(supplierItem);

        // set exteinsion info 
        setCombineProductExtension(supplierItem);

        // remove sellout tag
        removeSellOutTag(offerId);

        // fire domian event
        fireDomainEvent(supplierItem);
        
        // close to-do issues
        closeIssues(supplierItem);
    }
}

In this process of complex product launching scenario, it is crucial to answer the following two questions:

  • Is a process engine required?
  • Is the support of a design mode required?

The answer to both the questions is "No", and hence the simple composed method cannot be more applicable to express such a business process.

Therefore, while implementing process decomposition, it is suggested that engineers should not focus too much on tools or the flexibility brought about by design modes. Instead, we should spend more time on problem analysis, structural decomposition, and reasonable abstraction to finally obtain appropriate phases and steps.

5

Post Process Decomposition Challenges

The code after process decomposition is clearer and easier to maintain than before. However, it is important to note the following two problems associated with decomposition:

  1. Fragmented Domain Knowledge: There is no place for domain knowledge aggregation. The code for each use case is only specific to its own process, and the knowledge is not centrally accumulated. The same business logic is implemented repeatedly in multiple use cases, which leads to a severe code repetition. Though code can be reused, only one snippet can be extracted for reuse at most.
  2. Code Failure to Express Business Semantics: While coding for a process, it is expected to express how to obtain data, perform computation, and store the resulting data. In this case, it is hard to realize the same because models and the relationship between the models are missing. Being separated from models, business semantic expressions lose their rhythm and soul.

For example, a verification is performed to check the inventory during the product launching process. The inventory processing of combined products (CombineBackOffer) is different from that of ordinary products. The original code mentioned below:

boolean isCombineProduct = supplierItem.getSign().isCombProductQuote();

// supplier.usc warehouse needn't check
if (WarehouseTypeEnum.isAliWarehouse(supplierItem.getWarehouseType())) {
// quote warehosue check
if (CollectionUtil.isEmpty(supplierItem.getWarehouseIdList()) && !isCombineProduct) {
    throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "You cant publish offer, since there is no warehouse info");
}
// inventory amount check
Long sellableAmount = 0L;
if (!isCombineProduct) {
    sellableAmount = normalBiz.acquireSellableAmount(supplierItem.getBackOfferId(), supplierItem.getWarehouseIdList());
} else {
    //combination product
    OfferModel backOffer = backOfferQueryService.getBackOffer(supplierItem.getBackOfferId());
    if (backOffer != null) {
        sellableAmount = backOffer.getOffer().getTradeModel().getTradeCondition().getAmountOnSale();
    }
}
if (sellableAmount < 1) {
    throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "Your stock is less than 1, please supply more items. The product id:" + supplierItem.getId() + "]");
}
}

However, if we introduce the domain model in the system, the code will be simplified as follows:

if(backOffer.isCloudWarehouse()){
    return;
}

if (backOffer.isNonInWarehouse()){
    throw new BizException("You cant publish offer, since there is no warehouse info");
}

if (backOffer.getStockAmount() < 1){
    throw new BizException("Your stock is less than 1, please supply more items,The product id:" + backOffer.getSupplierItem().getCspuCode() + "]");
}  

Obviously, the expression after using a model is much clearer and easier to understand. In addition, you do not need to make judgments on whether they are combined products or not. Due to a more realistic object model (CombineBackOffer inherits BackOffer) adopted in the system, we can eliminate most of the if-else statements in our code through object polymorphism.

6

Process Decomposition Plus Object Models

From the preceding case, we can infer that using process decomposition is better than none. Furthermore, process decomposition plus object models are better than process decomposition alone. In the case of product launching, if we adopt process decomposition plus object models, we will get the following system structure:

7

Methodologies Used to Code Complex Applications

In the preceding sections, we covered how to code complex applications. To be precise, it is the combination of top-down structured decomposition and bottom-up object-oriented analysis. Now, let us further abstract the preceding case to form a feasible methodology that can be used in more complex business scenarios.

Top-down and Bottom-up Combination

The top-down and bottom-up combination suggests combining top-down process decomposition and bottom-up object modeling to spirally build our application system. This is a dynamic process. The two operations can be carried out alternately or simultaneously. Moreover, they complement each other. The upper layer analysis can help us better clarify the relationship between models, while the lower layer model expression can improve our code reusability and business semantic expression capabilities.

The following figure shows the process:

8

This combination helps us to write clean and easy-to-maintain code for any complex business scenarios.

Capability Sink-in

While using the domain-driven design (DDD) in practice, we experience the following two phases:

  1. Using Only Concepts: In this phase, you may understand few concepts of DDD, and eventually use some of them including the Aggregation Root, Bonded Context, Repository concepts while coding. Further, you may also use some layered strategies. However, this has little effect on complexity governance.
  2. Achieving Mastery: In this phase, terms become less important. You can understand the essence of DDD, which is a method of establishing a ubiquitous language, defining boundaries, and performing object-oriented analysis.

With reference to my approach, I would define myself near the second phase due to the questions that have been perplexing me: What capabilities should be placed on the Domain layer, and is it reasonable to follow the tradition to collect all services to the Domain layer? To be honest, I have never found answers to these questions.

In real business scenarios, many capabilities are specific to use cases. If you use the Domain layer to collect services blindly, it is likely that little benefit is obtained. On the contrary, the collection will lead to the expansion of the Domain layer, which will affect reusability and expression capability.

In this view, I think that we should adopt the strategy of capability sink-in. It implies that we do not force ourselves to design all capabilities of the Domain layer at one time, and we do not have to place all business capabilities on the Domain layer. Instead, we must adopt a pragmatic attitude, for abstracting and extracting only the capabilities that need to be reused in multiple scenarios and temporarily put the capabilities that are not reused in the use cases at the App layer.

Note: Use case is a term used in the book Clean Architecture. To express this term in a simple manner, it is the process of responding to a request.

Through practice, I have found that this step-by-step capability sink-in strategy is a more practical and agile method, as we agree that the model is not designed at one time, and is a result of iterations.

The sink-in process is shown in the following figure. If we find that step 3 of use case 1 and step 1 of use case 2 have similar capabilities, we can consider extracting and migrating the capabilities to the Domain layer. In this way, code reusability is improved.

9

Code Reusability and Cohesion: Two Key criteria in the Sink-in

Reusability is about determining when the sink-in should be performed or to put it simply, when the code is repeated. Cohesion is about ascertaining how the sink-in should be performed, in other words, whether a capability is cohesive to an appropriate entity, and whether it is placed on the appropriate layer.

The Domain layer has two levels of capabilities: One is domain service, which is relatively coarse-grained, and the other is the domain model, which is the most fine-grained reuse. For example, in our product domain, a capability is often required to determine whether a product is the smallest unit or a middle package. It is necessary that such a capability should be directly cohesive to a domain model.

public class CSPU {
    private String code;
    private String baseCode;
    //omit other attributes

    /**
     * check if it is minimumu unit
     *
     */
    public boolean isMinimumUnit(){
        return StringUtils.equals(code, baseCode);
    }

    /**
     * check if it is middle package
     *
     */
    public boolean isMidPackage(){
        return StringUtils.equals(code, midPackageCode);
    }
}

Traditional models didn't have any domain model or CSPU entity. As a result, you can find that the logic for determining whether a single product is the smallest unit is scattered in the code in the form of StringUtils.equals (code, baseCode). Such code always has poor intelligibility and it is difficult to infer what it means at first sight.

How Should We Practice Application Development

Here, I would like to answer the questions that have confused many peers who are engaged in application development.

  • Should application development focus on business implementation or technology?
  • What is the technical significance of application development?

From the preceding case, we can comprehend that the complexity of application development is no less than framework development. It is not easy to code applications. The only difference between application and framework development personnel is that they are dealing with different problem domains.

While application development involves more domain changes and more people, framework development involves more stable problem domains but more sophisticated technologies. For example, if you want to develop Pandora, you must have a deep understanding of the Classloader.

However, all the application and framework development personnel share certain thinking patterns and abilities. For example, the ability to break down problems, abstract thinking, and structured thinking.

10

In my opinion, if a developer cannot do well in application development, he or she cannot do well in framework development either, and vice versa. Application development is not simple at all. However, many of us have treated it in a simple manner.

In addition, from the perspective of changes, the difficulty of application development is not inferior to that of framework development, and application development faces even greater challenges. Therefore, as closing thoughts, I would like to suggest to all peers engaged in application development, to:

  • Consolidate their capabilities, including basic technical capabilities, object-orientation (OO) capabilities, and modeling capabilities.
  • Constantly improve abstract, structured, and critical thinking.
  • Continue to learn and improve code quality. We can do a lot of technical work as an application developer.

Postscript

This article is a summary of my recent thoughts and is based on some knowledge of DDD and application architecture. If you are not thorough with the domain knowledge, some parts may appear abrupt or you might not comprehend what I am trying to convey in the article

If time permits, you can read the books, Domain-Driven Design and Clean Architecture to get some preliminary knowledge.

0 0 0
Share on

Frank Zhang

3 posts | 0 followers

You may also like

Comments

Frank Zhang

3 posts | 0 followers

Related Products