《DDD 六边形架构入门:8000 字带你彻底搞懂聚合根、领域服务、防腐层》

从 CRUD 到 DDD:一个 Java 业务开发的认知突围笔记

这是我转型记录系列的第二篇。上一篇我聊了从外包 Java 转型 Agent 开发的心路历程,这一篇聊聊我入职新公司后接触到的、彻底改变我对"业务代码"理解的 DDD 六边形架构

写这篇的初衷是:作为一个干了三年 CRUD 的 Java 开发,我第一次见到一个不是"Controller-Service-DAO"的项目时,受到的冲击是巨大的。 这种冲击让我意识到,所谓"高级开发"和"CRUD 开发"的区别,不在于会用多少中间件,而在于怎么组织业务代码


一、为什么需要 DDD?先看看 CRUD 的痛

我之前做过几年 ERP 和 MES 的 CRUD 开发,写代码的方式基本是:

markdown 复制代码
Controller 接参数
    ↓
Service 写业务逻辑(一个方法 200 行起步)
    ↓
DAO 操作数据库

这种写法表面上"简单直接",但项目大了之后会出现一堆经典问题:

  1. 业务逻辑散落在到处都是------同一个"订单状态流转"的逻辑,可能在 Controller 里有一段、Service 里有一段、工具类里又有一段,改一处忘一处
  2. DO(数据对象)属性被多个 Service 随意访问和修改------A 服务改了状态、B 服务改了金额、C 服务改了备注,最后状态对不上
  3. 业务规则缺乏明确归属------"机票退改签政策"这个逻辑应该放哪?放 Service 里 Service 会膨胀,放 Util 里又找不到归属
  4. 多表操作通过 Manager 层堆砌------一个方法里 5 个 DAO 调用 + 3 个外部 RPC 调用,改一行就可能引发雪崩
  5. 改个枚举要改十几个文件------因为同一个状态在 DO、VO、DTO、PO 里到处都是

核心问题:业务逻辑和数据结构没有归属,所有人都能改任何东西,最终代码变成"谁都不敢动"的祖传代码。

DDD 解决的就是这个问题------给业务逻辑找一个明确的家,谁都不能越界


二、DDD 五个核心概念,一句话讲清楚

概念 一句话解释 类比
通用语言 业务说啥代码就叫啥 业务说"下单",方法就叫 placeOrder(),不叫 saveOrderData()
限界上下文 划地盘,不同地盘里同一个词意思不一样 销售系统里的"商品"和物流系统里的"商品"是两个概念
聚合根 一组对象的"老大",对外是唯一入口 Order类包含 List<OrderItem>,外部只能操作 Order,不能直接动 OrderItem
领域服务 不属于任何对象的独立逻辑 "汇率转换"不属于某个具体订单,单独放领域服务
仓储 假装自己有数据库,只定义接口 领域层定义 OrderRepository接口,基础设施层实现它

三、聚合根、实体、值对象、领域服务------四大概念的直觉

用一个生活场景建立直觉

想象你去机场办登机手续:

DDD 概念 类比 特征
聚合根 一张机票订单 有唯一编号、是你和航空公司交互的入口、所有操作通过它
实体 订单里的每个乘客 有自己的身份证号(有标识),但不能脱离订单独立存在
值对象 乘客的行李额度 没有自己的 ID,只描述一个属性,20kg 就是 20kg
领域服务 值机柜台的工作人员 协调多个对象完成一件事(验票+分配座位+打印登机牌)

1. 聚合根(Aggregate)------ 团队的队长

Java 复制代码
// 一个典型的订单聚合根
public class OrderAggregate extends BaseAggregate {
    private String orderId;                    // 全局唯一标识
    private CustomerEntity customer;            // 客户实体
    private List<OrderItemEntity> items;        // 商品列表
    private List<PaymentEntity> payments;       // 支付记录
    private DiscountValue discount;             // 折扣值对象
}
复制代码

关键特征

