
1. 这不是又一篇“概念堆砌”的DDD文章——它解决的是你写代码时真实卡住的那几秒你有没有过这样的时刻接到一个新需求比如“用户下单后库存要扣减、积分要增加、风控要校验、通知要发出去”你打开IDE手指悬在键盘上三秒——先建哪个类Controller里塞多少逻辑Service层到底该不该调用另一个Service领域模型是放DTO里还是Entity里聚合根怎么划才不算越界最后你点了运行功能跑通了但心里发虚这代码半年后还能改吗别人接手时会不会想删库跑路这就是DDD真正要解决的问题不是画几张UML图交差也不是把“限界上下文”“值对象”“防腐层”这些词背熟去面试。它是一套面向复杂业务系统的编码决策系统——当你面对模糊的需求、频繁变更的规则、多人协作的边界、技术债滚雪球式的膨胀时它给你一套可落地的判断依据什么该放一起什么必须隔离什么该暴露什么该封装什么时候该妥协什么时候死守边界。我带过6个不同行业的DDD落地项目从金融风控引擎到社区团购履约系统从医疗影像归档平台到工业设备IoT告警中心。最深的体会是DDD不是设计出来的是在每天改bug、加字段、救线上故障的过程中被逼出来的肌肉记忆。它不教你“怎么画图”而是教你在数据库字段加完、接口联调完、测试用例补完之后回过头看那段代码能一眼指出“这里本该是个值对象却用了String硬扛”“这个Service方法其实横跨了两个限界上下文迟早要拆”。这篇文章不列定义、不讲历史、不对比MVC或六边形架构优劣。我们直接切进你写代码时最常卡壳的5个现场当产品经理说“订单状态流转规则下周要变三次”你怎么让状态机不变成if-else地狱当财务、仓储、客服三个团队对“已发货”有完全不同的理解代码里怎么避免互相污染当一个“客户”实体在CRM里要存37个字段在营销系统里只关心3个标签在风控系统里只认身份证号和近30天行为你怎么避免“上帝对象”当新同事问“为什么这个方法叫applyDiscount()而不是calculateDiscount()”你怎么用一句话让他懂背后的设计意图当线上突然出现“库存扣成负数”排查日志时发现扣减逻辑散落在OrderService、InventoryService、PromotionService三个地方你怎么重构才能让问题定位时间从2小时缩短到2分钟下面所有内容都来自这些真实场景里踩过的坑、撕过的代码、熬过的夜。没有理论推演只有“当时我这么干结果省了3天联调时间”“那个参数设错导致压测时QPS掉了一半”“这个命名约定让新成员第三天就能独立改需求”。如果你正被类似问题困扰或者刚学完DDD概念却不知从哪下手这篇就是为你写的。2. DDD不是一套框架而是一张“业务决策地图”——先搞清它到底在解决什么问题2.1 为什么传统分层架构在复杂业务里会“失灵”先看一个典型反例某电商促销系统上线后第3个月运营提出新需求——“满299减50”的优惠券要支持“同一用户每日限领1张但可叠加使用多张”。开发小哥照着老套路改了CouponService里的isEligible()方法加了countByUserIdToday()查询又在OrderService里循环调用applyCoupon()。上线后发现高并发下单时库存扣减和优惠券核销不同步出现超发更糟的是风控团队要求“单笔订单优惠总额不得超过订单金额50%”这个规则被硬塞进同一个CouponService里导致每次改优惠策略都要重新走风控审批流程。问题出在哪不是代码写得差而是架构没对齐业务复杂度。这个系统里实际存在三套独立的业务规则营销域关注用户领取行为、券池生命周期、面额叠加逻辑交易域关注订单创建、支付、状态流转、资金结算风控域关注实时欺诈识别、资损控制、合规红线。但代码里它们全挤在CouponService这个“万能胶水层”里。就像把厨房、卧室、卫生间的功能全塞进一个房间——短期能用长期必然混乱。DDD的起点就是承认业务复杂度无法靠技术分层Controller/Service/DAO来消化必须靠业务语义来划分。它不回答“代码放哪层”而回答“这段逻辑属于哪个业务世界”。提示别急着画“战略设计”图。先拿一张白纸写下你当前项目里最近3个让你改得最痛苦的需求然后问自己这些需求改动主要影响的是同一群人的工作方式吗他们用的术语一致吗他们考核的KPI相同吗如果答案是否定的恭喜你已经找到了第一个限界上下文的候选区。2.2 四个核心概念不是名词解释而是四把手术刀很多教程把Entity、Value Object、Aggregate、Domain Service列成名词表但实际编码中它们是你做决策时的“条件反射”Entity实体不是“有ID的类”而是生命周期内身份持续重要的东西。比如“订单”ID不变但地址、商品、状态全可变而“订单项”虽然也有ID但一旦订单取消它就该被整体删除它的存在依附于订单——所以订单是Entity订单项是Value Object稍后详解。判断标准很简单如果删掉它业务上是否还有“这个东西曾经存在过”的记录需求有就是Entity没有优先考虑Value Object。Value Object值对象不是“不可变对象”而是通过属性组合定义相等性的东西。比如“收货地址”两个地址只要省市区街道门牌号完全相同业务上就认为是同一个地址哪怕ID不同。它没有独立生命周期不能单独存在必须依附于某个Entity如订单。实操中我强制团队所有VO都重写equals/hashCode且禁止提供setter——不是为了炫技而是防止有人误以为“改地址”需要先查再update其实应该直接new Address(newProvince, newCity...)赋值给订单。Aggregate聚合不是“一组相关对象”而是数据修改的原子边界。比如“订单聚合”包含Order根、OrderItem内部实体、Address值对象。任何对聚合的修改必须通过根Order发起且整个聚合的状态变更必须在一个事务内完成。这意味着你不能直接new OrderItem().save()也不能在OrderService里先update Order再update Inventory——后者违反了“库存扣减应属于库存聚合”的边界。聚合的真正价值是让“一致性”变得可预期只要守住根你就知道哪些数据一定同步更新。Domain Service领域服务不是“放工具方法的地方”而是协调多个聚合或跨领域逻辑的场所。比如“订单支付成功后触发积分发放库存扣减物流单生成”这三个动作分别属于用户域、库存域、物流域单个聚合无法完成。这时Domain Service就像一个“业务指挥官”它不持有状态只编排流程并确保最终一致性比如发消息而非直接调用。关键区别如果逻辑只涉及单个聚合内部它就该是聚合根的方法如果涉及多个聚合或外部系统才轮到Domain Service出场。注意别一上来就建AggregateRoot接口或BaseEntity抽象类。我见过太多团队花两周搭“DDD基础框架”结果第一版业务代码里全是空实现。正确顺序是先写清楚一个核心业务流程比如下单手动画出对象关系再根据上面四条标准自然区分出哪些是Entity、哪些该是VO、哪里该划聚合边界。框架是结果不是前提。2.3 战略设计限界上下文不是画圈游戏而是划清“谁说了算”的权力线限界上下文Bounded Context常被误解为“技术模块划分”但它本质是业务语义的自治单元。举个血泪案例某供应链系统里“供应商”在采购部叫“合作方”在质检部叫“准入厂商”在财务部叫“应付账款主体”。三套系统各自维护自己的Supplier表字段名、主键规则、状态码全不同。当采购部新增一个供应商财务系统要等3天人工同步数据期间所有付款申请失败。DDD的解法不是搞个“统一供应商主数据平台”而是明确采购上下文Supplier {code, name, contact, contractStatus}状态码DRAFT/APPROVED/TERMINATED质检上下文Vendor {id, companyName, auditResult, lastAuditDate}状态码PENDING/QUALIFIED/REJECTED财务上下文Payee {taxId, bankAccount, creditLevel, paymentTerms}状态码NORMAL/ARREARS/BLACKLISTED。它们之间不共享数据库不共用类甚至不共用英文名。通信只通过明确定义的API或事件如采购上下文发布SupplierApprovedEvent财务上下文消费后创建PayeeRecord。每个上下文内部语言、规则、数据模型完全自治。实操中我用三个问题快速识别上下文边界术语冲突同一个词如“客户”“订单”“审核”在不同团队口中含义是否不同变更节奏A团队的需求迭代频率是否远高于B团队高频变更的上下文必须隔离决策权归属某个业务规则如“退货时效”是由销售定、还是客服定、还是法务定决策权分散处必是上下文边界。实操心得第一次划上下文别追求完美。我们曾用一周时间把一个大系统粗粒度划为5个上下文每个上下文只定义3个核心领域事件和2个对外API。上线后发现采购和仓储上下文耦合太紧就把“库存扣减”逻辑从采购上下文剥离变成仓储上下文提供的独立服务——这比一开始就设计“完美架构”快得多也准得多。3. 从零开始构建一个真实订单聚合——手把手拆解每行代码背后的DDD决策3.1 场景设定一个足够简单但足够真实的电商下单流程我们聚焦最核心链路用户提交订单 → 校验库存 → 扣减库存 → 创建订单 → 发送通知。不考虑支付、物流、售后等延伸流程但保留真实痛点库存扣减必须强一致不能超卖订单状态机需支持未来扩展如增加“待支付”“已锁定”状态不同渠道APP/小程序/H5对“订单”理解略有差异但核心结构一致运营可能随时调整“库存预警阈值”“限购数量”等规则。目标写出一段代码让新成员看到Order类就能明白“订单”在业务中到底代表什么看到placeOrder()方法就知道它为何不能拆成多个小方法看到InventoryService调用就清楚为什么这里必须用最终一致性而非本地事务。3.2 第一步定义聚合根——Order不是数据容器而是业务行为载体// Order.java - 聚合根 public class Order { private final OrderId id; // Entity标识不可变 private final UserId userId; private final ListOrderItem items; // 值对象集合内部不可变 private OrderStatus status; // 状态枚举非原始类型 private final LocalDateTime createdAt; // 私有构造强制通过工厂方法创建 private Order(OrderId id, UserId userId, ListOrderItem items) { this.id id; this.userId userId; this.items Collections.unmodifiableList(items); this.status OrderStatus.CREATED; this.createdAt LocalDateTime.now(); } // 工厂方法封装创建逻辑隐藏内部细节 public static Order create(UserId userId, ListOrderItem items) { // 业务规则至少一个商品 if (items null || items.isEmpty()) { throw new IllegalArgumentException(订单必须包含商品); } return new Order(OrderId.generate(), userId, items); } // 核心业务行为提交订单 public void placeOrder(InventoryService inventoryService) { // 1. 校验库存调用外部服务返回结果 InventoryCheckResult checkResult inventoryService.checkAndReserve(items); if (!checkResult.isSufficient()) { throw new InsufficientInventoryException(checkResult.getInsufficientItems()); } // 2. 扣减库存异步最终一致 inventoryService.deductInventoryAsync(checkResult.getReservationId()); // 3. 更新自身状态 this.status OrderStatus.CONFIRMED; } // 状态变更必须通过明确方法禁止直接赋值 public void markAsPaid() { if (this.status ! OrderStatus.CONFIRMED) { throw new IllegalStateException(只有已确认订单才能支付); } this.status OrderStatus.PAID; } }关键决策解析OrderId、UserId用值对象封装不是String而是new OrderId(ORD-2024-001)。好处类型安全不会把userId当orderId用、可扩展未来加校验逻辑、语义清晰。OrderItem是值对象它没有独立生命周期只描述“某商品买了几件”相等性由skuIdquantity决定。所以用ListOrderItem而非ListOrderItemEntity。status用枚举而非String避免created/CREATED/order_created等混乱字符串且枚举可自带业务逻辑如OrderStatus.canBeCancelled()。placeOrder()方法不返回值不抛异常给上层它只做三件事——调用库存服务、更新自身状态、保证原子性。异常由调用方Application Service捕获并处理聚合根只负责“我能做到什么”。markAsPaid()加状态校验不是防御性编程而是把业务规则固化在代码里。下次有人想绕过状态机直接setPaid(true)编译就报错。实操心得聚合根方法命名必须是动宾结构placeOrder、cancelOrder、refundAmount且每个方法对应一个明确的业务动作。如果发现一个方法里有超过3个if分支或者同时操作多个聚合立刻停下来——这说明职责过重该拆了。3.3 第二步定义值对象——让“地址”“商品项”自己说话// OrderItem.java - 值对象 public final class OrderItem { private final SkuId skuId; // 值对象非String private final int quantity; private final BigDecimal unitPrice; // 精确价格非double public OrderItem(SkuId skuId, int quantity, BigDecimal unitPrice) { if (quantity 0) { throw new IllegalArgumentException(数量必须大于0); } this.skuId skuId; this.quantity quantity; this.unitPrice unitPrice; } // 值对象必须重写equals/hashCode基于属性计算 Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; OrderItem orderItem (OrderItem) o; return quantity orderItem.quantity skuId.equals(orderItem.skuId) unitPrice.equals(orderItem.unitPrice); } Override public int hashCode() { return Objects.hash(skuId, quantity, unitPrice); } // 只读访问器无setter public SkuId getSkuId() { return skuId; } public int getQuantity() { return quantity; } public BigDecimal getUnitPrice() { return unitPrice; } } // SkuId.java - 值对象嵌套 public final class SkuId { private final String value; private SkuId(String value) { if (value null || value.trim().isEmpty()) { throw new IllegalArgumentException(SkuId不能为空); } this.value value.trim(); } public static SkuId of(String value) { return new SkuId(value); } public String getValue() { return value; } Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; SkuId skuId (SkuId) o; return value.equals(skuId.value); } Override public int hashCode() { return value.hashCode(); } }为什么不用Lombok因为Value会生成final字段但不校验构造参数而业务规则如quantity0必须在构造时强制执行。值对象的核心价值是让“什么是相等的”这件事不再依赖开发者记忆而是由代码本身保证。注意值对象不是越多越好。初期只封装那些业务上需要独立校验、比较、传递的概念。比如“订单总金额”可以是BigDecimal不必封装成OrderTotal类除非未来要加汇率转换、分账计算等复杂逻辑。3.4 第三步定义领域服务——当逻辑跨聚合时谁来指挥// InventoryService.java - 领域服务接口非Spring Bean public interface InventoryService { /** * 检查并预留库存强一致 * return 预留结果含预留ID用于后续扣减 */ InventoryCheckResult checkAndReserve(ListOrderItem items); /** * 异步扣减库存最终一致 * param reservationId 预留ID确保幂等 */ void deductInventoryAsync(String reservationId); } // InventoryServiceImpl.java - 具体实现可注入DAO Component public class InventoryServiceImpl implements InventoryService { private final InventoryRepository inventoryRepository; private final MessagePublisher messagePublisher; // 发送扣减事件 public InventoryServiceImpl(InventoryRepository inventoryRepository, MessagePublisher messagePublisher) { this.inventoryRepository inventoryRepository; this.messagePublisher messagePublisher; } Override Transactional // 本地事务保证检查与预留原子性 public InventoryCheckResult checkAndReserve(ListOrderItem items) { ListInventoryCheckResult.ItemResult results new ArrayList(); boolean sufficient true; for (OrderItem item : items) { Inventory inventory inventoryRepository.findBySku(item.getSkuId()); if (inventory.getAvailableQuantity() item.getQuantity()) { sufficient false; results.add(new InventoryCheckResult.ItemResult( item.getSkuId(), inventory.getAvailableQuantity(), item.getQuantity() )); } } if (sufficient) { // 预留库存available - quantity, reserved quantity items.forEach(item - inventoryRepository.reserve(item.getSkuId(), item.getQuantity()) ); return new InventoryCheckResult(true, Collections.emptyList(), UUID.randomUUID().toString()); } else { return new InventoryCheckResult(false, results, null); } } Override public void deductInventoryAsync(String reservationId) { // 发送消息由库存上下文的消费者处理扣减 messagePublisher.publish(new InventoryDeductCommand(reservationId)); } }关键点InventoryService是接口不是具体类它定义了“库存领域”对外提供的能力契约具体实现由库存上下文负责。订单聚合只依赖接口不关心库存如何存储、如何扣减。checkAndReserve()用本地事务确保“检查预留”原子性避免超卖。但注意它不扣减真实库存只做预留reserved字段这是应对高并发的常见模式。deductInventoryAsync()发消息订单聚合不直接调用库存DAO而是发领域事件。这样即使库存服务暂时不可用订单仍可创建后续重试即可——这是最终一致性的实践。实操心得领域服务方法名必须体现业务意图而非技术动作。“checkAndReserve”比“checkInventory”好“deductInventoryAsync”比“updateInventory”好。前者告诉调用者“我能做什么”后者只说“我做了什么”。3.5 第四步应用服务层——连接UI与领域不掺杂业务逻辑// OrderApplicationService.java - 应用服务 Service public class OrderApplicationService { private final OrderRepository orderRepository; private final InventoryService inventoryService; public OrderApplicationService(OrderRepository orderRepository, InventoryService inventoryService) { this.orderRepository orderRepository; this.inventoryService inventoryService; } Transactional public OrderId placeOrder(UserId userId, ListOrderItem items) { try { // 1. 创建聚合根 Order order Order.create(userId, items); // 2. 执行业务行为 order.placeOrder(inventoryService); // 3. 持久化 orderRepository.save(order); // 4. 发布领域事件供其他上下文消费 eventPublisher.publish(new OrderPlacedEvent(order.getId(), order.getUserId())); return order.getId(); } catch (InsufficientInventoryException e) { // 转换为应用层异常便于API返回友好提示 throw new BusinessValidationException(库存不足 e.getInsufficientItems()); } catch (Exception e) { // 记录完整上下文日志便于排查 log.error(下单失败userId{}, items{}, userId, items, e); throw new SystemException(下单失败请稍后重试, e); } } }应用服务的唯一职责编排领域对象处理事务边界转换异常发布事件。它不写if-else业务规则不计算价格不校验库存——那些都在Order或InventoryService里。这样做的好处单元测试极简只需mock Order和InventoryService验证placeOrder()是否被调用替换实现容易未来用Saga模式替代本地事务只需改InventoryService实现应用服务代码零修改权限控制清晰在应用服务入口加PreAuthorize(hasRole(USER))比在每个领域方法里加安全注解干净得多。提示应用服务方法参数必须是领域对象UserId、OrderItem而非DTO或Map。DTO转换应在Controller层完成。这样保证领域层完全纯净不沾染HTTP协议细节。4. 避坑指南那些没人告诉你但会让你加班到凌晨的DDD陷阱4.1 “贫血模型”陷阱把Entity写成getter/setter集合等于没用DDD现象团队定义了Order类但里面只有private字段public getter/setter所有业务逻辑放在OrderService里比如// ❌ 反模式贫血模型 public class Order { private String id; private String userId; private ListOrderItem items; // ... 一堆getter/setter } Service public class OrderService { public void placeOrder(Order order) { // 校验逻辑全在这里 if (order.getItems().size() 0) { ... } // 状态变更也在这里 order.setStatus(CONFIRMED); // 甚至调用DAO inventoryDao.deduct(...); } }问题业务规则泄露校验逻辑散落在Service里Order类只是数据容器无法表达“订单是什么”状态不一致风险Order.setStatus(CONFIRMED)后如果库存扣减失败Order状态已变但库存未扣数据不一致复用困难其他场景如后台强制下单想复用校验逻辑只能复制粘贴或引入Service依赖破坏封装。破解方法把校验、状态变更、聚合内协调逻辑全部收回到聚合根内部。Order.create()做基础校验placeOrder()做核心流程markAsPaid()做状态迁移约束。Service只负责“调用它”不“替它做事”。实操心得每周代码审查时我必问“这个if判断能不能移到Order类里” 如果答案是“能”立刻重构。三个月后团队自然养成习惯看到new Order()第一反应是“它该有哪些校验”。4.2 “过度设计”陷阱为不存在的扩展性提前写10个抽象层现象刚接到“用户下单”需求就开始设计抽象OrderStrategy接口准备支持“普通下单”“预售下单”“拼团下单”创建OrderFactory工厂未来可切换不同创建策略定义OrderEventPublisher接口预留消息中间件替换能力甚至为OrderItem预埋了“促销价”“会员价”“渠道价”字段。结果第一版上线花了3周其中2周在写永远用不到的扩展点真正需求变更时比如加个“赠品”字段反而要改5个抽象类和3个配置文件。DDD的奥义是恰如其分的抽象不是“所有可能都要覆盖”。Eric Evans在《领域驱动设计》里明确说“不要为未来的可能性设计要为当前最痛的业务问题设计。”破解方法YAGNI原则You Arent Gonna Need It。只实现当前需求明确需要的逻辑。比如现在只有普通下单Order.create()就足够下周要加赠品直接在OrderItem里加giftFlag字段或新增GiftItem值对象下个月要支持预售那时再提取OrderStrategy用简单if-else过渡比提前设计更可靠。注意抽象的价值在于降低修改成本不是增加代码行数。如果加一个字段要改8个类说明抽象错了如果加一个字段只改Order和OrderItem说明抽象刚好。4.3 “上下文混淆”陷阱把“技术模块”当成“业务上下文”现象系统按技术分层划分为user-service、order-service、inventory-service每个服务对应一个微服务。但业务上user-service要查订单列表需join order表order-service要调用user-service获取用户等级影响运费计算inventory-service要监听order-service的事件更新库存。结果服务间循环调用部署牵一发而动全身一个服务升级全站停服。根本原因技术模块划分 ≠ 业务上下文划分。user-service里混入了“用户资料管理”用户上下文和“用户订单查询”订单上下文两种语义。破解方法按业务能力而非技术职能划分上下文。例如用户上下文只管用户注册、登录、资料、等级、积分提供UserId→UserProfile的查询API订单上下文管订单创建、状态流转、售后需要用户信息时通过UserId调用用户上下文API或消费UserUpdatedEvent事件同步必要字段如nickname库存上下文只管库存扣减、预警、调拨对订单只消费OrderPlacedEvent不反向调用订单服务。实操心得画上下文映射图时箭头方向代表“谁依赖谁”但更重要的是标注集成模式实时API调用强一致慎用领域事件最终一致推荐数据库共享仅限只读报表严禁写文件同步离线批量如日终对账。我们曾用一张A3纸画清所有上下文关系贴在团队墙上新人入职第一件事就是看懂这张图。4.4 “术语不统一”陷阱同一个词在不同地方意思完全不同现象前端传参叫order_status值为created/paid数据库字段叫status值为1/2/3运营文档写“已支付”客服系统叫“付款成功”财务系统叫“收款确认”代码里OrderStatus枚举用UPPER_CASE但DTO用camelCaseMapper还要手动转换。结果联调时发现“前端显示已支付数据库却是2日志里打印的是PAID”排查2小时才发现是枚举序列化配置漏了JsonFormat。DDD的解决方案在每个限界上下文内定义统一的通用语言Ubiquitous Language。这不是翻译工作而是业务与开发共同约定这个上下文里“订单已支付”严格对应OrderStatus.PAID所有API、数据库字段、日志、文档都用这个词外部系统传来的数据必须在适配器层如Controller转换为本上下文语言。破解方法建立上下文词汇表。例如订单上下文词汇表业务术语代码表示说明订单Order聚合根生命周期由创建到关闭已确认OrderStatus.CONFIRMED库存已预留等待支付已支付OrderStatus.PAID支付成功触发履约赠品GiftItem值对象与主商品绑定无独立库存提示词汇表不是文档而是活代码。我们把词汇表做成Enum每个枚举值带Documented注释CI流程强制检查DTO字段名是否匹配词汇表。不匹配编译失败。4.5 “测试失焦”陷阱写了一堆Mock测试却没覆盖核心业务规则现象单元测试覆盖率90%但全是mock OrderRepository验证save()被调用一次mock InventoryService验证checkAndReserve()返回true用DataJpaTest测JPA映射是否正确。结果线上出现“库存扣减失败但订单状态已变CONFIRMED”因为测试没覆盖placeOrder()里库存检查通过但扣减失败的分支。DDD测试的核心验证业务规则是否被严格执行。不是测“代码能不能跑”而是测“业务能不能正确发生”。破解方法用Given-When-Then写场景化测试。例如Test void should_throw_exception_when_inventory_insufficient() { // Given: 有1件库存用户要买2件 InventoryService mockInventory mock(InventoryService.class); when(mockInventory.checkAndReserve(any())) .thenReturn(new InventoryCheckResult(false, List.of(new ItemResult(SkuId.of(SKU001), 1, 2)), null)); // When: 用户下单 Order order Order.create(UserId.of(U001), List.of(new OrderItem(SkuId.of(SKU001), 2, BigDecimal.TEN))); // Then: 必须抛出业务异常 assertThatThrownBy(() - order.placeOrder(mockInventory)) .isInstanceOf(InsufficientInventoryException.class) .hasMessageContaining(SKU001); }这种测试不依赖数据库、不启动Spring直接验证聚合根行为描述业务场景库存不足时下单失败失败时立刻知道哪条规则没生效。实操心得我们规定每个聚合根的核心方法create、placeOrder、cancel必须有3个以上场景测试正常流、边界流如数量为0、异常流如库存不足。少一个CR不通过。5. DDD落地的最小可行路径——从今天下午就能开始的5个动作5.1 动作一用“业务术语清单”代替“需求文档”别再写“用户点击提交按钮系统校验库存扣减库存创建订单记录”。改成业务术语业务规则触发条件后置动作订单创建必须至少包含1个商品用户余额充足库存可用用户点击“立即下单”生成订单号预留库存发送订单创建通知库存预留预留量 ≤ 可用库存预留后可用库存 可用库存 - 预留量订单创建成功更新库存表reserved字段记录预留日志订单确认预留成功即视为订单确认库存预留返回success订单状态变更为CONFIRMED发布OrderConfirmedEvent这份清单就是你的DDD起点。它不谈技术只谈业务且每个词订单、库存、预留都在后续代码中一一对应。5.2 动作二给现有代码加一道“领域校验门”找一个你最常改的Service方法比如updateUserProfile()。现在把它拆成两步在Controller层把DTO转成领域对象如UserProfile profile UserProfile.from(dto)在Service里调用profile.updateEmail(newEmail)而不是user.setMail(newEmail)。UserProfile.updateEmail()内部做校验邮箱格式、是否已存在、是否需验证码。这样下次改邮箱逻辑只改UserProfile类不碰Service。效果一天就能完成但团队立刻感受到“业务规则集中管理”的好处。我们第一个月就用这招把用户中心的12个校验点收归UserProfile类Bug率降了40%。5.3 动作三画一张“最简上下文图”拿出白板只画3个圆圈左边用户上下文管注册、登录、资料中间订单上下文管下单、状态、售后右边库存上下文管库存、预警、调拨。用箭头标出依赖订单→用户查等级订单→库存预留/扣减。其他所有模块暂时归入这三个之一。别纠结“营销该放哪”先跑通主链路。5.4 动作四强制值对象化一个高频概念选一个天天用的字符串比如“手机号”。把它变成public final class PhoneNumber { private final String value; private PhoneNumber(String value) { if