×
Community Blog The Simplicity of COLA, an Alibaba Open-Source Application Architecture

The Simplicity of COLA, an Alibaba Open-Source Application Architecture

This blog talks about the latest updates of Alibaba's open source COLA, and shares some ideas on the importance of simplicity in application architecture design.

1

The Clean Object-Oriented and Layered Architecture (COLA) is designed to provide a set of simple guidelines and constraints for application architectures. These guidelines are replicable, readable, implementable, and able to control the degree of complexity. However, COLA is not simple enough in its implementation, so I upgraded COLA by removing some of its concepts and features, but did not add any new features. This upgrade makes COLA simpler and more effective.

Recently, a colleague told me that COLA had been selected as one of the candidate application architectures for the initialization of Alibaba Cloud Java applications.

2

This is really something. Therefore, I decided to review the strengths and weaknesses of COLA during its evolution.

Undoubtedly, COLA is a successful architectural concept, but it is not an effective framework because it is not simple enough and has redundant features.

As the developer of COLA, even I myself seldom used some of its features and could not figure out why they existed.

Therefore, I upgraded COLA 2.0 to 3.0. In this upgrade, I removed some of COLA's concepts and features, but did not add any new features. This makes COLA a more concentrated application architecture rather than just a provider of framework support and architectural constraints.

This upgrade follows the principle of Occam's Razor.

Occam's Razor

Occam's Razor states that "Entities should not be multiplied unnecessarily." Though not explicitly found in the writings of William of Ockham, the idea is hinted at in his Commentary on the Sentences of Peter Lombard.

In practice, Occam's Razor can be translated into the following guidelines:

"If there are multiple theories that can explain the same phenomenon, the simplest one is most likely to be correct. If you can achieve your goal in n actions, there is no need to take additional actions."

For example, in the fable of the Emperor's New Clothes, we cannot know for sure whether or not the emperor's clothes are invisible or he has been tricked. Imagine you are one of the ministers of the emperor, you can solve this puzzle using Occam's Razor.

The first possibility requires three assumptions: (1) The emperor is actually wearing clothes; (2) The foolish cannot see the emperor's clothes; and (3) You are foolish. Therefore, you cannot see the emperor's clothes.

The second possibility assumes only that the emperor is not wearing any clothes. Therefore, you cannot see the clothes.

The two lines of thinking lead to the same result, but the second argument is simpler. The first argument makes an incorrect assumption from the start, so it needs more assumptions to remedy it.

All truths are simple and clear, with no need for disguise.

Another example is the geocentric theory versus the heliocentric theory. Ptolemy's geocentric theory is based on the complex epicycle-deferent model. This model allows us to quantitatively calculate the motion of a planet and then infer its position.

As observational instruments continuously improved in the late Middle Ages, people were able to measure the positions and motions of planets more accurately and observed that the actual positions of planets deviated from the predictions of the epicycle-deferent model. These deviations were minor in the beginning but later seriously affected the accuracy of planet position calculations even when the number of epicycles was increased to over 80.

In 1543, the Polish astronomer Copernicus published the revolutionary work De Revolutionibus Orbium Coelestium (On the Revolutions of the Celestial Spheres). Copernicus's theory proposes that the Sun is the center of the universe, with all planets rotating around the Sun. This theory also holds that the Earth is also one of these planets and rotates on its own axis like a gyroscope while rotating around the Sun like other planets.

3

Copernicus's calculations were rigorous but simple. Compared with the geocentric theory with more than 80 epicycles, Copernicus's formula was in better agreement with the observed data. Therefore, the geocentric theory was ultimately replaced by the heliocentric theory.

Unnecessarily Multiplied Design

A deep insight into our systems reveals many cases of unnecessarily multiplied design elements similar to the geocentric theory.

From the perspective of system architecture, unnecessarily multiplied design elements result from an improper definition of system boundaries, producing unclear responsibilities and confusing dependencies.

From the perspective of application architecture, unnecessarily multiplied design elements are the result of improper and excessive designs made to achieve flexibility and scalability. Such a design results in a series of packing, hide, and forward actions in the code logic that should be directly presented. This makes the code barely readable and comprehensible and significantly increases maintenance costs.

For example, the emphasis on workflow orchestration is a kind of unnecessary design typical of business systems.

The following figure shows the pipeline design of a business system that aims to break down a complex business operation into multiple small processing units.

4

The structural breakdown has no problem at all but it does not follow Occam's Razor. For more information about structural breakdown, see my article How to Code Complex Applications: Core Java Technology and Architecture. The code logic here is neither direct nor simple because it requires a maintenance engineer to query the database after the handler function is executed in order to know what components are called.

The code logic can be rewritten in the following way:

public class CreateCSPUExecutor {
    @Resource
    private InitContextStep initContextStep;
    @Resource
    private CheckRequiredParamStep checkRequiredParamStep;
    @Resource
    private CheckUnitStep checkUnitStep;
    @Resource
    private CheckExpiringDateStep checkExpiringDateStep;
    @Resource
    private CheckBarCodeStep checkBarCodeStep;
    @Resource
    private CheckBarCodeImgStep checkBarCodeImgStep;
    @Resource
    private CheckBrandCategoryStep checkBrandCategoryStep;
    @Resource
    private CheckProductDetailStep checkProductDetailStep;
    @Resource
    private CheckSpecImgStep checkSpecImgStep;
    @Resource
    private CreateCSPUStep createCSPUStep;
    @Resource
    private CreateCSPULogStep createCSPULogStep;
    @Resource
    private SendCSPUCreatedEventStep sendCSPUCreatedEventStep;
    public Long create(MyCspuSaveParam myCspuSaveParam){
        SaveCSPUContext context = initContextStep.initContext(myCspuSaveParam);
        checkRequiredParamStep.check(context);
        checkUnitStep.check(context);
        checkExpiringDateStep.check(context);
        checkBarCodeStep.check(context);
        checkBarCodeImgStep.check(context);
        checkBrandCategoryStep.check(context);
        checkProductDetailStep.check(context);
        checkSpecImgStep.check(context);
        createCSPUStep.create(context);
        createCSPULogStep.log(context);
        sendCSPUCreatedEventStep.sendEvent(context);
        return context.getCspu().getId();
    }
}
public class CreateCSPUExecutor {
    @Resource
    private InitContextStep initContextStep;

    @Resource
    private CheckRequiredParamStep checkRequiredParamStep;

    @Resource
    private CheckUnitStep checkUnitStep;

    @Resource
    private CheckExpiringDateStep checkExpiringDateStep;

    @Resource
    private CheckBarCodeStep checkBarCodeStep;

    @Resource
    private CheckBarCodeImgStep checkBarCodeImgStep;

    @Resource
    private CheckBrandCategoryStep checkBrandCategoryStep;

    @Resource
    private CheckProductDetailStep checkProductDetailStep;

    @Resource
    private CheckSpecImgStep checkSpecImgStep;

    @Resource
    private CreateCSPUStep createCSPUStep;

    @Resource
    private CreateCSPULogStep createCSPULogStep;

    @Resource
    private SendCSPUCreatedEventStep sendCSPUCreatedEventStep;


    public Long create(MyCspuSaveParam myCspuSaveParam){
        SaveCSPUContext context = initContextStep.initContext(myCspuSaveParam);

        checkRequiredParamStep.check(context);

        checkUnitStep.check(context);

        checkExpiringDateStep.check(context);

        checkBarCodeStep.check(context);

        checkBarCodeImgStep.check(context);

        checkBrandCategoryStep.check(context);

        checkProductDetailStep.check(context);

        checkSpecImgStep.check(context);

        createCSPUStep.create(context);

        createCSPULogStep.log(context);

        sendCSPUCreatedEventStep.sendEvent(context);

        return context.getCspu().getId();
    }
}

The rewritten code logic is direct and easy to maintain and supports the same degree of component reusability as the initial code logic. The rewritten code logic follows Occam's Razor. Though the pipeline design follows the open/closed principle (OCP), its code logic is barely readable and clearly inferior to the rewritten code logic.

Upgrade to COLA 3.0

The following sections explain the changes made when COLA was upgraded to 3.0.

1. Removed the Command Mode

In the initial development of COLA, I used the Command mode to process user requests by drawing on the experience of Command Query Responsibility Segregation (CQRS). This design aimed to build a framework to impose constraints on the processing of commands and queries and to split the service logic into the CommandExecutor, which prevents the service volume from increasing too fast.

This design is similar to the tedious, indirect pipeline design mentioned above. See the following code logic:

public class MetricsServiceImpl implements MetricsServiceI{

    @Autowired
    private CommandBusI commandBus;

    @Override
    public Response addATAMetric(ATAMetricAddCmd cmd) {
        return commandBus.send(cmd);
    }

    @Override
    public Response addSharingMetric(SharingMetricAddCmd cmd) {
        return commandBus.send(cmd);
    }

    @Override
    public Response addPatentMetric(PatentMetricAddCmd cmd) {
        return  commandBus.send(cmd);
    }

    @Override
    public Response addPaperMetric(PaperMetricAddCmd cmd) {
        return  commandBus.send(cmd);
    }
}

The code logic seems clean at first glance but is not direct at all because it does not clarify what executor processes ATAMetricAddCmd. Besides, we need to understand the workings of the CommandBus and how it registers executors. This makes the code logic very hard to read.