特征 说明 体现
唯一入口 外部不能直接操作 OrderItem,必须通过 OrderAggregate aggregate.addItem(item)而不是 items.add(item)
事务边界 一个聚合根 = 一个事务单元 repository.save(aggregate)整体保存
一致性保证 聚合根方法内部保证业务规则不被破坏 "订单总价 = 各商品价格之和"这个规则在聚合根方法里保证

新人最容易踩的坑 :把聚合根当 POJO 用,到处 getXxx().setYyy()。这等于绕过了聚合根的所有业务规则,直接破坏一致性。

2. 实体(Entity)------ 有身份的成员

Java 复制代码
public class OrderItemEntity extends BaseEntity {
    private String itemId;          // 局部 ID
    private String productCode;     
    private Integer quantity;
    private BigDecimal price;
}

和聚合根的区别

  • 聚合根有全局 ID,外部可以通过 ID 直接引用
  • 实体有局部 ID,只在聚合根内部有意义
  • 想操作 OrderItemEntity?必须先拿到 OrderAggregate,再通过聚合根的方法操作

3. 值对象(Value Object)------ 可替换的属性描述

Java 复制代码
public class DiscountValue extends BaseValue {
    private BigDecimal amount;       // 折扣金额
    private String reason;            // 折扣原因
    private DiscountType type;        // 折扣类型
}

和实体的核心区别

维度 实体 值对象
有没有 ID ✅ 有,通过 ID 区分 ❌ 没有,通过值区分
能不能改 ✅ 可以修改属性 ❌ 理论上不可变
独立性 有自己的生命周期 依附于实体或聚合根
比喻 像一个人(即使长得一样也是不同的人) 像一张 100 元钞票(不关心是哪一张)

4. 领域服务(Domain Service)------ 协调多方的调度员

什么时候用聚合根方法,什么时候用领域服务?

场景 放在哪里 原因
计算订单总价 aggregate.calculateTotal() 只涉及聚合根内部状态
校验库存是否充足 domainService.checkInventory() 需要协调聚合根 + 仓储 + 外部系统
改变订单状态 aggregate.confirmOrder() 只涉及聚合根内部状态变更
退款流程 domainService.refund() 需要协调订单 + 支付 + 通知多个系统

判断标准

  • 能放聚合根就放聚合根(优先)
  • 只有当操作需要跨对象协调调用仓储/适配器时,才放领域服务

四者关系图

PlainText 复制代码
┌─────────────────────────────────────────────┐
│  OrderDomainService(领域服务)               │
│  "我负责协调,但我不持有数据"                   │
│  refund() → 调用仓储 → 调用聚合根 → 调用支付    │
│           │                                 │
│           ▼ 操作                             │
│  ┌────────────────────────────────────────┐ │
│  │  OrderAggregate(聚合根)                │ │
│  │  "我是入口,所有操作通过我"                │ │
│  │  confirmOrder() / addItem() / refund() │ │
│  │                                        │ │
│  │  ┌─────────┐  ┌──────────────────┐     │ │
│  │  │Customer │  │ List<OrderItem>  │     │ │
│  │  │Entity   │  │ Entity           │     │ │
│  │  │(实体)  │  │(实体)           │      │ │
│  │  └─────────┘  └────┬─────────────┘     │ │
│  │                    │                   │ │
│  │       ┌────────────▼──────────────┐    │ │
│  │       │  DiscountValue(值对象)    │    │ │
│  │       │  无 ID,描述属性,可替换      │    │ │
│  │       └────────────────────────────┘   │ │
│  └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

四、六边形架构:DDD 的标准落地姿势

DDD 不只是几个概念,它有一套完整的代码组织方式------六边形架构(Hexagonal Architecture,也叫端口和适配器架构)。

四层职责划分

PlainText 复制代码
┌──────────────────────────────────────────────┐
│  Application 层(应用层 / 编排层)              │
│  ⚙️ 流程编排,无业务逻辑                         │
│  Scenario + BPM 工作流                        │
└──────────────────────────────────────────────┘
                    ↓ 调用
