From Monolith to Distributed: How We Transformed a Stock System with CQRS

This article explains what software architecture is, why choosing an architectural style matters, compares monolithic and distributed approaches using a real‑world inventory system case study, and details the step‑by‑step functional and business splitting, CQRS implementation, code refactoring, and handling of distributed transactions.

Programmer DD
Programmer DD
Programmer DD
From Monolith to Distributed: How We Transformed a Stock System with CQRS

Background

We first ask what architecture is and why we need to choose one to solve specific problems.

What is Architecture

Book definition: “Software architecture is an abstract structure composed of software components and their dependencies.” In my view, architecture means selecting appropriate technologies and middleware based on business needs and assembling them with suitable design patterns to satisfy functional requirements.

Purpose of Choosing an Architecture Style

My “Three‑Better” principle: reduce cost & improve efficiency, accelerate releases, and enhance system stability. A good architectural style not only meets functional needs but also improves non‑functional requirements such as scalability and stability.

Monolithic Architecture

Initially we built a single‑point deployment system that packaged all business logic together, which suited early rapid iteration for small teams without a PaaS platform.

Monolithic Architecture Types

Big Muddy Monolith : no layers, all modules interwoven; hard to split later.

Layered Monolith : typical MVC three‑layer structure.

Modular Monolith : evolves from layered monolith by introducing multiple business modules with service capabilities.

Advantages of Monolithic Architecture

Simple development.

Easy large‑scale changes.

Straightforward testing.

Clear deployment.

Horizontal scaling is trivial.

Disadvantages of Monolithic Architecture

Codebase bloat.

Complexity scares developers.

Slower development speed.

Long deployment cycles and higher failure risk.

Difficult to scale.

Stability cannot be guaranteed.

Dependency on potentially outdated tech stacks.

Monolithic Architecture Case – Inventory System

The initial inventory system provided a single service for stock maintenance, query, and deduction. Its code layers were:

api : external Dubbo service.

common : common utilities.

dao : database interaction.

domain : entity classes.

inner‑api : internal API interaction.

router : deprecated.

rpc : upstream/downstream RPC.

service : business logic.

web : web service layer.

worker : task scheduling.

Monolithic architecture diagram
Monolithic architecture diagram

Two monolithic services (API and Web) met early fast‑release needs, but as traffic grew, performance and stability degraded, leading to a major outage during a promotion event.

Outage incident
Outage incident

Distributed Architecture

Advantages

High availability.

High scalability.

Strong fault tolerance.

Better code readability.

Simpler maintenance.

Disadvantages

Increased service count and learning cost.

Technology stack upgrades require effort.

Distributed transaction handling.

RPC overhead between services.

How Monolith Transforms to Distributed Architecture

Because stability was the biggest pain point, we started with functional splitting.

Functional Splitting

We grouped services by business direction and deployed each group to a separate cluster without major code changes.

Functional split diagram
Functional split diagram

After isolating stability, we tackled code coupling by refactoring the Service layer.

Business Splitting

We used event‑driven modeling (event storming, four‑color modeling) to define business events such as stock initialization, quantity maintenance, deduction, and alerts. Each event forms an independent workflow.

Business events
Business events
Event flow
Event flow

How to Split Existing Modules

Typical monolith services mix DTO/VO/BO and violate the Dependency Inversion Principle, resulting in “God Classes”. Example:

@Service
public class SkuMainServiceImpl implements SkuMainService {
    private static final Logger LOGGER = LoggerFactory.getLogger(SkuMainServiceImpl.class);
    @Resource private SkuMainDao skuMainDao;
    @Resource private ZkConfManagerCenterService zkConfManagerCenterService;
    @Resource private ProductImagesService productImagesService; // same‑level reference, no DIP
    @Resource private MqService mqServiceImpl;
    @Value("${system.group.environment}") private String systemGroupEnvironment;
    /**
     * Problem: Service aggregates too many business logics, cannot unify upper‑level methods.
     */
    public void editorSaveProuct(SkuMainInfoMQEntity skuMainInfoMQEntity) throws Exception {
        try {
            SkuMainBean skuMainBean = skuMainInfoMQEntity.getSkuMainBean();
            if (skuMainBean == null) { throw new Exception("修改参数为空!"); }
            SkuMainBean originalSku = this.getSkuMainBeanBySkuId(skuMainBean.getId());
            if (originalSku == null) { throw new Exception("无效SkuId!"); }
            // ... business logic ...
        } catch (Exception e) {
            LOGGER.error("修改商品信息失败.e:", e);
            throw new Exception(e);
        }
    }
}

