
我见过一个 CRUD 接口,保存一条用户记录需要改七个文件。
Repository 接口、Repository 实现、Domain Service、Application Service、DTO、Assembler、Controller。七层间接。每一层都有它存在的道理——关注点分离、依赖倒置、领域模型纯洁性。
加在一起就是:你的团队每写一个新接口,都要过七道门。规矩就是规矩,一道都不能少。
这不是反 DDD 的文章。DDD 是我见过最体系化的领域建模方法论,Eric Evans 在 2003 年给了我们一套语言来思考复杂业务。二十年间确实有大型系统靠 DDD 保持了长期可演进性。但二十年后的现实是,大多数团队在用 DDD 的时候,并没有算清楚一笔账:
为了领域模型的纯洁性,你的团队每天在付出多少工程成本?这些成本什么时候能赚回来?
问题不是"DDD 是不是好方法论"。问题是:你的团队,扛得住吗?
我做了一个实验。同一个 feature,分别用简单三层和 DDD 战术模式实现一遍。然后逐文件、逐行数、逐跳转地量化比较。
下面的文件数和行数来自这次对比,时间数据为经验估算。你可以不同意我的结论,但数据摆在这里。
一、成本清单:同一个 feature,两种写法
实验对象选了一个恰好处在"DDD 边界"上的 feature——用户注册。
为什么选它?因为它有 2-3 条业务规则(邮箱格式校验、密码强度校验、注册后发欢迎邮件),不是纯 CRUD,但也没有复杂到"不用 DDD 就活不下去"的程度。这种"可上可不上"的 feature 最能体现成本差异——因为两种架构都能实现,只是代价不同。
简单三层:3 个文件搞定
// handler.go — 接收请求、调 service、返回响应
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
var req RegisterRequest
json.NewDecoder(r.Body).Decode(&req)
user, err := service.Register(r.Context(), req.Email, req.Password, req.Name)
// ... 返回
}
// service.go — 校验 + 持久化 + 事件通知,全在这
func Register(ctx context.Context, email, password, name string) (*User, error) {
if _, err := mail.ParseAddress(email); err != nil {
return nil, errors.New("invalid email")
}
if err := validatePassword(password); err != nil {
return nil, err
}
// 检查唯一性 → 创建 → 发欢迎邮件
user, err := repo.CreateUser(ctx, email, password, name)
go sendWelcomeEmail(user.Email, user.Name)
return user, nil
}
// repo.go — SQL 操作
func CreateUser(ctx context.Context, email, password, name string) (*UserRecord, error) {
// INSERT INTO users ...
}
3 个文件,约 95 行代码。新人从 handler 跟到 repo,跳 2 次就理解全流程。整个调用链条是直觉性的:“谁处理请求?谁管逻辑?谁碰数据库?“每一跳都有明确的职责变化,不需要额外的概念储备。
DDD 战术模式:7 个文件
// controller.go — HTTP 入口
// dto.go — RegisterCommand + RegisterResult + Assembler
// user_application_service.go — 编排:调 domain service + 发布事件
// user_domain_service.go — 业务规则:构造值对象 + 唯一性校验 + 创建聚合根
// user_entity.go + value_objects.go — 聚合根 + Email/Password 值对象
// user_repository.go — Repository 接口定义
// user_repository_mysql.go — Repository 实现
7 个核心文件,约 210 行代码。新人从 controller 跟到 repository 实现,跳 6 次。而且这 6 次跳转的性质不一样——简单三层的 2 次跳转都是"下一层做什么"的直觉理解,DDD 的 6 次中有 3 次需要你先理解一个 DDD 概念才能跳过去。

量化对比
| 维度 | 简单三层 | DDD 战术模式 | 倍率 |
|---|---|---|---|
| 文件数 | 3 | 7 | 2.3x |
| 代码行数 | ~95 | ~210 | 2.2x |
| 依赖层数 | 2 | 4 | 2x |
| 新人跳转次数 | 2 | 6 | 3x |
2.2 倍的代码量。这是看得见的成本——打开编辑器就能感受到。
但更贵的是看不见的成本。那 6 次跳转中,有 3 次不是"下一层做什么"的自然跳转,而是需要理解 DDD 概念才能过的关卡:
- “为什么校验逻辑在值对象构造函数里?"——你得理解值对象的设计意图是"创建即合法”
- “为什么有接口还有实现?就一个 MySQL。"——你得理解依赖倒置,让领域层不依赖具体数据库实现,同时方便测试时用 mock 替代真实存储
- “Application Service 和 Domain Service 有什么区别?"——这是新手最容易搞混的区分
每一个概念门槛,新人都需要额外的学习时间。