┌──────────────────────────────────────────────┐
│  Domain 层(领域层 / 业务核心)                 │
│  💡 纯业务逻辑,零框架依赖                       │
│  Aggregate + Entity + Value + DomainService  │
└──────────────────────────────────────────────┘
                    ↑ 实现(依赖倒置)
┌──────────────────────────────────────────────┐
│  Infrastructure 层(基础设施层 / 技术实现)      │
│  🔧 实现 Domain 层定义的 Repository 接口        │
│  数据库、缓存、消息队列                          │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Adaptor 层(适配器层 / 防腐层)                │
│  🛡️ 隔离外部系统,做数据内外转换                  │
│  Input Adaptor + Output Adaptor              │
└──────────────────────────────────────────────┘

各层职责详解

层级 职责 关键约束
Application 流程编排(先做 A,再做 B,最后做 C) ❌ 禁止包含业务逻辑 ❌ 写服务禁止调用读服务
Domain 业务规则、状态变更、业务校验 ❌ 禁止依赖任何技术框架 ❌ 领域服务禁止互相调用
Infrastructure 数据库、缓存、消息队列等技术实现 必须实现 Domain 层定义的接口
Adaptor 接收外部请求 / 调用外部系统 Input + Output 两个方向

核心思想:依赖倒置

所有层都依赖 Domain 层,Domain 层不依赖任何层。这意味着:

  • Domain 层定义 OrderRepository 接口
  • Infrastructure 层实现这个接口(用 MyBatis、JPA 都行)
  • Domain 层只调用接口,不知道也不关心底层用了什么技术

好处

  1. 可测试性 ------ Domain 层可以单元测试,不需要启动 Spring
  2. 可替换性 ------ MySQL 换 MongoDB,Domain 层一行代码不用改
  3. 关注点分离 ------ 业务逻辑和技术实现彻底解耦

五、四种 DDD 开发模式

实战中并不是所有业务都需要"完整 DDD",根据场景的复杂度可以选择不同模式:

模式 使用场景 是否变更状态 例子
写模式 创建订单、状态流转 ✅ 通过聚合根方法 下单、支付确认、取消订单
读模式 查询 查订单详情、列表查询
规则+计算模式 带聚合根的规则匹配 ❌(纯计算) 定价规则匹配
纯计算模式 无聚合根的无状态计算 汇率换算、税费计算

写模式标准流程(最重要!)

PlainText 复制代码
1. 获取分布式锁(防止并发修改)
   ↓
2. 通过仓储加载聚合根
   ↓
3. 调用聚合根方法执行业务逻辑(状态变更)
   ↓
4. 如需调用外部服务,在状态变更之后
   ↓
5. 通过仓储持久化(repository.save(aggregate))
   ↓
6. finally 块中释放锁

为什么要这个流程?

  • 加锁 ------ 防止两个请求同时修改同一个订单(经典超卖问题)
  • 聚合根方法变更状态 ------ 保证业务规则不被破坏
  • 外部调用在变更后 ------ 避免外部成功但本地失败的不一致
  • finally 释放锁 ------ 防止异常导致死锁

六、应用层的灵魂:Scenario + BPM 工作流模式

这是我接触新项目时受冲击最大的部分。之前在 ERP 项目里,业务流程都是写在 Service 里的几百行 if-else,而新项目是这样组织的:

传统 Service 写法(反例)

Java 复制代码
public class OrderService {
    public void placeOrder(OrderDTO dto) {
        // 第 1 步:参数校验
        if (dto == null) throw new Exception();
        // 第 2 步:查库存
        Inventory inv = inventoryService.query(...);
        if (inv.getStock() < dto.getQuantity()) {
            throw new Exception("库存不足");
        }
        // 第 3 步:扣库存
        inventoryService.deduct(...);
        // 第 4 步:创建订单
        Order order = new Order(...);
        orderDao.insert(order);
        // 第 5 步:发消息
        mq.send(...);
        // ... 200 行后 ...
    }
}

问题

  • 一个方法做了 N 件事,无法局部修改
  • 异常处理散落各处
  • 想加一个新步骤要改整个方法
  • 流程不可视化

