思维转变

领域驱动设计(Domain-Driven Design,以下简称 DDD)的核心价值在于其对「业务领域」的深度聚焦。这里的「领域」并非单纯的技术范畴,而是指代软件系统所要映射的现实业务场景及其核心价值主张。DDD 通过建立与业务高度契合的领域模型,使得技术实现与业务本质形成同频共振,从而有效解决复杂业务场景下的认知鸿沟问题。

在 VUCA(Volatile 易变性、Uncertain 不确定性、Complex 复杂性、Ambiguous 模糊性)特征愈发显著的现代商业环境中,任何架构设计都面临固有局限。这种局限性既源于业务需求本身的动态演进,也受制于人类认知的有限性——正如 Eric Evans 在开山之作中强调的“模型永远是对现实的近似抽象”。但正是这种局限性,凸显了 DDD 方法论的战略意义:

它通过"战略设计"构建业务全景图,运用限界上下文划定领域边界,通过"战术设计"落地聚合根、实体/值对象等模式,形成应对复杂性的结构化解决方案。

需要特别指出的是,DDD 的复杂性并非方法论本身的缺陷,而是其应对现实业务复杂度的必要代价。这种复杂性体现在三个维度:

  1. 认知复杂性:要求开发团队与领域专家共建"通用语言",实现业务概念与代码模型的精准映射。
  2. 架构复杂性:通过分层架构实现业务逻辑与技术实现的解耦,采用防腐层处理系统集成问题。
  3. 演进复杂性:借助子域划分和上下文映射,为持续演进的业务提供可扩展的架构基础。

对于实践者而言,DDD 的价值不在于提供完美无缺的终极方案,而是为 VUCA 环境下的系统建设提供基础性指引。其核心思想——无论是通过限界上下文实现的领域自治,还是通过聚合根维护的业务一致性——都为控制软件熵增提供了可落地的模式库。即便不完全采用DDD 完整体系,其领域建模思想、分层架构理念等核心要素,仍能显著提升复杂系统的可维护性和演进能力。这种开放包容的哲学,恰是 DDD 历经二十年仍保持生命力的关键所在。

贫血模型 vs. 充血模型

  • 贫血模型:指的是只有属性而没有行为的模型。
  • 充血模型:指的是既有属性又有行为的模型。

笔者过往的实践中,基本上都使用类似于 controller→service→repository[model] 的三层架构:

  • conrtoller 负责暴露对外接口。
  • service 负责执行所有的业务逻辑。
  • repository 复杂数据的存储和缓存,包含数据对象 model 的定义。

在这个模式下,基本上所有的核心逻辑都充斥在 service 层中,所以 service 层一般都会非常大,它要扮演多面手,即要负责跟各个模块协作,还要负责处理具体的业务规则,最终完成一个业务行为。这个过程中,model 即为贫血模型,因为逻辑都给 service 处理了,这种架构也称为贫血三层架构

在 DDD 的理念下,很多的核心业务概念都会被建模为「领域对象」,这些「领域对象」本身就是一种业务规则的体现,所以把业务的处理逻辑,都归属到这些「领域对象」的行为当中了,即所谓的充血模型。

在这个理念下,一个优化后的充血四层架构如下图所示:

充血四层架构

贫血模型推荐场景:业务简单、迭代快速、团队技术栈偏传统(如 Spring Boot+MyBatis)时,避免过度设计。

充血模型推荐场景:业务复杂、需长期演进(如核心交易系统)、团队具备 DDD 经验时,通过实体、值对象、领域服务等战术设计理念降低系统熵增。

混合使用的场景:部分核心领域用充血模型(如订单、支付),非核心模块用贫血模型(如日志、配置),平衡效率与质量。

实际上,充血模型因其状态完整,适合进行状态变更类的操作,以确保业务操作符合领域规则;贫血模型由于其轻量级,更适合作为不会涉及状态变更的操作的数据容器。这其实就是 CQRS 的理念。

概念清单

战术设计

实体

定义:会随着业务变化发生变化的业务概念叫作实体对象。

关键点:实体需要唯一表示

值对象

定义:一些对象在表达业务概念时是必须的,可业务并不围绕着它们进行,它们仅是对这些重要业务概念的描述,这一类对象叫作值对象。

关键点

  1. 值对象的意义取决于属性,只要对象的属性一模一样,那么对象就是相同的。
  2. 尽量把值对象实现为不可变对象。

领域服务

定义:领域服务自身是没有数据的,只是表达了某种业务计算逻辑,或者业务的某种策略。

关键点

  1. 领域服务是无状态的。
  2. 只有在确实表达了一个相对独立的业务概念或者业务策略,并且不能简单地把它归结到某个既有的业务对象上时,才是一个真正的领域服务。

领域事件

定义:领域事件代表从业务专家视角看到的某种重要的事情发生了。

关键点

  1. 领域事件是一种特殊的值对象。
  2. 应该根据限界上下文中的通用语言来命名事件:AccountActivited。
  3. 应该将事件建模成值对象或贫血对象。

聚合

定义:聚合从本质上讲是在基础的构造块上增加了一层边界,用边界把那些紧密相关的对象放到了一起。

关键点

  1. 紧密相关的对象存在数据一致性问题;
  2. 缺乏边界时,维护数据一致性是困难的;
  3. 划分边界的关键在于既不要让整个系统成为一个整体,又让每个单独划分出的聚合具有明确的业务意义;
  4. 聚合需要关注三条法则:
    1. 生命周期一致性:如果一个对象在聚合根消失之后仍然有意义,那么说明此时在系统中必然存在能够访问该对象的方法。这和聚合的定义矛盾,所以聚合内的其他元素必然在聚合根消失后失效。
    2. 问题域一致性:不属于同一个问题域的对象,不应该出现在同一个聚合中。
    3. 尽量小的聚合:聚合的本质作用是提升对象系统的粒度,确保一致性、降低复杂度。不过,粒度绝不是越大越好。如果聚合的粒度太大,那内部的逻辑复杂度也会大大增加还会影响到复用度。因此,要能够比较容易地断开聚合。

资源库

定义:对于查询、创建、修改、删除数据的操作,领域模型使用“资源库(Repository)”这个概念来承载它们。

关键点:一个聚合对应一个资源库,应以聚合根命名资源库,除了聚合根之外的其他对象,都不应该提供资源库对象。

工厂

定义:工厂用于构建聚合。

关键点:一个聚合往往包含多个对象,这些对象的数据之间又可能存在联系,如果允许分别创建这些对象,就会让聚合是业务完整性的单元这个定义面临失败。

战略设计

统一语言

定义:与业务专家协作定义全团队通用的术语表,消除沟通歧义。

关键点

  1. 同一个概念在不同的上下文中可能存在不同的含义;
  2. 同一个概念在同一上下文中的不同环节,也可能存在不同的含义,需要非常明确清晰的界定,降低沟通成本。

子域

定义:子域是对业务领域的逻辑划分,用于分解复杂问题。通常分为核心子域(业务核心竞争力)、支撑子域(辅助核心业务)和通用子域(可复用的标准化能力)。

关键点:因业务目标、团队定位和组织发展阶段等方面的不同,这三个子域的划分并非一成不变,而是会互相转换。

限界上下文

定义:限界上下文本质上是一个自治的小世界,它有完备的职责,还有清晰的边界。

关键点

  1. 一个子域的一切资产,包括领域模型、数据库、包、可执行程序、接口声明等,都应该封装在限界上下文中,避免跨越边界。
  2. 如何平衡边界的价值和不利影响,是划分边界时要做的一种重要取舍。一个较为稳妥的策略是考虑认知的渐进特征,不要过早隔离。在已经确定的边界上进行划分,延缓划分那些尚具模糊性的边界,在这些边界逐渐变得清晰时再分离它们。

上下文映射

定义:限界上下文约定了基于领域模型的架构层次的设计分解,而分解必然意味着集成和协作。上下文映射就是对限界上下文之间的协作关系的模式总结。

关键点

  1. 在边界上完成概念映射是一种基本模式。通过在应用层组装或者使用适配器完成概念映射,可以保持领域概念的清晰,避免领域模型遭到不必要的污染。
  2. 防腐层模式、标准开放服务模式、客户-供应商模式、追随者模式。

串讲

在应对复杂业务系统时,DDD 通过分治策略将业务领域拆分为多个子域(如电商系统的订单、支付子域),每个子域对应一个限界上下文——这是技术与业务对齐的关键边界,既承载领域模型的实现,也通过上下文映射(如防腐层、共享内核等模式)实现跨子域协作,避免模型污染。

限界上下文内的领域对象是业务逻辑的载体:具备唯一标识和生命周期的实体(如订单实体通过 ID 跟踪状态变化)、描述特征且不可变的值对象(如地址由省市构成,修改需整体替换),以及通过聚合根统一操作保证一致性的聚合(如订单聚合根管理订单项和配送信息)。当业务逻辑跨越多个聚合时,由无状态的领域服务协调(如支付计算需整合订单、账户聚合)。

对象的创建与持久化分别由工厂(封装复杂初始化逻辑)和资源库(隔离存储细节)负责,而领域事件(如订单支付成功事件)则驱动跨上下文的异步协作。

战术设计

factory

  • factory 用于构建复杂的领域对象。

repository

  • 只有聚合根有 repository。
  • repository 就只提供 loadsave 功能,且要保证事务一致性。
  • 尽可能提供行级的 repository,而不是表级的 repository,对于表级的 repository,可以抽成一个领域服务。

设计模式

责任链模式

将请求的发送者和接受者解耦,使多个对象都有机会处理请求。

  • 责任链模式的使用要点在于要将维护责任链的代码和业务代码分开。
  • 在 DDD 中使用责任链模式时,应创建一个领域服务,在领域服务中完成责任链的创建和执行。
  • 尽量不要在责任链的处理器中通过 set 修改领域对象(聚合根)的状态,责任链应仅用于某些值的计算,最终将计算结果交给聚合根完成业务操作。

笔者实现了一个快速构建责任链的工具:

策略模式

允许在运行时根据需要选择不同的实现。

  • 在 DDD 中使用策略模式时,通常先定义一个领域服务接口,再在其实现类中完成策略的加载、选择和执行。
  • 注意屏蔽策略模式的实现细节,避免上层关注领域服务内的设计模式细节。

桥接模式

旨在通过解耦抽象和实现,使两者能够独立扩展和变化。

  • 多维解耦机制:桥接模式通过组合/聚合关系替代继承关系,将原本紧密耦合的抽象层(功能定义)与实现层(具体操作)分离例如遥控器(抽象)与电视(实现)的协作,遥控器通过接口控制电视,无需关注具体品牌。
  • 正交扩展能力:支持两个独立变化维度(如消息类型与通知渠道、图形与渲染方式),避免类数量呈指数级增长(M×N 组合问题)。电商物流系统中,新增微信通知渠道时,无需修改所有消息类即可实现扩展。

规约模式

规约模式是一种用于定义业务领域中规则和约束的模式,通常由规约接口(Specification)和验证器(Validator)两个部分组成。

  • 在 DDD 中,规约模式并不是在聚合根进行业务操作之前做前置校验,而是在聚合根完成业务操作之后做后置校验,确保 Repository 保存的聚合根符合业务规则。

适配器模式

被适配者(Adaptee)的接口转换为目标接口(Target),使原本因接口不兼容而无法协同工作的类能够协同。

  • 在 DDD 中,可以使用适配器模式来实现防腐层,以将外部上下文接口(如开放主机服务)返回的模型转换为本地上下文定义的领域模型,并将本地上下文的操作转换为对外部上下文的操作。可以有效隔离外部上下文的领域模型,避免互相污染。

领域事件

幂等性

领域事件的定义

领域事件是领域模型的组成部分,它通常由聚合根产生,并被其他聚合或者限界上下文订阅和处理,触发相应的业务逻辑。

注意点:

  • 应该根据限界上下文中的通用语言来命名事件:AccountActivited。
  • 应该将事件建模成值对象或贫血对象。

应用:

  1. 解耦领域对象之间的关系;
  2. 触发其他领域对象的行为;
  3. 记录领域内已发生的状态变化;
  4. 实现跨聚合的最终一致性;
  5. 进行限界上下文集成。

消息体:

1
2
3
4
5
6
7
{
"event_id": "",
"event_type": "",
"entity_id": "",
"event_time": 0,
"extra_data": "{}"
}