新人看懂简单三层全流程:大约 15 分钟。打开 3 个文件,从上到下读一遍,完了。
新人看懂 DDD 版全流程:大约 45-60 分钟(基于 3 名不同背景新人的观察,非精确实测)。打开 7 个文件,边读边查"这个概念是什么意思”。如果这位新人完全没有 DDD 背景,再额外加 2-4 小时去读什么是聚合根、什么是值对象、为什么 Repository 要定义接口。
这意味着什么?假设你的团队每季度有 1 个新人入职,每个新人在 DDD 代码库上的 onboarding 比简单架构多花 3-4 小时。一年下来,仅仅是"让新人看懂代码"这一项,就多投入了 12-16 小时的纯理解时间。
二、何时浪费:纯 CRUD 场景的空转成本
DDD 最尴尬的场景:模块没有业务规则,但团队规范要求所有模块统一 DDD。
后台管理系统里的标签管理、分类管理、配置管理——增删改查,字段简单,没有校验规则,没有级联逻辑,没有事件通知。简单三层写这种模块:3 个文件,120 行,逻辑清晰到不需要注释。DDD 走规范:7 个文件,210 行,但打开 domain_service.go 一看是空壳,只做参数透传。entity 只有 getter,没有行为方法。DTO 字段和 Entity 一模一样,纯粹一对一映射。
5 个文件是"仪式性存在”——运行时只做数据透传,不包含任何判断、校验或编排逻辑。存在的唯一理由是"架构规范要求这一层存在”。

每个纯 CRUD 接口,DDD 版多写约 90 行仪式代码。
一个典型的后台管理系统有 15-20 个模块。以我经历的项目来看,其中约 15 个是纯 CRUD 模块,几乎没有业务规则。15 × 90 = 1350 行仪式代码。它们需要维护、需要 code review、需要写测试、需要在每次重构时一起动,但不产生任何业务价值。
一旦团队确立了 DDD 规范,所有模块都要走这个流程。你很难跟团队说"标签管理用简单三层,订单模块用 DDD”——两套规范并存的心智负担比一套复杂规范更让人崩溃。于是大多数团队的选择是全上 DDD,大半模块都在为"领域模型纯洁性"缴税,而它们根本没有领域模型可言。
你不是在为复杂业务付费,你是在为"统一规范"付费。复杂业务只占少数,但 100% 的模块都在承担成本。
三、何时值回:变更隔离的真正价值
上一章把 DDD 说得一无是处。但如果 DDD 真的只有成本没有收益,它不会存活二十年还有人用。
DDD 的价值不在你第一次写代码的时候,它的价值在第 N 次需求变更时才显现。
场景:用户注册模块上线三个月后,产品经理带着三个新需求来了——
- 增加手机号注册(与邮箱二选一)
- 增加邀请码逻辑(有邀请码的用户获得额外权限)
- 注册成功后除了发邮件,还要发站内通知
这三个需求叠加在一起,就是一次典型的"业务规则扩展"。
简单三层改起来
service.go 的 Register 函数从 30 行膨胀到 80+ 行。手机号校验逻辑、邮箱变可选的判断、邀请码查验、站内通知调用,全塞进同一个函数。函数签名从 3 个参数变成 5 个。内部多了 4 个 if-else 分支。
你改邀请码逻辑的时候,得小心别碰到旁边的邮箱校验。你加站内通知的时候,得确认不会影响欢迎邮件的发送。改完之后,测试得覆盖整个函数——因为所有逻辑挤在一起,你不确定这次修改有没有副作用。
三个月后再来一轮需求(加微信登录、加风控校验),这个函数就是 150 行的意面代码了。每个人改之前都要先花 10 分钟读懂整个函数。
DDD 版改起来
手机号校验 → 新增一个 Phone 值对象,独立文件,25 行。校验逻辑封装在构造函数中,和 Email 值对象互不干扰。
邀请码逻辑 → domain_service 加一段规则,15 行。不影响原有的邮箱唯一性校验流程。
站内通知 → 新增一个事件 handler,15 行。主流程完全不碰——它只是多监听了一个事件。
每个改动都住在自己的"房间"里。你改手机号校验出了 bug,不会影响邀请码逻辑。你加站内通知出了 bug,注册主流程依然正常运行。

