简介:本资源是一套面向Java开发者与架构师的DDD(领域驱动设计)实战代码案例,聚焦复杂业务系统建模与分层架构落地,帮助初学者理解核心概念、助力中高级工程师提升领域建模能力。压缩包共343个文件,含142个Java源码(覆盖实体、值对象、聚合根、领域服务、仓储接口及实现)、119个XML配置(Spring/MyBatis集成)、28个HTML文档(含patterns-reference.apt、architecture.apt等设计说明),辅以CSS样式、JSP视图及少量测试资源,整体仅687KB,轻量易读。已有7978人学习下载,体现其在实践DDD落地中的广泛参考价值。读者可直接运行并剖析完整分层结构:从领域模型POJO定义、聚合一致性边界控制,到仓储与ORM对接、事件注册机制(handlingEventRegistration.apt),再到项目整体架构演进说明(roadmap.apt)与模式对照指南,是少有的兼顾理论阐释与可编译代码的DDD学习范本。
1. DDD领域设计模式代码案例:不是照搬概念,而是让聚合根、值对象、仓储在真实业务里跑通
你手头有一份「DDD领域设计模式代码案例」资源,但打开后发现全是UML图和教科书式定义——聚合根不能被直接new、仓储接口要隔离实现、领域事件必须异步发布……这些话你背得滚瓜烂熟,可一写订单取消逻辑就卡在“该不该在Order聚合里调用PaymentService?”,一建用户注册流程就纠结“邮箱校验是值对象还是实体?”。这不是你理解力的问题,是绝大多数DDD入门资料缺了一块关键拼图:可运行、可调试、可修改的最小闭环代码实例。这份案例不讲“为什么DDD重要”,只聚焦“怎么让一个带库存扣减+优惠券核销+积分发放的下单动作,在Spring Boot里真正按DDD分层跑起来”。它适合正在做Java后端毕设/课设、需要交付可演示系统的同学,也适合刚接手遗留系统改造、想用DDD切出第一个干净模块的工程师——你不需要重构整个系统,只要能从“创建订单”这个用例开始,把领域层真正立住,后续就能复用这套结构。
2. 总体架构设计:四层结构与五个核心模块的职责边界
DDD不是堆砌术语,而是通过清晰的分层把业务复杂度锁进对应区域。这份案例采用经典四层架构(表现层、应用层、领域层、基础设施层),但做了关键裁剪:去掉冗余的DTO转换层,用Lombok+Record直连领域对象;基础设施层仅保留MySQL+Redis+RabbitMQ三种真实依赖,不引入Mock框架干扰调试路径。所有模块均基于Spring Boot 3.2+JDK 17构建,Maven坐标严格对齐Spring官方BOM,避免版本冲突导致的编译失败。
2.1 模块划分与包结构设计
项目采用Maven多模块结构,共5个子模块,命名直指职责:
domain-core:纯Java模块,无Spring依赖,含所有领域模型(Aggregate、Entity、ValueObject)、领域服务接口、领域事件定义;application-service:Spring Boot模块,实现Application Service,协调多个领域对象完成用例,不包含任何业务规则;infrastructure-persistence:JPA实现,含JpaRepository定制(如OrderRepositoryImpl重写findByUserIdAndStatus)、数据库表结构SQL脚本;infrastructure-messaging:RabbitMQ消息生产者/消费者,处理OrderCreatedEvent等事件;web-api:Spring MVC控制器,仅做参数接收与响应封装,禁止出现if-else业务判断。
提示:
domain-core模块的pom.xml中显式排除spring-boot-starter依赖,这是验证领域层是否真正解耦的关键检查点——编译通过且无Spring类引用,才算合格。
2.2 领域层核心实现:以Order聚合为例的完整闭环
订单是电商系统最典型的聚合根,案例中Order类严格遵循DDD约束:
- 构造函数私有,强制通过工厂方法
Order.create()创建; - 所有状态变更通过领域方法
confirm(),cancel(),ship()触发,内部校验业务规则(如“已发货订单不可取消”); - 关联的
OrderItem为实体,Money、OrderId为不可变值对象; - 聚合内不持有外部服务引用,状态变更时发布
OrderConfirmedEvent事件。
// domain-core/src/main/java/com/example/domain/order/Order.java public class Order { private final OrderId id; private OrderStatus status; private final List<OrderItem> items; private final Money totalAmount; // 私有构造,强制工厂创建 private Order(OrderId id, List<OrderItem> items) { this.id = id; this.items = new ArrayList<>(items); this.totalAmount = calculateTotal(); this.status = OrderStatus.CREATED; } public static Order create(OrderId id, List<OrderItem> items) { if (items == null || items.isEmpty()) { throw new IllegalArgumentException("订单项不能为空"); } return new Order(id, items); } // 领域方法:状态变更入口,内聚业务规则 public void confirm() { if (this.status != OrderStatus.CREATED) { throw new IllegalStateException("仅CREATED状态可确认"); } this.status = OrderStatus.CONFIRMED; // 发布领域事件,由应用层订阅处理 DomainEvents.publish(new OrderConfirmedEvent(this.id)); } private Money calculateTotal() { return Money.of(items.stream() .mapToLong(item -> item.getPrice().getAmount() * item.getQuantity()) .sum()); } }这段代码的关键设计点在于:confirm()方法不返回值、不抛出Checked Exception、不调用任何外部服务。它只做三件事——校验前置状态、变更自身属性、发布事件。后续的库存扣减、优惠券核销等操作,全部交给应用层监听OrderConfirmedEvent后调用对应领域服务完成。这种设计让单元测试变得极其简单:只需验证confirm()执行后status是否变为CONFIRMED,事件是否正确发布,无需启动数据库或消息队列。
2.3 应用层协调逻辑:用例驱动的事务边界控制
应用服务是DDD落地的“指挥官”,案例中OrderApplicationService类精准控制事务范围:
placeOrder()方法标注@Transactional,确保订单创建、库存预占、优惠券锁定在同一个数据库事务内;handleOrderConfirmedEvent()方法不加事务注解,因事件处理需最终一致性,失败后由消息队列重试;- 所有领域对象通过构造函数注入,杜绝
@Autowired导致的循环依赖风险。
// application-service/src/main/java/com/example/application/order/OrderApplicationService.java @Service public class OrderApplicationService { private final OrderRepository orderRepository; private final InventoryService inventoryService; // 领域服务接口,非具体实现 private final CouponService couponService; public OrderApplicationService(OrderRepository orderRepository, InventoryService inventoryService, CouponService couponService) { this.orderRepository = orderRepository; this.inventoryService = inventoryService; this.couponService = couponService; } @Transactional public OrderId placeOrder(PlaceOrderCommand command) { // 1. 创建聚合根(领域层) Order order = Order.create( OrderId.generate(), command.getItems().stream() .map(i -> OrderItem.create(i.getSkuId(), i.getQuantity())) .toList() ); // 2. 调用领域服务执行跨聚合操作(库存预占) inventoryService.reserveStock(order.getId(), order.getItems()); // 3. 调用领域服务执行跨聚合操作(优惠券锁定) couponService.lockCoupon(command.getCouponCode(), order.getTotalAmount()); // 4. 持久化聚合根(基础设施层) orderRepository.save(order); return order.getId(); } // 事件处理器:最终一致性保障 @EventListener public void handleOrderConfirmedEvent(OrderConfirmedEvent event) { // 异步发送通知、更新统计报表等 sendOrderConfirmationEmail(event.getOrderId()); } }这里的设计哲学是:应用层只做“编排”,不做“实现”。inventoryService.reserveStock()和couponService.lockCoupon()都是接口,其具体实现位于infrastructure-persistence模块中,通过Spring的@Primary或@Qualifier注入。这种分离让placeOrder()的单元测试只需Mock两个领域服务接口,即可覆盖90%以上的业务路径,无需连接真实数据库。
3. 关键模式落地:聚合根、值对象、仓储、领域事件的代码级实现
DDD的四大支柱不是抽象概念,而是可触摸的代码结构。本节逐个拆解案例中如何用Java语法落实这些模式,重点说明“为什么这样写”而非“是什么”。
3.1 聚合根:用构造约束+方法封装替代getter/setter
聚合根的核心价值是保护业务不变性。案例中Order类彻底禁用public setter,所有状态变更必须走领域方法。例如取消订单:
// domain-core/src/main/java/com/example/domain/order/Order.java public void cancel() { if (this.status == OrderStatus.CANCELLED) { return; // 幂等处理 } if (this.status == OrderStatus.SHIPPED) { throw new IllegalStateException("已发货订单不可取消"); } if (this.status == OrderStatus.CONFIRMED) { // 取消时需释放库存和优惠券 DomainEvents.publish(new OrderCancelledEvent(this.id)); } this.status = OrderStatus.CANCELLED; }关键设计点:
- 状态机校验:明确列出所有允许的状态转移路径,避免
status = CANCELLED这种裸赋值导致的非法状态; - 事件驱动补偿:
OrderCancelledEvent触发库存回滚、优惠券解锁,将副作用解耦到事件处理器; - 幂等性内置:首行
if (CANCELLED) return防止重复取消产生脏数据。
对比反模式:若用Lombok@Data生成setter,则任何地方都可order.setStatus(CANCELLED),领域规则瞬间崩塌。
3.2 值对象:用Record+equals/hashCode实现不可变性
值对象的核心是相等性由属性决定,且不可变。案例中Money类采用Java 14+ Record语法:
// domain-core/src/main/java/com/example/domain/common/Money.java public record Money(long amount, String currency) { public static Money of(long amount) { return new Money(amount, "CNY"); } public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("货币类型不匹配"); } return new Money(this.amount + other.amount, this.currency); } // Record自动实现equals/hashCode/toString,符合值对象语义 }为什么用Record不用Class?
- 不可变性保障:Record字段默认final,编译期阻止修改;
- 相等性语义正确:
new Money(100,"CNY").equals(new Money(100,"CNY"))返回true,符合业务直觉; - 序列化友好:Spring Boot 3.2默认支持Record JSON序列化,无需额外配置。
若用传统Class,需手动重写equals()、hashCode()、toString(),且易遗漏currency字段导致相等性错误。
3.3 仓储:JPA实现中的领域契约保持
仓储接口定义在domain-core,实现放在infrastructure-persistence,这是分层关键。OrderRepository接口仅声明领域所需操作:
// domain-core/src/main/java/com/example/domain/order/OrderRepository.java public interface OrderRepository { void save(Order order); Optional<Order> findById(OrderId id); List<Order> findByUserId(UserId userId); void delete(Order order); }OrderRepositoryImpl实现类使用JPA但不暴露JPA细节:
// infrastructure-persistence/src/main/java/com/example/infra/persistence/order/OrderRepositoryImpl.java @Repository public class OrderRepositoryImpl implements OrderRepository { private final JpaOrderRepository jpaOrderRepository; // Spring Data JPA仓库 public OrderRepositoryImpl(JpaOrderRepository jpaOrderRepository) { this.jpaOrderRepository = jpaOrderRepository; } @Override public void save(Order order) { // 将领域对象转为JPA实体,隐藏转换逻辑 jpaOrderRepository.save(OrderJpaEntity.from(order)); } @Override public Optional<Order> findById(OrderId id) { return jpaOrderRepository.findById(id.value()) .map(OrderJpaEntity::toDomain); // 转回领域对象 } @Override public List<Order> findByUserId(UserId userId) { return jpaOrderRepository.findByUserId(userId.value()) .stream() .map(OrderJpaEntity::toDomain) .toList(); } }关键设计点:
- 转换逻辑内聚:
OrderJpaEntity.from()和.toDomain()方法封装在JPA实体类中,避免应用层感知ORM细节; - 查询方法语义化:
findByUserId()返回List<Order>而非List<OrderJpaEntity>,保持领域层纯净; - 无自定义SQL污染:所有查询通过Spring Data JPA方法名推导(如
findByUserId),避免HQL/JPQL泄露到领域层。
3.4 领域事件:同步发布+异步消费的可靠性保障
领域事件是解耦跨聚合操作的桥梁。案例采用Spring Event机制,但做了关键加固:
// domain-core/src/main/java/com/example/domain/common/DomainEvents.java public class DomainEvents { private static final ApplicationEventPublisher publisher = ApplicationContextProvider.getApplicationContext(); public static void publish(Object event) { if (publisher != null) { publisher.publishEvent(event); } } }ApplicationContextProvider是静态工具类,解决领域层无法注入Spring Bean的问题。事件消费端使用@EventListener并配置重试:
// application-service/src/main/java/com/example/application/event/OrderEventListeners.java @Component public class OrderEventListeners { private final InventoryService inventoryService; public OrderEventListeners(InventoryService inventoryService) { this.inventoryService = inventoryService; } @EventListener @Retryable(value = {RuntimeException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) public void onOrderConfirmed(OrderConfirmedEvent event) { // 调用库存服务扣减实际库存 inventoryService.deductStock(event.getOrderId()); } @Recover public void recover(RuntimeException e, OrderConfirmedEvent event) { // 重试失败后告警,人工介入 log.error("订单确认事件处理失败,订单ID:{}", event.getOrderId(), e); } }这里用@Retryable替代手动while循环,@Recover提供失败兜底,确保即使库存服务临时不可用,事件也不会丢失。
4. 常见问题排查:五个高频翻车点及血泪解决方案
DDD落地最痛苦的不是学不会,而是调试时卡在某个看似微小的细节上。以下是我在多个项目中踩过的坑,按发生频率排序,每条都附带可复现现象和根治方案。
4.1 现象:单元测试报错“Could not resolve placeholder 'spring.profiles.active'”
原因:domain-core模块被错误添加了spring-boot-starter依赖,导致测试时加载Spring上下文,而application.properties未配置profile。
解决:检查domain-core/pom.xml,确认无任何Spring依赖。若需测试领域对象,用JUnit 5原生断言,禁止在domain-core中写@SpringBootTest。正确做法:
// domain-core/src/test/java/com/example/domain/order/OrderTest.java class OrderTest { @Test void should_throw_exception_when_cancel_shipped_order() { Order order = Order.create(OrderId.generate(), List.of()); order.ship(); // 先发货 assertThatThrownBy(() -> order.cancel()) // 再取消 .isInstanceOf(IllegalStateException.class) .hasMessage("已发货订单不可取消"); } }4.2 现象:OrderRepository.save()后数据库无数据,但日志显示“Hibernate: insert into…”
原因:应用层@Transactional未生效,常见于:① 方法被同一类内其他方法调用(Spring AOP代理失效);②@Transactional写在private方法上;③@EnableTransactionManagement未开启。
解决:
- 在
application-service模块的@SpringBootApplication类上添加@EnableTransactionManagement; - 确保
placeOrder()是public方法,且被Controller直接调用(勿在Service内调用自身方法); - 启动时检查日志是否有
Using DataSource [HikariDataSource],无则说明事务管理器未加载。
4.3 现象:OrderConfirmedEvent被触发两次,库存被重复扣减
原因:Spring Event默认同步执行,若事件监听器抛异常且未捕获,事务回滚后事件重发。更隐蔽的是:@EventListener方法被多次注册(如配置类重复扫描)。
解决:
- 在事件监听器上添加
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT),确保仅在事务提交后触发; - 检查
@ComponentScan路径,避免application-service和web-api模块重复扫描同一包; - 为事件添加唯一ID,消费端用Redis Set记录已处理ID,实现幂等。
4.4 现象:Money.add()计算结果错误,如100+200=30000
原因:Money类用int存储金额,未考虑小数精度,或amount单位混淆(如用分作单位却当元计算)。
解决:
- 强制约定单位:
Money的amount字段单位为“分”,100表示1元,200表示2元; - 禁止float/double:全部用
long,避免浮点误差; - 构造器校验:
Money.of(BigDecimal)方法中校验小数位数≤2,超限抛异常。
4.5 现象:OrderRepository.findById()返回null,但数据库明明有数据
原因:JPA实体类@Id字段名与数据库列名不一致,或@Column(name="order_id")未指定。
解决:
- 在
OrderJpaEntity类中,@Id字段必须与数据库主键列名完全一致(如数据库列是order_id,则字段名必须为orderId且加@Column(name="order_id")); - 启动时开启
spring.jpa.show-sql=true,观察生成的SQL是否为select * from order where order_id = ?,若为where id = ?则映射错误; - 使用
@Table(name="t_order")明确指定表名,避免JPA自动转下划线规则出错。
5. 验收与答辩:用三张表+一个演示脚本证明你真的懂DDD
答辩时老师最常问:“你说用了DDD,那它和普通三层架构到底有什么区别?” 这个问题不能靠背概念回答,要用可验证的事实。我建议你准备以下三张表和一个终端演示脚本,它们共同构成一份“DDD落地证据链”。
5.1 表1:领域层独立性验证表
这张表证明你的domain-core模块真的不含Spring,是纯粹的业务逻辑。在IDEA中右键domain-core→ “Open Module Settings” → “Dependencies”,截图粘贴到答辩PPT。表格内容如下:
| 依赖项 | 是否存在 | 说明 |
|---|---|---|
spring-boot-starter | ❌ 否 | 领域层无Spring依赖 |
javax.persistence | ❌ 否 | 无JPA注解污染 |
lombok | ✅ 是 | 仅用于简化代码,非业务框架 |
org.junit.jupiter | ✅ 是 | 测试依赖,不影响运行时 |
提示:若表中出现任何✅的Spring相关依赖,立即删除并重新编译。这是DDD分层的底线,答辩时老师会现场检查。
5.2 表2:用例-代码映射表
老师会抽查一个业务场景(如“用户下单”),要求你指出代码位置。提前整理好这张表,答辩时直接翻页:
| 业务用例 | 代码位置 | 关键代码片段 | 体现的DDD原则 |
|---|---|---|---|
| 创建订单 | web-api/OrderController.placeOrder() | return orderAppService.placeOrder(command); | 表现层仅转发,无业务逻辑 |
| 订单状态校验 | domain-core/Order.confirm() | if (this.status != CREATED) throw... | 聚合根保护不变性 |
| 库存预占 | application-service/OrderApplicationService.placeOrder() | inventoryService.reserveStock(...) | 应用层协调跨聚合操作 |
| 订单确认后扣库存 | application-service/OrderEventListeners.onOrderConfirmed() | inventoryService.deductStock(...) | 领域事件实现最终一致性 |
这张表的价值在于:把抽象原则锚定到具体行号。答辩时老师说“看看订单确认的校验在哪”,你秒开Order.java第45行,比讲十分钟理论更有说服力。
5.3 表3:测试覆盖率证据表
DDD强调可测试性,而测试覆盖率是硬指标。用IntelliJ IDEA的Coverage工具生成报告,截图关键模块:
| 模块 | 行覆盖率 | 分支覆盖率 | 达标线 |
|---|---|---|---|
domain-core | 92% | 85% | ≥80% |
application-service | 78% | 65% | ≥70% |
infrastructure-persistence | 65% | 50% | ≥60% |
注意:
infrastructure-persistence覆盖率低是正常的,因为JPA操作难Mock。重点保证domain-core和application-service高覆盖,这证明你的业务规则被充分验证。
5.4 终端演示脚本:三分钟跑通核心链路
答辩最后,老师可能让你现场演示。别准备花哨的前端,用curl命令链展示DDD如何工作:
# 1. 创建订单(触发应用层事务) curl -X POST http://localhost:8080/api/orders \ -H "Content-Type: application/json" \ -d '{"userId":"U1001","items":[{"skuId":"S2001","quantity":2}]}' # 2. 查看订单状态(验证聚合根状态机) curl http://localhost:8080/api/orders/ORD-20240501-001 # 3. 触发确认(验证事件发布) curl -X POST http://localhost:8080/api/orders/ORD-20240501-001/confirm # 4. 检查库存表(验证最终一致性) mysql -u root -p -e "SELECT * FROM t_inventory WHERE sku_id='S2001';"这个脚本的价值在于:所有步骤均可逆、可重复、可解释。当老师看到curl返回{"status":"CONFIRMED"},再查数据库库存减少,他就明白你不是在画饼,而是真把DDD跑通了。
从那以后我每次做DDD项目,答辩前必做三件事:① 用mvn dependency:tree确认domain-core无Spring依赖;② 把Order.java的confirm()方法打印出来贴在显示器边;③ 用上述curl脚本在本地跑三遍。这不仅是验收,更是对自己设计的敬畏——DDD不是用来炫技的,是让代码在业务变化时依然可控的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取