领域事件的生成

  1. 应用层创建领域事件。
  2. 聚合根创建领域事件。

要避免在聚合根内部调用基础实施发布领域事件,而是生成后返回给应用层,由应用层去发布。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Entity struct {
Events []Event
}

func(e *Entity)ResgisterEvent(event Event) {
e.Events = append(e.Events. event)
}

func(e *Entity) GetEvents() []Event {
res := e.Events()
e.Events = []Event{}
return res
}

领域事件的发布

  1. 直接发布并轮询补偿:为事件存储一个发布状态标识,用于记录是否补发成功。并提供定时任务检索超时未发布成功的事件进行重新发布。
  2. 采用事务日志拖尾:引入变更数据捕获组件(Change Data Capture,简称 CDC),捕获数据的变更日志,解析后获得领域事件并发布。

领域事件的订阅

将领域事件订阅者放置在用户接口层 user-interface-subscriber,收到事件后调用应用服务执行业务逻辑。

事件溯源

事件溯源(Event Sourcing)是一种将所有的领域事件(Domain Event)存储到事件存储(Event Store)中,并通过重放历史事件来还原领域对象状态的模式。

核心思想是将系统中所有的状态变更都视为事件,将这些事件以事件顺序记录下来,并存储到事件存储中。这样,可以通过重放这些事件,来还原任意时刻的系统状态。

三种方案:

  1. 通过回放所有的历史事件重建聚合根。
  2. 通过快照提高重建聚合根的效率。
  3. 通过拉链表生成所有事件对应的快照。

拉链表是一种用于处理缓慢变化维度问题的数据结构,它可以有效地处理维度数据的历史变化。在拉链表中,每个记录都有一个开始时间和结束时间,用于描述该记录的存活时间,即该记录的有效期。

拉链法示意图

CQRS

CQRS 将系统的操作分为两类:

  • 命令(Command):负责数据的写操作(增、删、改),不返回数据。
  • 查询(Query):负责数据的读操作,仅返回结果且不修改数据。

两者的数据模型可独立设计,甚至使用不同的数据库或存储技术。

适用场景

应对高并发读写场景

  • 案例 1:B 站点赞系统

    在日均活跃用户近亿的 B 站,点赞功能通过 CQRS分离读写操作。写入端通过消息队列(如Kafka)异步处理请求,避免数据库锁竞争;查询端通过缓存优化读取性能,显著提升系统吞吐量和稳定性。

  • 案例 2:实时答题 PK 游戏

    高并发的答题得分计算场景中,CQRS 结合事件溯源(EventSourcing)记录每个操作事件,确保读写模型的最终一致性,同时支持复杂战况数据的实时展示。

解决复杂查询需求

  • 案例 3:电商订单查询

    随着订单查询需求多样化(如按时间筛选、跨实体聚合数据),CQRS通过独立读模型简化查询逻辑,避免领域模型被复杂查询逻辑污染。

  • 案例 4:微服务数据聚合

    在微服务架构中,CQRS允许通过事件同步跨服务数据到专用读库,避免跨服务联表查询的性能瓶颈(如行程管理服务与用户信息服务的聚合查询)。

提升数据模型灵活性

  • 案例 5:文本增量更新

    针对大型文本编辑场景,CQRS拆分读写模型,增量保存修改记录并通过事件合并,减少网络传输数据量,同时支持任意版本的历史数据恢复。

不适用场景

  • 简单 CRUD 系统(如小型管理后台)
  • 强一致性要求的金融交易场景(如实时扣款)
  • 团队缺乏事件驱动架构经验时

一致性

聚合内事务实现

  • 聚合内事务控制不要放在应用层,会使应用层承担过多的责任。应用层应专注于协调领域对象和基础设施以完成业务操作,不应过多涉及数据访问和事务控制的细节。
  • 聚合内事务控制可以交给 Repository 来实现,采用乐观锁解决并发问题,可以基于版本号和时间戳,一般重试 1-3 次即可。

聚合间事务实现

  • 聚合间控制可以单独建立一个领域服务 Domain Service 来完成。

  • 对于实时性要求不高,仅需最终一致性,可以使用本地消息表或者最大努力通知的方案。

  • 对于实时性一致性要求比较高,可以采用 TCC(Try-Confirm-Cancel) 事务方案。

  • 对于长事务场景,或者涉及外部系统、遗留系统,可以考虑 Saga 事务方案。

    Saga 将事务分为多个事务,这些分支事务按照一定的顺序执行。当某个分支事务执行成功后,会通过消息通知下一个分支执行;当某个分支事务执行失败时,会按照正常事务执行顺序的相反方向进行一系列的补偿操作,以确保全局事务的一致性。

战略设计

事件风暴

核心概念与元素

元素名称 颜色标识 说明
领域事件(Domain Event) 橙色 表示已发生的业务事实,以“动词过去式”命名(如“订单已提交”),是事件风暴的核心起点。
命令(Command) 深蓝色 触发领域事件的操作或意图(如“提交订单”),通常由用户或系统触发。
参与者(Actor) 黄色 执行命令的角色,包括用户、部门或外部系统(如“客户”触发支付命令)。
外部系统(External System) 粉色 与当前系统交互的第三方服务(如支付网关回调生成事件)。
策略(Policy) 紫色 业务规则或约束条件(如“库存不足时取消订单”),决定事件触发的逻辑。
读模型(Read Model) 绿色 为查询优化的数据视图(如“用户订单列表”),支持决策展示。
聚合(Aggregate) 大黄色 业务对象集合(如“订单聚合”包含订单项和状态),维护一致性和完整性。
问题(Question) 红色 未达成共识的争议点(如事件定义分歧),需后续专项讨论。

实施流程与步骤

  1. 准备工作

    • 参与人员:业务专家、开发、产品、测试等跨职能角色,需领域专家主导。
    • 物料:多色便签、白板、马克笔,线上工具辅助远程协作。
  2. 识别领域事件 团队通过头脑风暴罗列所有可能事件(如电商场景的“订单已创建”“库存已扣减”),按时间轴排列,争议事件用红色便签标记并暂存。

  3. 补充命令与角色 为每个事件关联触发命令及执行者(如“客户”执行“支付订单”命令生成“支付完成”事件),区分内部操作与外部系统调用。

  4. 定义策略与读模型 添加业务规则(如“订单金额≥1000元需审核”)和数据展示需求(如“实时库存看板”)。

  5. 构建聚合与划分子域 将相关事件、命令归类为聚合(如“支付聚合”),划分限界上下文(如“订单服务”“库存服务”),明确微服务边界。

注意事项

  1. 事件粒度的把控:避免过度细化(如“用户已睁眼")或过于宽泛(如“订单已修改”),需聚焦业务关键节点。

  2. 争议处理与迭代:对未达成共识的事件标记为“问题”(红色便签),后续专题讨论;定期回顾模型,修正错误或补充遗漏。

  3. 技术实现衔接 :事件风暴的输出需转化为代码模型,例如通过事件溯源(Event Sourcing)持久化事件流,或结合 CQRS 分离读写逻辑。

C4 架构模型

层级 核心目标 受众 关键元素
Context(上下文) 描述系统与外部实体(用户、第三方系统)的交互关系 非技术人员(如业务方、客户) 系统边界、用户角色、外部依赖(如支付网关)
Container(容器) 展示系统内部的高阶技术组件(进程级单元) 技术管理者、架构师 Web 应用、数据库、消息队列等独立进程单元,关注技术选型与通信协议(如 REST API、gRPC)
Component(组件) 细化容器内部的业务模块与交互逻辑 开发团队 服务、模块、接口(如订单服务、库存服务),强调职责划分与依赖关系
Code(代码) 展示组件实现的代码结构 开发者 类、方法、数据库表(如 UML 类图、ER 图),通常由 IDE 工具自动生成

除了四层核心视图,C4 模型还提供:

  • 部署图:展示容器在物理环境中的分布(如 Kubernetes 集群部署)。
  • 动态图:描述业务流程(如用户下单到支付完成的时序交互)。
  • 系统景观图:多系统协同的全局视图(如企业级中台架构)。

实践案例

参考作者的 ddd-archetype ,笔者实现了一个 Go 版本的 ddd-archetype-go

整体架构如下: