Frontend Development 10 min read

Frontend Architecture Optimization of Ctrip Ticket App Using Clean Architecture and Plugin‑Based AOP

This article details how Ctrip's ticket‑search list page was refactored with clean‑architecture principles, introducing a unidirectional data flow, business Service layer, and plugin‑based aspect‑oriented modules to improve maintainability, testability, and cross‑platform reuse of front‑end code.

Ctrip Technology
Ctrip Technology
Ctrip Technology
Frontend Architecture Optimization of Ctrip Ticket App Using Clean Architecture and Plugin‑Based AOP

In early 2019 the Ctrip ticket front‑end team rebuilt the flight‑search list page based on clean‑architecture ideas, separating view and logic, modularising business scenarios and reducing overall complexity, which enhanced maintainability and testability.

After a year of iterations new problems emerged in the original 1.0 architecture, prompting further optimisation.

Architecture Optimisation

The original design split the page into multiple homogeneous business modules using an MVP pattern, where each module contained View (React), Presenter, and Model. This caused tight coupling between business and presentation logic, poor reusability, and complex inter‑module data communication.

Key issues identified:

Business logic was embedded inside modules and tightly coupled with presentation logic, leading to bugs when a module was hidden.

Logic could not be reused across modules or pages.

Data communication between tree‑structured modules became tangled, often resulting in inconsistent states.

The new architecture introduces a unidirectional data flow and a dedicated Business Service layer. Modules now only communicate with Services, which encapsulate all business logic, while modules focus solely on rendering and interaction.

Core changes include:

Optimised data communication: modules interact with Services via a single‑direction flow.

Added Business Service concept to host domain‑specific logic; modules handle only presentation.

These changes align with the Single‑Responsibility Principle, improve reusability, and enable cross‑platform sharing of Service code.

Unidirectional Data Flow

Modules cannot talk to each other directly; they invoke Service methods, and Services push updated state back, ensuring a clear one‑way flow.

Typical flows:

Module triggers data update → Service processes logic → Service updates module state.

Service updates data → multiple modules refresh based on the new state.

Business Service

Pages are divided into homogeneous business modules and multiple Business Services. Modules still use MVP for UI management, while Services are organised by domain and implemented with object‑oriented techniques.

In this setup:

View responsibilities remain unchanged.

Presenter now connects only to Services, not to other modules.

Model focuses on presentation state only.

Business Services can be extracted as shared libraries, allowing reuse across H5, online, and native apps despite differing UI frameworks.

Plugin Function Optimisation

Non‑business concerns such as analytics and monitoring were previously scattered throughout the code, tightly coupled with business modules.

Original monitoring implementation (TypeScript):

// ModuleA/Presenter/index.ts
export class ModuleAPresenter {
    private monitor: Monitor;
    constructor(monitor: Monitor) {
        this.monitor = monitor;
    }

    public updateView() {
        // collect latest state of module A
        this.monitor.updateState(this.model.getViewModel(), 'ModuleA');
        this.view.updateView(this.model.getViewModel());
    }
}
// Monitor/index.ts
export class Monitor {
    private stateMap = new Map();
    public updateState(state, moduleName) {
        this.stateMap.set(moduleName, state);
        // check synchronization between modules A and B
        this.checkState();
    }
}

Problems with this approach:

Monitoring code intrudes into business code, creating strong coupling.

Modules depend on the Monitor class, reducing reusability and testability.

Changes to monitoring require modifications in many places (shot‑gun changes).

The solution adopts aspect‑oriented programming (AOP) by providing a plugin interface that isolates non‑business functionality.

Refactored monitoring implementation:

// ModuleA/Presenter/index.ts
export class ModuleAPresenter {
    public updateView() {
        // business module no longer contains unrelated logic
        this.view.updateView(this.model.getViewModel());
    }
}
// Monitor/index.ts
export class MonitorPlugin implements IGrtPlugin {
    private stateMap = new Map();

    // "aspect" method
    public onUpdateState(state, moduleName) {
        this.stateMap.set(moduleName, state);
        // check synchronization between modules A and B.
        this.checkState();
    }
}

After refactoring, business and non‑business concerns are fully decoupled, improving readability and maintainability.

Conclusion

The revised architecture separates business logic into Services and presentation into lightweight modules, establishing a clear unidirectional data flow that simplifies complex scenarios. Plugin‑based AOP further isolates cross‑cutting concerns such as monitoring, making the front‑end codebase more modular, testable, and reusable across platforms.

frontendtypescriptPluginclean architectureAspect-Oriented ProgrammingMVPUnidirectional data flow
Ctrip Technology
Written by

Ctrip Technology

Official Ctrip Technology account, sharing and discussing growth.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.