中德AI开发者社区那场DDD分享,我替你们把2.5万字浓缩成了一套能直接落地的实操笔记
DDD(领域驱动设计)这个话题,我在开发者社区聊过很多次,也见过不少团队把它当成“银弹”引进来,结果不到两个迭代就悄悄退回CRUD。中德AI开发者社区那场DDD专题分享,全场2.5万字的深度讲解,我看完之后最大的感受是:DDD从来不是一套代码模板,而是一整套把业务复杂度管理起来的思维方式。这篇文章会把那场分享里的核心内容和我在多个项目里实践DDD的体会,从战略设计到战术设计、从分层架构到代码落地,完整地梳理一遍。
我的目标很直接:让你读完这篇文章后,敢在自己的项目里动手做一次DDD落地的尝试。如果你正被“改一个需求要动十几个类”“业务规则散落得到处都是”“每次上线都提心吊胆”这些问题困扰,这篇文章就是为你准备的。
1. 先搞清楚DDD到底在解决什么问题
1.1 业务复杂度才是系统崩溃的根源
很多团队把系统的混乱归咎于“技术选型不行”“代码写得烂”,但实际上,真正让系统失控的往往是业务复杂度。想想看:一个订单状态从“已下单”变成“已支付”,中间要经过哪些校验?库存要不要扣?优惠券要不要锁定?通知要不要发?这些规则分散在Controller、Service、工具类里,今天你在下单逻辑里加了一个判断,明天另一个接口却绕过它直接改了状态,后天上线时才发现两边行为不一致。
DDD的核心思想,就是让软件模型和业务模型保持一致。这句话听起来很虚,但落到实处的意思是:业务里说“订单”,代码里就有一个叫做Order的类;业务里说“一个订单必须包含至少一个订单项”,代码里的Order类就必须在构造时强制这个约束。当业务词汇和代码词汇一一对应时,需求变更就能直接翻译成代码改动,而不是先在业务术语和技术实现之间做一道翻译。
我见过太多系统,业务方口中的“订单”和开发手里的“订单表”根本不是一回事。业务方说的“取消订单”是指整个订单作废,开发理解的“取消”是把status字段置为某个数字。这种信息失真,最终都会变成遥遥无期的维护成本。
1.2 复杂业务需要建模,而不是堆CRUD
那场分享里反复强调一个观点:CRUD是数据库思维,不是业务思维。你写一个updateById(),数据库层面确实把一行数据改了,但业务层面“这个订单能不能取消”“取消后要不要退款”“退款是否受风控限制”这些规则,CRUD完全不管。这些规则才是系统的核心价值,也是DDD里说的“领域逻辑”。
DDD把工作分成两部分:战略设计和战术设计。战略设计解决“系统应该分成哪几个模块、每个模块负责什么、模块之间怎么协作”的问题;战术设计解决“模块内部的代码应该怎么组织、业务规则放哪里、数据怎么访问”的问题。战略设计在前,战术设计在后,顺序不能反。很多团队一上来就写实体、写仓储,结果边界画错了,后面的精致建模全都是白费。
这里还有一个跟AI时代相关的点:现在很多团队用AI辅助写代码,但AI生成代码的质量取决于你给它的上下文和模型结构。如果领域模型本身是混乱的,AI只会把混乱放大地更快。反过来,如果你的领域模型边界清晰、术语统一,AI能帮你省掉大量机械性编码工作。DDD的模型价值在AI时代只会被放大,不会缩小。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 战略设计:先画边界,再写代码
2.1 领域、子域和核心域:把力气花在刀刃上
战略设计的起点是划分领域。整个业务空间叫领域,领域内部可以拆成多个子域。子域分成三类:核心域、支撑域、通用域。核心域是系统存在的理由,是你和竞争对手拉开差距的地方。做电商,推荐算法可能是核心域;做支付,风控可能是核心域;做医疗,诊疗决策可能是核心域。支撑域是业务需要但非核心的部分,比如订单履约、客户管理。通用域则是那些买现成方案就行的东西,比如短信通知、权限认证。
这个划分最大的价值,是告诉团队:不要在通用域上投入过多精力。那场分享里推荐了一个判断方法:找业务负责人问一句“这个模块如果做得不好,客户会不会明显不满?”如果答案是不会,它很可能不是核心域。把最优秀的人放到核心域,用成熟开源方案或外部服务解决通用域,你的开发资源才能产生最大的杠杆。
2.2 限界上下文与上下文映射:模块间的“墙”和“桥”
限界上下文是战略设计的另一个核心概念。它定义了一个模型有效的边界,边界内部有完整的通用语言和一套自洽的业务规则。同一个词在不同限界上下文里可以有不同的含义——电商场景里,“订单”在销售上下文里代表交易凭证,在库存上下文里则代表占用库存的凭据。你不需要强行统一这些含义,只需要在不同上下文之间建立明确的映射。
上下文映射描述了限界上下文之间的协作关系,常见的有以下几种:
| 关系类型 | 说明 | 适用场景 |
|---|---|---|
| 防腐层(ACL) | 隔离外部模型的污染,做模型转换 | 对接外部系统或老系统 |
| 开放主机服务(OHS) | 用一套API对外暴露内部模型 | 作为服务方对多个上游提供能力 |
| 发布语言(PL) | 定义通信双方统一使用的数据格式 | 跨上下文的数据交互 |
| 共享内核 | 几个上下文共享一部分模型 | 团队耦合度高、模型重叠大的场景 |
| 客户-供应商 | 下游依赖上游,双方按契约协作 | 上下游开发节奏能对齐的场景 |
| 合作关系 | 两边一起改、一起发布 | 高耦合但高协作力的团队 |
我在实际项目里最常用的就是防腐层。接手过一个老系统,里面的“订单”表有几十个状态值,新系统完全没法用。我们的做法是在新系统里定义自己的Order模型,写一个防腐层把老系统的字段翻译过来,老系统怎么改都影响不到新系统。这个翻译层会有一定开发量,但保护了核心模型的纯洁性,长期看完全值得。
2.3 通用语言:让业务专家和开发说同一种话
通用语言是DDD里容易被忽视、但影响最深远的实践。它指的是团队内部使用一套统一的术语,这套术语在代码、文档、讨论中都保持一致。业务专家说“订单生效”,开发就不要说“订单表的状态字段变成2”;业务专家说“保证金”,开发就不要说“押金”。词汇统一之后,需求评审、代码评审、测试用例的理解成本都会大幅下降。
要让通用语言落地,最简单的方式是建一个团队词汇表,把每个关键业务词的定义、使用场景、与之关联的代码类名都列出来。中德AI开发者社区那场分享里,讲师提到一个细节:他们会在代码评审时检查PR里能不能找到对应的业务词汇,找不到就说明模型可能偏了。我试用过这个办法,初期会有点繁琐,但坚持几个迭代后,开发和业务方的沟通效率明显提升。
3. 战术设计:把业务规则变成代码结构
如果说战略设计是画地图,战术设计就是在这张地图上盖房子。战术设计的核心元素包括实体、值对象、聚合、领域服务、领域事件和仓储。
3.1 实体、值对象和聚合:建模的基本积木
实体是有唯一标识、并且会经历不同状态的对象。一个订单从创建到支付到完成,状态在变,行为在变,但它的OrderId始终不变。这个唯一标识是实体的灵魂。值对象则不同,它没有唯一标识,只用来描述实体的某些属性,比如金额、地址、颜色。值对象最重要的特征是不可变性——你不需要修改一个地址,你只会把它替换成一个新的地址。
聚合是一组为了保证业务不变量而必须保持一致性的实体和值对象的集合。聚合根是聚合对外的唯一访问入口。在设计聚合时,判断标准是:聚合内部的事务一致性必须被保证,聚合之间只讲最终一致性。举个例子:订单和订单项就应该在一个聚合里,因为一个订单必须至少有一个订单项,这个规则必须强约束;而订单和支付记录就应该分在不同的聚合里,因为“订单被支付”和“生成支付记录”可以用事件来解耦。
java复制// 聚合根示例:Order
public class Order {
private OrderId orderId;
private Money totalAmount;
private List<OrderItem> items;
private OrderStatus status;
// 业务规则:只有待支付状态才能添加订单项
public void addItem(Product product, int quantity) {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单已提交,不能修改");
}
this.items.add(new OrderItem(product.getProductId(), quantity));
this.recalculateTotalAmount();
}
private void recalculateTotalAmount() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
这段代码里,Order是聚合根,所有新增订单项的操作都必须经过它。别小看这一点,很多系统里的订单项可以被任意Service直接修改,这是数据一致性问题的根源。
3.2 领域服务、领域事件和仓储:让模型动起来
有些业务规则不属于任何一个实体或值对象,例如“转账”涉及两个账户,放在哪个账户里都不合适。这时候用领域服务,它的职责就是编排多个聚合完成一个完整的业务操作。领域服务里写的是业务规则,不是数据访问,这一点要特别留意。
领域事件用于聚合之间的解耦。订单创建后,可能需要通知物流、通知财务、给用户发消息。如果这些逻辑都写在订单聚合根的createOrder()方法里,聚合会膨胀得不可维护。更合理的做法是,Order聚合根在状态变化时发布OrderCreatedEvent,由其他限界上下文的事件监听器去处理。这种模式在分布式系统里格外好用,配合消息队列可以实现最终一致性。
仓储是数据访问的抽象。在DDD里,仓储接口定义在领域层,实现放在基础设施层,目的就是让领域层不依赖具体的数据库技术。你在领域层调用orderRepository.save(order)时,领域层根本不知道背后是MySQL还是MongoDB。这种依赖倒置能力,让领域模型能专注于业务规则,而不是被持久化框架绑架。
4. 分层架构:DDD落地的标准姿势
4.1 四层架构的职责划分
DDD最常见的落地形态就是分层架构,一般分为接口层、应用层、领域层和基础设施层。四层架构的依赖方向是向内的:接口层依赖应用层,应用层依赖领域层,基础设施层实现领域层定义的接口。下面是每一层的职责拆解。
| 层级 | 核心职责 | 不应该做的事 |
|---|---|---|
| 接口层 | 接收HTTP请求、参数校验、组装响应 | 不写业务逻辑、不直接操作数据库 |
| 应用层 | 用例编排、事务控制、权限校验 | 不写业务规则、不写持久化SQL |
| 领域层 | 业务规则、业务不变量、领域事件 | 不依赖框架、不依赖基础设施 |
| 基础设施层 | 数据库访问、消息发送、外部API调用 | 不包含业务规则 |
刚开始实践时最容易混淆的是应用服务和领域服务的边界。它们的区分标准很简单:应用服务描述的是“用户用例”,比如“下单”“取消订单”;领域服务描述的是“领域行为”,比如“计算运费”“校验库存”。应用服务负责协调事务、调仓储、发事件,领域服务负责处理那些跨聚合的业务逻辑。
4.2 依赖倒置是关键中的关键
分层架构能不能跑起来,核心在于依赖倒置原则(Dependency Inversion Principle)。简单说:领域层定义仓储接口,基础设施层提供实现。传统开发里Service层直接调用Mapper,是一种典型的上层依赖下层的结构;而DDD分层架构里,依赖箭头被反转了,上层依赖的是领域层定义的接口,而不是基础设施层的具体类。
我在落地时有一个习惯:把聚合根、值对象、仓储接口、领域事件放在领域层,用纯Java或纯Kotlin实现,不引入任何Spring注解。这样做的最大好处是领域层可以用单元测试单独验证,不需要启动Spring容器,测试跑得飞快。基础设施层里才放仓储的JPA实现、MQ消息发送实现,这些类负责“技术翻译”。
4.3 用完整代码走一遍订单创建流程
纸上谈兵没有意义,我直接用实际项目里的订单创建流程演示一下落地过程,从接口层到基础设施层完整走一遍。
先定义应用服务的入参:
java复制public class CreateOrderCommand {
private String customerId;
private List<OrderItemCommand> items;
// getter/setter省略
}
接口层Controller:
java复制@RestController
@RequestMapping("/orders")
public class OrderController {
private final CreateOrderAppService createOrderAppService;
@PostMapping
public OrderResponse create(@RequestBody CreateOrderRequest request) {
CreateOrderCommand command = new CreateOrderCommand();
// 组装command
OrderId orderId = createOrderAppService.createOrder(command);
return OrderResponse.from(orderId);
}
}
应用层AppService:
java复制@Service
@Transactional
public class CreateOrderAppService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final OrderEventPublisher eventPublisher;
public OrderId createOrder(CreateOrderCommand command) {
// 加载依赖的聚合
List<Product> products = productRepository.findByIds(command.getProductIds());
Order order = Order.create(command.getCustomerId(), products, command.getQuantities());
orderRepository.save(order);
eventPublisher.publish(new OrderCreatedEvent(order.getOrderId()));
return order.getOrderId();
}
}
注意应用服务里没有订单的业务规则,它只负责编排:加载依赖、调聚合根创建订单、保存、发事件。业务规则的校验,比如“库存不足不能下单”“同一商品不能重复提交”,全部在领域层。
领域层聚合根:
java复制public class Order {
private OrderId orderId;
private CustomerId customerId;
private List<OrderItem> items;
private OrderStatus status;
public static Order create(CustomerId customerId, List<Product> products, List<Quantity> quantities) {
Order item = new Order();
for (int i = 0; i < products.size(); i++) {
item.addOrderItem(products.get(i), quantities.get(i));
}
return item;
}
}
基础设施层仓储实现:
java复制@Repository
public class OrderRepositoryJpa implements OrderRepository {
private final OrderJpaMapper jpaMapper;
@Override
public void save(Order order) {
jpaMapper.save(OrderEntityMapper.toEntity(order));
}
}
这个流程看着简单,但它体现了一个核心结果:业务规则被完整地收拢到了领域层的聚合根里,其他层都只是“搬运工”。后续加一个新用例,不修改领域层,只需要加一个AppService方法就行了。
4.4 事务与一致性边界:什么时候强一致,什么时候最终一致
DDD落地时最头疼的往往是事务。聚合内部必须强一致,因此一个聚合的修改应该在同一个事务内完成。聚合之间则应该尽量走最终一致,特别是跨限界上下文时,通过领域事件来异步协调。
我在项目里遇到过一笔“下单同时扣库存”的业务需求,如果按数据库思维,很自然地会在一个事务里同时锁定订单表和库存表。但DDD的思路会把订单和库存分别建模为两个聚合,订单创建成功后发布OrderCreatedEvent,库存上下文消费这个事件去扣减库存。这中间有一段短暂的时间窗口,库存还没扣,但这在绝大多数业务场景里是可以接受的。事务边界收缩到聚合内部,并发冲突的概率也大大降低,系统整体吞吐量会明显提升。
5. 落地DDD最常见的坑,我帮你踩平了
5.1 把DDD当成微服务的入场券
DDD不等于微服务,DDD照样可以在单体架构里用。中德AI开发者社区那场分享里,讲师专门泼了一盆冷水:如果你的系统不需要独立部署、不需要独立扩容,单体架构加DDD的分层照样能把代码组织得清清楚楚。微服务是部署维度的事情,限界上下文是模型维度的事情,两者可以独立决策。我见过太多团队为了“上微服务”而强行拆限界上下文,结果网络调用比业务逻辑还多,问题比没拆之前更严重。
5.2 过度建模,连一个校验逻辑都要建个聚合
另一个极端是过度建模。一个简单的配置查询接口,非要建实体、值对象、聚合、仓储、应用服务全套,结果一个方法牵出七八个类。DDD是为复杂业务准备的,如果你的模块只是简单的增删改查,直接用普通的分层就行,不需要强行套DDD。判断标准很简单:这个模块的业务规则多不多?会不会频繁变化?如果答案都是否,请放过它,也放过你自己。
5.3 分层变成“伪分层”,依赖方向乱掉
最隐蔽的坑是名义上分了四层,实际依赖方向完全反了。比如领域层里直接写Spring的@Autowired,或者应用层直接调用基础设施层的类。一旦依赖方向乱掉,领域的独立性就没了。我经常在代码评审时做的一步检查是:把领域层单独抽出来看,能不能不依赖任何Spring注解。如果抽不出来,说明分层已经病入膏肓了。解决的办法只有一个:把那些外泄的依赖一点点收回来,领域层该定义的接口定义出来,基础设施层的实现慢慢补齐。
5.4 贫血模型泛滥:实体只有getter和setter
贫血模型指的是实体类里只有属性、getter和setter,没有任何业务行为,所有业务逻辑都堆在Service里。这样的建模方式本质上是披着DDD外衣的CRUD。真正的领域模型,实体应该包含业务行为:状态如何流转、金额如何计算、哪些业务不变量必须保护。我也承认,把业务逻辑从Service挪进实体是有迁移成本的,而且初期会让代码看起来“不熟悉”,但这个转型一旦完成,你会发现Service瘦身了,业务逻辑变得可测试、可复用。
5.5 业务专家和技术专家没有真正对话
DDD落地最难的从来不是技术,而是沟通。很多团队做事件风暴,业务专家没到场,开发自己闭门造车,最后建模建的是“想象中的业务”。我的经验是,第一次建模工作坊,业务专家必须全程在场。开发要做的不是把自己当需求的执行者,而是当业务的翻译者:通过追问“这个操作是什么场景触发的”“触发之后有什么限制条件”“数据不一致会带来什么后果”,把业务专家脑子里默认的规则挖出来。这个挖的过程,比写一万行代码都值钱。
6. 团队落地DDD的实战建议:从一场事件风暴开始
6.1 事件风暴工作坊怎么开才高效
事件风暴是落地DDD最有效的工作坊形式。操作方式不复杂:找一面墙,准备不同颜色的便利贴,邀请业务专家、产品经理、开发、测试一起参与。先让所有人把业务里会发生的事件写出来,橙色贴纸代表领域事件,比如“订单已创建”“库存已扣减”;蓝色贴纸代表命令,比如“提交订单”“取消订单”;红色贴纸代表触发规则,比如“订单超过30分钟未支付自动取消”。把这些贴纸按时间顺序贴在墙上,业务全貌就会逐步浮现。
工作坊最需要注意的几点:
- 时间盒控制在两小时以内,时间太长参与者会疲惫。
- 不要急着进入技术方案讨论,谁提“这个表怎么设计”,就把话题拉回“这个业务事件是什么”。
- 找一个有经验的主持人,他能识别出哪些讨论是核心域的,哪些是通用域的,引导团队把精力放在最重要的问题上。
- 会后要把壁上结果拍照存档,并且快速整理成一份“限界上下文初版图”,趁大家的记忆还新鲜。
6.2 推荐的工具、资源和AI辅助建模的思路
工具方面,远程协作的团队可以用Miro或类似的在线白板做事件风暴,内容实时同步,历史记录也能保留。建模阶段我建议画几张核心图:限界上下文图、聚合关系图、领域事件时序图,用普通的绘图工具画到能讲清楚的程度就够了,不要为了画图而画图。
学习资源方面,Eric Evans的《领域驱动设计:软件核心复杂性应对之道》是必读的经典,但很多人卡在读不进去。我的建议是先读Vaughn Vernon的《实现领域驱动设计》,里面有大量可操作的代码,入门会顺畅很多。再配合社区里的实战文章和案例,把理论和自己的项目对照着看。
关于AI辅助,我最近的一个尝试是:把团队整理出的通用语言词汇表、限界上下文和聚合设计文档喂给大模型,让它用这些材料辅助生成代码骨架。在模型边界清楚的前提下,AI生成的CRUD代码几乎不需要改就能用,效果相当惊艳。这让我更确信,DDD的模型价值在AI时代不仅没有过时,反而会成为驾驭AI辅助开发的关键底座。
有一点必须提醒:AI生成的代码虽然能省力气,但聚合根里的业务不变量、领域事件的边界这些核心逻辑,一定要人来把关,不能让AI替你定业务规则。
写在最后,一段来自实操中的体会
从第一次读完《领域驱动设计》到现在,我最大的领悟是:DDD不是一套可以“上线”的功能,而是一个持续演进的过程。模型永远不会一次建对,它会在一次次需求变更里被修正、被重塑。现阶段我建议你不要从零开始搭一个完美的DDD项目,而是从你手上最痛苦的那个模块开始,用限界上下文把它的边界画出来,用聚合把散落的业务规则收拢起来,一步一步地重构。过程中的每一次“为什么这里不能直接改数据库”、每一次“这个规则到底属于哪个聚合”,都是你业务建模能力的提升。
如果你正在准备做第一次DDD实践,最后分享一个小技巧:找一个愿意花时间梳理业务的业务专家搭档,拉上一位有经验的DDD实践者做技术护航,再加上一面可以让你随便贴便利贴的白墙。这三个条件齐全,你的DDD之旅就已经成功了一半。中德AI开发者社区那场2.5万字的分享给了我很多共鸣,希望这篇浓缩出来的笔记也能成为你落地路上的一块垫脚石。