| 维度 | 简单三层 | DDD |
|---|---|---|
| 改动文件数 | 3 | 5-6 |
| 单文件最大改动 | 50+ 行 | 25 行 |
| 引入 bug 风险 | 高 | 低 |
| 测试影响范围 | 重测整个 Register | 只测新增部分 |
注意一个反直觉的点:DDD 改动文件数更多,但每个改动更安全。
为什么能只测局部?因为 DDD 分层后领域逻辑不依赖数据库和外部服务,一个纯函数调用就能验证——不用起容器、不用 mock HTTP。这是变更隔离之外的第二重收益:可测试性。
这就是"变更隔离"的价值——不是让你少改文件,是让每次改动的爆炸半径更小。简单三层的痛点不在第一次写的时候,在第五次、第十次往同一个函数里塞东西的时候。而 DDD 的好处也不在第一次写的时候,在第五次、第十次变更时每个改动仍然只影响一小块的时候。
当你的业务每月迭代 2-3 次需求时,这种安全感会持续累积。但"每月 2-3 次业务规则变更"是门槛。如果你的模块一个季度才改一次,这份安全感的价格就太贵了。
前面讨论的成本和收益全部属于 tactical DDD——聚合根、值对象、Repository 分层那些。但 DDD 还有另一半。
四、Break-even:到底多少次变更才回本?
有了前面的数据,可以做一件大多数 DDD 文章不做的事情——定量回答"多少次变更后你的初始投入能回本"。
成本模型
以下时间数据为个人经验估算,建议用你团队的实际数据代入:
初始额外投入 C_init:
DDD 版比简单三层多 ~115 行代码 + 设计思考时间
≈ 2.5 小时(按团队平均编码+设计讨论速度)
每次变更节省 C_save:
简单版:改大函数 + 全函数回归测试 ≈ 1.5 小时
DDD版:改独立模块 + 局部测试 ≈ 1.0 小时
C_save ≈ 0.5 小时/次
Break-even = C_init / C_save = 2.5 / 0.5 = 5 次变更
5-15 次业务变更后回本(取决于变更命中率和团队 DDD 熟练度)。理想条件下 5 次,现实条件下可能需要 10-15 次。为什么有这个范围?三个前提:
第一,变更必须命中领域逻辑——新校验、新事件流、新领域概念。“加个字段"不触及领域逻辑,DDD 版不比简单三层省时间。按 50% 命中率算,实际 break-even 约 10 次。
第二,团队成员必须正确使用 DDD 模式。业务逻辑写进 Application Service 而不是 Domain Service,“变更隔离"就打折——逻辑没放对位置,隔离无从谈起。
第三,C_init 不止写代码。还包括团队概念对齐、code review 中反复讨论"逻辑该放哪层”。新手团队的 C_init 可能是 2.5 小时的 3-5 倍。

团队门槛:不是一个人的决定
DDD 不是个人运动。你可以一个人写出完美的三层架构,但你不能一个人"做 DDD”。
DDD 要求团队成员共享一套概念体系——聚合根的边界在哪里?这个逻辑是放值对象还是领域服务?这个事件应该用领域事件还是集成事件?这些问题没有标准答案,需要团队讨论达成共识。
讨论成本与团队规模正相关。3 人团队每次边界讨论 30 分钟就能对齐,8 人团队同一个问题要讨论 1.5 小时,因为每个人对 DDD 的理解深度不同,观点分歧更多。
还有一个更隐蔽的坑:如果团队中 DDD 经验不足的人超过一半,讨论的产出质量会明显下降。结果就是形式上是 DDD(文件结构、命名都对),实际上是"披着 DDD 外衣的意面代码"。Domain Service 里面全是 if-else,值对象只做了类型包装不做校验,Repository 接口的抽象粒度和具体实现完全耦合。这种"半吊子 DDD"才是最坑的——你付出了 DDD 的全部成本(7 个文件、4 层间接),但收获了零收益(没有真正的变更隔离)。