To simplify the code, I removed the CommandBus to comply with Occam's Razor and rewrote the code in a more direct way. The new code gets rid of the interceptor feature provided by the framework. This feature is also involved in the second change made in the COLA 3.0 upgrade.

public class MetricsServiceImpl implements MetricsServiceI{

    @Resource
    private ATAMetricAddCmdExe ataMetricAddCmdExe;
    @Resource
    private SharingMetricAddCmdExe sharingMetricAddCmdExe;
    @Resource
    private PatentMetricAddCmdExe patentMetricAddCmdExe;
    @Resource
    private PaperMetricAddCmdExe paperMetricAddCmdExe;

    @Override
    public Response addATAMetric(ATAMetricAddCmd cmd) {
        return ataMetricAddCmdExe.execute(cmd);
    }

    @Override
    public Response addSharingMetric(SharingMetricAddCmd cmd) {
        return sharingMetricAddCmdExe.execute(cmd);
    }

    @Override
    public Response addPatentMetric(PatentMetricAddCmd cmd) {
        return  patentMetricAddCmdExe.execute(cmd);
    }

    @Override
    public Response addPaperMetric(PaperMetricAddCmd cmd) {
        return  paperMetricAddCmdExe.execute(cmd);
    }
}

2. Removed the Interceptor

The interceptor was designed based on the CommandBus to better implement the Command mode. In essence, the interceptor works in the same way as Spring Aspect-Oriented Programming (AOP).

As AOP already provides complete capabilities, the interceptor is not an indispensable component and seldom used by COLA users, including me. Therefore, I removed the interceptor from COLA 3.0.

3. Removed the Convertor, Validator, and Assembler

The importance of naming is not described here. When I designed COLA, I wanted to standardize the names of common features from the framework perspective. However, this idea was not feasible.

When my team implemented COLA early on, we often argued about how to correctly define a converter and an assembler.

Later, I realized that naming, despite its importance, was only part of the specification formulated by a team. There is no essential difference between a validator and a checker. It is only a matter of naming that varies from team to team. It does little good to unify conventions within a team from the framework perspective, so I removed the convertor, validator, and assembler from COLA 3.0.

4. Optimized Class Scanning

Business identity and extension points are key concepts of the TMF and also the key methodology that the Alibaba business mid-end uses to support multiple businesses.

COLA aims to implement lightweight extensions, so the business identity and extension point features are retained in COLA 3.0. The extension point feature was designed by drawing on the experience of the mid-end TMF. Therefore, the corresponding class scanning solution completely follows the implementation mode of TMF.

In fact, the class scanning solution of TMF is unnecessary for COLA because COLA is based on Spring, which relies on class scanning. Therefore, we can reuse the class scanning feature of Spring instead of developing a solution on our own.

Native Spring provides at least three methods to fetch the beans of custom annotations. The simplest method is ListableBeanFactory.getBeansWithAnnotation. Alternatively, you can scan packages by using ClassPathScanningCandidateComponentProvider.

When I upgraded COLA 2.0 to 3.0, I used the getBeansWithAnnotation method to fetch the bean of @Extension so that COLA 3.0 can support the extension point feature. The class scanning solution of TMF is no longer used by COLA.

Summary

The upgrade from COLA 2.0 to 3.0 aims to remove the redundant features that I identified when using COLA in actual scenarios. As a basic application architecture of Alibaba Cloud, COLA has become increasingly influential. I have the responsibility to provide correct guidelines on COLA implementation. Therefore, compared with the 2.0 version, COLA 3.0 is simpler, more effective, and easier to use after I removed the redundant features.

COLA can be viewed as an architectural idea and also a componentized framework.

Viewed as an architectural idea, COLA is an application architecture that integrates the ideas of a layered architecture, adapter architecture, domain-driven design (DDD), clean architecture, and TMF.

5

COLA 3.0 retains a large portion of the architectural idea of the 2.0 version and gets rid of the Command concept. This turns CQRS into an optional feature.

Viewed as a componentized framework, I removed most of the component capabilities of COLA 2.0 based on Occam's Razor so that COLA 3.0 only retains the extension point feature. Therefore, COLA 3.0 is simple and effective, with minimum constraints on application developers.

In a sense, the upgrade of COLA 2.0 to 3.0 is actually a feature cleanup of COLA.

I believe that this cleanup will make COLA more compliant with Occam's Razor, lighter-weight, and more sustainable.

Link to the open-source COLA project: https://github.com/alibaba/COLA

Alibaba Cloud Java Application Scaffold

The page start.aliyun.com (page in Chinese) is an engineering scaffold generation platform based on Spring Initializr. It allows you to quickly build distributed application systems by adding annotations and a few settings. It provides a wide range of localized component dependencies and is free of network latency. Currently, start.aliyun.com is only available in Chinese.

0 0 1
Share on

You may also like

Comments