Scenario + Activity 写法(DDD 推荐)

每个业务场景拆成一个 Scenario,每个 Scenario 由若干 Activity(步骤)组成,用 BPM 工作流引擎按顺序调度

Java 复制代码
// 极其简洁的 Scenario 定义
@Component
public class PlaceOrderScenario extends BaseScenario<PlaceOrderContext> {
    @Override
    protected String scenarioName() {
        return "placeOrderScenario";
    }
    
    @Override
    protected String standardProcessName() {
        return "bpm.PlaceOrderScenario";  // 指向 BPM 流程定义(XML)
    }
}

真正的流程在 BPM XML 里定义:

XML 复制代码
<process name="placeOrderScenario">
    <activity name="ValidateActivity"/>
    <activity name="CheckInventoryActivity"/>
    <activity name="DeductInventoryActivity"/>
    <activity name="CreateOrderActivity"/>
    <activity name="SendMessageActivity"/>
</process>

每个 Activity 长这样(只有一件事):

Java 复制代码
@Component
public class CheckInventoryActivity extends BaseActivity<Void, PlaceOrderContext> {
    
    private final InventoryDomainService inventoryDomainService;
    
    @Override
    public Void process(PlaceOrderContext context) {
        // 1. 从 Context 取数据
        OrderAggregate aggregate = context.getOrderAggregate();
        
        // 2. 调用领域服务(核心逻辑就这一行)
        inventoryDomainService.checkInventory(aggregate);
        
        // 3. 把结果放回 Context(如果有)
        return null;
    }
}

这样写的好处

  1. 流程可视化 ------ BPM XML 一眼看清整个业务流程
  2. 职责单一 ------ 每个 Activity 只做一件事
  3. 易于扩展 ------ 加步骤只需新增 Activity + 改 XML
  4. 复用性强 ------ ValidateActivity 可以被多个 Scenario 复用
  5. 支持分支 ------ BPM 支持条件分支、循环、异常处理

Context:流水线上的"托盘"

Activity 之间通过 Context 传递数据。Context 就像流水线上的托盘:

PlainText 复制代码
PlaceOrderContext(托盘)
    ├── OrderDTO          ← 入参
    ├── OrderAggregate    ← 中间数据(聚合根,多步共享)
    └── OrderResultDTO    ← 出参

关键设计

  • 第一个 Activity 把聚合根放进 Context
  • 中间的 Activity 直接修改聚合根(Java 引用传递)
  • 最后一个 Activity 从 Context 取出聚合根持久化

七、防腐层(Anti-Corruption Layer):隔离外部系统

复杂业务系统往往要对接 5-10 个外部系统(支付、库存、风控、物流......)。每个外部系统都有自己的 DTO 格式、协议、错误码。

如果直接在业务代码里调用,会发生什么?

  • 外部系统 DTO 改字段 → 业务代码到处改
  • 外部系统宕机 → 业务代码到处加 try-catch
  • 业务想换一个供应商 → 业务代码大改

防腐层的作用就是隔离这些外部系统的复杂性,确保业务代码不被"污染"。

两层模式:Proxy + AdaptorImpl

PlainText 复制代码
业务代码
   ↓ 调用 Domain 层定义的接口(如 PaymentAdaptor)
   ↓
AdaptorImpl(适配器实现)
   ├── Converter:领域对象 ↔ 外部 DTO 转换
   └── 调用 Proxy
        ↓
Proxy(外部 RPC 薄封装)
   ├── 监控埋点(成功率、RT)
   ├── 日志记录
   └── 真正的 RPC 调用
        ↓
外部系统

Proxy 层职责 :薄封装、监控、日志
AdaptorImpl 层职责:数据转换、组合多个 Proxy

比喻:你可以把 Adaptor 模块想象成一个**"海关"**------

  • Input 是入境大厅,检查进来的旅客(请求)是否合规
  • Output 是出境大厅,把内部货物(领域对象)打包成符合国际标准的集装箱(外部 DTO)
  • Converter 是翻译中心,确保内外语言互通

八、一次完整的业务调用链路

把上面所有概念串起来,一次"用户下单"的完整流转是这样的:

PlainText 复制代码
用户点击"立即下单"
    ↓ HSF/HTTP 调用
┌─────────────────────────────────────────────┐
│ ① Adaptor 层(Input)                       │
│   接收外部请求,DTO → Context                 │
└──────────────────┬──────────────────────────┘
                   ↓
┌─────────────────────────────────────────────┐
│ ② Application 层                            │
│   PlaceOrderScenario.startScenario()        │
│   ├── 获取分布式锁                            │
│   ├── 启动 BPM 工作流                         │
│   │   ├── ValidateActivity                  │
│   │   ├── CheckInventoryActivity            │
│   │   ├── DeductInventoryActivity           │
│   │   ├── CreateOrderActivity               │
│   │   └── SendMessageActivity               │
│   └── 释放锁                                 │
└──────────────────┬──────────────────────────┘
                   ↓
┌─────────────────────────────────────────────┐
│ ③ Domain 层                                 │
│   OrderDomainService.placeOrder()           │
│   ├── 调用聚合根方法(业务规则)                 │
│   └── 调用 Repository 接口(持久化)           │
└──────────────────┬──────────────────────────┘
                   ↓
┌─────────────────────────────────────────────┐
│ ④ Infrastructure 层                         │
│   OrderRepositoryImpl.save()                │
│   ├── MyBatis 写数据库                       │
│   ├── Redis 缓存                            │
│   └── 发布领域事件                            │
└──────────────────┬──────────────────────────┘
                   ↓
┌─────────────────────────────────────────────┐
│ ⑤ Adaptor 层(Output)                      │
│   PaymentAdaptorImpl.deduct()               │
│   ├── Converter 转换领域对象 → 外部 DTO        │
│   └── Proxy 调用支付系统                      │
└─────────────────────────────────────────────┘
                   ↓
            外部支付系统
                   ↓
              原路返回
                   ↓
            用户看到"下单成功"

数据在每一层换"衣服"

这是新人最容易混淆的点------同一份数据在不同层有不同的格式:

数据格式 所在层 转换器
外部 DTO Client -
Context Application Assembler
Aggregate / Entity / Value Domain -
Param / Result Domain ↔ Infrastructure -
外部 Request / Response Adaptor (Output) Converter
PO(数据库对象) Infrastructure Convert

口诀:每一层只认自己的格式,通过转换器沟通,所以改一层不会影响其他层。


九、命名规范:让代码自己说话

DDD 的命名是强约束,通过后缀就能识别组件类型:

类型 后缀 继承 例子
聚合根 名词 + Aggregate BaseAggregate OrderAggregate
实体 名词 + Entity BaseEntity OrderItemEntity
值对象 名词 + Value BaseValue DiscountValue
参数对象 方法名 + Param BaseParam PlaceOrderParam
结果对象 方法名 + Result BaseResult PlaceOrderResult
领域服务 名词 + DomainService - OrderDomainService
仓储接口 名词 + Repository - OrderRepository
仓储实现 名词 + RepositoryImpl - OrderRepositoryImpl
适配器 名词 + Adaptor - PaymentAdaptor
写应用服务 名词 + AppService ApplicationCmdService OrderAppService
读应用服务 名词 + QueryAppService ApplicationQueryService OrderQueryAppService
组装器 功能 + Assembler - OrderAssembler

聚合根方法命名:必须用业务动词

Java 复制代码
// ❌ 反例
order.save();
order.update();
order.process();

// ✅ 正例
order.confirmPayment();
order.cancel();
order.addItem();
order.applyDiscount();

为什么?

  • save/update/process 是技术词汇,毫无业务含义
  • 业务动词让代码自己讲业务故事
  • 业务方和开发能用同一套语言沟通(通用语言

十、DDD 不是银弹:什么场景适合 DDD?

最后必须泼一盆冷水------DDD 不是万能的,它有明显的适用边界:

适合 DDD 的场景

  • 业务复杂度高(电商、金融、出行、供应链)
  • 业务规则需要长期演进
  • 多人协作的大型项目
  • 需要和业务方紧密沟通

不适合 DDD 的场景

  • 简单 CRUD 项目(后台管理系统、小工具)
  • 业务规则极少(数据展示类应用)
  • 小团队、短生命周期项目
  • 强调极致性能的底层组件

强行上 DDD 的代价

  • 类的数量爆炸(一个简单功能可能涉及 10+ 个类)
  • 学习曲线陡峭(新人要 1-3 个月才能上手)
  • 简单需求开发慢(写 5 个类来完成一个 CRUD)

所以:是否用 DDD,要看业务复杂度,不要为了 DDD 而 DDD。


十一、给新人的学习建议

如果你和我一样,是从传统 CRUD 开发转 DDD,给你几条踩坑总结:

1. 先读项目,再读理论

DDD 的书(如《领域驱动设计》《实现领域驱动设计》)非常厚,纯看理论会劝退。先扎进一个真实的 DDD 项目,对照代码看理论,理解会快 10 倍。

2. 从聚合根开始读

找到项目里的核心聚合根(通常名字叫 XxxAggregate 且体量最大),把它的所有方法过一遍。聚合根的方法就是业务的全貌

3. 跟着 Scenario 走一遍流程

找一个核心业务场景(比如下单),从 AppService 入口开始,一路追到 Activity → DomainService → Aggregate → Repository。走通一个完整链路,整个架构就活了

4. 不要害怕"过度设计"的感觉

你会发现 DDD 项目里很多类只有 5-10 行代码,会觉得"为什么要拆这么细"。这种"细"是为了未来 3 年的可维护性买的保险,初看冗余,长期受益。

5. 学习业务比学习技术更重要

DDD 的核心是"领域",不是"驱动"也不是"设计"。你越懂业务,DDD 用得越好。所以遇到不懂的业务术语,别绕过去,问业务方或老同事。


写在最后

我转型路上一直在思考一个问题:为什么同样是写 Java,外包公司的代码和大厂的代码差距那么大?

接触 DDD 之后我有了答案------差距不在技术栈(都是 Spring + MyBatis),差距在于代码组织能力

CRUD 写法的代码,所有人都能写,所以也就没有壁垒。而 DDD 写法的代码,需要你既懂业务又懂架构,这就形成了护城河

如果你也是从 CRUD 转型的开发者,我的建议是:别只学 Redis、RocketMQ、SpringCloud 这些"工具",多花时间学 DDD、整洁架构、领域建模这些"思想" 。前者会被 AI 替代,后者不会。

相关推荐
刀法如飞5 天前
一款Python语言Django框架DDD脚手架,适合中大型项目
后端·python·领域驱动设计
Karl_wei11 天前
Vide Coding 的基础:LLM 大模型
llm·ai编程·领域驱动设计
都说名字长不会被发现17 天前
领域驱动 - 战略设计实践
领域驱动设计·领域驱动·战略设计·领域划分·微服务划分·领域驱动实战
一条咸鱼_SaltyFish22 天前
DDD 架构重构实践:AI Skills 如何赋能DDD设计与重构
java·人工智能·ai·重构·架构·ddd·领域驱动设计
小的时候可菜了23 天前
DDD架构设计的本质与演进
领域驱动设计
TT_Close1 个月前
AI 生图不听话?给它戴上“封面金箍”,分分钟搞定全平台封面
ai编程·领域驱动设计
G探险者1 个月前
架构演进之 DDD:从 CRUD 到领域驱动设计
后端·架构·领域驱动设计
唯一世1 个月前
务实 DDD:在 Spring Boot 中平衡“纯粹性”与“开发效率”的落地实践
领域驱动设计
递归尽头是星辰2 个月前
DDD 认知升级:从单服务战术落地,到分布式中台战略全景
领域驱动设计·架构设计·微服务拆分·ddd 落地实践·ddd 战略战术