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.
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.
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.
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.
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.
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
Write Service
Business Layer After Extraction
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.
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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
