
简介本资源是一份面向软件架构师、后端开发工程师及技术团队的领域驱动设计DDD入门与实践指南聚焦业务建模与技术实现的深度对齐解决复杂业务系统中模型失真、沟通断层与架构腐化等典型问题。内容源自InfoQ中文站免费发布的《领域驱动设计精简版》PDF教程由Eric Evans思想凝练而来系统讲解通用语言、聚合根、限界上下文、事件风暴等核心概念并结合模型驱动设计、持续对话机制与团队协作要点提供可落地的DDD实施路径。资源为单文件PDF格式共1个文件大小1.27MB轻量易读适合作为DDD学习第一手材料或团队内部知识共享载体。目前已有483人下载学习内容涵盖译序反思、核心理念拆解、实践模式详解及挑战应对策略特别适合希望从理论快速过渡到建模实践的中初级开发者。1. DDD不是画图游戏为什么你照着教程写了聚合、值对象、领域事件上线后还是被业务方骂“改个字段要三天”你肯定见过这样的场景团队花两周时间啃完《实现领域驱动设计》画出漂亮的限界上下文图建好聚合根、实体、值对象三层结构连领域事件总线都搭好了——结果第一个需求来了“订单页加个‘预计送达时间’字段前端要显示后台要参与计算”。开发一查代码发现这个字段横跨订单聚合、物流服务、库存服务三个上下文改一处要动八处数据库要加字段、API要改契约、DTO要重序列化……最后上线拖了五天业务方在站会上直接问“你们DDD到底解决了什么问题”这不是DDD错了而是绝大多数“很好的教程”只教你怎么画模型不教你怎么守边界只讲Eric Evans书里的概念定义不讲在Spring Boot里怎么用Aggregate注解真正锁住聚合内一致性不告诉你为什么“用户”在会员上下文是聚合根在订单上下文只能是ID引用更不会提醒你当产品经理说“我要一个能查所有订单所有售后所有评价的统一搜索页”时你该立刻拉他开个上下文映射会议而不是默默去写一个跨三库的MyBatis联查SQL。这篇笔记不是概念复读机。它基于我在3个中型电商系统落地DDD的真实路径从用ddd-spring-boot-starter快速搭建骨架到用Eventuate Tram做最终一致性补偿再到用ArchUnit写测试断言强制校验“仓储接口不能返回DTO”——每一步都踩过坑、回滚过、重写过。你会看到怎么用六边形架构分层把Spring MVC Controller钉死在适配器层让Application Service真正只依赖Domain Model为什么“搞不懂DDD”的人90%卡在上下文映射模式选错——不是所有上下游都该用Published Language有些必须用Customer/Supplier关系倒逼对方改接口以及最关键的DDD不是设计阶段的仪式感而是上线后每次PR合并前用ArchUnit跑一次的硬性门禁。如果你正被“模型很美、代码很乱、改需很慢”折磨这篇就是给你写的。我们不聊“什么是防腐层”我们直接看AntiCorruptionLayerImpl.java里那行if (legacyStatus.equals(SUC)) return OrderStatus.SUCCESS;是怎么救了你第三次线上事故。2. 用Spring Boot ArchUnit搭出DDD骨架从空项目到能跑通的最小可验证单元DDD落地最致命的陷阱是先花两周画UML图再花三个月写代码最后发现模型和代码根本对不上。真实项目里模型必须长在代码里而不是PPT里。所以第一步不是建包结构而是用工具把DDD约束变成编译期/测试期的硬规则。我用的是Spring Boot 3.2 ddd-spring-boot-starterv2.4.0archunit-junit5v1.2.1这套组合能让你在写第一行业务代码前就建立不可绕过的分层纪律。2.1 创建符合DDD分层规范的Maven模块结构不要在一个Spring Boot工程里用package模拟分层真实项目必须物理隔离。我坚持用多模块Maven结构!-- pom.xml -- modules moduledomain/module !-- 纯Java无Spring依赖 -- moduleapplication/module !-- Service调用domain不依赖infra -- moduleinfrastructure/module !-- JPA, Redis, MQ等具体实现 -- moduleinterface/module !-- Controller, DTO, OpenAPI -- /modules关键点domain模块的pom.xml里绝对不能出现spring-boot-starter。如果IDE提示“找不到Aggregate”说明你已经违规——DDD的聚合根定义必须是纯Java类靠ddd-spring-boot-starter在运行时注入行为而不是编译期依赖Spring。提示domain模块的src/main/java下只允许存在com.example.order.domain.*包且禁止出现任何import org.springframework.*。这是ArchUnit后续校验的基础。2.2 用ArchUnit写第一条防线禁止基础设施层反向调用领域层很多人以为“分层”就是包名写对就行。但实际开发中一个手滑的new OrderRepositoryImpl()就能让整个分层崩塌。ArchUnit的作用就是把这种手滑变成编译失败。在infrastructure模块的src/test/java下新建测试// InfrastructureLayerTest.java Test public void infrastructure_should_not_depend_on_domain() { JavaClasses classes new ClassFileImporter() .importPackages(com.example); ArchRuleDefinition.noClasses() .that().resideInAPackage(..infrastructure..) .should().accessClassesThat().resideInAPackage(..domain..) .because(Infrastructure must depend only on domain interfaces, not implementations) .check(classes); }这段代码会在mvn test时扫描所有类一旦发现infrastructure包下的类直接new了domain包里的实体类比如new Order()或调用了domain包里的静态方法测试立刻失败。注意它允许infrastructure调用domain包里的接口如OrderRepository因为接口在domain模块实现类在infrastructure模块——这才是真正的依赖倒置。逻辑说明resideInAPackage(..infrastructure..)匹配所有infrastructure子包下的类accessClassesThat().resideInAPackage(..domain..)检查这些类是否访问了domain包下的类noClasses()表示“不允许存在这样的类”即禁止反向依赖。参数说明because()是失败时的提示语写给团队看的不是给机器的check(classes)是执行校验必须调用否则测试不生效这个测试要放在infrastructure模块因为它是被约束的对象。2.3 在domain模块定义聚合根用Aggregate注解而非继承很多教程教你让Order继承AggregateRootOrderId这会导致领域模型污染——Order本应只关心业务规则不该知道“我是聚合根”这种框架概念。ddd-spring-boot-starter的正确用法是注解驱动// domain/src/main/java/com/example/order/domain/Order.java Aggregate public class Order { private OrderId id; private ListOrderItem items; private OrderStatus status; // 私有构造函数强制通过工厂方法创建 private Order() {} public static Order create(OrderId id, ListOrderItem items) { Order order new Order(); order.id id; order.items new ArrayList(items); order.status OrderStatus.CREATED; // 聚合内一致性规则金额必须大于0 if (order.getTotalAmount().compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(Total amount must be positive); } return order; } public void confirm() { if (this.status ! OrderStatus.CREATED) { throw new IllegalStateException(Only CREATED order can be confirmed); } this.status OrderStatus.CONFIRMED; // 发布领域事件由Application Service处理 DomainEvents.publish(new OrderConfirmedEvent(this.id)); } }关键点Aggregate注解告诉框架这是聚合根它的生命周期由仓储管理所有业务方法如confirm()必须保证聚合内一致性比如状态流转规则DomainEvents.publish()不是发MQ消息而是内存事件由框架在事务提交后异步触发——这是DDD里“最终一致性”的起点不是“马上发Kafka”。为什么不用继承因为继承会把技术细节如getId()强加给业务模型。而注解方式下Order类完全干净OrderId可以是String、UUID或自定义类型框架通过反射获取id字段。3. 六边形架构落地把Controller钉死在适配器层让Application Service真正无框架“六边形架构”常被误读为“画六个边的图”。其实它就干一件事把业务逻辑核心六边形和所有外部依赖数据库、HTTP、MQ隔开让核心逻辑能脱离Spring、脱离MySQL独立测试。很多团队说“DDD搞不懂”本质是没把六边形架构的“端口与适配器”思想落到代码里——Controller还在调ServiceService还在new Repository整个链路全是Spring Bean哪来的可替换性3.1 定义端口PortApplication Service接口必须在application模块application模块是六边形的“应用层”它只定义接口不写实现。比如下单用例// application/src/main/java/com/example/order/application/PlaceOrderUseCase.java public interface PlaceOrderUseCase { OrderId execute(PlaceOrderCommand command); } // application/src/main/java/com/example/order/application/PlaceOrderCommand.java public record PlaceOrderCommand( String customerId, ListPlaceOrderItemCommand items, String shippingAddress ) {}注意PlaceOrderUseCase是接口不是Service类PlaceOrderCommand是纯数据载体不含任何Spring注解所有入参、出参必须是record或final class禁止public字段这是保证不变性的基础。为什么放这里因为application模块不依赖infrastructure所以它的接口天然可被任何适配器实现——Web适配器Controller、CLI适配器命令行、甚至测试适配器JUnit。3.2 实现适配器AdapterController只负责HTTP协议转换interface模块的Controller唯一职责是把HTTP请求转成PlaceOrderCommand再调用PlaceOrderUseCase最后把OrderId转成JSON响应。它不能碰任何领域对象// interface/src/main/java/com/example/order/interface/web/OrderController.java RestController RequestMapping(/api/orders) public class OrderController { private final PlaceOrderUseCase placeOrderUseCase; public OrderController(PlaceOrderUseCase placeOrderUseCase) { this.placeOrderUseCase placeOrderUseCase; } PostMapping public ResponseEntityOrderIdResponse placeOrder(RequestBody PlaceOrderRequest request) { // 1. 协议转换HTTP Request - Command var command new PlaceOrderCommand( request.customerId(), request.items().stream() .map(i - new PlaceOrderItemCommand(i.productId(), i.quantity())) .toList(), request.shippingAddress() ); // 2. 调用端口不关心实现只认接口 OrderId orderId placeOrderUseCase.execute(command); // 3. 协议转换Domain ID - HTTP Response return ResponseEntity.ok(new OrderIdResponse(orderId.value())); } }关键点PlaceOrderRequest是DTO字段名可与前端约定如customerId但绝不等于Customer实体OrderIdResponse是纯响应体orderId.value()是把领域ID转成字符串避免暴露OrderId内部结构Controller里没有new Order()、没有orderRepository.save()、没有Transactional——这些都交给Application Service和Infrastructure。逻辑说明Controller是“胶水”粘合HTTP协议和应用层接口。如果明天要改成gRPC只需新增OrderGrpcService复用同一个PlaceOrderUseCase实现。3.3 Application Service实现用领域对象编排不碰技术细节application模块的实现类才是真正的业务编排中枢。它依赖domain模块的实体但不依赖任何基础设施// application/src/main/java/com/example/order/application/PlaceOrderUseCaseImpl.java Service public class PlaceOrderUseCaseImpl implements PlaceOrderUseCase { private final OrderRepository orderRepository; private final CustomerService customerService; // 领域服务非基础设施 public PlaceOrderUseCaseImpl(OrderRepository orderRepository, CustomerService customerService) { this.orderRepository orderRepository; this.customerService customerService; } Override Transactional // 此处Transactional是业务事务非技术事务 public OrderId execute(PlaceOrderCommand command) { // 1. 用领域服务校验客户有效性可能调用外部API但封装在CustomerService里 customerService.validateCustomer(command.customerId()); // 2. 创建聚合根业务规则在此执行 var order Order.create( OrderId.generate(), command.items().stream() .map(i - OrderItem.create(i.productId(), i.quantity())) .toList() ); // 3. 保存聚合调用仓储接口具体实现由infrastructure提供 orderRepository.save(order); // 4. 返回领域ID不暴露内部结构 return order.getId(); } }参数说明OrderRepository是domain模块定义的接口infrastructure模块提供JpaOrderRepository实现CustomerService是领域服务可能封装了调用会员中心HTTP API的逻辑但它对PlaceOrderUseCaseImpl来说只是个接口Transactional标记此处需要ACID事务框架会自动开启数据库事务——但注意这是业务层面的事务声明不是技术细节。为什么这样分因为当你要把订单保存从MySQL换成MongoDB时只需重写infrastructure模块的JpaOrderRepository为MongoOrderRepositoryPlaceOrderUseCaseImpl一行代码都不用改。4. 上下文映射实战当订单要查会员等级时别写跨库JOIN用Customer/Supplier关系DDD最难的部分从来不是建模而是处理多个限界上下文之间的协作。教程里总说“用防腐层”但没人告诉你什么时候该用Published Language什么时候必须用Customer/Supplier什么时候干脆该合并上下文。我在电商项目里踩过最深的坑就是让订单服务直接调用会员服务的HTTP API查等级——结果会员服务一抖动订单创建超时整个下单链路雪崩。4.1 识别上下文边界用“变更频率”和“归属权”双维度判断先别急着画图。打开你的需求文档找三个关键词谁提的需求业务方A提“订单页加会员等级图标”B提“会员等级升级规则调整”谁改的最多过去半年订单表字段改了7次会员表改了2次谁拥有数据主权会员等级计算规则由会员中心制定订单中心只消费结果如果答案分别是A、A、B那就清晰了订单是Customer会员是Supplier。这意味着订单上下文不能强依赖会员API必须接受“最终一致性”会员上下文要主动发布等级变更事件订单上下文订阅并更新本地缓存双方约定的数据格式如MemberLevelUpdatedEvent就是Published Language必须版本化管理。注意不要用“订单需要会员数据”就认为该合并上下文。合并的代价是会员中心每次发版都要同步通知订单中心双方耦合度飙升。Customer/Supplier关系下订单中心可以独立发版只要保证事件订阅逻辑健壮。4.2 实现Customer/Supplier用事件驱动的最终一致性在infrastructure模块用Spring Kafka实现事件订阅。先定义Published Language// domain/src/main/java/com/example/member/domain/event/MemberLevelUpdatedEvent.java Value Builder public class MemberLevelUpdatedEvent { String memberId; int level; Instant updatedAt; String version; // 1.0 }会员上下文Supplier发布事件// member-infrastructure/src/main/java/com/example/member/infrastructure/kafka/MemberEventPublisher.java Service public class MemberEventPublisher { private final KafkaTemplateString, String kafkaTemplate; public void publish(MemberLevelUpdatedEvent event) { // 序列化为JSON发送到topic: member-level-updated kafkaTemplate.send(member-level-updated, event.getMemberId(), new ObjectMapper().writeValueAsString(event)); } }订单上下文Customer订阅并更新本地缓存// order-infrastructure/src/main/java/com/example/order/infrastructure/kafka/MemberLevelEventListener.java Component public class MemberLevelEventListener { private final MemberLevelCache memberLevelCache; // 本地Caffeine缓存 KafkaListener(topics member-level-updated, groupId order-group) public void listen(String payload) throws JsonProcessingException { MemberLevelUpdatedEvent event new ObjectMapper() .readValue(payload, MemberLevelUpdatedEvent.class); // 更新本地缓存不操作订单数据库 memberLevelCache.put(event.getMemberId(), event.getLevel()); } }关键点订单服务不调用会员HTTP API只消费事件本地缓存有过期策略如10分钟即使事件丢失也能兜底version字段用于兼容性控制当会员中心发version2.0事件时订单服务可选择忽略或升级处理器。逻辑说明这就是Customer/Supplier的落地形态——Supplier单向发布Customer被动接收。没有循环依赖没有网络超时风险故障隔离彻底。4.3 防腐层ACL编码实践把外部API响应转成领域对象当必须调用外部系统如物流查询时ACL不是“写个代理类”而是严格的数据翻译层。以物流上下文为例// order-infrastructure/src/main/java/com/example/order/infrastructure/anti_corruption/LogisticsApiClient.java Component public class LogisticsApiClient { private final RestTemplate restTemplate; // 外部API返回的原始DTO含各种奇怪字段 public record ExternalTrackResponse( String trackingNo, String status, String updateTime, ListExternalTrackStep steps ) {} public LogisticsTrackingResult queryTracking(String trackingNo) { ExternalTrackResponse response restTemplate.getForObject( https://logistics-api.com/track?no{no}, ExternalTrackResponse.class, trackingNo ); // 防腐层核心把外部DTO转成领域对象过滤无关字段 return LogisticsTrackingResult.builder() .trackingNo(trackingNo) .status(mapStatus(response.status())) // DELIVERED - DeliveryStatus.DELIVERED .updateTime(Instant.parse(response.updateTime)) .steps(response.steps().stream() .map(this::mapStep) // 外部step转领域step .toList()) .build(); } private DeliveryStatus mapStatus(String externalStatus) { return switch (externalStatus) { case DELIVERED - DeliveryStatus.DELIVERED; case IN_TRANSIT - DeliveryStatus.IN_TRANSIT; default - DeliveryStatus.UNKNOWN; }; } }参数说明ExternalTrackResponse是对外部API的精确映射只存在于infrastructure模块LogisticsTrackingResult是领域对象定义在domain模块字段名、类型、约束都符合业务语义mapStatus()是防腐逻辑把外部字符串转成本地枚举屏蔽外部变更影响。为什么叫“防腐”因为当物流API把IN_TRANSIT改成TRANSITING时你只需改mapStatus()这一行domain模块的DeliveryStatus枚举和所有业务逻辑完全不受影响。5. 避坑指南那些让DDD项目上线即翻车的5个血泪经验DDD落地不是技术问题而是认知问题。很多团队按教程走完流程上线后却陷入“模型很美、代码很乱、改需很慢”的怪圈。以下是我亲身踩过的5个坑每一条都附带现象、原因和可立即执行的解决方案。它们不是理论推演而是从三次线上事故、四次重构、七次Code Review中熬出来的。5.1 现象聚合根里塞了太多业务方法导致类膨胀到2000行新人不敢改原因混淆了“聚合内一致性”和“业务用例编排”。聚合根只应保护自身不变性如“订单状态只能从CREATED→CONFIRMED”而不该承担跨聚合的协调逻辑如“确认订单后扣减库存”。后者是Application Service的职责。解决用ArchUnit强制校验聚合根方法数。在domain模块加测试Test public void aggregate_root_should_have_less_than_10_methods() { JavaClasses classes new ClassFileImporter().importPackages(com.example.order.domain); ArchRuleDefinition.classes() .that().areAnnotatedWith(Aggregate.class) .should().haveNumberOfMethodsLessThan(10) .check(classes); }提示超过10个方法说明你把Application Service逻辑塞进了聚合根。拆出来用领域事件解耦。5.2 现象用DTO当领域对象传参Controller里new OrderDto()再转Order改字段要改6处原因把“传输对象”和“领域对象”混为一谈。DTO是协议契约领域对象是业务规则载体二者生命周期、变更频率、约束条件完全不同。解决用MapStruct做零反射转换并禁止在领域层使用DTO。在application模块定义转换器接口Mapper public interface OrderMapper { OrderMapper INSTANCE Mappers.getMapper(OrderMapper.class); Mapping(target id, ignore true) // 领域ID由工厂生成 Order toDomain(PlaceOrderCommand command); Mapping(target customerId, source order.customerId.value) OrderDto toDto(Order order); }然后在PlaceOrderUseCaseImpl里直接调OrderMapper.INSTANCE.toDomain(command)。DTO只在interface模块存在application和domain模块永远看不到OrderDto。5.3 现象所有仓储接口都叫XxxRepository但有的查数据库有的查Redis有的调HTTP调用方无法感知差异原因违反了“接口隔离原则”。仓储接口应该表达业务意图如findActiveOrdersByCustomerId而不是技术实现如findByStatusAndCustomerId。解决按业务场景命名方法并用ArchUnit校验。在domain模块定义public interface OrderRepository { // ✅ 表达业务意图 ListOrder findActiveOrdersByCustomerId(CustomerId customerId); // ❌ 技术导向删除 // ListOrder findByStatusAndCustomerId(String status, String customerId); }再加ArchUnit测试Test public void repository_method_name_should_contain_business_intent() { JavaClasses classes new ClassFileImporter().importPackages(com.example.order.domain); ArchRuleDefinition.methods() .that().areDeclaredInClassesThat().implement(OrderRepository.class) .should().haveNameMatching(find.*By.*|count.*By.*|exists.*By.*) .check(classes); }5.4 现象领域事件用String发Kafka消费者收到后JSON.parse()报错排查3小时才发现字段名大小写不一致原因Published Language没有契约管理。事件结构随心所欲发布方和订阅方各自解析毫无版本兼容性。解决用Avro Schema定义事件并用Confluent Schema Registry管理。在domain模块定义Avro Schema文件member-level-updated.avsc{ type: record, name: MemberLevelUpdatedEvent, namespace: com.example.member.domain.event, fields: [ {name: memberId, type: string}, {name: level, type: int}, {name: updatedAt, type: long, logicalType: timestamp-millis}, {name: version, type: string} ] }然后用avro-maven-plugin生成Java类。所有事件生产者和消费者都用同一份Schema字段名、类型、默认值全部强制一致。5.5 现象团队画出完美的限界上下文图但代码里order包下有member类member包下有order类边界形同虚设原因没有把上下文边界变成可执行的代码约束。画图是起点不是终点。解决用ArchUnit定义上下文边界规则。在order-infrastructure模块加测试Test public void order_context_should_not_access_member_domain_classes() { JavaClasses classes new ClassFileImporter().importPackages(com.example); ArchRuleDefinition.noClasses() .that().resideInAPackage(..order..) .should().accessClassesThat().resideInAPackage(..member.domain..) .because(Order context must not depend on Member domain model) .check(classes); }这条规则会拦截所有import com.example.member.domain.Member;语句。如果真需要会员数据必须通过MemberService接口——而MemberService定义在application模块实现类在member-infrastructure模块完美符合Customer/Supplier。6. 终极验证用ArchUnit写“DDD健康度报告”让每次PR都自动打分DDD落地最大的幻觉是以为“模型画完了、代码写完了、功能上线了”就结束了。真实情况是新同学加入后随手在Controller里加了个Autowired OrderRepository三个月后没人记得当初为什么要把仓储抽成接口。DDD不是一次性的设计活动而是持续的代码纪律维护。我最后压箱底的技巧就是用ArchUnit生成一份可量化的“DDD健康度报告”让它成为CI流水线的硬性门禁。6.1 定义6项核心指标每项对应一个ArchUnit规则我把DDD落地质量拆解为6个可测量维度每个维度用一条ArchUnit规则实现。这些规则全部放在infrastructure模块的src/test/java下作为mvn verify的一部分指标规则描述代码片段合格线分层纯净度infrastructure不能importdomain实现类noClasses().that().resideInAPackage(..infrastructure..).should().accessClassesThat().resideInAPackage(..domain..).andDoNot().resideInAnyPackage(..domain..repository..)100%通过聚合内聚度Aggregate类的方法数≤8classes().that().areAnnotatedWith(Aggregate.class).should().haveNumberOfMethodsLessThan(9)≥95%通过率上下文隔离度order包不引用member包的领域类noClasses().that().resideInAPackage(..order..).should().accessClassesThat().resideInAPackage(..member.domain..)100%通过DTO纯洁度domain模块不出现Dto、VO、Response字样noClasses().that().resideInAPackage(..domain..).should().haveSimpleNameContaining(DtoVO事件契约度所有领域事件类必须在domain.event包下classes().that().haveSimpleNameEndingWith(Event).should().resideInAPackage(..domain.event..)100%通过仓储意图度Repository接口方法名必须含find/count/existsmethods().that().areDeclaredInClassesThat().implement(Repository.class).should().haveNameMatching(find.*By.*|count.*By.*|exists.*By.*)≥90%通过率6.2 生成HTML报告让健康度可视化ArchUnit本身不支持HTML报告但我们可以用archunit-junit5的ArchRules结合JUnit Platform的TestExecutionSummary生成。在pom.xml中添加插件plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId configuration systemPropertyVariables archunit.report.dir${project.build.directory}/archunit-report/archunit.report.dir /systemPropertyVariables /configuration /plugin然后写一个汇总测试类// DDDHealthCheckTest.java SpringBootTest public class DDDHealthCheckTest { private static final ListArchRule RULES Arrays.asList( LAYER_PURITY_RULE, AGGREGATE_COHESION_RULE, CONTEXT_ISOLATION_RULE, DTO_PURITY_RULE, EVENT_CONTRACT_RULE, REPOSITORY_INTENT_RULE ); Test public void run_ddd_health_check() { JavaClasses classes new ClassFileImporter() .importPackages(com.example); RuleResults results new RuleResults(); for (ArchRule rule : RULES) { results.add(rule.evaluate(classes)); } // 输出Markdown格式报告到target/archunit-report/health.md generateReport(results); } private void generateReport(RuleResults results) { try (PrintWriter writer new PrintWriter( Paths.get(target, archunit-report, health.md).toFile())) { writer.println(# DDD Health Report); writer.println(Generated on LocalDateTime.now()); writer.println(); int passed 0, total results.size(); for (RuleResult result : results) { String status result.isViolated() ? ❌ FAIL : ✅ PASS; if (!result.isViolated()) passed; writer.printf(- %s: %s (%d violations)%n, result.getRule().getDescription(), status, result.getErrorCount()); } writer.println(); writer.printf(## Overall: %d/%d (%.1f%%)%n, passed, total, (double) passed / total * 100); } catch (IOException e) { throw new RuntimeException(e); } } }运行mvn verify后target/archunit-report/health.md会生成类似这样的报告# DDD Health Report Generated on 2024-06-15T14:22:33.123 - Infrastructure layer purity: ✅ PASS (0 violations) - Aggregate cohesion: ✅ PASS (0 violations) - Context isolation: ❌ FAIL (3 violations) - DTO purity: ✅ PASS (0 violations) - Event contract: ✅ PASS (0 violations) - Repository intent: ✅ PASS (0 violations) ## Overall: 5/6 (83.3%)6.3 CI流水线集成健康度低于90%时阻断发布在Jenkins或GitLab CI中把健康度报告作为门禁# .gitlab-ci.yml ddd-health-check: stage: test script: - mvn verify -DskipTestsfalse artifacts: paths: - target/archunit-report/health.md rules: - if: $CI_PIPELINE_SOURCE merge_request_event when: always publish-release: stage: deploy script: - | HEALTH_SCORE$(grep Overall: target/archunit-report/health.md | awk {print $3} | sed s/%//) if (( $(echo $HEALTH_SCORE 90 | bc -l) )); then echo DDD health score $HEALTH_SCORE% 90%. Blocking release. exit 1 fi echo DDD health OK. Proceeding... needs: [ddd-health-check]这就是我坚持了三年的习惯DDD不是设计文档里的漂亮图形而是每次PR合并前CI流水线里那个红色的❌和绿色的✅。当新同学想在Controller里直接new一个领域对象时ArchUnit会立刻报错他必须停下来思考“这个逻辑真的属于这一层吗”——那一刻DDD才真正活在了代码里。希望帮到你。本文还有配套的精品资源点击获取