DDD 不是万灵药但它提供的限界上下文Bounded Context和聚合Aggregate这两个工具能强制你把业务边界画清楚、把代码物理隔离。今天把它们掰碎了讲——用你能复用的代码骨架不是用 Domain Expert、Ubiquitous Language 这种让人犯困的词汇。一、先搞懂 DDD 在解决什么问题很多人对 DDD 有个误区以为它是面向对象设计的升级版。错。DDD 的核心价值是战略设计——它告诉你怎么切分业务边界怎么让多个团队同时开发互不干扰。战术设计聚合、实体、值对象这些只是落地的工具。来看个真实案例的对比维度传统三层架构DDD 限界上下文架构包结构controller/service/dao/entity按层划分order/inventory/marketing/按业务划分一个新人熟悉业务要多久看完整套代码2 周起进入对应包3 天可上手代码冲突率高所有人改 Service 层低团队各管一摊业务变更影响面难评估一个改了全都得测边界清晰改 A 包基本不影响 B 包微服务化难度高要先重构再做拆分低限界上下文天然是微服务候选一句话总结DDD 的价值不在于代码变优雅而在于业务边界变清晰。你不需要信仰它但你需要它来处理复杂业务——尤其是那种已经活了 3 年以上、每年还在加功能的老系统。我们今天的目标学会 Event Storming 怎么开 学会把限界上下文映射到代码结构 学会写一个完整的聚合根。这三条覆盖了 80% 的实战场景。二、Event Storming20 分钟画一张图搞定业务梳理很多人学 DDD 卡在第一步怎么从一堆需求文档里提炼出限界上下文。答案是别看文档直接拉业务方开会——这叫 Event Storming 工作坊。完整工作坊要开 2~3 天但咱们做精简版也能用。下面我用一个电商下单业务给你演示给你一个能立刻拿去开会的脚本。2.1 准备工具一面大白板或一块空地的大桌子三色便利贴橙、蓝、黄 黑色马克笔一个懂业务的产品经理 一个懂技术的你 任何能拍板的产品总监2.2 五步工作坊Step 1枚举领域事件橙色便利贴让产品经理把业务里发生过什么按时间顺序写出来。每张便利贴 一个事件用过去时态。比如[用户提交订单] [订单已创建] [库存已锁定] [优惠券已使用] [订单已支付] [库存已扣减] [订单已发货] [订单已完成]Step 2找触发命令蓝色便利贴问产品经理谁/什么导致了这件事。命令总是以动词开头比如[下单] → 触发 [订单已创建] [锁定库存] → 触发 [库存已锁定] [应用优惠券] → 触发 [优惠券已使用]Step 3找聚合根黄色便利贴问产生这个事件需要哪些数据放在一起改——这就是聚合根的候选。把命令贴到对应聚合的边上。聚合订单聚合订单订单项 命令下单、取消订单、支付 事件订单已创建、订单已支付 聚合库存聚合库存批次锁库记录 命令锁定库存、释放库存 事件库存已锁定、库存已扣减 聚合优惠券聚合优惠券记录领取记录使用记录 命令领取优惠券、应用优惠券 事件优惠券已领取、优惠券已使用Step 4找限界上下文用虚线圈起来把所有聚合按业务上是不是一起变分组。比如订单聚合和订单项聚合必须一起改就放在一个限界上下文里。库存聚合单独一个上下文。优惠券独立一个上下文。然后识别跨上下文的事件订单上下文 --[库存已锁定事件]-- 库存上下文 订单上下文 --[优惠券已使用事件]-- 优惠券上下文Step 5识别上下文映射关系问这两个上下文怎么打交道。如果库存上下文需要知道订单状态是库存调用订单的接口还是订单发布事件让库存订阅前者是防腐层ACL后者是事件驱动Pub/Sub。我们这套订单→库存是库存被动的设计应该是后者——订单发订单已创建事件库存订阅、消费、锁库。这就是最终一致性的雏形。2.3 工作坊的真实产出20 分钟能产出下图我手画给你看┌─────────────────┐ ┌─────────────────┐ │ 订单上下文 │ │ 库存上下文 │ │ │ │ │ │ 聚合: Order │ │ 聚合: Inventory │ │ 命令: 下单 │ │ 命令: 锁库 │ │ 事件: 已创建 ────────── │ 事件: 已锁定 │ │ 命令: 支付 │ │ 命令: 扣减 │ │ 事件: 已支付 ────────── │ 事件: 已扣减 │ └─────────────────┘ └─────────────────┘ │ │ 发布事件 ▼ ┌─────────────────┐ │ 优惠券上下文 │ │ 聚合: Coupon │ │ 命令: 应用 │ │ 事件: 已使用 │ └─────────────────┘这就是你的 DDD 战略设计的图。这张图画完限界上下文的边界就明确了——它就是你将来 Java 包的根目录。三、战术设计聚合根的代码骨架怎么写战略设计画完图就要落到代码。这一步才是大多数文章一笔带过、让你真正卡住的环节。我直接给你一套基于 Spring Boot 3 JPA 的完整模板。3.1 包结构按限界上下文划分不按层划分这是 DDD 落地最反直觉的一点不要用controller/service/dao/entity这种分层目录。要用业务包com.example.ecommerce ├── order/ # 订单限界上下文 │ ├── interfaces/ # 对外接口层Controller、DTO │ ├── application/ # 应用服务层编排业务流程 │ ├── domain/ # 领域层聚合根、实体、值对象、领域事件 │ └── infrastructure/ # 基础设施层JPA仓储实现、MQ发送 ├── inventory/ └── coupon/每个限界上下文是一个独立的 maven module 或者 Gradle 子项目编译期就能强制边界——订单上下文想调库存的实体直接编译报错。这才是 DDD 的工程价值。3.2 聚合根充血模型不是 OO 装样子聚合根要承担业务逻辑不是贫血模型那种只有 getter/setter 的 POJO。下面这个Order聚合根把下单的业务规则全部封装进了领域/** * 订单聚合根 * 环境Spring Boot 3.3 JPA Lombok * 依赖spring-boot-starter-data-jpa * * 聚合根的核心约束 * 1. 所有修改必须经过聚合根的方法不允许外部直接 new OrderItem * 2. 聚合内部一致性由聚合根维护 * 3. 跨聚合调用通过领域事件解耦 */ Entity Table(name t_order) Getter NoArgsConstructor(access AccessLevel.PROTECTED) // 强制走工厂方法 public class Order { ​ Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ​ /** 订单编号业务标识 */ private String orderNo; ​ /** 用户ID */ private Long userId; ​ /** 订单金额值对象类型 */ Embedded private Money totalAmount; ​ /** 订单状态 */ Enumerated(EnumType.STRING) private OrderStatus status; ​ /** 订单项集合聚合内实体 */ OneToMany(mappedBy order, cascade CascadeType.ALL, orphanRemoval true, fetch FetchType.LAZY) private ListOrderItem items new ArrayList(); ​ /** 版本号乐观锁 */ Version private Long version; ​ // 领域行为 ​ /** * 工厂方法创建订单 * 这里封装下单的所有业务规则 */ public static Order create(Long userId, ListOrderItem items) { // 业务规则1至少要有一个订单项 if (items null || items.isEmpty()) { throw new BusinessException(ORDER_EMPTY, 订单必须包含至少一个商品); } // 业务规则2单个订单商品数量不能超过 100 if (items.stream().anyMatch(i - i.getQuantity() 100)) { throw new BusinessException(ORDER_ITEM_QTY_OVERFLOW, 单个商品数量不能超过100); } // 业务规则3订单金额必须为正 Money total items.stream() .map(OrderItem::getSubTotal) .reduce(Money.ZERO, Money::add); if (total.isNegativeOrZero()) { throw new BusinessException(ORDER_AMOUNT_INVALID, 订单金额必须大于零); } ​ Order order new Order(); order.orderNo generateOrderNo(); order.userId userId; order.totalAmount total; order.status OrderStatus.CREATED; ​ // 关联聚合内实体 items.forEach(item - item.attachTo(order)); order.items.addAll(items); ​ // 发布领域事件 OrderEvents.orderCreated(order); ​ return order; } ​ /** * 领域行为支付订单 */ public void pay(PaymentInfo paymentInfo) { if (this.status ! OrderStatus.CREATED) { throw new BusinessException(ORDER_STATUS_INVALID, 只有已创建状态的订单可以支付当前状态 this.status); } // 业务规则支付金额必须等于订单金额 if (!paymentInfo.getAmount().equals(this.totalAmount)) { throw new BusinessException(PAYMENT_AMOUNT_MISMATCH, 支付金额与订单金额不一致); } this.status OrderStatus.PAID; ​ // 发布领域事件库存和优惠券通过订阅这个事件来做后续动作 OrderEvents.orderPaid(this); } ​ /** * 领域行为取消订单 */ public void cancel(String reason) { if (this.status OrderStatus.SHIPPED || this.status OrderStatus.COMPLETED) { throw new BusinessException(ORDER_CANCEL_FORBIDDEN, 已发货或已完成的订单不能取消); } this.status OrderStatus.CANCELLED; ​ // 发布领域事件触发库存释放、优惠券退还等 OrderEvents.orderCancelled(this, reason); } }注意几个关键点构造器是 protected强制外部用Order.create()工厂方法不能直接new Order()。这是保护业务规则的第一道关。业务规则在聚合根里金额校验、状态流转校验都在领域里不在 Service 层。领域事件通过静态方法发布OrderEvents.orderCreated(order)是把领域事件暂存到ThreadLocal事务提交后由 Spring 的ApplicationEventPublisher统一发送这是为了让事件在事务一致性之后发布。3.3 值对象用 Embeddable 表达业务概念Money这个值对象看似简单但它承载了金额这个业务概念避免了散落的BigDecimal amount字段/** * 金额值对象不可变 * 业务概念所有金额相关的计算都通过这个类确保不会出现负金额、币种不一致等问题 */ Embeddable Getter EqualsAndHashCode public class Money implements Serializable { ​ public static final Money ZERO new Money(BigDecimal.ZERO, Currency.RMB); ​ Column(name amount, precision 12, scale 2, nullable false) private BigDecimal amount; ​ Column(name currency, length 8, nullable false) Enumerated(EnumType.STRING) private Currency currency; ​ protected Money() {} // JPA需要 ​ public Money(BigDecimal amount, Currency currency) { if (amount null || currency null) { throw new IllegalArgumentException(金额和币种不能为空); } this.amount amount; this.currency currency; } ​ public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException(币种不一致无法相加); } return new Money(this.amount.add(other.amount), this.currency); } ​ public boolean isNegativeOrZero() { return this.amount.compareTo(BigDecimal.ZERO) 0; } ​ /** 用于 JPA 的字段访问 */ public BigDecimal getAmount() { return amount; } }值对象是不可变的修改一律返回新对象。这跟 String、BigDecimal 的语义一致能避免线程安全问题也更容易推理。3.4 仓储接口领域层定义、实现层做聚合根不依赖任何持久化框架。仓储接口在domain包下实现在infrastructure包下。这样以后想从 JPA 换 MyBatis-Plus 只需要换一个实现业务代码一行不动/** * 订单仓储接口领域层定义 * 只暴露聚合根级别的操作不暴露 EntityManager 这种基础设施 */ public interface OrderRepository { Order findById(Long id); Order findByOrderNo(String orderNo); void save(Order order); } ​ /** * 订单仓储 JPA 实现基础设施层 */ Repository RequiredArgsConstructor public class OrderRepositoryImpl implements OrderRepository { ​ private final OrderJpaRepository jpaRepository; ​ Override public Order findById(Long id) { return jpaRepository.findById(id) .orElseThrow(() - new BusinessException(ORDER_NOT_FOUND, 订单不存在)); } ​ Override public Order findByOrderNo(String orderNo) { return jpaRepository.findByOrderNo(orderNo) .orElseThrow(() - new BusinessException(ORDER_NOT_FOUND, 订单不存在)); } ​ Override Transactional public void save(Order order) { jpaRepository.save(order); // JPA自己判断新增还是更新 } }3.5 应用服务编排跨聚合业务应用服务是用例的实现层。它编排多个聚合来完成一个完整的业务流程但不包含业务规则——业务规则都在聚合里/** * 下单应用服务 * 职责编排订单聚合 库存聚合 优惠券聚合完成下单流程 */ Service RequiredArgsConstructor Slf4j public class PlaceOrderAppService { ​ private final OrderRepository orderRepository; private final InventoryRepository inventoryRepository; // 库存上下文 private final CouponRepository couponRepository; // 优惠券上下文 private final DomainEventPublisher eventPublisher; ​ /** * 下单用例 */ public PlaceOrderResult execute(PlaceOrderCommand command) { log.info(开始下单, userId{}, items{}, command.getUserId(), command.getItems()); ​ // 1. 检查并锁定库存跨上下文调用 → 通过防腐层或服务调用 command.getItems().forEach(item - { inventoryRepository.lock(item.getSkuId(), item.getQuantity()); }); ​ // 2. 应用优惠券如有 Order order; try { ListOrderItem orderItems buildOrderItems(command); Money discount Money.ZERO; if (command.getCouponId() ! null) { discount couponRepository.apply(command.getCouponId(), command.getUserId(), orderItems); } ​ // 3. 创建订单聚合业务规则在 Order.create() 里 order Order.create(command.getUserId(), orderItems); order.applyDiscount(discount); ​ // 4. 保存 orderRepository.save(order); ​ log.info(下单成功, orderNo{}, order.getOrderNo()); return PlaceOrderResult.success(order.getOrderNo()); } catch (BusinessException e) { log.warn(下单失败, {}, e.getMessage()); // 回滚库存补偿事务 command.getItems().forEach(item - inventoryRepository.release(item.getSkuId(), item.getQuantity())); throw e; } } }注意几个细节应用服务只编排、不计算金额计算、状态流转全在聚合根里。跨上下文调用通过仓储或领域服务不直接调对方上下文的 Service避免上下文穿透。异常补偿分布式场景下最终还是要靠事件驱动的最终一致性。这里只是演示单服务内的回滚。四、领域事件让上下文解耦的润滑剂前面的代码里出现的OrderEvents.orderCreated(order)是怎么实现的我直接给你基于 Spring 的事件机制 ThreadLocal 的完整实现保证事件在事务提交后发送/** * 领域事件收集器基于 ThreadLocal * 目的保证领域事件只在事务成功提交后才发布 */ public class DomainEventCollector { private static final ThreadLocalListDomainEvent EVENTS new ThreadLocal(); ​ public static void collect(DomainEvent event) { ListDomainEvent events EVENTS.get(); if (events null) { events new ArrayList(); EVENTS.set(events); } events.add(event); } ​ public static ListDomainEvent drainAndClear() { ListDomainEvent events EVENTS.get(); EVENTS.remove(); return events null ? List.of() : events; } } ​ /** * 订单领域事件集合静态门面让聚合根内方便调用 */ public final class OrderEvents { private OrderEvents() {} ​ public static void orderCreated(Order order) { DomainEventCollector.collect(new OrderCreatedEvent(order)); } ​ public static void orderPaid(Order order) { DomainEventCollector.collect(new OrderPaidEvent(order)); } ​ public static void orderCancelled(Order order, String reason) { DomainEventCollector.collect(new OrderCancelledEvent(order.getId(), reason)); } } ​ /** * 订单已创建事件 */ public record OrderCreatedEvent(Order order) implements DomainEvent {} ​ /** * Spring 事务同步器在事务提交后批量发布事件 */ Component RequiredArgsConstructor public class DomainEventTransactionSync implements TransactionSynchronization { private final ApplicationEventPublisher publisher; private final ListDomainEvent events; ​ public static void register(ApplicationEventPublisher publisher, ListDomainEvent events) { DomainEventTransactionSync sync new DomainEventTransactionSync(publisher, events); TransactionSynchronizationManager.registerSynchronization(sync); } ​ Override public void afterCommit() { events.forEach(publisher::publishEvent); } } ​ /** * 使用在应用服务的方法结束后触发 */ Transactional public PlaceOrderResult execute(PlaceOrderCommand command) { // ...业务逻辑Order.create() 会触发 DomainEventCollector.collect() orderRepository.save(order); ​ // 注册事务提交后的事件发布 DomainEventTransactionSync.register( eventPublisher, DomainEventCollector.drainAndClear() ); return PlaceOrderResult.success(order.getOrderNo()); }这样设计的好处事务一致性领域事件只在事务成功提交后才发到 MQ/EventBus避免订单没创建但库存已锁定。解耦库存上下文订阅OrderCreatedEvent优惠券上下文订阅OrderPaidEvent完全不知道订单聚合的内部结构。可回放所有事件落到 EventStore 后可以做时间旅行调试、做对账、做事件溯源Event Sourcing。五、建议DDD 落地的三条经验一先战略后战术不要一上来就钻 UML我见过太多团队学 DDD 卡在聚合根怎么命名上纠结两周但限界上下文根本就没分对。先用 Event Storming 工作坊把限界上下文画出来战术设计水到渠成。策略不对再精妙的聚合设计也是空中楼阁。判断限界上下文分对了的标准每个上下文能用一句话说清楚它的核心职责。订单上下文管理订单的生命周期库存上下文管理商品库存的数量优惠券上下文管理优惠券的发放与核销如果你划出来的上下文要三句话才能描述清楚要么切得太细了要么没切干净。二上下文之间的通信协议提前定好限界上下文之间的调用方式决定了你的系统是分布式事务还是最终一致性。我见过团队限界上下文画得很好但跨上下文依然同步调用Transactional嵌套最后分布式事务回滚的噩梦全来了。经验法则强一致场景同限界上下文内用Transactional搞定一个库。弱一致场景跨限界上下文必须用事件驱动 幂等消费不接受同步调用。跨上下文同步调用用OpenFeign或Dubbo走 RPC配上Sentinel做熔断 监控但不参与本地上下文事务。三别在初期就追求完美的 DDDDDD 的很多设计会增加代码量聚合根、仓储接口、值对象、领域事件、应用服务。如果你的业务还处在快速试错阶段先把核心业务的聚合根和领域事件做出来边边角角可以先用 Service 层搞定。但有一条底线核心业务流订单、支付、库存必须用 DDD因为这些是高频变更区域是出生产事故的高发地。在这里偷的懒会在未来的每一次需求评审中加倍偿还。判断标准如果一个业务逻辑被改过的次数≥3 次以上还经常出 bug就该 DDD 重构了。下篇预告DDD 重构后的项目怎么在线上撑住高并发下一个 Day84我们聊Arthas 诊断实战——线上 CPU 飙到 100%、死锁、内存泄漏这三种最让 Java 程序员秃头的问题怎么用 Arthas 三板斧在 5 分钟内定位。如果你想把这套 DDD 模板直接搬到项目里可以参考这套结构自己搭一遍——项目骨架约 200 行代码跑通下单→事件发布→消费完整链路。代码层面没有魔法都是 Spring Boot JPA 的标准 API重点是结构上把业务边界划清楚。