微服务中的领域驱动设计 (DDD)
当组织从整体架构过渡到微服务时,他们面临一个关键的、高风险的问题:我们如何划定服务的边界?
理论上,微服务应该是松散的、解耦的单元,可以独立开发、部署和扩展。然而,在实践中,许多团队最终构建了一个分布式单体,该系统中的服务紧密耦合,以至于单个业务变更需要同时修改和部署多个服务,从而加剧了网络延迟和部署僵局。
为了避免这个陷阱,软件架构师转向领域驱动设计(DDD)。 DDD 是一种软件开发方法,由 Eric Evans 于 2003 年首次提出,它将代码结构与其所代表的复杂业务领域保持一致。
在本文中,我们将探讨 DDD 如何为设计干净、解耦和高度可维护的微服务提供战略和战术蓝图。
1. 战略蓝图:划定服务边界
DDD 分为两个主要阶段:战略设计和战术设计。战略设计是关于对业务环境进行建模并定义服务边界。它提供了架构的“宏观”视图。
A. 无处不在的语言
在企业内部,不同的部门使用相同的词语来表示完全不同的事物。例如:
- 对于 销售团队,“用户”是潜在客户或潜在客户。
- 对于安全团队来说,“用户”是一组登录凭据。
- 对于运输团队来说,“用户”是实际收件人地址。
尝试为“用户”构建一个满足每个人需求的单一、统一的数据库模型会导致庞大、混乱的代码库。 DDD 通过建立通用语言来解决这个问题,这是一种由业务领域专家和开发人员在特定边界内定义和使用的共享词汇表。
B. 有界上下文
有界上下文是领域模型应用的显式边界。在边界内,通用语言中的所有术语都具有单一的、明确的含义。
我们不是使用单个“用户”模型,而是在各自的限界上下文中定义单独的模型:
- 在订单上下文中,我们有一个包含客户联系信息的
Order模型。 - 在身份上下文中,我们有一个
Credentials模型。 - 在 Shipping Context 中,我们有一个
DeliveryAddress模型。
微服务的黄金法则:一个有界上下文直接映射到一个微服务。
通过将系统划分为限界上下文,我们确保微服务松散耦合并代表不同的业务功能。
C. 上下文映射:服务如何通信
有界上下文并不是孤立存在的;他们必须互动。 上下文映射定义了上下文之间的关系和转换机制。关键模式包括:
- 共享内核:两个上下文共享域模型和数据库的一小部分(由于耦合,通常在微服务中不鼓励)。
- 客户-供应商:一个上下文(供应商)必须向另一个上下文(客户)提供数据。供应商必须与客户协调发布时间表。
- 反腐败层 (ACL):一个转换层,它将来自外部系统的传入数据转换为客户端的内部域模型,防止外部模型污染内部架构。
2. 战术蓝图:构建微服务代码库
使用战略设计绘制服务边界后,战术设计提供一组设计模式来在单个微服务中构建代码。
A. 实体和值对象
在微服务内部,模型分为两类:
- 实体:具有随时间推移持续存在的唯一身份的对象,即使其属性发生变化。示例包括
Order或Product。 - 值对象:没有唯一标识并且完全由其属性定义的对象。它们是一成不变的。示例包括
ShippingAddress或ProductDimensions。如果两个值对象具有相同的属性,则认为它们相等。
B. 聚合和聚合根
聚合是关联实体和值对象的集群,它们被视为数据更改的单个单元。 每个聚合都有一个聚合根,它是外部对象可以与聚合交互的唯一入口点。根确保所有业务不变量(规则)得到执行。
例如,在上图中:
- 在 订单服务 中,
Order是聚合根。它包含OrderItem(实体)和ShippingAddress(值对象)。外部服务不能直接修改OrderItem;他们必须调用Order根上的方法(例如order.AddItem()),以验证订单尚未发货。
C. 领域事件
领域事件是在业务专家关心的领域中发生的事情。它用于跨有界上下文异步通信更改。
当聚合执行命令时,它会发布域事件(例如,OrderCreated)。其他服务侦听此事件并相应地更新其状态,从而确保最终的一致性,而无需同步 API 依赖。
3. 具体示例:订单服务与库存服务
让我们看一下 Go 中针对 订单服务 的战术 DDD 的简化实现,展示了聚合根如何协调业务规则。
package domain
import (
"errors"
"time"
)
// Value Object (Immutable, identity-less)
type ShippingAddress struct {
Street string
City string
ZipCode string
}
// Entity (Has identity, mutable)
type OrderItem struct {
ProductID string
Quantity int
Price float64
}
// Aggregate Root (Enforces transactional boundaries)
type Order struct {
ID string
Items []OrderItem
Address ShippingAddress
Status string
CreatedAt time.Time
}
// NewOrder creates a new Order Aggregate Root
func NewOrder(id string, address ShippingAddress) *Order {
return &Order{
ID: id,
Items: []OrderItem{},
Address: address,
Status: "PENDING",
CreatedAt: time.Now(),
}
}
// AddItem enforces business rules before mutating state
func (o *Order) AddItem(productID string, qty int, price float64) error {
if o.Status != "PENDING" {
return errors.New("cannot add items to a finalized or cancelled order")
}
if qty <= 0 {
return errors.New("quantity must be greater than zero")
}
o.Items = append(o.Items, OrderItem{
ProductID: productID,
Quantity: qty,
Price: price,
})
return nil
}
成功保存订单后,服务会向消息代理发布一个事件:
{
"event_id": "evt_98231",
"event_type": "OrderCreated",
"timestamp": "2026-06-26T00:15:00Z",
"payload": {
"order_id": "ord_5521",
"items": [
{ "product_id": "prod_88", "quantity": 2 }
]
}
}
库存服务 侦听此事件,更新其本地 StockLevel 实体,并异步完成流程。
4. 微服务中的战略 DDD 与战术 DDD
| 相位/相位 | 战略领域DDD | 战术DDD |
|---|---|---|
| 范围 | 全局系统架构(宏观) | 单个服务代码库内部(微型) |
| 主要目标 | 绘制清晰、解耦的服务边界 | 对丰富的业务逻辑进行建模并强制执行不变量 |
| 关键概念 | 有界上下文、通用语言、上下文映射 | 实体、值对象、聚合、领域事件 |
| 观众 | 建筑师、产品经理、开发人员 | 软件开发人员、代码审阅者 |
| 对微服务的影响 | 确定服务的数量和范围 | 确定数据库事务和目录结构 |
结论:首先从战略 DDD 开始
将领域驱动设计应用于微服务是确保您的架构与业务结构相匹配的有效方法。然而,团队经常犯这样的错误:过于关注战术模式(例如编写存储库接口和值对象),而忽略战略设计。
如果你没有正确界定边界,那么再多干净的战术代码也无法避免你的系统变成分布式整体。始终从战略映射开始 - 定义您的通用语言,将逻辑分组到有界上下文中,映射通信流,并让您的微服务边界自然地从这些定义中显现出来。