相应的设计思想已全部蒸馏为agent skills:https://gitee.com/zrclass/ddd-spec-skills
一、为什么需要 DDD?
想象你在开发一个电商平台。
产品经理说:"用户下单。"
运营说:"用户领券。"
商家说:"用户退款。"
客服说:"用户投诉。"
仓配团队说:"用户催发货。"
乍一看,大家都在说"用户"和"订单"。
但如果你仔细听,会发现他们虽然用了同样的词,关心的却不是同一件事。
- 产品经理说"用户下单",关心的是交易流程:选商品、下单、支付、取消订单。
- 运营说"用户领券",关心的是营销转化:活动规则、优惠门槛、券是否可用。
- 商家说"用户退款",关心的是售后处理:退什么、退多少、商品是否退回、责任由谁承担。
- 客服说"用户投诉",关心的是问题处理过程:投诉原因、处理记录、工单状态、最终答复。
- 仓配团队说"用户催发货",关心的是履约流程:仓库是否出库、快递是否揽收、物流进度是否异常。
看起来大家都在讨论"订单",但其实:
- 在交易场景里,订单是一笔购买行为。
- 在履约场景里,订单是一项发货和配送任务。
- 在售后场景里,订单是退款、退货、换货处理的依据。
- 在客服场景里,订单是用户问题的上下文。
- 在营销场景里,订单还是满减、优惠券、会员折扣是否生效的结算载体。
也就是说,同样叫"订单",在不同业务场景里,含义、属性、规则、操作都不一样。
如果你图省事,整个系统只放一个 Order 类,时间一久就会变成这样:
java
class Order {
// 交易相关
BigDecimal totalAmount;
PaymentStatus paymentStatus;
String cancelReason;
// 履约相关
ShipmentStatus shipmentStatus;
String warehouseCode;
String trackingNumber;
LocalDateTime deliveredAt;
// 售后相关
RefundStatus refundStatus;
ReturnStatus returnStatus;
String refundReason;
String responsibilityType;
// 客服相关
ComplaintStatus complaintStatus;
String serviceRemark;
String ticketNo;
// 营销相关
CouponId couponId;
BigDecimal discountAmount;
String promotionType;
}
这时问题就来了:
Order越来越胖,什么都往里塞。- 谁都能改订单状态,状态到处乱跳。
- 一个需求变化,可能牵动交易、营销、履约、售后、客服一大片代码。
- 开发和业务虽然都在说"订单",但说的根本不是同一个东西。
所以 DDD 要解决的核心问题不是"代码怎么分层"这么简单,而是:
当同一个词在不同业务场景里含义会变化时,怎么划清边界,让每个模型只负责自己该负责的业务。
一句话总结:
DDD 关心的不是"大家都在说订单",而是"大家说的虽然都是订单,但其实不是同一个订单"。
二、DDD 到底是什么?
DDD 是 Domain-Driven Design ,中文叫领域驱动设计。
先把"领域"说成人话:
领域,就是你要解决的那类业务问题。
对电商平台来说,"做一个电商平台"太大了,里面其实包含很多不同问题:
- 用户怎么浏览商品、加购物车、下单?
- 订单怎么支付、取消、拆单?
- 仓库怎么拣货、发货、更新物流?
- 用户怎么退款、退货、换货?
- 营销活动怎么满减、发券、核销?
- 客服怎么处理投诉、催单、补偿?
这些都属于电商平台,但它们不是同一个问题。
DDD 的核心思想就是:
不要用一个大而全的模型去硬扛所有问题,而是先识别不同业务问题的边界,再分别建模。
所以 DDD 不是先问:
- Controller 怎么写?
- 数据库怎么分表?
- 用 MySQL 还是 Redis?
而是先问:
- 当前到底在解决什么业务问题?
- "订单"在这个场景里是什么意思?
- 哪些规则必须绑在一起保证?
- 哪些概念虽然同名,其实不是同一个模型?
三、统一语言(Ubiquitous Language)
3.1 什么叫统一语言?
统一语言的意思是:
开发、产品、测试、运营、商家、客服,在同一个业务场景里,要用同一套词,而且这些词的含义要一致。
这不是简单的"变量名起得好看",而是:
- 业务怎么说,代码就尽量怎么表达。
- 一个词在当前上下文里,含义必须稳定。
- 讨论、文档、代码、接口最好说的是同一种语言。
3.2 电商平台里的例子
如果业务说"提交订单",代码最好别写成:
java
createRecord()
而应该写成:
java
placeOrder()
如果业务说"支付订单",不要只写:
java
updateStatus()
而要写成:
java
payOrder()
如果业务说"申请退款",就明确叫:
java
applyRefund()
如果业务说"仓库发货",就写:
java
shipOrder()
如果业务说"发放优惠券",就写:
java
issueCoupon()
3.3 统一语言为什么重要?
因为模糊词最容易让系统失控。
比如一个 close() 方法,谁也不知道它到底是:
- 关闭交易订单?
- 关闭售后单?
- 关闭客服工单?
- 关闭营销活动?
统一语言的目标,就是让这些词在当前场景里说得清清楚楚。
统一语言不是让全公司只说一套词,而是在每个边界内,把词说准。
四、限界上下文(Bounded Context)
4.1 什么是限界上下文?
你可以把它理解成:
某个模型成立的边界。
也就是说:
- 在这个边界里,一个词的含义是统一的。
- 出了这个边界,同一个词可以有别的含义。
4.2 用电商平台理解最合适
电商平台里,"订单"这个词就很典型。
在交易上下文里
"订单"表示一次购买。
它关心的是:
- 买了什么
- 总价多少
- 支付了没有
- 能不能取消
java
public class TradeOrder {
private OrderId orderId;
private UserId userId;
private List<OrderLine> items;
private Money totalAmount;
private PaymentStatus paymentStatus;
public void place() { ... }
public void pay() { ... }
public void cancel() { ... }
}
在履约上下文里
"订单"表示一次发货任务。
它关心的是:
- 是否拆单
- 仓库是否出库
- 快递单号是什么
- 是否签收
java
public class FulfillmentOrder {
private OrderId orderId;
private WarehouseId warehouseId;
private ShipmentStatus shipmentStatus;
private TrackingNumber trackingNumber;
public void allocateStock() { ... }
public void ship() { ... }
public void confirmDelivery() { ... }
}
在售后上下文里
"订单"表示一次售后处理依据。
它关心的是:
- 是否申请退款
- 是否退货
- 原因是什么
- 处理进度到哪了
java
public class AfterSaleCase {
private OrderId orderId;
private RefundStatus refundStatus;
private ReturnStatus returnStatus;
private String reason;
public void applyRefund(String reason) { ... }
public void requestReturn(String reason) { ... }
}
在客服上下文里
"订单"表示一次用户问题的上下文。
它关心的是:
- 用户咨询了什么
- 客服怎么处理
- 工单状态如何
- 是否投诉完成
java
public class ServiceTicket {
private TicketId ticketId;
private OrderId orderId;
private TicketStatus ticketStatus;
private String serviceRemark;
public void openTicket(String content) { ... }
public void closeTicket() { ... }
}
在营销上下文里
"订单"表示优惠规则结算的载体。
它关心的是:
- 是否满足满减门槛
- 哪张券可用
- 折扣怎么计算
- 活动是否叠加
java
public class PromotionOrder {
private OrderId orderId;
private Money originalAmount;
private Money discountAmount;
private CouponId couponId;
public void applyCoupon(CouponId couponId) { ... }
public void calculateDiscount() { ... }
}
4.3 限界上下文的核心价值
DDD 想表达的不是"多建几个类",而是:
交易、履约、售后、客服、营销虽然都和订单有关,但它们不是同一个业务问题,所以不该强行共享一个模型。
你可以把它理解成:
- 交易上下文有自己的
TradeOrder - 履约上下文有自己的
FulfillmentOrder - 售后上下文有自己的
AfterSaleCase - 客服上下文有自己的
ServiceTicket - 营销上下文有自己的
PromotionOrder
同样叫"订单",但每个边界里都只保留自己关心的部分。
五、实体(Entity)与值对象(Value Object)
5.1 实体:重点是"它是谁"
在电商平台里,下面这些通常是实体:
- 用户
- 商品
- 商家
- 交易订单
- 售后单
- 优惠券
因为这些对象都有明确身份,要长期跟踪。
比如一个用户改了手机号,还是同一个用户;
一个订单改了收货地址,还是同一个订单。
java
public class User {
private UserId userId;
private String nickname;
private PhoneNumber phoneNumber;
public void changePhoneNumber(PhoneNumber newPhoneNumber) {
this.phoneNumber = newPhoneNumber;
}
}
实体的特点:
- 有唯一标识
- 有生命周期
- 重点在身份连续性
5.2 值对象:重点是"它是什么值"
电商平台里常见的值对象有:
MoneyAddressPhoneNumberSkuCodeTimeRange
这些对象一般不需要单独追踪身份,只关心值本身。
java
public final class Address {
private final String province;
private final String city;
private final String detail;
public Address(String province, String city, String detail) {
this.province = province;
this.city = city;
this.detail = detail;
}
}
5.3 怎么判断?
问自己一句:
业务上,我需要持续追踪"它是谁"吗?
- 需要:实体
- 不需要,只关心值:值对象
5.4 为什么值对象很有用?
因为它能让代码更像业务。
比如:
java
public void placeOrder(String address, BigDecimal amount, String phone) { ... }
不如:
java
public void placeOrder(Address address, Money amount, PhoneNumber phone) { ... }
后者更清楚,也更容易做校验。
六、聚合与聚合根(Aggregate / Aggregate Root)
6.1 先讲人话
聚合是一组需要一起维护一致性的对象。
聚合根是这组对象对外的总入口。
6.2 用交易订单举例
一个交易订单里,通常会包含:
- 订单本身
- 多个订单项
- 收货地址
- 价格信息
这些对象不是独立乱改的,因为它们之间有规则:
- 已取消订单不能再加商品
- 已支付订单不能改金额
- 已完成订单不能改地址
所以应该通过订单这个聚合根统一修改。
java
// 聚合根:TradeOrder
public class TradeOrder {
private OrderId orderId;
private List<OrderLine> items; // 聚合内部对象
private Address deliveryAddress; // 聚合内部对象
private TradeOrderStatus status;
public void addItem(ProductId productId, int quantity) {
if (status == TradeOrderStatus.CANCELLED) {
throw new IllegalStateException("已取消订单不能添加商品");
}
items.add(new OrderLine(productId, quantity));
}
public void changeAddress(Address newAddress) {
if (status == TradeOrderStatus.COMPLETED) {
throw new IllegalStateException("已完成订单不能修改地址");
}
this.deliveryAddress = newAddress;
}
}
聚合是一组要一起守规则的对象;聚合根是这组对象对外的总入口。
放到这个例子里就是:
- 聚合:交易订单这一整组对象
- 聚合根:TradeOrder
6.3 为什么要这么做?
因为如果外部代码随便拿内部对象乱改:
java
order.getItems().add(...);
order.setStatus(...);
order.setDeliveryAddress(...);
业务规则就守不住了。
所以更准确的说法是:
聚合根是外部修改聚合内部状态的唯一业务入口。
6.4 聚合的几个实用原则
- 外部通过聚合根执行业务修改。
- 聚合尽量小。
- 一个事务应尽量只修改一个聚合。
- 跨聚合协作优先考虑事件或最终一致性。
七、领域事件(Domain Event)
7.1 什么是领域事件?
领域事件表示:
业务上已经发生的重要事实。
电商平台里常见的领域事件有:
OrderPlacedPaymentCompletedOrderShippedRefundAppliedCouponIssued
7.2 它有什么用?
比如用户完成下单后,可能会发生很多后续动作:
- 交易上下文记录订单已创建
- 营销上下文冻结或核销优惠券
- 履约上下文创建发货任务
- 通知系统发送下单成功消息
如果全写在一个对象里,会耦合得很厉害。
领域事件的作用就是:
让"事情发生了"与"谁来处理这个事情"解耦。
7.3 示例
java
public class OrderPlaced {
private OrderId orderId;
private UserId userId;
private LocalDateTime occurredOn;
}
交易上下文发布 OrderPlaced,履约上下文监听后创建自己的 FulfillmentOrder。
7.4 一个重要提醒
领域事件不等于事件溯源。
- 领域事件:表达业务事实
- 事件溯源:用事件重建状态
很多系统会用领域事件,但并不会做事件溯源。
八、领域服务(Domain Service)
8.1 什么逻辑适合放领域服务?
如果一段业务逻辑:
- 不自然属于某个实体
- 也不适合放在值对象里
- 但它本身又是明确的业务规则
那就适合做成领域服务。
8.2 电商平台里的自然例子:促销价格计算
促销价格通常和这些因素有关:
- 商品原价
- 优惠券
- 满减活动
- 会员折扣
- 活动叠加规则
这个逻辑很难说完全属于 TradeOrder、Coupon 或 Product 某一个实体。
那就可以抽成领域服务:
java
public class PromotionPricingService {
public Money calculatePayableAmount(
Money originalAmount,
Coupon coupon,
PromotionRule rule,
MembershipLevel membershipLevel) {
...
}
}
8.3 注意别把所有东西都塞进 Service
DDD 不是"所有业务逻辑都写进 XXXService"。
更合理的原则是:
- 能放进实体,就放进实体
- 能放进值对象,就放进值对象
- 真正不属于任何单一对象的业务规则,再放进领域服务
九、应用服务(Application Service)
9.1 应用服务负责什么?
应用服务不负责核心业务规则,它负责:
- 接收请求
- 参数转换
- 加载聚合
- 调用领域对象
- 保存结果
- 返回响应
9.2 下单流程示例
java
public class TradeApplicationService {
public void placeOrder(PlaceOrderCommand command) {
UserId userId = new UserId(command.getUserId());
Address address = new Address(
command.getProvince(),
command.getCity(),
command.getDetail());
TradeOrder order = TradeOrder.create(userId, command.getItems(), address);
tradeOrderRepository.save(order);
}
}
这里它做的是"流程编排",不是在定义"什么叫下单成功"。
9.3 和领域服务的区别
一句话区分:
- 应用服务:这件事怎么走完
- 领域服务:这件事在业务上怎么算成立
十、仓储(Repository)
仓储可以理解为:
聚合的存取窗口。
领域层只关心:
- 怎么把聚合取出来
- 怎么把聚合存回去
不关心底层是数据库、缓存还是别的技术。
java
public interface TradeOrderRepository {
TradeOrder orderOfId(OrderId orderId);
void save(TradeOrder order);
}
一个重要提醒
仓储通常是面向聚合根的,不是每个对象都要建一个 Repository。
不要变成这样:
OrderLineRepositoryAddressRepositoryCouponRuleRepository
不然聚合边界就散了。
十一、防腐层(Anti-Corruption Layer, ACL)
11.1 它解决什么问题?
当你的电商平台要接第三方系统时,比如:
- 第三方支付平台
- 第三方物流平台
- 第三方会员平台
最怕的是这些外部系统的模型直接污染你的领域模型。
11.2 用物流场景举例
第三方物流平台可能返回一个很技术化的 DTO:
java
class LogisticsGatewayResponse {
String code;
String waybillNo;
String nodeStatus;
String rawMessage;
}
但在履约上下文里,你真正关心的可能只是:
java
class ShipmentTracking {
TrackingNumber trackingNumber;
ShipmentStatus shipmentStatus;
}
这时就应该通过 ACL 做一层翻译,而不是让物流平台的结构直接进领域层。
11.3 ACL 的核心
把外部模型翻译成内部模型,保护自己的领域不被污染。
十二、DDD 的分层架构
12.1 四层架构
| 层 | 包约定 | 电商示例 |
|---|---|---|
| 接口层 | resource/ 或 port/adapter/ |
TradeOrderResource, FulfillmentOrderAdapter |
| 应用层 | application/ |
TradeOrderApplicationService, PlaceOrderCommand, TradeOrderData |
| 领域层 | domain.model/ |
TradeOrder, OrderId, Money, OrderPlaced, TradeOrderRepository(接口), OrderPricingService(接口) |
| 基础设施层 | infrastructure/ 或 port/adapter/ |
HibernateTradeOrderRepository, RabbitMQOrderPlacedListener, LogisticsGatewayAdapter |
12.2 依赖方向
最关键的规则:
领域层不依赖外层技术实现。
text
接口层 → 应用层 → 领域层
基础设施层 → 领域层
这意味着:
- 领域层定义仓储接口,基础设施层实现它
- 领域层定义领域服务接口,基础设施层适配外部系统
- 领域对象不应该出现
@Entity、@Autowired、@Component等框架注解
12.3 交易上下文包结构(传统四层 + Hibernate)
text
com.ecommerce.trade/
domain.model.order/ → TradeOrder, OrderLine, OrderPlaced, TradeOrderRepository(接口)
domain.model.pricing/ → OrderPricingService(接口), Money, CouponId
application/ → TradeOrderApplicationService, command/, representation/
infrastructure.persistence/ → HibernateTradeOrderRepository
infrastructure.messaging/ → RabbitMQOrderPlacedListener
resource/ → TradeOrderResource
12.4 履约上下文包结构(六边形/端口适配器 + LevelDB)
text
com.ecommerce.fulfillment/
domain.model.fulfillment/ → FulfillmentOrder, ShipmentStatus, FulfillmentOrderRepository(接口)
domain.model.tracking/ → ShipmentTrackingService(接口)
application/ → FulfillmentApplicationService
port.adapter.persistence/ → LevelDBFulfillmentOrderRepository
port.adapter.messaging/ → RabbitMQOrderPlacedListener
port.adapter.service/ → LogisticsGatewayAdapter, TranslatingShipmentService
12.5 仓储接口与实现的分离
java
// 领域层 --- 仓储接口(不依赖任何框架)
package com.ecommerce.trade.domain.model.order;
public interface TradeOrderRepository {
void add(TradeOrder aTradeOrder);
void remove(TradeOrder aTradeOrder);
TradeOrder nextIdentity();
TradeOrder orderOfId(OrderId anOrderId);
}
// 基础设施层 --- Hibernate 实现(ORM 模式)
package com.ecommerce.trade.infrastructure.persistence;
public class HibernateTradeOrderRepository extends AbstractHibernateSession
implements TradeOrderRepository {
@Override
public void add(TradeOrder aTradeOrder) {
this.session().save(aTradeOrder);
}
}
// 基础设施层 --- EventStore 实现(事件溯源模式)
package com.ecommerce.trade.infrastructure.persistence;
public class EventStoreTradeOrderRepository implements TradeOrderRepository {
public TradeOrder orderOfId(OrderId anOrderId) {
EventStreamId streamId = new EventStreamId("TradeOrder:" + anOrderId.id());
EventStream eventStream = this.eventStore().eventStreamSince(streamId, 0);
return new TradeOrder(eventStream.events(), eventStream.version());
}
}
12.6 反模式 --- JPA 注解污染领域模型
java
// ❌ 反模式 --- 领域层依赖框架
@Entity
@Table(name="TRADE_ORDER")
public class TradeOrder extends ConcurrencySafeEntity {
@Id // ❌ 领域对象不应知道持久化细节
private Long id;
}
12.7 领域服务 --- 跨聚合逻辑
java
// OrderPricingService --- 促销定价(跨聚合逻辑)
public interface OrderPricingService {
Money calculatePayableAmount(
Money originalAmount,
Coupon coupon,
PromotionRule rule,
MembershipLevel membershipLevel);
}
12.8 DomainRegistry 与依赖注入
java
// 方式1:DomainRegistry(服务定位器模式)
public class TradeOrder extends ConcurrencySafeEntity { // ConcurrencySafeEntity 提供乐观并发控制
protected Money calculateDiscount(Coupon coupon) {
return DomainRegistry.orderPricingService()
.calculatePayableAmount(this.totalAmount(), coupon, null, null);
}
}
// 方式2:构造函数注入(更推荐)
public class TradeOrder extends ConcurrencySafeEntity {
private final OrderPricingService pricingService;
public TradeOrder(..., OrderPricingService pricingService) {
this.pricingService = pricingService;
}
}
// ❌ 反模式 --- 领域模型直接依赖 Spring
@Component // ❌ 领域对象不应是 Spring Bean
public class TradeOrder extends ConcurrencySafeEntity {
@Autowired // ❌ 领域对象不应使用字段注入
private OrderPricingService pricingService;
}
12.9 应用服务 --- 薄编排层
java
// 应用服务不做业务判断,只编排领域对象
public void placeOrder(PlaceOrderCommand command) {
OrderId orderId = this.tradeOrderRepository().nextIdentity();
TradeOrder order = new TradeOrder(
orderId,
command.getUserId(),
command.getItems(),
new Address(command.getProvince(), command.getCity(), command.getDetail()));
this.tradeOrderRepository().add(order);
}
十三、进阶:事件溯源(Event Sourcing)与 CQRS
这部分是进阶,不是入门必选。
13.1 事件溯源
传统做法通常只保存订单当前状态:
text
订单:已支付,待发货,实付 199 元
事件溯源保存的是订单经历过什么:
text
1. OrderPlaced
2. CouponApplied
3. PaymentCompleted
4. OrderShipped
当前状态是通过回放事件得到的。
适合:
- 审计要求高
- 希望完整保留状态变化历史
- 领域天然事件驱动
不适合:
- 业务简单
- 团队经验不足
- 只是普通 CRUD
13.2 CQRS
CQRS 的核心是:
读和写用不同模型。
写模型 --- 命令端
java
// TradeOrderApplicationService --- 写模型入口
public class TradeOrderApplicationService {
public void placeOrder(PlaceOrderCommand command) {
OrderId orderId = this.tradeOrderRepository().nextIdentity();
TradeOrder order = new TradeOrder(
orderId,
command.getUserId(),
command.getItems(),
new Address(command.getProvince(), command.getCity(), command.getDetail()));
this.tradeOrderRepository().add(order); // 追加事件到事件存储
}
public void payOrder(PayOrderCommand command) {
TradeOrder order = this.tradeOrderRepository().orderOfId(new OrderId(command.getOrderId()));
order.pay(); // 业务规则在聚合根内
this.tradeOrderRepository().add(order); // 追加事件到事件存储
}
}
读模型 --- 查询端
java
// TradeOrderQueryService --- 读模型入口
public class TradeOrderQueryService extends AbstractQueryService {
public TradeOrderData orderOfId(String orderId) {
// 纯 JDBC 查询投影表,不访问领域模型
// 返回轻量级 TradeOrderData,不是 TradeOrder 实体
}
}
投影 --- 事件到读模型
java
// MySQLTradeOrderProjection --- 监听事件并更新读模型
public class MySQLTradeOrderProjection extends AbstractProjection {
public void handleEvent(OrderPlaced event) {
// 收到 OrderPlaced 事件后,在投影表中插入记录
this.insertOrderRow(event);
}
public void handleEvent(PaymentCompleted event) {
// 收到 PaymentCompleted 事件后,更新投影表状态
this.updateOrderStatus(event.orderId(), "PAID");
}
public void handleEvent(OrderShipped event) {
this.updateOrderStatus(event.orderId(), "SHIPPED");
}
}
注意:
CQRS 不等于一定要分库分服务。
单体里也能先做逻辑上的读写分离。关键是写模型操作聚合、读模型查投影表,两者模型独立。
13.3 事件溯源代码示例
前面讲了事件溯源的概念,这里用电商订单给出核心代码示例。
事件溯源聚合根
java
// TradeOrder 事件溯源聚合根
public class TradeOrder extends EventSourcedRootEntity {
// 无参构造函数:初始化事件处理基础设施
private TradeOrder() {
super();
}
// 从命令创建:通过 apply() 发布事件
public TradeOrder(OrderId orderId, UserId userId,
List<OrderLine> items, Address deliveryAddress) {
this();
this.apply(new OrderPlaced(orderId, userId, items, deliveryAddress));
}
// 从事件流重建:委托给 EventSourcedRootEntity
public TradeOrder(List<DomainEvent> eventStream, int streamVersion) {
super(eventStream, streamVersion); // 重放所有事件
}
// when() 方法从事件重建状态 --- 绝不直接设置状态
protected void when(OrderPlaced event) {
this.setOrderId(event.orderId());
this.setUserId(event.userId());
this.setItems(event.items());
this.setDeliveryAddress(event.deliveryAddress());
this.setStatus(TradeOrderStatus.PLACED);
}
protected void when(PaymentCompleted event) {
this.setStatus(TradeOrderStatus.PAID);
}
protected void when(OrderShipped event) {
this.setStatus(TradeOrderStatus.SHIPPED);
}
protected void when(OrderDelivered event) {
this.setStatus(TradeOrderStatus.DELIVERED);
}
protected void when(OrderCancelled event) {
this.setStatus(TradeOrderStatus.CANCELLED);
}
// 命令方法只发布事件,不直接修改状态
public void pay() {
this.assertStateTrue(this.status() == TradeOrderStatus.PLACED, "只有已下单的订单才能支付");
this.apply(new PaymentCompleted(this.orderId()));
// 注意:没有 this.setStatus(PAID) --- 状态由 when() 修改
}
public void cancel(String reason) {
this.assertStateTrue(this.status() != TradeOrderStatus.DELIVERED, "已发货的订单不能取消");
this.apply(new OrderCancelled(this.orderId(), reason));
}
}
事件存储仓储
java
// EventStoreTradeOrderRepository --- 通过事件流加载聚合
public class EventStoreTradeOrderRepository implements TradeOrderRepository {
public TradeOrder orderOfId(OrderId orderId) {
EventStreamId streamId = new EventStreamId("TradeOrder:" + orderId.id());
EventStream eventStream = this.eventStore().eventStreamSince(streamId, 0);
return new TradeOrder(eventStream.events(), eventStream.version());
// 通过重放所有事件重建 TradeOrder 当前状态
}
// save() 实现省略
}
关键区别:
- 传统 ORM 模式 :
HibernateTradeOrderRepository直接加载聚合当前状态 - 事件溯源模式 :
EventStoreTradeOrderRepository通过重放事件流重建状态
也就是说,事件溯源仓储的 orderOfId() 不是查数据库拿一行记录,而是把该订单的所有历史事件按顺序重放一遍,最终得到当前状态。
13.4 应用服务事务生命周期
在事件溯源架构中,应用服务的每次操作都遵循一个固定的事务生命周期。前面 13.2 的应用服务示例省略了事务管理,这里补全:
java
// ApplicationServiceLifeCycle --- 事务生命周期管理
// LevelDBUnitOfWork 是一种具体实现,不同的存储后端有不同的 UnitOfWork 实现
public class ApplicationServiceLifeCycle {
public static void begin() {
LevelDBUnitOfWork.start(); // 开始工作单元
listen(); // 注册事件监听器(收集聚合产生的领域事件)
}
public static void success() {
LevelDBUnitOfWork.current().commit(); // 提交事件到存储
publishNotifications(); // 发布通知给其他上下文
}
public static void fail() {
LevelDBUnitOfWork.current().rollback(); // 回滚,所有事件丢弃
}
}
使用示例 --- 把 13.2 的 payOrder 方法补上事务生命周期:
java
public void payOrder(PayOrderCommand command) {
ApplicationServiceLifeCycle.begin();
try {
TradeOrder order = this.tradeOrderRepository().orderOfId(new OrderId(command.getOrderId()));
order.pay();
this.tradeOrderRepository().add(order);
ApplicationServiceLifeCycle.success();
} catch (Exception e) {
ApplicationServiceLifeCycle.fail();
}
}
三个阶段的职责:
- begin():开启工作单元,注册事件监听器
- success():提交事件到存储,发布通知给其他上下文
- fail():回滚,所有事件丢弃
这个模式确保了业务操作的原子性:要么聚合的所有事件都持久化成功,要么全部回滚,不会出现"改了一半"的中间状态。
十四、什么时候适合用 DDD?
适合:
- 业务复杂
- 同一个词在不同场景里含义经常变化
- 多团队协作
- 需求变化快
- 系统要长期演进
不太适合:
- 纯 CRUD 后台
- 很小的工具系统
- 业务规则极少
- 团队没有精力做业务建模
十五、DDD 落地步骤
如果你要在电商平台里落地 DDD,可以按这个顺序做:
1. 先和业务一起把词说清楚
比如明确:
- 什么叫下单成功
- 什么叫支付成功
- 什么叫发货
- 什么叫退款通过
- 什么叫投诉关闭
2. 划分业务边界
把交易、履约、售后、客服、营销拆开。
3. 在每个边界里建模
识别:
- 哪些是实体
- 哪些是值对象
- 哪些组成聚合
- 会发生哪些领域事件
4. 定义边界之间怎么协作
比如:
- 交易发布
OrderPlaced - 履约监听并创建发货任务
- 营销根据订单金额核销优惠
- 售后通过 API 查询交易订单信息
5. 从轻量做起
不要一上来就全套事件溯源 + CQRS + 微服务。
6. 持续演进
DDD 不是一次性画完图,而是随着业务理解不断重构。
十六、常见误区
| 误区 | 更准确的理解 |
|---|---|
| DDD 就是多写几个实体和值对象 | DDD 的核心是统一语言和限界上下文 |
| DDD 就是微服务 | 微服务只是可能的落地方式 |
| 所有逻辑都放 Service | 规则应尽量回到领域对象 |
| 一个事务绝不能改多个聚合 | 更准确是应尽量只改一个聚合 |
| 有领域事件就等于事件溯源 | 两者不是一回事 |
| 每个类都要建一个 Repository | 仓储通常面向聚合根 |
| DDD 一定更高级 | 合适比高级更重要 |
十七、一张图总结
text
领域驱动设计 DDD
│
┌───────────────┼───────────────┐
│ │ │
战略设计 战术设计 架构实现
│ │ │
- 统一语言 - 实体 - 分层架构
- 限界上下文 - 值对象 - 六边形架构
- 上下文映射 - 聚合 - 事件驱动
- 防腐层 - 领域事件 - 事件溯源
- 领域服务 - CQRS
- 仓储
十八、最后一句总结
如果你只记住一句话,那就记住:
DDD 不是教你把代码写复杂,而是教你承认业务的真实边界。
在电商平台里,大家虽然都在说"用户"和"订单",但交易、履约、售后、客服、营销说的其实不是同一个东西。
DDD 做的,就是把这些差异明明白白地建模出来,让代码终于和业务说同一种语言。