判断框架:你的团队满足几个信号?
下面的表格帮你诊断现状——你的团队打几个勾:
| # | 信号 | 你的情况 |
|---|---|---|
| 1 | 团队 ≥ 3 人有 DDD 实践经验(做过项目,不是看过书) | □ |
| 2 | 业务规则变更频率 ≥ 每月 2 次 | □ |
| 3 | 系统预期生命周期 ≥ 2 年 | □ |
| 4 | 有领域专家可以协作(Ubiquitous Language 有来源) | □ |
| 5 | 团队愿意为"概念讨论"投入时间(每周 1-2 小时设计讨论) | □ |
满足 ≥ 3 个 → DDD 战术模式可能值得投入。投入之前建议先选 1-2 个核心模块做试点,不要一上来就全面铺开。 满足 < 3 个 → 建议从 strategic DDD 入手(Bounded Context 划分 + 通用语言),不上 tactical patterns。Strategic DDD 的成本很低(主要是思维方式的转变),但收益很大(清晰的系统边界和团队沟通语言)。
这里有个被很多教程忽略的关键区分:Eric Evans 的 DDD 原书前几部分讲 tactical design(Entity、Value Object、Repository、Domain Service 等代码级构建块),后半部分(Part IV)集中讲 strategic design(Bounded Context、Context Map、Distillation)。很多我读过的中文教程直接跳到 tactical 部分,上来就教你写聚合根、写 Repository 接口。但 strategic DDD 的投入产出比远高于 tactical:它的思考过程几乎零代码成本,却能解决团队协作中最根本的问题——“我们说的是同一件事吗?“不过要注意,如果涉及已有系统重新划分 Bounded Context,落地时的组织协调和代码迁移成本不可忽视。
收束:三问决策表

上面的 5 信号帮你判断现状,下面三个问题帮你做最终决定:
| 你的场景 | 变更频率 | 团队 DDD 经验 | 建议 |
|---|---|---|---|
| 核心业务模块 | 月均 3-4 次 | ≥ 3人有经验 | ✅ 上 tactical DDD |
| 核心业务模块 | 月均 3-4 次 | < 3人有经验 | ⚠️ 先培训再上,或轻量级 DDD |
| 中等模块 | 月均 1-2 次 | 有经验 | ⚠️ 视系统生命周期决定(预期 > 2 年则值得) |
| 低频/配置类模块 | 季度 1 次 | — | ❌ 简单三层足够 |
| 纯 CRUD 无规则 | 几乎不变 | — | ❌ DDD 是明确浪费 |
轻量级 DDD:只用值对象封装核心校验规则 + 领域事件解耦通知,不强制全套分层和 Repository 接口抽象。
DDD 不是坏东西。它是一个好方法论,只是有成本的好方法论。问题是:DDD 在你的场景里值不值。
在你决定"全面推行 DDD"之前,先问自己三个问题:
- 你的业务规则变更频率,够不够让 DDD 的初始投入在半年内回本? 如果你的核心模块每月变更不到 2 次,break-even 遥遥无期。
- 你的团队,有没有至少 3 个人能正确使用这套工具? 半吊子 DDD 的结果比不用 DDD 更差——全部成本,零收益。
- 你是否区分了 strategic DDD 和 tactical DDD? 前者轻量高效——划清边界、统一语言,几乎零成本。后者昂贵笨重——聚合根、值对象、Repository 分层,成本翻倍。大多数团队只需要前者。
如果三个问题都指向"值得”——那就大胆上。DDD 在正确的条件下,确实能让系统在长期演进中保持可控。选 1-2 个核心模块先做试点,团队磨合出手感后再铺开。
如果有一个答案是"不确定”——先别上 tactical DDD。用 strategic DDD 打地基,等团队和业务都成熟了再加。
七层间接不是免费的。你得确保每道门后面确实有值得保护的东西。
附录:实验代码
本文两组代码实验(简单三层 vs DDD 战术模式)的完整实现已开源:
GitHub:zhiyulab-evidence/ddd-three-conditions
user-registration-simple/— 简单三层实现(3 文件,~95 行)user-registration-ddd/— DDD 战术模式实现(7 文件,~210 行)
每个子目录包含完整可编译的 Go 代码。文中的文件数、行数、跳转深度等数据均可复现验证。
原文发布于 止语Lab