DDD-领域驱动设计
DDD 是什么
Domain-Driven Design (领域驱动设计,简称 DDD),该思想源于 2003 年 Eric Evans 编写的《Domain Driven Design》的著作,是一种通过将实现与核心业务概念的演化模型深度联系起来来开发满足复杂需求的软件的方法。
为什么要面向领域设计
我们有很多 原则、模式 和哲学,使我们能够创建良好的软件模型。但是,如果在处理复杂领域的大型组织中工作,仍然存在未解决的问题。
在这种传统的软件开发中,技术实现往往主导设计过程,例如数据库表结构决定对象模型,功能模块划分优先于业务领域边界。系统分析和系统设计的割裂导致:对需求分析设计的结果无法直接落地为代码,而能够运行的代码却无法完整实现业务功能,同时,软件 Bug 频出,维护升级重写难度大。
DDD 则打破了这种隔阂,提出了领域模型概念,统一了分析和设计编程,使得软件能够更灵活快速跟随需求变化。它的核心思想,是 从“面向数据表”编程,转变为“面向业务模型”编程。这意味着你的代码结构将直接反映业务概念,而不是数据库表结构。这能有效解决随着业务复杂化,传统开发模式(如 MVC)下业务逻辑分散、代码难以维护的“大泥球”问题。
领域模型是对业务模型的抽象,DDD 是把业务模型翻译成系统架构设计的一种方式。
软件架构的演进
第一阶段:单机架构,面向过程编程,系统包括客户端 UI 层和数据库两层,采用 C/S 架构模式,整个系统围绕数据库驱动设计和开发。
第二阶段:集中式架构,面向对象编程,系统包括业务接入层、业务逻辑层和数据库层,经典的三层架构。这种架构容易使系统变得臃肿,可扩展性和弹性伸缩性差。
第三阶段:分布式微服务架构,很好地实现应用之间的解耦,解决单体应用扩展性和弹性伸缩能力不足的问题。