CQS and SRP Refactoring

We split the Service into Command‑Query Separation (CQS) and applied the Single Responsibility Principle (SRP), extracting a Business layer:

@Service
public class SkuMainBusinessServiceImpl implements SkuMainBusinessService {
    private static final Logger LOGGER = LoggerFactory.getLogger(SkuMainBusinessServiceImpl.class);
    @Resource private ZkConfManagerCenterService zkConfManagerCenterService;
    @Resource private MqService mqService;
    @Resource private SkuMainReadservice skuMainReadservice;
    @Resource private SkuMainWriteservice skuMainWriteservice;
    @Value("${system.group.environment}") private String systemGroupEnvironment;
    /**
     * Problem: Service aggregates too many business logics, cannot unify upper‑level methods.
     */
    public void editorSaveProuct(SkuMainInfoMQEntity skuMainInfoMQEntity) throws Exception {
        try {
            SkuMainBean skuMainBean = skuMainInfoMQEntity.getSkuMainBean();
            if (skuMainBean == null) { throw new Exception("修改参数为空!"); }
            SkuMainBean originalSku = skuMainReadservice.getSkuMainBeanBySkuId(skuMainBean.getId());
            if (originalSku == null) { throw new Exception("无效SkuId!"); }
            SkuMainBean skuMainUpdate = skuMainWriteservice.updateIsWeightMark(skuMainBean);
            SkuMainBean skuMainPre = skuMainReadservice.queryDbById(skuMainUpdate.getId());
            if (skuMainPre != null && skuMainPre.getSystemFixedStatus() != null &&
                skuMainPre.getSystemFixedStatus().equals(SystemFixedStatusEnum.SYSTEM_FIXED_STATUS_DOWN.getCode())) {
                skuMainUpdate.setFixedStatus(FixedStatusEnum.PRODUCT_DOWN.getCode());
            }
            boolean flag = skuMainWriteservice.editorProduct(skuMainUpdate);
            if (flag) {
                if (!zkConfManagerCenterService.isDefaultStoreStatisticsScore(skuMainBean.getOrgCode())) {
                    SkuMainBean saveSkumainBean = skuMainservice.queryDbById(skuMainUpdate.getId());
                    if (saveSkumainBean != null) { skuMainWriteservice.cacheSkuMainBean(saveSkumainBean); }
                    skuMainWriteservice.sendSkuModifyMq(SkuModifyOpSourceEnum.MIX_UPDATE_SKU, originalSku,
                        new SkuMainInfoMQEntity(skuMainUpdate));
                } else {
                    LOGGER.info("add open platform sku , not not not send mq! skuId = {}", skuMainBean.getId());
                }
            }
        } catch (Exception e) {
            LOGGER.error("修改商品信息失败.e:", e);
            throw new Exception(e);
        }
    }
}

Read Service

Read service diagram
Read service diagram

Write Service

Write service diagram
Write service diagram

Business Layer After Extraction

Business layer diagram
Business layer diagram

CQRS StockCenter

Applying CQRS, we built a stock‑centric CQRS‑StockCenter where the business layer issues commands, the write service updates Redis, and MQ messages asynchronously persist to MySQL and generate audit trails.

CQRS StockCenter diagram
CQRS StockCenter diagram

Distributed Transactions

When services are split, cross‑service transactions become hard to roll back. Traditional solutions like JTA/JTS require XA compliance, which many modern databases (e.g., MongoDB) and middleware (RabbitMQ, Kafka) do not support, making distributed transaction handling a significant challenge.

Source: 树洞君 – juejin.cn/post/712188516006834998
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Distributed SystemsSoftware ArchitectureMicroservicesmonolithCQRS
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

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.