Domain-Driven Design(DDD)
DDD 是处理复杂业务领域的建模与设计方法:通过 Ubiquitous Language、Subdomain、Bounded Context 建立战略边界,再在上下文内部用 Entity、Value Object、Aggregate、Domain Service、Domain Event 等战术模式表达业务规则。
[!info] related notes
- 统一语言: Ubiquitous Language
- 边界: DDD 限界上下文
- Service: 领域服务 vs 应用服务
- 状态变化: DDD 实体数据变动流程
- 架构关系: 事件驱动架构
- 分层参考: 项目分层架构
Domain-Driven Design(DDD)
一句话定义
DDD 的核心不是“使用很多设计模式让代码更优雅”。
更准确地说:
DDD 用业务语言和领域模型控制复杂业务知识,使软件边界与业务边界尽量一致。
它最适合:
- 业务规则复杂;
- 术语容易产生歧义;
- 多团队协作;
- 规则长期演化;
- 数据模型不能只靠 CRUD 表结构表达;
的系统。
DDD 先解决什么问题
一个复杂系统常出现:
业务说“订单已确认”
开发说 status = 2
财务说“确认”代表已记账
仓储说“确认”代表可出库
这时问题不是缺一个 Repository pattern。
真正问题是:
- 语言不统一;
- 边界不清楚;
- 同一个词在不同上下文含义不同;
- 代码结构无法表达业务 invariant。
DDD 首先解决这些建模问题。
Strategic DDD
Strategic DDD 负责:
系统应该按哪些业务边界拆开,每个边界里语言和模型是什么。
Ubiquitous Language
开发者与领域专家在一个上下文中使用同一套业务语言。
例如:
Order.confirm()
Order.cancel()
Payment.authorize()
比:
updateStatus(2)
processType(7)
更接近领域含义。
Subdomain
大的业务 Domain 可以拆成多个 subdomain。
常见分类思路:
- Core Domain;
- Supporting Subdomain;
- Generic Subdomain。
核心不是标签本身,而是:
哪些业务能力真正形成不同的问题空间和竞争价值。
Bounded Context
Bounded Context 定义:
某一个领域模型和语言在哪个边界内有效。
例如同一个 “Customer”:
Sales Context
→ lead / buyer / account
Billing Context
→ payer / invoice owner
Support Context
→ ticket requester
不要求整个系统只有一个万能 Customer model。
详见 DDD 限界上下文。
Bounded Context 不等于 Microservice
这是常见误区。
Bounded Context 是:
模型与语言的业务边界。
Microservice 是:
一个 deployment/runtime/service boundary。
两者经常有关联,但不是数学等号。
一个 bounded context 可以:
- 位于 modular monolith;
- 最终成为一个 service;
- 在合理情况下由多个 deployment component 支撑。
不要为了“做 DDD”强制拆微服务。
Tactical DDD
Tactical DDD 负责:
在某个 Bounded Context 内,怎样把业务规则表达成模型。
Entity
Entity 的关键是 identity continuity。
两个对象即使属性相同,只要业务上需要区分“是谁”,就是不同 entity。
例如:
Order #A123
Order #B456
即使金额完全相同,也不是同一个订单。
Entity 不应该只是数据库字段容器。
它应该保护自己的业务规则和有效状态变化。
Value Object
Value Object 没有业务 identity,主要由值定义。
例如:
- Money;
- EmailAddress;
- DateRange;
- Coordinates。
常见特征:
- immutable;
- equality based on value;
- 可以封装 validation 和 domain operation。
如果两个 Money 都是:
100 CNY
在大多数业务语境里可以互换。
Aggregate
Aggregate 是:
必须在一个 consistency / transaction boundary 内保持 invariant 的对象集合。
其中有一个 Aggregate Root。
外部通常通过 root 修改 aggregate 内部状态。
例如:
Order Aggregate
├─ Order (root)
└─ OrderItem[]
规则可能是:
- OrderItem 不能绕过 Order 自己改变订单总价;
- confirm 前必须至少有一个 item;
- cancelled order 不能继续新增 item。
Aggregate 不是“把有关联的表都塞一起”
Aggregate 的边界应该由:
哪些业务 invariant 必须原子保持一致
决定。
不是由 ER foreign key 数量决定。
小 Aggregate
过大的 aggregate 会带来:
- 更大的 transaction;
- 更多 lock/contention;
- 更高并发冲突;
- 不必要的对象加载;
- 更难独立演化。
常见原则:
只把真正需要强一致的对象放进同一个 aggregate。
跨 aggregate 通常通过 ID 引用,并接受 eventual consistency。
Domain Service
如果一条业务规则:
- 是真正的 domain logic;
- 但不自然属于某个 Entity / Value Object;
可以放 Domain Service。
例如:
DeliveryScheduler.schedule(...)
可能需要多个 domain object 才能决定结果。
详见 领域服务 vs 应用服务。
Application Service
Application Service 负责 orchestrate use case:
authenticate principal
→ load aggregate
→ call domain behavior
→ persist
→ publish event
→ return DTO
它通常不应该包含核心业务 invariant。
所以:
Application Service
= workflow / orchestration
Domain Model
= business rules
Domain Event
Domain Event 表达:
领域中已经发生的一件有业务意义的事。
例如:
- OrderConfirmed;
- PaymentAuthorized;
- DeliveryCancelled。
不要把:
row inserted
record updated
机械称为 domain event。
Domain Event 讲的是业务事实,而不是数据库操作事实。
Domain Event 与 Integration Event
在同一个 bounded context 内,Domain Event 可以协调多个 aggregate 的业务反应。
跨 bounded context / service 对外发布时,通常需要稳定的 Integration Event contract。
所以:
Domain Event
≠ 必然直接发到 message broker
需要有明确 boundary mapping。
Repository
Repository 用领域语言表达 aggregate 的 persistence access。
例如:
orderRepository.findById(orderId)
orderRepository.save(order)
它的目标不是把 SQL 包一层而已,而是:
让 domain/application 层依赖 aggregate collection-like contract,而不是数据库细节。
DDD 与数据库表不是一一对应
常见错误:
一张表
= 一个 Entity
= 一个 Aggregate
= 一个 Service
这些属于不同设计层。
真实 domain model 可能:
- 一个 aggregate 跨多张表;
- 多个 bounded context 使用同一个业务 ID,但模型不同;
- Value Object 被映射成多列;
- read model 完全不同于 write domain model。
DDD 与 CQRS / Event Sourcing / Microservices
DDD 可以和这些方法组合,但不等于它们:
DDD ≠ CQRS
DDD ≠ Event Sourcing
DDD ≠ Microservices
DDD ≠ Clean Architecture
是否采用这些架构,要根据复杂度和实际约束决定。
什么时候不值得用重型 DDD
如果系统主要是:
- 简单 CRUD;
- 业务规则少;
- 模型很稳定;
- 团队没有复杂领域知识协作;
完整 tactical DDD 可能带来:
- 过度抽象;
- 文件/层级膨胀;
- 大量无价值 mapper/service/repository;
- 学习和维护成本。
DDD 的价值来自管理领域复杂度,不是让目录看起来更高级。
一个最小建模流程
真实业务场景
→ 和领域专家统一语言
→ 识别 Subdomain
→ 定义 Bounded Context
→ 找核心 invariant
→ 识别 Entity / Value Object
→ 定义 Aggregate boundary
→ 将 workflow 放 Application Service
→ 将业务规则留在 Domain
→ 用 tests/examples 验证模型
常见失败模式
Anemic Domain Model
Entity 只有 getter/setter,所有规则都在 Service。
→ 领域模型退化成数据库 DTO。
Giant Aggregate
所有相关对象都塞进一个 aggregate。
→ consistency boundary 过宽。
技术术语替代业务语言
接口叫:
updateStatus()
processData()
doAction()
而不是业务真实动作。
为了 DDD 强拆微服务
先拆 deployment,再寻找 domain boundary。
→ 顺序反了。
Repository Everywhere
每个表一个 repository。
→ 这是数据访问模板,不一定是 DDD。
学习检查
- DDD 最主要解决的是什么复杂度,而不是哪种技术栈?
- Ubiquitous Language 与 Bounded Context 为什么必须一起理解?
- Bounded Context 为什么不等于 Microservice?
- Entity 与 Value Object 的判断标准分别是什么?
- Aggregate boundary 应由 foreign key 还是 invariant 决定?
- Domain Service 与 Application Service 有什么区别?
- Domain Event 为什么不等于数据库 row updated?
- 为什么简单 CRUD 系统未必值得完整 tactical DDD?
- 给一个“订单 + 支付 + 库存”系统,尝试划分至少两个 bounded contexts,并说明同一个 Order/Customer 概念是否必须共享同一模型。