写在前面:这篇文章不教你怎么画架构图,也不教你怎么背八股。而是分享一个真实的故事——我是怎么从「增删改查」走到「领域驱动设计」的,以及为什么在经历了三次大促翻车后,决定彻底重构自己的架构思维。
🍔 故事开场:一个让所有人睡不着觉的夜晚
那是某一次的双11,我负责的电商项目第一次参与大促。
晚上10点,流量开始起飞。我盯着监控面板,看着QPS从500一路飙升到5000,心里默念「顶住顶住顶住」。
然后,崩了。
订单突然卡住,用户点击下单后转圈转了几十秒,最后提示「系统繁忙,稍后重试」。
我赶紧上服务器查日志,各种超时异常扑面而来。业务那边电话一个接一个,用户投诉铺天盖地。
那天晚上,我们熬到凌晨4点才把系统救回来。后来复盘,发现问题出在一个看似毫不起眼的地方——
订单模块和库存模块,写在了同一个Service里。
@Service
public class OrderService {
@Autowired private OrderDao orderDao;
@Autowired private StockDao stockDao;
@Autowired private ProductDao productDao;
@Autowired private UserDao userDao;
// ... 下面还有20个dao
public void createOrder(OrderDTO dto) {
// 查库存
Stock stock = stockDao.findByProductId(dto.getProductId());
if (stock.getCount() < dto.getQuantity()) {
throw new RuntimeException("库存不足");
}
// 扣库存
stock.setCount(stock.getCount() - dto.getQuantity());
stockDao.update(stock);
// 创建订单
Order order = new Order();
order.setProductId(dto.getProductId());
order.setQuantity(dto.getQuantity());
// ... 复制20个字段
orderDao.insert(order);
// 扣用户余额
// 发货通知
// 记录日志
// ... 又是一堆逻辑
}
}
你发现问题了吗?
一个方法里,干了太多件事。查库存、扣库存、创订单、扣余额、发通知、记日志...全挤在一起。
这就导致:
- 代码难以维护 — 改库存逻辑要拽整个订单方法
- 性能无法优化 — 库存扣减和订单创建绑死,无法异步
- 测试根本无法写 — 20个依赖,谁敢mock?
那晚之后,我开始问自己:「是不是我写的代码,从根子上就是错的?」
🔧 核心干货:什么是DDD?先扔掉那些晦涩的定义
网上关于DDD的定义一搜一大把,什么「领域模型」「限界上下文」「聚合根」...看完更懵了。
我换个说法。
DDD的本质:像划分城市一样划分系统。
想象一下你要设计一座城市。你会让所有功能——住宅、商场、医院、学校、工厂——都混在一起吗?
当然不会。
你会把城市分成若干个区域:商业区、住宅区、工业区、政务区。每个区域有自己的职责,区域之间通过道路和桥梁连接。
DDD做的事情一模一样:
| 城市概念 | DDD术语 | 我的理解 |
|---|---|---|
| 城市分区 | 限界上下文(Bounded Context) | 一个独立的业务领域,有自己的边界 |
| 区域职责 | 领域(Domain) | 这个区域「做什么」——比如订单域、库存域 |
| 区域间的路 | 防腐层(ACL) | 两个区域对话的「翻译官」,各自说自己的话 |
| 区域负责人 | 聚合根(Aggregate Root) | 一个领域的老大,统一管理内部事务 |
用人话说:DDD就是教你怎么把一个庞大的系统,像切披萨一样切成若干块,每块「各管一摊事」,块与块之间通过标准接口沟通。
💡 线上翻车实录:三层架构是怎么坑我的
在说DDD之前,先说说我们原来的架构——贫血三层架构。
这是大部分Java程序员最熟悉的模式:
Controller → Service → DAO
它的特点是:所有的「业务逻辑」都写在Service里,Model只是一个个「数据容器」,只有属性,没有行为。
// 贫血模型——只有属性,没有行为
@Data
public class Order {
private Long id;
private Long productId;
private Integer quantity;
private BigDecimal price;
// ... 20个字段
}
// 所有的业务逻辑都在Service里
@Service
public class OrderService {
public void createOrder(OrderDTO dto) {
// 业务逻辑全在这里
// 校验参数
// 查库存
// 扣库存
// 创建订单
// 发通知
// 记录日志
}
}
这种方法有什么问题?
问题1:业务知识泄露
想象一下,「订单创建时必须校验库存」这个规则,应该放在哪里?
在贫血模型里,你只能放在Service里。于是你会看到:
public void createOrder(...) {
// 规则1:校验库存
Stock stock = stockDao.findByProductId(productId);
if (stock.getCount() < quantity) {
throw new Exception("库存不足");
}
// ... 后面还有30个类似规则
}
public void cancelOrder(...) {
// 规则2:取消订单要回滚库存
Stock stock = stockDao.findByProductId(productId);
stock.setCount(stock.getCount() + quantity);
stockDao.update(stock);
// ...
}
「校验库存」「回滚库存」这些业务规则,散落在Service的各个方法里。
改的时候你要全文搜索「库存」关键字,漏掉一个可能就是线上bug。
问题2:Service层越来越胖
每次新增需求,都在Service里加方法。三个月后,一个Service有2000行代码,20个依赖。
不敢改、不敢测、不敢接新需求。
问题3:无法应对高并发
还是那个双11的case。
订单和库存绑死在同一个事务里,库存扣减必须等订单创建完成才能释放数据库连接。流量一高,连接池瞬间耗尽。
这就是为什么我们崩了。
⚡ 架构重构:我是怎么用DDD拆系统的
痛定思痛,我开始用DDD的思路重构系统。
第一步:划分限界上下文
我把整个电商系统拆成了几个「域」:
| 限界上下文 | 职责 | 包含的核心概念 |
|---|---|---|
| 订单域 | 处理订单相关业务 | 订单、订单项、售后单 |
| 库存域 | 管理商品库存 | 库存、库存流水 |
| 支付域 | 处理支付流水 | 支付单、支付流水 |
| 用户域 | 用户账户管理 | 用户、地址、积分 |
每个域独立开发、独立部署,域与域通过防腐层通信。
第二步:充血模型改造
原来散落在Service里的业务逻辑,现在收进对应的领域模型里:
// 充血模型——订单自己知道怎么被创建
public class Order {
private Long id;
private List<OrderItem> items;
private OrderStatus status;
// 订单的「行为」,业务逻辑封装在模型内部
public void create(List<Product> products) {
// 校验库存
for (Item item : items) {
Product product = findProduct(item.getProductId());
if (!product.hasEnoughStock(item.getQuantity())) {
throw new BusinessException("库存不足");
}
}
// 扣减库存(委托给库存域)
for (Item item : items) {
product.reduceStock(item.getQuantity());
}
// 设置订单状态
this.status = OrderStatus.CREATED;
}
public void cancel() {
if (this.status != OrderStatus.CREATED) {
throw new BusinessException("订单已发货,无法取消");
}
// 回滚库存
// ...
}
}
Service变薄了:从2000行变成200行,只负责「编排」——调用领域模型的方法,把领域模型存入仓库。
@Service
public class OrderApplicationService {
@Autowired private OrderRepository orderRepository;
@Autowired private StockService stockService; // 防腐层
public void createOrder(CreateOrderCommand cmd) {
// 1. 构造领域模型
Order order = new Order();
order.create(cmd.getItems());
// 2. 保存
orderRepository.save(order);
// 3. 异步发消息(不阻塞主流程)
eventPublisher.publish(new OrderCreatedEvent(order));
}
}
第三步:聚合根设计
每个域内部,用聚合根来统一管理实体。
以订单域为例:
Order(聚合根)
├── OrderItem(实体)
└── OrderLog(值对象)
聚合根的职责:所有对OrderItem的修改,必须通过Order发起,外部不能直接操作OrderItem。
public class Order {
private List<OrderItem> items;
// 只有Order自己能添加订单项
public void addItem(Product product, int quantity) {
// 业务校验逻辑
if (quantity <= 0) throw new Exception("数量必须大于0");
// 创建订单项
OrderItem item = new OrderItem(product, quantity);
this.items.add(item);
}
// 外部无法直接操作items,只能调用order的方法
}
好处是什么? 业务规则被严格封装在这个「小王国」里,不会出现「某个地方漏写了校验」的情况。
💯 面试必背:DDD核心概念表
| 概念 | 一句话解释 | 面试加分 |
|---|---|---|
| 限界上下文 | 领域的边界,不同上下文用不同语言 | 「我们按业务划分了X个限界上下文」 |
| 聚合根 | 一个域的老大,统一管理内部实体 | 「订单是聚合根,订单项受订单管理」 |
| 充血模型 | 模型自带业务逻辑,不只是数据容器 | 「业务规则封装在领域模型里」 |
| 防腐层 | 两个域之间的「翻译官」 | 「库存域和订单域通过ACL通信」 |
| 领域事件 | 领域内发生的大事,触发后续动作 | 「订单创建后发布OrderCreatedEvent」 |
| 贫血模型 | 只有getter/setter,没有业务方法 | 「传统三层架构的痛点」 |
🛠️ 实际开发建议:DDD怎么落地?
不要一开始就追求完美。
我的建议是:
-
先从「 Service太胖」的地方开始
- 找一个2000行的Service,尝试把业务逻辑移到一个Domain Model里
-
从一个聚合根试点
- 选一个核心业务(比如订单),严格按照「聚合根管一切」的方式重构
-
防腐层先用最简单的形式
- 两个域之间,先用HTTP接口或消息队列通信,不着急上复杂的协议
-
领域事件可以先用Spring Event
- 简单,不用额外搭基础设施
DDD不是银弹,它的目的是让复杂业务可维护。 如果你的项目只是简单的CRUD,别为了DDD而DDD。
📌 总结:记住这口诀
业务先行再架构,
域内充血域外薄。
聚合根管一切,
防腐层来把话接。
说人话:先想清楚业务怎么划分,领域内部用充血模型把逻辑封装好,聚合根统一管理,域之间通过防腐层标准化通信。
🤔 思考题
你现在的项目里,有哪个模块的Service已经「胖」到改不动了?如果用DDD思路,你会怎么拆?
评论区聊聊。