DDD实践:如何建模影响两个聚合根的操作?
问题背景
领域背景
我拥有InventoryItem聚合根和Kit聚合根。一个InventoryItem只能被添加至一个Kit中,InventoryItem包含Type属性(业务所用各类物品类型的枚举),一个Kit可包含多个InventoryItem。
我采用事件溯源架构,每个InventoryItem和Kit聚合根都有独立的事件流。
核心问题
InventoryItem需知晓自身是否已加入Kit,以执行阻止已加入Kit的物品删除、禁止重复加入其他Kit等约束;Kit需知晓已添加的InventoryItem数量及类型,以执行未满足要求则禁止发货等约束。
但DDD要求单个事务仅能操作一个聚合根,因此将InventoryItem加入Kit时,无法通过单事务同时更新两个聚合根的状态。我考虑了几种方案,但均存在问题:
- 用流程管理器/saga建模为长流程:该操作并非长流程,此方案会引入额外开销,还存在阶段失败需回滚的风险。
- 用领域事件更新其中一个聚合根:先通过事务更新一个聚合根,再分发领域事件更新另一个,但存在事件失败导致领域模型不一致的风险,还可能出现物品被重复加入其他
Kit的情况。 - 新增聚合根:考虑新增
KitItem聚合根存储两者关联关系,Kit和InventoryItem持有其引用,但仍存在模型不一致风险(如Kit删除时KitItem删除失败),且Kit仅持有引用无法知晓物品类型信息。
请问有何可行方案建议,或是否有未考虑到的其他思路?
解决方案建议
1. 调整聚合边界,依托读模型做约束校验
重新梳理聚合边界:如果将物品加入套件是Kit的核心业务动作,可以把跨聚合的约束校验转移到读模型层面:
- 执行
AddInventoryItemToKit命令时,先通过事件投影生成的只读视图(比如InventoryItemStatusProjection)查询目标物品是否已被加入其他套件,这一步是无状态的读操作,不修改任何聚合。 - 校验通过后,仅在
Kit聚合内生成InventoryItemAddedToKit事件并持久化,事件中携带物品的Type和ID信息。 Kit自身的状态(物品数量、类型统计)直接通过重放自身事件流计算得出,满足发货约束的校验需求。- 后续对
InventoryItem执行删除、加入其他套件操作时,同样先查询只读视图确认状态,再执行对应聚合的命令。
这种方式完全遵循单事务操作单个聚合的原则,用最终一致的读模型替代跨聚合的写操作,从根源上避免一致性风险。
2. 优化领域事件方案,加幂等性与补偿机制
如果聚合边界无法调整,保留领域事件方案但补充以下保障:
- 全局幂等ID:给每个
AddInventoryItemToKit命令生成唯一幂等标识,InventoryItem处理AssignedToKit事件时,先检查该标识是否已处理,杜绝重复执行。 - 补偿流程:若
Kit的事件成功持久化,但InventoryItem的事件处理失败,自动触发补偿:要么重试事件处理(最多N次),要么反向触发InventoryItemRemovedFromKit事件回滚Kit的状态。 - 乐观锁校验:执行
Kit的添加操作前,再次查询InventoryItem的当前状态(通过事件流重放或投影视图),若此时物品已被占用,直接拒绝命令,避免并发冲突。
3. 用领域服务封装跨聚合协调逻辑
创建KitInventoryCoordinator领域服务,统一处理物品加入套件的跨聚合操作:
- 服务先查询
InventoryItem的状态,确认未被占用,同时给该物品加分布式锁,防止并发修改。 - 依次开启两个独立事务:第一个提交
Kit的InventoryItemAdded事件,第二个提交InventoryItem的AssignedToKit事件。 - 若第二个事务失败,服务立即触发
Kit的InventoryItemRemoved事件,回滚套件的状态,再释放分布式锁。
这种方式通过服务封装复杂的协调逻辑,用锁和补偿机制保障最终一致性,同时不破坏聚合的边界规则。
内容的提问来源于stack exchange,提问作者shawnseanshaun
相关产品推荐
相关产品推荐

