资讯详情

资讯详情

新零售场景下的DDD实战:从事件风暴到微服务拆分

简介“领域驱动设计案例-盒马实践”是一份面向中高级后端工程师、架构师及DDD学习者的PDF文档围绕盒马在领域驱动设计落地中的真实经验展开。资料从领域模型设计理念切入对比数据建模与对象建模的差异并系统梳理失血模型、贫血模型与充血模型在盒马流程中心、基础资料等场景中的具体应用同时讲解依赖注入含Autowired与构造器注入的取舍、Repository模式的实现方式以及微服务部署结构帮助读者理解如何在业务中平衡对象模型与数据库模型。压缩包内仅含1个PDF文件大小1.63MB短小精悍却覆盖了DDD核心概念与盒马实践细节。已有1798人学习下载尤其适合正在实践DDD、希望借鉴大厂案例优化领域模型设计的技术人员。1. 盒马实践新零售场景为什么把贫血模型逼到了墙角把盒马实践拆开看本质是处理一类极其“拧巴”的业务一个订单从 APP 下单开始要同时驱动门店拣货、库存预占、支付回调、配送调度、售后拦截五条业务线每条线都在改同一个聚合里的状态。传统那种 Controller-Service-DAO 三层架构做下来订单服务里最后全是 if/else 状态判断库存扣减散落在四个调用方里没人说得清某一块业务规则到底归哪个模块管。领域驱动设计DDD在这里的核心价值不是换一套代码分层而是先把业务的边界画清楚谁拥有订单状态、谁对库存负责、两个系统之间靠什么协议协作然后把这套边界固化成限界上下文、聚合和领域事件。这篇文章就按这个顺序从事件风暴建模、聚合落地、领域事件驱动一直讲到微服务拆分和那些只有踩过坑才知道的细节。适合正在做新零售、即时履约、线上线下融合系统的开发者和架构师尤其是被“订单状态散落、业务规则纠缠”折磨过的人。2. 事件风暴与限界上下文先把业务边界画出来2.1 事件风暴怎么开角色、贴纸和三个必问事件风暴是 DDD 建模里最常用的起点盒马这类新零售项目尤其适合因为它天然具备“高并发、多角色、强协作”的特征。一次标准的事件风暴工作坊参与者必须覆盖三类角色懂业务规则的领域专家运营、仓储、客服负责技术落地的开发骨干以及一个主教练facilitator。主教练不参与业务讨论只负责控场和追问。物料很简单一面足够宽的墙、不同颜色的便利贴和马克笔建议控制在 4 到 6 个人人多了讨论效率反而低。贴纸规则通常这样约定橙色代表领域事件过去式比如“订单已支付”蓝色代表引发事件的命令比如“提交订单”绿色代表聚合或实体比如“订单”“库存项”黄色代表外部系统或角色红色用来标记风险点和需要进一步澄清的地方。事件风暴的第一步不是画流程图而是让所有人把脑子里能想到的业务事件全部倒出来按时间轴贴在墙上不对、不评判先把全貌铺开。铺完之后进入追问环节我一般会带着三个问题反复砸向领域专家第一这个事件由哪个角色或哪个系统触发第二事件发生后哪些下游必须感知哪些可以异步容忍第三如果这个事件的数据不一致会造成什么级别的业务事故——是用户投诉还是资损还是法律风险这三个问题的答案决定了事件是不是要独立成领域事件、要不要引入最终一致性、以及聚合并发控制的强度。比如“订单已支付”这个事件盒马场景里支付回调如果丢了用户可能重复付款或订单无法流转这是资损级必须走可靠消息和幂等处理“用户浏览了商品”这类事件就算丢了也影响不大走异步日志即可。有一点要提醒不要把事件风暴开成需求评审会。如果讨论变成了“这个功能我要这样那样”说明已经滑到 CRUD 实现了。事件风暴只回答“业务发生了什么”不回答“页面怎么展示”“接口怎么传参”。如果讨论中出现“这个列表要分页”“这个按钮要置灰”主教练要把话题拉回来回到事件和决策本身。2.2 限界上下文划分新零售的六个核心域事件风暴的产出是一张事件时间轴从中可以识别出业务的有序片段。盒马这类新零售业务按我的实践通常可以划分出六个相对稳定的限界上下文商品域、门店域、交易域、库存域、履约域、会员营销域。也有拆分更细的团队会把支付、售后、结算单独拉出来但核心域一般逃脱不了这六块。划分限界上下文的关键不是看团队组织结构而是看“业务对象的生命周期归属”。以库存为例“门店实时库存”“可售库存”“物理库存”三个词听起来像一回事在业务上却是三个完全不同的概念可售库存要扣减预占物理库存是仓库实存门店实时库存要同步线上线下。它们被业务改动的频率、改动的发起方、一致性的要求都不同如果塞进同一个库存上下文就会变成一个大泥球。正确做法是让库存域自己负责可售库存和预占逻辑物理库存由仓储系统负责两侧通过防腐层同步。上下文之间不要直接共享数据库表或直接调用另一个上下文的 Service 接口这是新手最容易破的戒。正确的协作方式是通过领域事件或开放的 APIOpen Host Service并且把下游需要的协议封装在接口层避免数据细节泄露。比如交易域下单时不需要知道门店的精确库存数只需要知道“能否履约”这个结论这就是一个典型的防腐层场景。限界上下文一旦划定后续的微服务拆分基本是水到渠成的事。很多团队纠结微服务粒度其实就是因为在没画清楚上下文之前就按功能模块切了。我见过把“订单”和“购物车”拆成两个服务的项目结果购物车结算时要同步订单状态两个服务直接形成分布式循环依赖这就是上下文识别失败的典型症状。2.3 上下文映射图防腐层和共享内核的取舍上下文划分完成后要用上下文映射图把上下文之间的关系画出来。常见的几种关系合作关系Partnership、共享内核Shared Kernel、防腐层Anti-Corruption LayerACL、开放主机服务OHS。新零售项目里最常用也最容易被误用的是共享内核和防腐层。共享内核是把一小块公共模型比如支付金额、订单号生成规则在两个上下文之间共享听起来高效做起来容易失控。因为共享意味着双方都要遵守同一套变更流程任何一方改了共享模型另一方必须同步配合。盒马这种节奏很快的业务里我倾向于把共享内核压缩到最小只共享一些无状态的值对象比如金额、地址、时间戳其他一律靠防腐层隔离。防腐层的实现套路很固定在自己这侧定义一个领域模型在基础设施层写一个适配器把外部系统的数据转换成自己的模型。库存域对接 WMS 时WMS 返回的“skuId”“qty”“locationCode”这些字段不允许直接渗透到库存领域的核心模型里而是在防腐层里转换成领域内的“货品”“可售量”“库位”对象。这样做的目的是让领域层只面对一个自己定义的消息协议外部系统怎么变影响的只是适配器内部的映射逻辑。如果你发现防腐层的映射代码开始出现大量 if/else 处理不同外部系统的字段差异这是个信号外部系统不止一个或者你的领域模型建得太薄了。正确的做法是让防腐层做模型翻译而不是做业务判断。业务判断必须回到领域服务里。还有一种常见误用是拿防腐层包所有外部调用结果防腐层变成了一个中转 API这层就废了。防腐层要隔离的是模型差异不是功能调用。3. 聚合与领域模型订单和库存的落地建模3.1 先识别实体、值对象和领域服务进入代码层之前要先对每个限界上下文里的对象做类型识别。实体Entity有唯一标识且生命周期会变订单是实体用户是实体门店是实体。值对象Value Object没有唯一标识由属性决定相等性价格、地址、手机号都是典型值对象。领域服务Domain Service处理那些不天然属于某个实体或值对象的业务逻辑比如库存预占、运费计算。识别它们的意义在于不要让贫血模型悄悄回来。贫血模型的典型样子是实体里只有 getter/setter所有业务逻辑都堆在 Service 里。DDD 的底线是能放到实体的逻辑就不要放到 Service 里。比如“能否修改配送地址”这个规则和订单状态强相关就应该由订单聚合根自己来回答而不是由外面一个 OrderService 判断后 set 进去。在新零售场景里有几个判断技巧比较实用。如果一个对象只有两个属性且它们永远同时出现比如“纬度经度”那是值对象。如果一个对象的行为高度依赖另一个对象的内部状态比如“优惠券”依赖“订单金额”那它要么是订单聚合的一个内部成员要么应该通过领域事件交互而不是直接拉取订单数据在 Service 里计算。值对象做得好能省掉大量重复校验代码。比如金额不用 Double 而定义一个 Money 值对象内部封装币种和单位换算所有涉及金额的领域逻辑统一走它。盒马这种涉及线上线下一体化结算的业务金额的精度和币种问题会在每个角落里出现一个 Money 值对象能阻止大部分低级错误。3.2 订单聚合怎么设计状态机与不变量订单是交易域的核心聚合根。聚合根是 DDD 里最容易设计错的地方很多人把聚合根当成一个放一堆实体的大容器结果一个订单聚合里塞了订单明细、支付记录、配送记录、售后记录导致每次加载订单都要拖出一大坨数据并发冲突的概率直线上升。我的设计原则是聚合根只负责在业务上必须保持一致的原子边界。订单聚合应该包含订单基本信息、订单明细、支付快照、状态机但支付回调记录、配送轨迹这些可以通过领域事件异步追加不需要和订单状态锁在同一把锁里。这样订单聚合的加载体积小状态流转的控制也清晰。订单状态机是交易域的命脉。盒马订单的状态链大致是待支付、已支付待拣货、拣货中、配送中、已完成、已取消加上售后引入的退款中、退款完成。状态机设计最重要的规则是状态迁移必须封装在聚合根内部对外只暴露行为方法比如pay()、confirmPick()、deliver()、cancel()不允许外部直接 set 状态字段。代码层面我一般会用枚举把所有合法迁移枚举出来public enum OrderStatus { PENDING_PAYMENT, PAID_PICKING, PICKING_DELIVERY, DELIVERING, COMPLETED, CANCELLED, REFUNDING, REFUNDED; private MapOrderStatus, SetOrderStatus allowedTransitions Map.of( PENDING_PAYMENT, Set.of(PAID_PICKING, CANCELLED), PAID_PICKING, Set.of(PICKING_DELIVERY, CANCELLED, REFUNDING), PICKING_DELIVERY, Set.of(DELIVERING, REFUNDING), DELIVERING, Set.of(COMPLETED, REFUNDING), REFUNDING, Set.of(REFUNDED) ); public boolean canTransitTo(OrderStatus target) { return allowedTransitions.getOrDefault(this, Set.of()).contains(target); } }这段代码的逻辑很清楚定义了一个合法状态迁移表任何状态迁移都必须经过canTransitTo()校验。参数层面的设计要点是allowedTransitions一定要用不可变集合防止运行时被篡改Map.of 在元素超过 10 对时要用Map.ofEntries不然编译期会报错。状态机放在枚举里还有一个好处它天然是值对象可以被多个 JVM 实例共享配合分布式锁做并发控制时不会出现状态判断不一致。有人会把状态机做成独立的领域服务我觉得没必要除非你的状态迁移还依赖外部系统返回结果那就应该在应用层编排时先调领域方法再根据结果决定下一步。3.3 库存聚合与分布式事务的妥协库存是另一个必须认真设计聚合的领域。库存聚合的难点在于并发极高盒马这种场景一个爆品开售瞬间可能几万请求同时打到库存服务上。如果把“订单创建 库存扣减”放到同一个本地事务里性能一定崩但如果拆成两个事务又面临超卖和少卖的问题。我的做法是把库存预占作为独立的聚合操作和订单创建分离。下单请求先进入交易域交易域发出“订单已创建”事件库存域消费后执行预占预占成功发出“库存已预占”事件交易域收到后把订单状态推进到待支付。整个过程是最终一致性的异步流转。这里就容易发现聚合设计的一个坑有人会把库存聚合设计成包含“预占明细”这个大集合每次预占操作都要锁整个集合。正确设计是对库存做分桶比如按库位或按批次拆成子聚合。这样一次预占只锁一个桶并发能力能上去一个数量级。预占请求里带上桶 ID库存服务只需要校验桶内剩余量并扣减冲突概率大幅下降。库存预占失败的处理也要在建模时就想清楚。我建议在订单侧增加一个“预占超时待确认”状态库存域超时未返回预占结果时交易域主动查询或走对账补偿。这个补偿逻辑用一个独立的定时任务扫描不要塞进订单聚合里否则订单聚合会被定时任务的逻辑污染。分布式事务在这里的基调是“能不用就不用”。本地消息表和事务消息是更可靠的选择具体在第四章展开。库存领域要守住一条底线不允许事务域直接改库存表所有库存变更必须走库存域的领域服务或事件否则边界立刻就烂了。4. 领域事件驱动从“接口调用”到“事件流转”4.1 领域事件建模状态变更背后的消息契约领域事件是限界上下文之间协作的主要载体。盒马实践里订单、库存、履约、会员几个上下文之间如果靠同步 RPC 调用串起来任何一个下游抖动都会拖垮上游改成事件驱动后上下游只通过消息队列解耦抗冲击能力明显更强。领域事件建模的第一条规则是事件名用过去式表示“已经发生的事实”。OrderPaid、StockPrededucted、DeliveryCompleted这些都是标准命名。不要用PayOrder这种命令式命名命令是还没有发生的事它们应该留在 API 层。第二领域事件里只放事件发生时已经确定的数据不要为了省一次查询把未来可能变化的数据也塞进去。比如OrderPaid事件携带订单 ID、支付金额、支付时间就够了不要把顾客的完整信息都放进去否则下游一旦消费延迟拿到的是过期数据反而容易出错。第三事件定义应该放在独立的模块里而不是和聚合根放一起。我通常建一个domain-event模块专门放事件类这样发布方和订阅方都依赖这个模块不互相依赖。一个典型的领域事件代码public class OrderPaidEvent { private final String orderId; private final Money paidAmount; private final LocalDateTime paidAt; public OrderPaidEvent(String orderId, Money paidAmount, LocalDateTime paidAt) { this.orderId orderId; this.paidAmount paidAmount; this.paidAt paidAt; } public String getOrderId() { return orderId; } public Money getPaidAmount() { return paidAmount; } public LocalDateTime getPaidAt() { return paidAt; } }注意这里的Money是值对象而不直接用BigDecimal能避免在不同上下文之间传递时丢失币种信息。paidAt用LocalDateTime而不是long时间戳日志和排查时可读性好很多。领域事件发布的位置我放在聚合根的方法里在状态变更成功后通过领域事件发布器发出去。不要在应用服务里手动发事件因为那样容易漏发或不一致。聚合根在内存里完成了状态迁移然后发布事件如果事务回滚事件不应该被发出去这里要用事务性发件箱模式来保证。4.2 CQRS 与读写分离盒马查询场景为什么必须分盒马这种新零售业务查询的复杂度和写入完全不在一个量级。用户端的“订单列表”“门店商品展示”都是多条件组合查询经常要跨多个上下文的数据而写入侧聚焦在“下单”“支付”“改地址”这类低频操作上。如果用同一套模型来做聚合根必须为查询需求加载大量关联数据性能和模型纯度两头都不讨好。CQRS命令查询职责分离在这里是一个实用主义选择。命令侧用领域模型走聚合根方法、状态机校验查询侧用专门的读模型直接查表或查搜索引擎不做复杂的领域逻辑拼装。读模型的数据可以来自多个上下文的投影通过消费领域事件异步构建比如“订单列表页”需要展示商品图片和门店名投影服务订阅OrderPaid、StockPrededucted等事件后把数据组装进一张宽表。落地时不要搞成全面 CQRS那是过度设计。我一般只对“强一致写入 复杂查询”的区域采用这个模式交易上下文配一个读库库存上下文配一个实时查询缓存其他上下文保持普通模式。查询侧的模型更新是异步的会有短暂延迟。盒马这种业务订单支付后用户立刻查订单状态延迟必须控制在秒级。我通常让投影消费消息后直接更新缓存缓存失效时间设置得很短同时提供一个兜底查询路径如果读模型没有该订单的数据直接穿透到命令侧查询——但要对这种穿透做限流防止读模型一直没建好把写服务打挂。4.3 事件总线与消息可靠性参数调优的底线事件驱动最怕的是消息丢失尤其订单支付这种资损级事件。我在项目里用的是本地消息表方案业务操作和事件写入在同一个本地事务里然后由一个定时任务把事件表里的记录发到 MQMQ 消费成功后删除记录。这个方案比单纯依赖 MQ 的事务消息更可控排查问题也更直观。本地消息表的核心表结构大致是事件 ID、聚合类型、聚合 ID、事件类型、事件内容JSON、状态、创建时间、发送时间。状态流转是“待发送 - 发送中 - 已发送”定时任务每次捞最近 5 分钟没发出去的事件重发。这里要注意${}拼接 SQL 的注入风险用参数绑定还要在事件表上对“状态 创建时间”建联合索引否则扫描慢。CREATE TABLE domain_event_outbox ( id BIGINT AUTO_INCREMENT PRIMARY KEY, aggregate_id VARCHAR(64) NOT NULL, aggregate_type VARCHAR(64) NOT NULL, event_type VARCHAR(128) NOT NULL, event_body JSON NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待发送 1-发送中 2-已发送, created_at DATETIME NOT NULL, sent_at DATETIME NULL, KEY idx_status_created (status, created_at) ) COMMENT 领域事件发件箱;这个建表语句里的event_body用 JSON 类型比 TEXT 更规范MySQL 5.7 以上都支持。status加注释方便排查。最关键的是联合索引idx_status_created定时任务WHERE status 0 AND created_at ?时能走索引不然事件量上来后会拖垮数据库。消费侧必须做幂等。同一个订单支付事件可能因为网络重投到达两次消费方的处理逻辑必须是幂等的处理前先查一下本地记录是否已处理过。幂等键用事件 ID 聚合 ID 拼接。参数调优方面我倾向把可靠性参数设保守一点生产者重试次数 3 次重试间隔用指数退避初始 500ms消费线程数根据下游接口耗时调一般 10 个线程起步消息消费失败不要无限重试超过 3 次进死信队列定时人工处理。订单支付事件的消费线程数反而要少一些因为下游是财务系统处理速度要是跟不上线程再多也是堆积不如把线程数压到 5 个保证顺序性。顺序消息的问题要单独说。盒马的订单状态流转理论上同一个订单的“已支付”“拣货中”“配送中”必须按顺序消费否则状态会乱。我一般会对订单 ID 做 hash 取模绑定到固定队列这样同一个订单的事件始终进同一个队列消费端单线程处理。代价是队列增删的运维复杂度但因为只绑定订单事件可控。5. DDD落地避坑新零售项目最常翻车的五个场景5.1 拆得太碎把限界上下文切成了十几个微服务现象项目上线半年后一个用户下单请求要经过 8 个微服务最多时链路耗时 3 秒排查问题要在十几个服务之间跳来跳去一个版本发布要协调五个团队。原因把限界上下文直接等价于微服务粒度忽略了上下文内部还有聚合的聚合。比如把“支付”从交易域里拆出来单独部署还拆了“退款”和“支付渠道管理”两个服务导致一个支付成功回调要跨两个服务传播事件。解决限界上下文是逻辑边界微服务是部署边界两者不能画等号。我的原则是一个限界上下文默认先做成一个模块只有当模块内部的某个聚合有独立的性能伸缩需求或独立的团队维护时才拆成独立服务。支付和退款在 DDD 模型里是同一个限界上下文但部署上可以拆因为支付回调频率高退款低频两者混在一个进程里会互相影响。5.2 防腐层失效外部系统字段穿透到了领域层现象库存领域的领域模型里出现了 WMS 的locationCode字段订单领域服务里直接判断 ERP 返回的状态码业务规则和外部系统耦合在一起换一个供应商就要改核心代码。原因防腐层只做了接口转发没有做模型翻译。有人觉得外部系统字段传进来不亏反正下游也要用结果外部系统的数据结构慢慢渗透进了领域模型限界上下文形同虚设。解决防腐层必须做到“进得来、出不去”。外部系统的请求先通过适配器转换成领域模型再进领域层。领域层对外的输出也要经过适配器转成外部系统的协议。我一般会在防腐层里做单元测试专门验证外部系统返回的数据能正确映射到领域模型有些字段转换不了就直接丢弃不在领域层留扩展点。5.3 领域事件失控事件满天飞形成循环依赖现象订单域发布了“订单已取消”事件库存域消费后发布“库存已释放”事件交易域又消费“库存已释放”去更新订单状态结果两个域之间事件互相触发日志里出现同一个订单 ID 的连环事件流出了问题根本定位不了源头。原因把领域事件当成万能解耦工具任何状态变化都发事件但不区分事件是“事实通知”还是“业务指令”。订单已取消是事实通知库存去释放是业务指令两者混淆之后领域之间就变成了隐式的循环调用。解决明确事件方向一个领域只对“自己发起的、且对外部有意义的事实”发布事件。库存释放不该由“订单已取消”事件直接触发而是由订单域调用库存域的释放接口或者订单域发一个“订单已取消”事件库存域作为一个独立的策略参与者决定是否释放。更重要的是一旦发现 A 域和 B 域之间事件相互往来超过一个来回就要停下来重新画上下文映射图看是不是边界画错了。5.4 聚合过大一个订单聚合拖垮了并发现象订单表的数据量涨到千万级后订单操作越来越慢。排查发现一个订单聚合加载时要关联订单明细、支付记录、配送记录、优惠记录一次下单要查 12 张表事务锁的范围大冲突概率高。原因建模时把所有看起来和订单有关的数据都塞进一个聚合里没有考虑真实业务场景下的访问频率和一致性要求。聚合的目的是保证业务不变量不是为了装数据。解决把聚合缩小。订单聚合只保留订单主信息、明细、状态支付记录和配送轨迹改成独立的聚合通过订单 ID 关联。如果查询时需要关联展示走 CQRS 的读模型。聚合缩小的代价是需要额外处理分布式一致性但这个代价换来的是并发能力和模型清晰度在盒马这种业务里是值得的。5.5 为了 DDD 而 DDD业务没理顺就强行建模现象团队读完 DDD 的书觉得项目结构不够“领域驱动”于是花了两周时间重构代码把类名从OrderService改成OrderDomainService包名从service改成domain但业务逻辑还是原来的贫血模型只是换了个壳。原因把 DDD 理解成代码组织风格忽略了它的核心是先梳理业务、画出边界。没有事件风暴、没有限界上下文分析直接套 DDD 的分层结构只是给旧代码换了套便宜衣裳。解决动手改代码之前先接受一个事实DDD 的建模成本主要在前期。哪怕只对一个高频业务链路比如下单做完整的流程梳理也比全量重构要好。从一个小领域开始跑通事件风暴、画清上下文、写出聚合根再逐步推广。代码分层只是最后一步的呈现不是 DDD 本身。6. 一个可复用的项目骨架从建包到跑通订单状态机这一章给出一套可以直接抄走的项目骨架基于 Spring Boot Maven核心是包结构和聚合根写法。包结构我一般这样设计com.example.order ├── interfaces // Controller、DTO、防腐层适配器 ├── application // 应用服务、事务编排 ├── domain │ ├── model // 聚合根、实体、值对象 │ ├── service // 领域服务 │ ├── event // 领域事件 │ └── repository // 仓储接口 └── infrastructure // Repository 实现、MQ、缓存、外部系统封装domain 层不允许依赖 infrastructure 和 interfaces 层这个依赖规则是 DDD 能不能落地的关键。很多项目写着写着就破戒了比如直接在聚合根里调 MQ 发事件。我的习惯是在 domain 层定义事件发布接口在 infrastructure 层实现聚合根通过构造器注入接口。订单聚合根的骨架从pay()方法看起public class Order extends AbstractAggregateRoot { private OrderId orderId; private OrderStatus status; private Money paidAmount; private ListOrderItem items; public void pay(Money amount, LocalDateTime paidAt) { if (!status.canTransitTo(OrderStatus.PAID_PICKING)) { throw new IllegalStateException(当前状态不允许支付: status); } if (!this.paidAmount.equals(amount)) { throw new IllegalArgumentException(支付金额与订单金额不一致); } this.status OrderStatus.PAID_PICKING; this.paidAt paidAt; registerEvent(new OrderPaidEvent(orderId.getId(), amount, paidAt)); } }AbstractAggregateRoot是 Spring Data 提供的基类registerEvent会把事件暂存在内存里事务提交后由应用层统一发布。这里的参数amount用Money而不是基本类型能避免精度问题。paidAt用LocalDateTime方便排查。在应用服务里调用聚合根Service public class OrderApplicationService { Transactional public void payOrder(String orderId, BigDecimal amount, LocalDateTime paidAt) { Order order orderRepository.findById(new OrderId(orderId)); order.pay(new Money(amount, Currency.CNY), paidAt); orderRepository.save(order); order.publishDomainEvents(eventPublisher); } }Transactional保证聚合根修改和 save 在同一个事务里publishDomainEvents把聚合根内存里的事件发给发件箱表后续由定时任务投递到 MQ。注意这里的事务只覆盖本地操作不覆盖 MQ 发送MQ 发送是异步的。这套做法是你验证 DDD 建模是否跑偏的最快方法。如果哪一天你发现聚合根里的某个业务规则需要查外部系统才能判断那就说明职责放错地方了。把方法挪到领域服务里让领域服务协调聚合根和外部系统的防腐层接口。最后说一个我的个人教训最早我把事件风暴当成一次性的“启动会”开完就再也没回过那面墙。后来模型和现实业务越来越对不上翻旧账时发现很多当时标记为红色的风险点根本没有处理。现在我的习惯是每次大版本迭代之前把核心链路的事件时间轴重新贴一遍用半小时和最熟的领域专家对一遍成本不高但能避免模型悄悄漂移。希望这个习惯能帮到你少走一段弯路。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →