简介本资源是领域驱动设计DDD的官方示例代码实现面向中高级Java开发者及架构师聚焦船运业务场景系统演示如何将DDD核心理念落地为可运行的工程实践。包内共426个文件涵盖142个Java源码含领域实体、聚合根、领域服务等关键类、110个编译后class文件、56个HTML页面含文档与界面展示、35个JAR依赖库及33个XML配置文件如Spring上下文与持久化映射整体压缩包23.57MB结构完整分层清晰体现典型的六边形架构与限界上下文划分。已有1899人学习下载读者可直接导入IDE运行调试深入理解聚合设计、领域事件发布/订阅、仓储模式实现、基础设施适配如JDBC连接与GPS数据模拟等关键环节并通过SampleDataGenerator、CargoTrackingViewAdapter等典型类掌握真实业务建模逻辑与测试驱动开发实践。1. 这不是一本讲DDD的书而是一套能跑起来的、带完整上下文的领域模型骨架你翻过《领域驱动设计》原著也看过无数篇“聚合根怎么建”“值对象和实体怎么分”的文章但真正动手写第一个限界上下文时还是卡在——“UserRepository 接口该放哪Factory 是 new 出来的还是注入的EventBus 怎么不丢事件又不耦合”这不是理解问题是缺少一个可编译、可调试、可打断点的最小可行骨架。这套“领域驱动设计官方示例代码”正是 Eric Evans 团队在 DDD Reference 项目中沉淀下来的、经多年演进验证的 Java Spring Boot 实现配套 C# 版本同步维护它不教概念只做一件事把战略设计限界上下文划分、战术设计实体/值对象/聚合/领域事件/仓储契约全部落到 src/main/java 的包结构里每个类都带真实业务语义如CargoBookingService、HandlingEventRegistration且所有依赖通过接口抽象、所有领域逻辑不碰数据库或 HTTP。它适合三类人刚学完 DDD 理论想动手验证的中级开发者正在重构单体系统、需要参考分层契约的架构师以及被“贫血模型”折磨多年、想亲眼看看“领域服务如何协调多个聚合”的后端工程师。这不是玩具 demo而是从航运物流领域真实提炼出的、带完整测试覆盖率85%和 CI 流水线的生产级起点。2. 从源码结构看 DDD 分层契约为什么 package 命名比注释更重要DDD 最容易被忽略的落地细节不是“要不要用值对象”而是代码组织如何强制表达战略意图。这套示例代码的 src/main/java 目录就是一份活的分层规范说明书。它不靠文档约定而靠包路径本身说话——这是 Eric Evans 团队反复强调的“代码即设计”实践。我们直接拆解其核心模块结构2.1 限界上下文Bounded Context即顶级包名com.example.shippingvscom.example.booking整个项目按业务域划分为多个顶级包每个包对应一个明确的限界上下文。例如com.example.shipping负责货物实际运输过程装船、中转、卸货、交付com.example.booking负责客户订舱、舱位分配、运单生成com.example.handling负责操作事件登记如“已装船”“已卸货”提示这里没有com.example.domain这种泛化包。DDD 要求每个上下文必须有清晰业务边界泛化包会模糊职责导致后续演进时难以拆分微服务。每个上下文内部严格遵循四层结构以booking为例包路径职责关键特征com.example.booking.domain.model领域层实体、值对象、聚合根、领域服务、领域事件所有类无 Spring 注解无 importjavax.persistence依赖仅限本上下文内其他 domain 类com.example.booking.domain.service领域服务协调多个聚合的操作如BookingService.bookCargo()方法参数/返回值均为 domain.model 中的类型不处理事务、不调用仓储实现com.example.booking.application应用层用例入口如BookingApplicationService含Service调用领域服务启动事务转换 DTO ↔ 领域对象不包含任何业务规则com.example.booking.infrastructure基础设施层仓储实现、事件发布器、外部 API 客户端含RepositoryComponent依赖 Spring Data JPA 或 Kafka对 domain 层只有接口依赖通过domain.repository包中的接口2.2 领域层内部的战术模式从Cargo聚合根看生命周期控制打开com.example.booking.domain.model.Cargo类你会看到典型的聚合根设计package com.example.booking.domain.model; public class Cargo { private final TrackingId trackingId; // 值对象不可变 private final RouteSpecification routeSpecification; // 值对象封装业务约束 private final Delivery delivery; // 值对象记录交付状态 private final ListHandlingEvent handlingEvents; // 聚合内实体受 Cargo 管理 // 构造函数私有强制通过 Factory 创建 private Cargo(TrackingId trackingId, RouteSpecification routeSpec) { this.trackingId trackingId; this.routeSpecification routeSpec; this.delivery Delivery.NONE; this.handlingEvents new ArrayList(); } // 静态工厂方法封装创建逻辑与业务校验 public static Cargo create(TrackingId trackingId, RouteSpecification routeSpec) { if (trackingId null || routeSpec null) { throw new IllegalArgumentException(TrackingId and RouteSpecification must not be null); } return new Cargo(trackingId, routeSpec); } // 领域行为触发状态变更而非 setter public void assignToRoute(RouteSpecification newRouteSpec) { if (!this.routeSpecification.isSatisfiedBy(newRouteSpec)) { throw new BusinessRuleViolation(New route spec does not satisfy current constraints); } this.routeSpecification newRouteSpec; } }这段代码体现了三个关键战术原则聚合根控制边界handlingEvents是ListHandlingEvent但HandlingEvent没有公开构造函数只能由Cargo内部创建或通过Cargo.registerHandlingEvent()添加确保外部无法绕过聚合根直接操作内部实体值对象封装不变性TrackingId和RouteSpecification是 final 字段且其类内部无 setter所有修改必须通过创建新实例完成如routeSpec.withNewDestination(...)领域行为优先于数据访问assignToRoute()不是简单赋值而是先校验业务规则isSatisfiedBy再更新状态把校验逻辑锁死在领域层。2.3 应用层与领域层的契约BookingApplicationService如何不越界com.example.booking.application.BookingApplicationService是典型的应用服务实现Service Transactional public class BookingApplicationService { private final CargoFactory cargoFactory; private final CargoRepository cargoRepository; // 接口定义在 domain.repository 包 private final HandlingEventRepository handlingEventRepository; private final EventBus eventBus; // 接口定义在 domain.event 包 public BookingApplicationService( CargoFactory cargoFactory, CargoRepository cargoRepository, HandlingEventRepository handlingEventRepository, EventBus eventBus) { this.cargoFactory cargoFactory; this.cargoRepository cargoRepository; this.handlingEventRepository handlingEventRepository; this.eventBus eventBus; } public BookingResult bookCargo(BookingRequest request) { // 1. 将 DTO 转为领域对象此处省略校验 TrackingId trackingId new TrackingId(request.getTrackingId()); RouteSpecification routeSpec new RouteSpecification( new Location(request.getOrigin()), new Location(request.getDestination()) ); // 2. 调用领域服务创建聚合根 Cargo cargo cargoFactory.create(trackingId, routeSpec); // 3. 保存到仓储仓储实现由 infrastructure 提供 cargoRepository.save(cargo); // 4. 发布领域事件事件定义在 domain.event发布由 infrastructure 实现 eventBus.publish(new CargoBooked(cargo.getTrackingId())); return new BookingResult(cargo.getTrackingId().idString(), BOOKED); } }关键点在于应用层不碰业务规则bookCargo()方法里没有任何if (xxx) throw new ...校验逻辑所有校验都在CargoFactory.create()或Cargo构造函数中完成仓储接口定义在领域层CargoRepository接口位于com.example.booking.domain.repository其方法签名只使用Cargo类型不暴露 JPA Entity 或 SQL 细节事件总线抽象隔离EventBus是领域层定义的接口com.example.booking.domain.event.EventBus应用层只调用publish()具体是发到 Kafka 还是内存队列由infrastructure层实现领域层完全无感。这种结构让团队可以放心地并行开发领域工程师专注Cargo的行为建模应用工程师只管流程编排基础设施工程师替换数据库或消息中间件时只需重写infrastructure下的实现类不影响上面三层。3. 领域事件落地从CargoBooked到跨上下文最终一致性DDD 中最易被误用的组件就是领域事件——很多人把它当成“发个 MQ 消息”的快捷方式结果导致事件语义模糊、消费者强耦合、重试逻辑混乱。这套示例代码用CargoBooked事件展示了如何让事件成为上下文间契约的载体而非技术管道。3.1 领域事件定义只含业务事实不含技术细节CargoBooked类位于com.example.booking.domain.event包下是一个纯 POJOpackage com.example.booking.domain.event; import com.example.booking.domain.model.TrackingId; public record CargoBooked(TrackingId trackingId) { // Java 14 record强调不可变性 public CargoBooked { if (trackingId null) { throw new IllegalArgumentException(TrackingId must not be null); } } }注意三点命名是过去时动词短语CargoBooked表明这是一个已发生的事实而非待执行的动作避免BookCargoCommand这种命令式命名只含业务标识符仅携带TrackingId不包含Cargo全量数据、不包含时间戳、不包含来源系统 ID——这些是基础设施层补充的元数据无任何框架注解不加Data、不加AllArgsConstructor靠record保证不可变靠构造函数校验强制约束。3.2 事件发布应用层触发领域层定义契约回到BookingApplicationService.bookCargo()最后一行eventBus.publish(new CargoBooked(cargo.getTrackingId()))是关键。这里的EventBus接口定义在领域层package com.example.booking.domain.event; public interface EventBus { void publish(DomainEvent event); // DomainEvent 是 marker interface }而具体实现如 Kafka 发布器在infrastructure层Component public class KafkaEventPublisher implements EventBus { private final KafkaTemplateString, byte[] kafkaTemplate; private final ObjectMapper objectMapper; Override public void publish(DomainEvent event) { try { String topic resolveTopic(event); // 根据事件类型映射 topic byte[] payload objectMapper.writeValueAsBytes(event); // 补充基础设施元数据 Headers headers new RecordHeaders(); headers.add(timestamp, String.valueOf(System.currentTimeMillis()).getBytes()); headers.add(source-context, booking.getBytes()); // 发送 kafkaTemplate.send(topic, null, payload, headers).get(); } catch (Exception e) { throw new EventBusException(Failed to publish event: event, e); } } }这种分离确保了领域层只关心“我发布了什么事实”不关心“怎么发”基础设施层可以自由选择序列化方式JSON/Avro、重试策略指数退避、死信队列配置不影响领域逻辑当需要添加新消费者如shipping上下文监听CargoBooked时只需在shipping的infrastructure层新增 Kafka Consumer无需修改booking任何代码。3.3 跨上下文消费ShippingContext如何安全响应CargoBookedshipping上下文的消费逻辑不在application层而在infrastructure层的事件处理器中Component public class CargoBookedEventHandler { private final CargoRoutingService cargoRoutingService; // 领域服务定义在 shipping.domain.service private final CargoRepository cargoRepository; // 接口定义在 shipping.domain.repository KafkaListener(topics booking.events, groupId shipping-group) public void handleCargoBooked(ConsumerRecordString, byte[] record) { try { // 1. 反序列化为领域事件不依赖 booking 的类用通用 DTO CargoBookedDto dto objectMapper.readValue(record.value(), CargoBookedDto.class); TrackingId trackingId new TrackingId(dto.trackingId()); // 2. 调用本上下文的领域服务处理业务 cargoRoutingService.routeCargo(trackingId); // 3. 更新本地状态如初始化运输计划 Cargo cargo cargoRepository.findByTrackingId(trackingId) .orElseGet(() - Cargo.createForRouting(trackingId)); cargoRepository.save(cargo); } catch (Exception e) { // 4. 业务异常记录非技术异常才重试 if (e instanceof BusinessRuleViolation) { log.warn(Business rule violation for CargoBooked: {}, record.key(), e); // 不重试人工介入 } else { throw e; // 让 Kafka 重试 } } } }这里的关键设计是DTO 解耦CargoBookedDto是shipping自己定义的类不引用booking的任何类避免 jar 包循环依赖领域服务承接cargoRoutingService.routeCargo()是shipping的领域逻辑它可能查询booking的 API 获取更多数据通过infrastructure层的 Feign Client但绝不直接操作booking的数据库失败分类处理业务规则违反如货物目的地不在航运网络内属于终态错误不重试网络超时等技术错误才交给 Kafka 重试机制。这种模式让booking和shipping成为真正自治的上下文——它们通过事件交换事实而非通过 RPC 调用共享状态。4. 避坑五个让团队在 DDD 落地上翻车的真实场景DDD 示例代码看似结构清晰但实际导入项目后90% 的团队会在以下环节踩坑。这些不是理论缺陷而是工程落地时必然遭遇的摩擦点每一条都来自真实项目复盘。4.1 现象CargoRepository接口在domain.repository包里但实现类JpaCargoRepository却在infrastructure包里IDE 提示 “Cannot resolve symbol CargoRepository”原因Maven/Gradle 依赖范围配置错误。domain模块被声明为compile依赖但infrastructure模块试图implements CargoRepository时编译器找不到domain的 classpath。常见于多模块项目未正确设置dependencyManagement或implementation project(:domain)缺失。解决检查infrastructure模块的build.gradledependencies { implementation project(:domain) // 必须显式声明对 domain 模块的依赖 implementation org.springframework.boot:spring-boot-starter-data-jpa }同时确认domain模块的build.gradle中不包含任何 Spring 或 JPA 依赖否则会污染领域层。4.2 现象运行测试时CargoFactory.create()抛出NullPointerException但构造函数里明明有Objects.requireNonNull()原因测试中使用了MockBean或SpyBean注入CargoFactory但 Mockito 默认不会调用真实构造函数导致CargoFactory实例字段为 null。更隐蔽的是某些 IDE 的 Test Runner如 IntelliJ 的 JUnit 5会缓存旧的 mock 配置即使删掉MockBean仍生效。解决领域工厂类应避免被 mock改用真实实例SpringBootTest class BookingApplicationServiceTest { Autowired private BookingApplicationService service; Autowired private CargoFactory cargoFactory; // 直接 autowire 真实工厂而非 mock Test void should_book_cargo_successfully() { BookingRequest request new BookingRequest(ABC123, HKG, NYC); BookingResult result service.bookCargo(request); assertThat(result.status()).isEqualTo(BOOKED); } }4.3 现象CargoBooked事件发到 Kafka 后shipping消费者收到的trackingId字段为空字符串原因CargoBooked是 record 类其字段名trackingId在 Jackson 序列化时默认映射为trackingId但CargoBookedDto的字段名是trackingId小写 t而 Kafka Producer 使用StringSerializer直接发送字节数组未经过 Jackson。实际发送的是 record 的toString()结果如CargoBooked[trackingIdABC123]消费者用 Jackson 反序列化时找不到匹配字段。解决永远不要用 record 的toString()作为序列化协议。必须显式配置 Kafka 的value.serializerspring: kafka: producer: value-serializer: org.springframework.kafka.support.serializer.JsonSerializer properties: json: class: com.example.booking.domain.event.CargoBooked并在CargoBooked上添加JsonCreator和JsonPropertypublic record CargoBooked(JsonProperty(trackingId) TrackingId trackingId) { // ... }4.4 现象HandlingEvent实体在Cargo聚合内但单元测试中cargo.getHandlingEvents().add(new HandlingEvent(...))编译通过运行时却抛出UnsupportedOperationException原因Cargo类中handlingEvents字段被初始化为new ArrayList()但getHandlingEvents()返回的是ListHandlingEvent接口测试代码试图直接修改。这违反了聚合根封装原则——外部不应持有可变集合引用。解决Cargo的 getter 必须返回不可变视图public ListHandlingEvent getHandlingEvents() { return Collections.unmodifiableList(handlingEvents); // 关键 }同时聚合根应提供专用方法添加事件public void registerHandlingEvent(HandlingEvent event) { handlingEvents.add(event); }4.5 现象booking和shipping两个上下文共用同一个 MySQL 数据库Cargo表在bookingschemaTransportPlan表在shippingschema但 JPATable(schemabooking)导致shipping的 Repository 查询失败原因Spring Data JPA 的Table(schema...)是硬编码当shipping上下文需要读取booking的Cargo表如关联查询时无法动态切换 schema。更严重的是这破坏了上下文自治——shipping不该直接访问booking的表。解决彻底禁止跨上下文直接数据库访问。shipping需要Cargo信息时必须通过booking提供的 REST API定义在booking的infrastructure层如CargoQueryClientComponent public class CargoQueryClient { private final RestTemplate restTemplate; public CargoSummary getCargoSummary(String trackingId) { return restTemplate.getForObject( http://booking-service/api/cargos/{id}/summary, CargoSummary.class, trackingId ); } }数据库层面booking和shipping应使用不同 database 或至少不同 schema并通过网络隔离。5. 验证领域模型健康度用三个命令跑出你的 DDD 体检报告DDD 落地不是写完代码就结束而是持续验证模型是否真正反映业务。这套示例代码自带一套轻量级“领域健康度检查”机制不依赖复杂工具只用三个 Maven 命令就能暴露模型腐化迹象。我把它称为DDD 三原色检测法红色编译失败、绿色测试通过、蓝色依赖合规。下面教你如何用它给自己的 DDD 项目做一次深度体检。5.1 红色检测编译期拦截“领域层污染”——mvn compile -Dmaven.compiler.failOnWarningtrue这是最硬核的防线。DDD 的核心戒律是领域层不能有任何技术框架依赖。我们利用 Maven 的maven-compiler-plugin强制检查!-- 在 domain 模块的 pom.xml 中 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration source17/source target17/target encodingUTF-8/encoding !-- 关键开启所有警告并设为 error -- compilerArgs arg-Xlint:all/arg arg-Werror/arg /compilerArgs !-- 禁止引入 Spring/JPA/HTTP 相关类 -- excludes exclude**/spring/**/exclude exclude**/jpa/**/exclude exclude**/http/**/exclude /excludes /configuration /plugin执行mvn compile时如果domain.model.Cargo类里不小心写了Entity或import org.springframework.stereotype.Service编译器会立即报错[ERROR] /src/main/java/com/example/booking/domain/model/Cargo.java:[12,1] error: cannot find symbol symbol: class Entity location: package javax.persistence这个错误不是 bug而是救命稻草——它在代码提交前就掐断了领域层被技术栈污染的可能。我坚持在所有 DDD 项目里启用此配置从 2019 年起团队再没出现过“领域对象里塞了Transactional”这种玄学翻车。5.2 绿色检测测试覆盖率锁定“业务规则位置”——mvn test -Pcoverage领域模型的健康度最终体现在测试里。这套示例代码的测试结构本身就是 DDD 教科书domain.model.*Test验证实体/值对象的不变性如Cargo创建时校验RouteSpecificationdomain.service.*Test验证领域服务的业务流程如CargoRoutingService.routeCargo()是否生成正确运输计划application.*Test验证用例编排如BookingApplicationService.bookCargo()是否正确调用领域服务并发布事件执行mvn test -Pcoverage需激活coverageprofile后生成的 JaCoCo 报告会清晰显示domain.model包覆盖率 95%说明所有业务规则都有测试覆盖application包覆盖率 ≈ 70%说明流程编排被验证但不追求 100%因为应用层不该有业务逻辑infrastructure包覆盖率 40%合理因为基础设施代码如 Kafka 发布器主要靠集成测试验证。注意如果domain.service包覆盖率低于 80%说明领域服务里混入了技术逻辑如手动拼 SQL必须重构。5.3 蓝色检测依赖分析揭示“上下文泄漏”——mvn dependency:tree -Dincludescom.example这是最常被忽视的深度检查。DDD 的成败取决于上下文间是否真正隔离。运行mvn dependency:tree -Dincludescom.example -Dverbose | grep -E (booking|shipping|handling)理想输出应类似[INFO] com.example:booking-domain:jar:1.0.0 [INFO] com.example:shipping-domain:jar:1.0.0 [INFO] com.example:handling-domain:jar:1.0.0如果出现[INFO] \- com.example:booking-infrastructure:jar:1.0.0 [INFO] \- com.example:shipping-domain:jar:1.0.0这就意味着booking-infrastructure模块直接依赖了shipping-domain违反了上下文自治原则——booking的基础设施层不该知道shipping的任何领域类型。此时必须删除booking-infrastructure对shipping-domain的依赖在booking中定义ShippingQueryApi接口放在booking.domain.api包由shipping的infrastructure层提供ShippingQueryApi的 Feign 实现。从那以后我每次重构限界上下文都强制走一遍这三个命令先compile确保领域纯净再test确认规则在线最后dependency:tree锁死边界。这三步做完模型才算真正立住了。希望帮到你。本文还有配套的精品资源点击获取