战略设计 (Strategic Design)
战略设计的目标是从业务视角出发,建立业务领域模型,并清晰地划分出各个业务模块的边界(划分领域边界),确保整个团队(包括业务和技术)对业务有一致的理解。
领域与子域 (Domain & Subdomain)
领域就是软件系统所要解决的 整个业务范围。它指的是现实世界中的某个行业或业务板块,而不是代码。
把整个业务领域拆成核心域、支撑子域、通用子域,决定资源投入的优先级
核心域 (Core Domain):业务的核心竞争力,最需要投入的地方。例如,美团的核心域是“实时配送调度算法”,淘宝的核心域是“商品推荐与交易引擎”。
支撑域 (Supporting Subdomain):支撑核心业务运行的部分,如“商品分类”,优先级不高,实现难度也不大。
通用域 (Generic Subdomain):通用的、可被复用的部分,例如“用户认证”、“支付”,建议直接买现成的或使用开源方案。
统一语言 (Ubiquitous Language)
一个团队,一种语言
一条业务线由研发、产品、业务共同协作和维护,大家只是在不同维度做同一件事情。
统一语言是 DDD 的基石,它要求选择一种大家(团队内的业务专家、产品经理、开发人员)都能看懂的方式(同一套领域模型术语)来描述业务,不仅仅是代码(类名、方法名和变量名),也可以是 PRD、MRD、业务模型图,统一团队语言,从上层业务到底层实现都是同一个领域模型,减少信息传递经过翻译造成理解不一致。
限界上下文 (Bounded Context)
不要混淆全局语言,不同团队负责不同的模块或系统,两个系统可能有重合部分,但专注于解决不同的问题。每个单词/语句在特定上下文中都有意义,同样,每个领域概念在(有界的)上下文中都有含义,超出该范围,相同的术语可能就有不同含义,模型的规则也不再适用。
尽管 Customer 的 ID 标识 在 Billing 系统和 Marketing 系统之间是相同的,但 Customer 在两个氛围的定义不同,Billing 系统对账单地址和未结发票感兴趣,而 Marketing 系统对客户的联系方式及其最近的订单感兴趣。
限界上下文可以,定义术语的适用范围,避免同一概念在不同场景下的语义冲突
战术设计 (Tactical Design)
战术设计从技术视角出发,对战略设计的业务模型高质量地映射为代码,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
领域模型构成要素
实体 (Entity):有唯一 ID、有生命周期的业务对象,即使属性有所变化,但唯一标识没变,那就是同一个对象,例如
Order(订单)、User(用户)。值对象 (Value Object):没有唯一 ID、只关心属性值的对象,改变属性值代表替换了全新的值对象,例如
Money(金额)、Address(地址)。- 例如,“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了。
聚合 (Aggregate):Aggregate 是一组紧密关联的对象(Entity + Value Object)的集合,作为一个整体来维护业务规则的一致性。外部代码不能直接修改聚合内部的对象,必须通过聚合根来操作。
- 例如,Order(订单) 作为聚合根,内部包含了收货地址(VO)和 OrderItem(订单明细,一般也设计为实体)。外部代码想要修改明细,必须通过
Order提供的方法(如order.addItem()),而不能直接去操作OrderItem表。
- 例如,Order(订单) 作为聚合根,内部包含了收货地址(VO)和 OrderItem(订单明细,一般也设计为实体)。外部代码想要修改明细,必须通过
聚合根 (Aggregate Root):聚合根就是聚合的唯一入口,就比如说订单,所有订单里面的订单项、计算订单信息等,都只能通过订单作为唯一入口。
领域服务 (Domain Service):某个业务逻辑无法归属于任何一个实体或值对象时,就将其放在领域服务中。
- 例如,检查用户是否重复注册的逻辑,可能放在
UserUniquenessChecker这样的领域服务里。 - 例如,
TransferService(转账服务) 或PricingService(定价服务)。比如计算订单最终价格时,需要同时参考 Order(订单)、User(用户的会员等级)、Coupon(优惠券)三个聚合。
- 例如,检查用户是否重复注册的逻辑,可能放在
领域事件 (Domain Event):领域事件就是这个业务场景发生后,会有一系列需要做事情,而且这一系列事情是动态的,有可能会变化,如果传统的写成 pipeline,那这个地方的代码就需要根据需求经常改,使用领域事件的好处就是降低耦合度,当前聚合的事情做完无需关心其他聚合需要做什么。
- 例如,读写库分离的情况,修改完数据发布读库(可能是 ES、Redis、物化视图)更新的事件
仓储 (Repository):一个领域层的持久化接口,用于隔离领域层和基础设施层的数据库细节。
- 在传统的持久层开发中,我们习惯于创建 UserDAO 或 UserMapper,并在其中直接编写面向数据库表的 SQL 查询。这种模式在简单的 CRUD 应用中非常高效,但在复杂的微服务业务系统中,它会导致领域逻辑与数据访问技术(如 MyBatis-Plus 的 QueryWrapper、JPA 实体、甚至是具体的 SQL 语法)紧紧捆绑在一起。
- 为了切断这种“技术细节”对“核心业务”的入侵,DDD 推行仓储模式(Repository Pattern),并将其作为连接领域层与基础设施层的唯一闸门。
工厂 (Factory):封装复杂对象的创建逻辑。
- 如果是复杂的初始化逻辑应交给 Factory,而非在应用层直接 new 对象。
分层架构
传统三层架构是“数据库驱动”的,自上而下依赖,最终指向数据库。
- 表示层(UI/Controller):接收请求,参数校验。
- 业务逻辑层(Service/BLL):处理业务逻辑,通常写满
if/else和增删改查。 - 数据访问层(DAO/Mapper/DAL):直接操作数据库表(SQL 或 ORM)。
Entity 不属于三层中的任何一层,但是它是必不可少的一层,它贯穿于三层,在三层之间传递数据。
痛点:业务逻辑层严重依赖数据访问层。写 Service 代码时,脑子里想的是“我要查那个表”、“我要更新这个字段”。一旦数据库表结构变了,业务层代码跟着塌方。技术(数据库)绑架了业务(领域)。


代码示例(贫血)
// Controller接收后
// Service层
public void createOrder(OrderDTO dto) {
// 1. 查表
ProductDO product = productDao.selectById(dto.getProductId());
// 2. if判断库存
if (product.getStock() < dto.getNum()) throw new Exception();
// 3. 计算价格
BigDecimal price = product.getPrice() * dto.getNum();
// DAO层进行持久化
// 4. 插入订单表
orderDao.insert(new OrderDO(...));
// 5. 更新库存表
productDao.updateStock(...);
}DDD 通过依赖倒置原则(DIP)实现了业务驱动的 分层架构 (Layered Architecture),领域层在最核心,向内依赖(基础设施层依赖领域层)。
一个典型的项目结构如下:
interfaces(接口层/用户接口层):处理 HTTP 请求、参数校验等,不包含业务逻辑。application(应用层):协调领域对象,完成具体的业务用例(如“创建订单”),负责事务管理。它很“薄”,只做编排调度,不实现业务规则。domain(领域层):系统的核心。所有业务规则都实现在这里。包含实体、值对象、聚合、领域服务、仓储接口等。infrastructure(基础设施层):为上层提供技术支持,如数据库持久化、消息队列等。

代码示例(充血):
// 应用层
@Service
public class OrderAppService {
private final OrderRepository repo;
public void createOrder(Product product, int num) {
Order order = new Order(); // 新建聚合
order.addItem(product, num); // 调用领域行为
repo.save(order); // 交给基础设施去存(不关心SQL)
}
}// 领域层
// 聚合 Order实体 + OrderItem实体
// 聚合根 Order
public class Order {
private List<OrderItem> items;
public void addItem(Product product, int num) {
// 核心业务规则写在实体里!
if (product.getStock() < num) throw new DomainException("库存不足");
this.items.add(new OrderItem(product, num));
this.totalPrice = product.getPrice() * num; // 纯内存计算
}
}
// 定义“持久化能力”的接口(不关心怎么存)
public interface OrderRepository {
void save(Order order);
}// 基础设施层
@Repository // Spring实现
public class OrderRepositoryImpl implements OrderRepository {
@Override
public void save(Order order) {
// 这里才真正去调 MyBatis/JPA 插入 order 表和 item 表
orderDao.insert(order);
}
}
DDD 与 CQRS
CQS
CQS(命令查询分离, Command Query Separation) ,是由 Bertrand Meyer 在 1980 年代提出的一种编程原则。其核心思想是,将方法分为两类:命令(Command)和查询(Query)。
- 命令(Command):用于改变对象的状态。命令方法执行动作并修改对象,但不返回任何值。
- 查询(Query):用于获取对象的状态。查询方法返回一个值,但不改变对象的状态。
简单来说就是一个方法要么读取要么写入,但绝不能同时进行两者。
CQRS
CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种将数据的读取操作与更新操作分离的架构模式。
- query 指的是读操作
- command 是对会引起数据变化的操作的总称,新增,删除,修改,导入,导出,重定向等这些操作都是命令
为什么DDD需要CQRS
在领域驱动设计中,除了单个聚合根的加载,在实际开发中常常会遇到如:分页,列表查询,条件查询,跨表查询等需求,这些需求可能需要同时查询多个聚合根的数据。有些开发会直接在仓储中实现这些数据查询的处理逻辑,这会产生一些问题:
- 仓储职责不再单一:仓储的职责是为了维护单个聚合根的生命周期的,但这种实现会导致仓储承担了过多的查询职责,从而造成职责不在单一。
- 聚合整存整取的负担:有一些查询仅仅需要获得聚合根一部分字段,如果完整完整加载聚合根的所有状态,则可能会因为读取的数据过多而影响查询性能,并使得查询操作更加麻烦。因此,在只进行查询而不进行修改的场景下,其实没有必要完整加载聚合根。
使用 CQRS 的好处:
- 分离业务关注点:命令操作关心聚合内一致性,而查询操作不需要关注一致性,两者的分离使得用代码实现时可以分别独立处理不同的业务需求,更方便地进行变更和迭代
- 针对性地进行架构的优化:可以单独针对读或写进行优化,以便更好地处理高并发情况
CQRS 实现
只要命令和查询这两个操作使用的不是同一套模型,就已经实现了CQRS
CQRS的实现分为三个层面:方法级别的CQRS、相同数据源的CQRS、异构数据源的CQRS。
下面以相同数据源的CQRS为例:
CQRS 场景
- 不用CQRS就会污染领域模型的时候
- 复杂查询
- 条件复杂,需要冗余数据
- 搜索
- 高性能查询
- 需要查询专用数据库
- 异构查询存储(关系型数据库 +缓存 + NoSql)
领域很复杂时,一般都是使用 CQRS
开发启示
- 从战略设计开始:与产品经理等同事梳理业务流程,明确限界上下文和统一语言。
- 适当使用 DDD:DDD 适合最适合业务逻辑复杂、多变的系统,对于简单的 CRUD 应用,使用 DDD 可能会过度设计。
- 聚焦核心域:将 DDD 放在核心子域上。对于非核心部分,可以采用更简单的设计。
DDD 项目示例
daydayfantasy/ddd-example: DDD(领域驱动开发)模式的学习文档及代码示例
参考资料
DDD 领域驱动设计落地实践(十分钟看完,半小时落地) - 只会一点 java - 博客园
领域驱动设计(DDD: Domain-Driven Design) - 极道
领域驱动设计 DDD(Domain-Driven Design)软件架构学习笔记-腾讯云开发者社区-腾讯云
11.CQRSCQRS 1.CQS 介绍 CQS(命令查询分离)一个方法要么读取要么写入,但绝不能同时进行两者,换句话说 - 掘金
