ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

领域驱动设计官方示例代码拆解:分层架构、聚合边界与避坑指南

2026/10/2 23:00:47 拓冰建站 浏览量
领域驱动设计官方示例代码拆解:分层架构、聚合边界与避坑指南 简介这份资源是领域驱动设计DDD的官方示例代码面向希望深入理解 DDD 方法论并落地实践的 Java 开发者与架构学习者。它以船运业务为背景将领域模型、聚合、实体与值对象、领域事件、领域服务、边界上下文、仓储持久化等核心概念融入可运行的工程代码中帮助读者跳出理论、对照真实项目理解 DDD 的分层结构与建模思路。压缩包为 rar 格式共 426 个文件约 23.57MB其中 142 个 java 源文件承载领域逻辑与分层实现110 个 class 为编译产物另有 35 个 jar 依赖、33 个 xml 配置、56 个 html 与 9 个 jsp 页面以及 properties、sql、wsdl 等配套文件覆盖从领域层到基础设施层的完整结构。目前已有 1899 人学习下载。通过研读源码读者可掌握聚合边界划分、领域事件发布订阅、仓储解耦等实战技巧并借助单元测试理解业务规则验证方式适合作为 DDD 入门到进阶的参考范例。1. 从一份官方示例代码说起DDD 落地到底卡在哪很多人第一次接触领域驱动设计都是从啃《领域驱动设计》那本蓝皮书开始的。书翻完了聚合根、值对象、领域事件这些词都能说上两句可真到项目里一动手就懵了——实体和数据库表怎么对应仓储接口到底该放哪一层应用服务和领域服务怎么分工这些问题书里讲的是原则落到代码上全是空白。这份领域驱动设计官方示例代码解决的就是这个断层。它不是又一篇理论文章而是一套能跑起来、能拆开看的工程骨架把 DDD 里那些抽象概念翻译成了具体的目录结构、类关系和调用链路。适合已经了解 DDD 基本概念、但不知道工程上怎么落地的后端开发者也适合团队里想推 DDD 但缺一个参照标准的技术负责人。下面我按自己拆这套代码的顺序把关键设计、复现步骤和踩过的坑一条条讲清楚。2. 工程分层与模块依赖先看懂骨架再动手2.1 四层架构在代码里长什么样DDD 的经典分层是用户接口层、应用层、领域层和基础设施层。这套官方示例代码基本遵循了这个结构但不同语言版本的目录命名会有差异。以常见的 Java 版本为例顶层包通常按interfaces、application、domain、infrastructure划分。领域层是核心里面放聚合、实体、值对象、领域服务和仓储接口应用层负责编排用例调用领域对象完成业务动作本身不包含业务规则基础设施层实现仓储接口、发消息、调外部服务用户接口层就是 Controller 或事件监听入口。关键要看的是依赖方向。领域层不应该依赖任何其他层仓储接口定义在领域层实现放在基础设施层靠依赖倒置把方向掰回来。你打开domain包如果发现里面 import 了 Spring 的 JPA 注解或者 MyBatis 的 Mapper那说明分层已经被破坏了。官方示例代码在这一点上通常做得比较干净可以作为对照标准去检查自己项目里的分层是否串味。2.2 模块依赖检查的实操步骤拿到代码后别急着跑先做一轮依赖体检。以 Maven 多模块项目为例打开根目录的pom.xml看模块划分和依赖声明。# 查看模块列表 grep -A 20 modules pom.xml # 检查 domain 模块是否被其他模块反向依赖 grep -r domain --includepom.xml .如果发现infrastructure模块的 pom 里依赖了domain这是正常的因为要实现仓储接口。但如果domain模块的 pom 里依赖了infrastructure或者application那就是循环依赖说明分层设计有问题。另一种检查方式是看包之间的 import 关系用 IDE 的依赖分析工具或者 ArchUnit 这类测试框架都能做。注意不同语言版本的示例代码目录命名差异较大。比如 C# 版本可能用Domain、Application、Infrastructure、Web作为顶层项目名Go 版本可能用internal/domain、internal/app这种结构。不要死记目录名抓住依赖方向这个本质。2.3 聚合边界的识别方法分层看完了接下来要找到业务核心——聚合。官方示例代码通常会选一个典型业务场景比如订单、账户或者会议管理。找到聚合根的方法是看哪个实体持有其他实体的引用并且负责维护一致性边界。在代码里聚合根通常有Repository接口与之对应而且仓储的查询方法一般以聚合根为入口。// 典型的聚合根定义 public class Order { // Order 是聚合根 private OrderId id; private ListOrderItem items; // OrderItem 是聚合内实体 private Money totalAmount; // Money 是值对象 public void addItem(Product product, int quantity) { // 业务规则在这里不在应用层 if (this.items.size() MAX_ITEMS) { throw new OrderLimitExceededException(); } this.items.add(new OrderItem(product, quantity)); recalculateTotal(); } }这段代码里Order是聚合根OrderItem只能通过Order的方法来修改不能单独从仓储里查出来改。Money是值对象没有标识靠属性相等来判断。识别聚合边界的一个实用标准是一次事务只修改一个聚合。如果你发现一个业务操作要同时改两个聚合那要么是聚合边界划错了要么应该用领域事件做最终一致性。3. 从领域模型到可运行代码复现路径与参数配置3.1 环境准备与项目启动官方示例代码一般会提供 README 说明运行方式但有些版本年久失修依赖的数据库或中间件版本可能已经对不上。我一般会先看构建文件里的依赖版本再决定用哪个运行时。以 Java Spring Boot 版本为例常见做法是# 克隆代码后先看构建配置 cat pom.xml | grep -E spring-boot|java.version # 如果用的是 Maven Wrapper直接用 ./mvnw clean install -DskipTests # 启动前确认数据库配置 cat src/main/resources/application.properties配置文件里重点看三处数据库连接、JPA 的ddl-auto设置、以及是否启用了事件发布机制。ddl-auto在示例代码里常见create-drop或update生产环境当然不能用但本地跑通没问题。如果示例用的是 H2 内存数据库那基本开箱即用如果配的是 MySQL 或 PostgreSQL需要先建库再启动。提示有些示例代码的 README 里写的数据库版本和实际依赖不一致以pom.xml或build.gradle里的版本为准。遇到启动报错先看驱动版本和数据库版本是否匹配。3.2 领域事件的发布与监听配置DDD 里聚合之间通信用领域事件示例代码通常会演示一个完整的发布-监听链路。发布方在聚合根里收集事件应用层在事务提交后发布出去。不同框架的实现方式不一样Spring 里常见的是ApplicationEventPublisherAxon 框架有自己的事件总线。// 聚合根内部收集事件 public class Order { private ListDomainEvent domainEvents new ArrayList(); public void confirm() { this.status OrderStatus.CONFIRMED; domainEvents.add(new OrderConfirmedEvent(this.id, Instant.now())); } public ListDomainEvent getDomainEvents() { return Collections.unmodifiableList(domainEvents); } } // 应用服务里发布 Service public class OrderApplicationService { Transactional public void confirmOrder(OrderId orderId) { Order order orderRepository.findById(orderId); order.confirm(); orderRepository.save(order); order.getDomainEvents().forEach(eventPublisher::publishEvent); order.clearDomainEvents(); } }这里的关键参数是事务边界。事件发布必须在事务提交之后否则监听方可能读到还没落库的数据。Spring 里可以用TransactionalEventListener(phase AFTER_COMMIT)来保证这一点。如果示例代码里直接在领域层调用了事件发布器那说明它把基础设施的关注点混进了领域层属于反面教材你可以借这个机会看看不这么写会出什么问题。3.3 仓储实现与持久化映射仓储接口在领域层实现在基础设施层。示例代码里常见的实现方式是 Spring Data JPA 或者 MyBatis。JPA 的好处是聚合的保存和查询比较自然但要注意懒加载和级联配置。MyBatis 更灵活但需要自己写映射逻辑。// 领域层仓储接口 public interface OrderRepository { Order findById(OrderId id); void save(Order order); ListOrder findByCustomerId(CustomerId customerId); } // 基础设施层 JPA 实现 Repository public class JpaOrderRepository implements OrderRepository { private final OrderJpaDao dao; Override public Order findById(OrderId id) { return dao.findById(id.getValue()) .map(this::toDomain) .orElseThrow(() - new OrderNotFoundException(id)); } Override public void save(Order order) { OrderPO po toPersistence(order); dao.save(po); } }这里有个常见坑领域对象和持久化对象要不要分开示例代码里有的版本直接用领域对象加 JPA 注解有的版本做了 PO 转换。两种做法各有取舍。直接用注解写起来快但领域层就被 JPA 污染了做转换保持了领域层干净但代码量翻倍。我一般建议核心聚合做转换边缘的简单实体可以直接注解别一刀切。4. 避坑与排查拆这套代码时最容易翻车的几个点4.1 贫血模型陷阱实体只有 getter/setter现象打开示例代码的实体类发现除了属性定义和 getter/setter 之外没有任何业务方法所有业务逻辑都堆在 Service 里。原因这是最常见的 DDD 翻车方式。写代码的人把 DDD 理解成了“分层 实体”但实体本身没有行为退化成了数据容器。官方示例代码如果版本较老或者维护不积极也可能出现这个问题。解决把业务规则搬回实体方法里。判断标准很简单——如果一个操作需要读取实体内部状态并做出决策那它就应该在实体里。Service 只负责编排和事务控制不负责业务判断。你可以拿示例代码里的订单确认流程做对照看confirm()方法是写在 Order 里还是写在 OrderService 里。4.2 聚合过大导致并发冲突现象保存一个聚合时频繁出现乐观锁冲突或者一次加载把半个数据库都拉进来了。原因聚合边界划得太大把本来不该放在一起的实体塞进了一个聚合。比如把“订单”和“客户”放在一个聚合里每次改订单都要锁客户并发一上来就互相等。解决聚合只包含真正需要强一致性的对象。客户和订单之间用 ID 引用不用对象引用。如果示例代码里聚合根直接持有了另一个聚合根的引用那就要警惕了。正确的做法是持有 ID需要时通过仓储单独加载。4.3 领域事件重复消费现象监听方收到同一条事件多次导致重复扣款、重复发短信之类的业务异常。原因事件发布没有做幂等或者消息中间件本身是 at-least-once 语义。示例代码为了简化往往不做去重处理直接照搬到生产就会出问题。解决监听方维护一个已处理事件 ID 的表处理前先查是否已经处理过。或者在事件里带一个唯一标识消费时做去重。示例代码里如果用的是 Spring 的ApplicationEventPublisher默认是同步调用不会重复但如果换成了 Kafka 或 RabbitMQ就必须自己处理幂等。4.4 仓储接口泄漏持久化细节现象领域层的仓储接口里出现了Pageable、Example、Specification这类框架特有的类型。原因写仓储接口的时候直接照着 Spring Data 的JpaRepository抄把分页、动态查询这些基础设施关注点带进了领域层。解决仓储接口只定义业务需要的查询方法比如findByCustomerId、findOverdueOrders。分页参数可以用一个自定义的PageRequest值对象来传不要直接用框架的类。示例代码里如果出现了org.springframework.data.domain.Pageable在领域层那就是一个可以改进的点。4.5 应用服务事务边界不清现象一个用例里调了多个应用服务方法每个方法各自开事务中间失败导致数据不一致。原因事务边界划在了应用服务方法上但用例的原子性范围比单个方法大。解决事务边界应该和用例边界对齐。一个用例一个事务在应用服务入口方法上加Transactional内部调用的领域方法不再单独开事务。示例代码里如果用了Transactional在多个层级需要检查传播行为是否合理。5. 进阶用法用示例代码做架构守护与团队规范拆完这套代码最有价值的用法不是照抄而是把它变成团队里的架构守护工具。我一般会做三件事。第一提取依赖规则写成自动化测试。用 ArchUnit 或者类似的架构测试框架把“领域层不能依赖基础设施层”“应用层不能直接调仓储实现”这些规则固化成测试用例。每次 CI 跑一遍谁破坏了分层立刻报警。// ArchUnit 规则示例 ArchTest static final ArchRule domain_should_not_depend_on_infrastructure noClasses().that().resideInAPackage(..domain..) .should().dependOnClassesThat() .resideInAPackage(..infrastructure..);第二把示例代码里的聚合设计模式抽成团队模板。比如聚合根的标准结构、领域事件的收集和发布方式、仓储接口的命名规范做成代码模板或者脚手架。新项目直接生成省去每次重新讨论的时间。第三定期用示例代码做代码评审的参照。评审时遇到“这个逻辑该放哪层”的争论直接打开示例代码对照。虽然示例不一定百分百完美但至少提供了一个具体的讨论基础比空对空争论效率高得多。检查项合格标准常见问题领域层依赖不依赖基础设施和应用层import 了 JPA 或 Mapper聚合根行为业务方法在实体内只有 getter/setter仓储接口位置定义在领域层定义在基础设施层事件发布时机事务提交后事务内直接发布事务边界与应用服务方法对齐多层嵌套事务从那以后我每次拿到一个新的 DDD 示例项目都强制先跑一遍依赖检查再看聚合根有没有行为最后确认事件发布的事务边界。这三步走完基本就能判断这套代码值不值得深入拆解。希望帮到你。本文还有配套的精品资源点击获取