1. 先说清楚DDD到底在解决什么问题
有些团队买了不少DDD相关的书,也参加过培训,但落地的时候还是写成了CRUD——Service里面全是业务逻辑,实体变成纯数据容器,领域事件沦为摆设。这不是个别现象,我见过太多团队把DDD当成一套代码规范来执行,结果只学到了"分层""聚合""仓储"这些表面的形态,完全没有理解它真正要解决的问题是什么。
DDD,领域驱动设计,核心目标只有一个:在业务复杂度高的系统里,让代码结构能真实反映业务结构,让业务语言的变动能快速传导到代码实现层面。
它不是用来解决技术问题的。如果你的系统只是简单的增删改查,或者技术瓶颈在性能、并发、分布式一致性这些层面,那DDD大概率帮不上忙,甚至会让代码变得更啰嗦。我经常跟人讲一个判断标准:当业务规则复杂到"代码里表达不清楚"的地步,当业务人员和开发人员对同一个名词的理解开始分叉,当每一次需求变更都牵动全身的修改,这时候才该认真考虑引入DDD。
这里要先区分两个层面的东西——战略设计和战术设计。战略设计解决的是"系统边界怎么划、模块之间怎么通信"的问题,核心工具是限界上下文(Bounded Context)和上下文映射(Context Mapping)。战术设计解决的是"某个上下文内部怎么建模"的问题,核心工具是实体、值对象、聚合、领域服务、领域事件、仓储这些模式。
很多人上手DDD就是研究战术模式,天天琢磨聚合怎么建、仓储接口怎么定义,却不关心限界上下文划分得对不对。这等于在错误的地基上盖房子,你代码写得再规范,整个系统的边界是乱的,照样处处碰壁。
还有一个误区是把DDD和微服务强行绑定。DDD里的限界上下文确实可以对应微服务的拆分边界,但DDD绝不等于微服务,单体架构里照样可以用DDD来做模块划分。我甚至建议初学者先在单体应用里实践DDD,把建模思想和代码落地这套跑通,再去考虑服务拆分的事,否则建模和分布式问题混在一起,很难定位到底哪里出了问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从业务事件出发:事件风暴的具体做法
那DDD落地第一步该做什么?我的建议是:别急着写代码,先做事件风暴(Event Storming)。
事件风暴是一种工作坊式的建模方法,把业务人员、产品经理、开发人员、测试人员聚在一起,用不同颜色的便利贴在白板上还原整个业务流程。它的核心思路是从业务事件出发反向推导领域模型,不是从数据表出发,也不是从页面原型出发。
具体操作是这样的:
- 先用橙色便利贴把业务领域里发生过的、可能会发生的事件全部列出来。事件一定用过去式描述,比如"订单已创建""库存已扣减""支付已超时"。这一步先求全,不用管顺序和逻辑,把能想到的事件都贴出来。
- 然后所有人一起把这些事件按时间顺序排列,理出一条完整的时间线。排列过程中会发现某些事件的前提不成立、某些环节业务上有分歧,这些分歧点就是建模时要重点关注的业务规则。
- 接着补上触发每个事件的"命令"(通常是用户操作或外部系统的调用),通常在蓝色便利贴上。命令和事件之间会有很自然的因果关系,比如"提交订单"这个命令触发"订单已创建"这个事件。
- 命令必然来自某个"角色",通常用黄色便利贴标注。比如"买家""客服""仓库管理员"。这个角色不一定是系统用户,也可能是外部系统、定时器、甚至是另一个限界上下文。
- 每个命令执行时需要读取或修改的数据,聚在一起就可以识别出"聚合"的雏形。这些和数据相关的概念,通常用白色或绿色便利贴表示。
你别小看这个过程。很多团队做DDD做不下去,就是因为跳过事件风暴直接画类图。事件风暴最大的价值不光是产出领域模型,而是它让业务人员和开发人员用同一种语言把整个业务流程完整过了一遍。过程中会发现很多之前没人注意到的业务规则,比如"订单超过30分钟未支付必须自动关闭"这种规则,它就是一个"支付已超时"事件加上一个定时任务触发"关闭订单"命令的组合。
事件风暴做完之后,白板上会有几十张便利贴,这时候就需要做聚合识别了。聚合是DDD战术设计里最核心也最难把握的概念。简单说,聚合是一组有业务完整性的对象集合,外部只能通过聚合根访问内部对象。再直白一点:每次业务操作需要保证一致性的数据范围,就是一个聚合。
举个例子,电商里订单和订单项就是同一个聚合,因为"订单总金额"这个不变量要求订单和订单项必须同时修改、同时保存。但如果把订单和支付记录放同一个聚合就不太合适,因为支付状态的变化可以独立于订单其他信息的修改,它们各自有各自的完整性边界。这个判断过程需要经验,更重要的是需要业务人员的参与——数据到底该怎么变化,业务规则说了才算。
事件风暴的产出除了领域模型,还有一份非常重要的"副产品"——业务术语表(Ubiquitous Language,通用语言)。风暴过程中出现的每一个业务概念,都要明确它的名称、含义、边界,全团队统一使用。这份术语表后续要一直维护,代码命名、接口字段、需求文档里出现的术语都必须以它为准。很多团队模型建模没问题,最终死在术语混乱上——代码里叫"Order",业务系统里叫"销售单",数据库表叫"t_sales_order",每次沟通都要做一次名词翻译,这是最消耗团队认知成本的事情。
3. 识别限界上下文:系统边界划对了,架构就成功一半
事件风暴做完,你会得到一堆聚合和业务事件。接下来要做的是限界上下文划分——把这些聚合和事件归属到不同的业务边界里。
限界上下文(Bounded Context)是DDD里最重要的战略设计概念,没有之一。一个限界上下文就是一个完整的业务能力边界,它内部有一套自洽的领域模型和业务规则。不同上下文里甚至可以有同名的概念,但它们的含义和模型可以完全不同。
还是拿电商举例。"商品"这个名词,在商品目录上下文里,它包含SPU、SKU、属性、图片、规格这些信息,主要服务于前台展示和检索。但在订单上下文里,"商品"只需要商品ID、名称、单价这几个快照字段,目的是保证下单时价格不被后续修改影响。两个上下文里的"商品"根本不是同一个对象,如果强行复用一个模型,就会导致一个上下文里改了字段,另一个上下文跟着遭殃。
区分限界上下文的方法论主要有两种:按业务能力划分,或者按业务域(子域)划分。按业务能力分比较直观,比如订单、库存、支付、会员、营销,每个核心业务能力就是一个上下文。按子域分需要先做业务域分析,核心域、支撑域、通用域,然后每个子域可以对应一个或多个上下文。
我自己的经验是,事件风暴做完之后,一眼看过去那些聚合之间关系很紧密、业务事件因果链很强的一组,放在同一个上下文;聚合之间只是通过事件通知进行协作的,就放在不同上下文。 比如"订单已创建"这个事件会让库存上下文做预占操作,让支付上下文生成支付单,但订单、库存、支付各自的聚合并没有直接的强耦合,把它们三个切开是合理的。
上下文划完之后,还要画上下文映射图,明确上下文之间的集成方式。DDD里定义了多种集成关系:合作关系(Partnership)、共享内核(Shared Kernel)、客户-供应商(Customer-Supplier)、防腐层(Anti-Corruption Layer,ACL)、开放主机服务(Open Host Service,OHS)、发布语言(Published Language)等。
防腐层这词听着抽象,但你只要见过一次相关案例就懂了。假设你的订单系统要对接一套老旧的ERP,ERP里"客户"的概念、编码方式、规则跟你们完全不一样。如果你直接把ERP的客户数据往自己系统里塞,你的领域模型就被污染了。正确做法是建一个防腐层,负责把ERP的客户数据转成你们订单上下文里的模型,把外部系统的混乱挡在边界之外。
我开始接触DDD的时候,觉得这些战略概念很虚,不如写代码实在。直到后来参与一个项目,前期没认真做上下文划分,所有业务模块共用一套领域模型和数据库,结果每次改动都牵一发动全身。后来老老实实把上下文切清楚,加防腐层,再上事件驱动通信,系统的复杂度才算控制住了。那次之后我才真正理解——限界上下文这种东西,划错了早期看着没什么问题,系统演进到中后期,成本差异是数量级的。
4. 落地战术设计:用实际案例把实体、值对象、聚合、仓储讲透
战略设计解决"边界"问题,战术设计解决"边界内部怎么建模"的问题。这部分是大家最熟悉也最容易走样的,我直接用电商订单作为例子,把几个核心概念逐个过一遍。
先看一个订单聚合的代码骨架:
java复制@Entity
@Table(name = "orders")
public class Order {
@Id
private String id;
@Embedded
private Money totalAmount;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
private String customerId;
private LocalDateTime createdAt;
// 业务方法而非 getter/setter
public void addItem(ProductSnapshot product, Money price, int quantity) {
// 校验:订单已提交后不允许追加商品
if (this.status != OrderStatus.DRAFT) {
throw new IllegalStateException("已提交的订单不能追加商品");
}
this.items.add(new OrderItem(product, price, quantity));
this.totalAmount = this.totalAmount.add(price.multiply(quantity));
}
public void submit() {
if (this.items.isEmpty()) {
throw new IllegalStateException("订单不能为空");
}
this.status = OrderStatus.SUBMITTED;
// 注册领域事件
registerEvent(new OrderSubmittedEvent(this.id, this.customerId, this.totalAmount));
}
}
这里面有几个关键点要说明。
实体(Entity):Order和OrderItem都是实体,它们有唯一的标识(ID),并且生命周期里状态会变化。注意,实体不等于数据库表记录,它的本质是"有身份、会变化"的对象。订单从草稿到已提交再到已支付,状态一直在变,但ID始终是同一个,所以它是实体。
值对象(Value Object):Money就是一个典型的值对象。它只有数值和币种两个属性,没有ID,两个Money的数值相同就视为相等。值对象在设计里用得很妙——它把"多钱""多少钱"这类细粒度概念封装成不可变对象,避免了到处传double或者BigDecimal的尴尬。你在领域模型里最好优先把概念建模成值对象,只有在确实需要标识时才用实体。
聚合(Aggregate):Order包裹着OrderItem,Order是聚合根,外部对订单项的修改只能通过Order根上的方法进行。这个设计的本质是约束作用在聚合内对象上的操作路径,保证不变量不被破坏。什么叫不变量?比如"订单总金额必须等于所有订单项金额之和",这个规则只能在聚合内部确保,如果你让外部直接改items列表,总金额就会对不上。
仓储(Repository):仓储是为聚合服务的。它只对聚合根定义接口,屏蔽了数据库访问细节。
java复制public interface OrderRepository {
Order findById(String id);
void save(Order order);
}
注意两个细节。一是仓储的粒度是聚合根,不要给OrderItem单独建仓储,因为订单项不能脱离订单独立访问。二是实体先join再写库——你在代码里new一个Order,加入几个item,最后调用orderRepository.save(order),框架通过级联操作把整棵聚合树持久化。如果还用传统"先insert主表再insert明细"的思路,说明还是没跳出数据建模的思维惯性。
领域服务(Domain Service):有些业务动作不属于任何实体或值对象,比如"计算整单优惠后的实付金额",它要读取订单信息、优惠券信息、会员等级信息,这些信息分散在多个聚合里。这时候就需要领域服务。领域服务有个重要原则:它应该是无状态的,它的职责是编排领域对象完成某个业务动作,但业务规则仍然要放在领域对象内部。 如果领域服务里充满了if-else的业务判断,说明建模走偏了——业务规则又回到过程式代码里了。
领域事件(Domain Event):领域事件是DDD战术设计中最容易用错、但用对了收益极大的概念。Order在submit之后注册了一个OrderSubmittedEvent,这个事件可以同步触发库存预占、异步通知支付服务创建支付单等下游动作。领域事件的价值在于让聚合之间通过事件协作,而不是通过方法调用强耦合。用事件驱动的方式,订单聚合完全不需要知道库存聚合的存在,只需要发布"我完成了"这个事实,剩下的事让别的上下文去关心。
5. 分层架构的取舍:应用层、领域层、基础设施层的边界在哪
DDD的经典分层架构是四层:接口层(User Interface)、应用层(Application)、领域层(Domain)、基础设施层(Infrastructure)。实际落地时,大家普遍把接口层和应用层合并了,核心就剩应用层和领域层两层。这只是个简化,问题不大。真正需要注意的是各层的职责边界,一旦越界,整个架构就开始腐化。
领域层是你整个系统的核心资产,它只装业务规则和业务对象。实体、值对象、聚合、领域服务、领域事件都在这层。领域层不依赖任何技术框架和基础设施,不依赖Spring、不依赖JPA、不依赖数据库连接。这一层的类只表达业务,不掺和其他任何东西。如果你在领域层的实体里看到@Autowired注入了一个RedisTemplate,那说明架构已经被破坏了。
应用层是领域层的"协调者"。它负责接收接口层的请求,编排领域对象完成业务操作,调用仓储获取聚合,协调事务,发布领域事件。应用层的类通常以"XXApplicationService"或者"XXService"命名,方法名直接对应用例,比如submitOrder、cancelOrder。
举个反例。很多人的Service是这样的:
java复制@Service
public class OrderService {
@Transactional
public void submitOrder(String orderId) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}
// 业务校验都在service里
if (order.getStatus() != OrderStatus.DRAFT) {
throw new BusinessException("订单状态不正确");
}
if (order.getItems().isEmpty()) {
throw new BusinessException("订单项不能为空");
}
// 修改状态
order.setStatus(OrderStatus.SUBMITTED);
orderRepository.save(order);
// 发送事件
eventPublisher.publish(new OrderSubmittedEvent(...));
}
}
看出来问题了吗?所有的业务规则(状态校验、空订单校验)都被放在了应用层,订单这个实体沦为一个只有getter/setter的数据容器。这就是典型的贫血模型——领域对象不承载业务逻辑,所有的逻辑散落在Service里。这样的架构根本不算DDD,只是披着DDD外衣的三层架构。
正确做法是让业务判断回到实体内部:
java复制public void submit() {
if (this.status != OrderStatus.DRAFT) {
throw new IllegalStateException("订单状态不正确");
}
if (this.items.isEmpty()) {
throw new IllegalStateException("订单项不能为空");
}
this.status = OrderStatus.SUBMITTED;
registerEvent(new OrderSubmittedEvent(this.id, this.customerId, this.totalAmount));
}
应用层的submitOrder方法就变成纯粹的三步:查订单、调order.submit()、保存。业务逻辑放进实体之后,应用层薄得像一张纸,但整个系统的业务内聚性大大增强。判断一个代码是否具备DDD的味道,最简单的办法是拆掉所有Service,看看业务逻辑还能不能存活——如果实体里只有getter/setter,那业务逻辑就跟着Service一起消失了。
还有一个常见的问题是事务控制。DDD里事务的边界应该是"一个用例一个事务",应用层加@Transactional就好。不要在领域层方法里开启事务,也不要在实体里做事务控制。事务是技术关注点,不是业务关注点,放在领域层只会把业务和技术耦合得更紧。
6. 基础设施层的落地:仓储实现、ORM的选择和依赖倒置
基础设施层是很多人在DDD落地时最容易卡住的地方。原因在于,领域层不依赖技术框架这个原则虽然听起来很美好,但一写代码就不知道怎么处理数据库字段映射、JSON序列化这些技术细节了。
我的建议是:用依赖倒置解决"领域层不依赖基础设施"和"系统总得连数据库"之间的矛盾。 具体做法是:仓储接口定义在领域层,仓储实现在基础设施层,运行时通过Spring的依赖注入把实现类装进去。
java复制// 领域层
public interface OrderRepository {
Order findById(String id);
void save(Order order);
}
// 基础设施层
@Repository
public class JpaOrderRepository implements OrderRepository {
private final JpaRepository<OrderEntity, String> jpaRepository;
@Override
public Order findById(String id) {
return jpaRepository.findById(id).map(OrderEntity::toDomain).orElse(null);
}
@Override
public void save(Order order) {
jpaRepository.save(OrderEntity.from(order));
}
}
这里有一个很关键的细节:基础设施层操作的是OrderEntity(持久化对象),领域层操作的是Order(领域对象),两者之间是相互独立的类,需要用转换方法(from/toDomain)做映射。很多初学者不理解为什么要多这一层转换,觉得直接用JPA注解打在领域实体上不也一样吗?
区别非常大。JPA注解本质上是一种技术约束,当你把订单的关联关系、懒加载策略、级联操作用注解打在领域实体上时,领域对象的行为就被JPA绑架了。比如你可能会为了懒加载,把订单项的集合类型从List改成Set;为了级联删除,把集合的cascade设置为ALL。这些技术考量会反过来扭曲你的领域建模。保持领域对象的纯粹性,让它在内存里做完所有业务计算,最后再抽取快照存到数据库,是更稳妥的落地姿势。
当然,这里也存在取舍。项目规模小、团队对DDD不熟的时候,用注解直接打在实体上,省掉转换层的成本,也可能是合理选择。不过一旦你这么做了,就要接受领域层和技术框架耦合的现实。我个人的经验是,只要项目活过一年半载,业务复杂度上来了,领域对象和技术注解混在一起最终都会变成脏代码,只是时间早晚的问题。
还有一个经常被忽略但是非常实际的问题——ORM的事务边界和聚合加载策略。默认情况下,JPA实体的懒加载是有session的,如果session在业务方法执行完就关闭了,那么之后访问关联集合就会抛LazyInitializationException。这个问题在DDD里尤其突出,因为领域对象通常在基础设施层被组装好之后,要在应用层跑完一整套业务操作。解决办法是在领域对象被构建时,一次性把聚合内需要的数据都加载好,之后是完全离线的。换句话说——让领域对象在聚合构建完成后变成"无session"状态,不要在领域方法执行过程中触发数据库懒加载。
7. 建模时最容易翻车的位置:过度建模、万能聚合、被通用语言绑架
DDD落地最大的风险往往不是技术,而是建模失控。我总结了几个常见的翻车模式,值得专门拿出来说。
过度建模。 这是最常见的。很多人学完DDD热血沸腾,恨不得把系统里所有的东西都建模成实体、值对象、聚合、领域事件,一张订单的创建流程能拆出十几个事件,库存一个字段都要建个值对象。结果代码量翻倍、维护成本爆炸,业务上根本不需要那么精细。建模的粒度要以业务复杂度为准:规则复杂、变化频繁的地方,模型要精细;规则简单、稳定的地方,直接映射数据库表就行,没必要强行套DDD的框架。DDD是用来控制复杂度的,如果它本身变成了复杂度来源,那就该减负了。
万能聚合。 有些团队为了"数据一致性"把一大堆对象塞进一个聚合里,订单聚合里不仅放着订单项,还放了支付记录、物流信息、售后记录。表面上看,这么建模保证了数据强一致性,还方便做级联保存。但实际上这个聚合已经大得没法用——每一次订单相关的操作都要加载整棵对象树,性能急剧下降;而且聚合内的对象生命周期并不一致,支付记录可能是另一个限界上下文创建的,物流信息更是在订单上下文之外产生的。聚合大小的判断标准是"事务边界 + 业务不变量",一个聚合只保证自己的不变量,不追求全局强一致。
被通用语言绑架。 事件风暴时定义的通用语言,在后续代码里被机械地执行,导致中文术语和英文类名盲目对应。比如业务方把"提交订单"叫"下定",然后你就在代码里把方法名命名为xiaDing()。这属于走极端。通用语言的价值在于团队内部沟通时使用统一术语,不是为了把代码变成拼音翻译。英文类名应该反映业务概念的本质,而不是逐字翻译中文动词。
还有一种情况争议很大,就是一个上下文里到底能放几个聚合。我的经验是,多数业务场景下一个上下文里只有一两个核心聚合,其他都是辅助性聚合。如果你在一个上下文里画了七八个聚合,十有八九是上下文划分不够细,或者根本没有真正理解业务——那些聚合的边界全被切碎了。
做DDD建模还有一个容易犯的错误:把建模和数据库表结构一一对应。 聚合的边界是业务规则决定的,不是表关系决定的。你的订单聚合可以有OrderItem,但数据库里可能是冗余存储,也可能是JSON字段,这些是实现细节,不该反过来影响模型设计。碰到喜欢先画ER图再写领域模型的团队成员,我会劝他颠倒一下顺序——先把聚合画出来,再决定表怎么建。
8. 用事件驱动拆解聚合间协作:领域事件的落地范式
前面讲订单时,Order.submit()里注册了一个领域事件。要不要用事件驱动,以及怎么落地,这里系统说一下。
先明确一点:领域事件和消息中间件没有必然关系。领域事件首先是一个领域概念,是聚合对外表达"发生了某个事实"的语言。它可以只在进程内同步分发,也可以异步发到消息队列,这取决于你要不要跨限界上下文、要不要削峰填谷。
在同一个进程、同一个限界上下文内部,最简单的做法是使用Spring的ApplicationEventPublisher。订单提交后,通过TransactionSynchronizationManager注册一个afterCommit回调,等事务提交成功后发布事件:
java复制// 应用层
@Transactional
public void submitOrder(String orderId) {
Order order = orderRepository.findById(orderId);
order.submit();
orderRepository.save(order);
// 等待事务提交成功后发布领域事件
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
order.getDomainEvents().forEach(event -> {
applicationEventPublisher.publishEvent(event);
});
}
});
}
这个afterCommit的细节非常重要。如果你在事务尚未提交时发布事件,监听器如果去查库,会读不到刚写入的数据。很多系统在集成测试环境下看不出问题,一到高并发或分布式场景就开始出现各种诡异的bug,很多都是事件发送的时机不对。
跨限界上下文的话,领域事件会转换成集成事件。比如订单发布OrderSubmittedEvent,集成层(可以是Application层底下的一个组件)再决定是否把它丢进Kafka或者RocketMQ,供库存、支付等其他上下文消费。注意这里有一个原则:领域事件是领域层的概念,集成事件是架构层的概念,两者不要混在一层里。 很多人直接把领域事件丢给消息队列,短期看省事,长期看领域层又被消息中间件绑架了。
事件驱动还有个容易被忽略的坑:事件与聚合状态的一致性保证。 理想状态下,事件的发布应该在聚合状态变更的同一个事务里完成,上面用afterCommit就是这种思路。如果你用的是本地消息表或者事务性消息(transactional outbox),那是把"事件要可靠投递"这个需求进一步工程化了。如果项目对一致性要求没那么极端,afterCommit + 回调补偿已经够用,没必要一上来就引入outbox模式增加复杂度。
9. 用AI辅助DDD建模与编码的实战经验
最后聊一个比较新的话题:AI在DDD落地中的作用。
最近不少人都在讨论AI写代码、AI辅助架构设计。我的观点是:AI可以当DDD的"高并发助手",但做不了DDD的"领域专家"。 换句话说,它能帮你快速生成事件风暴的辅助材料、检查建模反模式、生成代码骨架、补单元测试,但聚合边界的判断、业务规则的提取、限界上下文的划分——这些核心决策还是要靠人来完成。
具体说几个我在实践中觉得比较靠谱的用法。
一是用AI辅助收集和整理业务规则。把开会的会议纪要、需求文档丢给AI,让它抽出候选的业务事件、命令、角色,生成一份事件风暴的初稿材料。这个初稿不用完全准确,能帮团队省掉大量从零开始的整理时间,然后团队基于初稿做增删改。这里AI是不可靠的——它经常会产生一些业务上根本没有的概念,所以需要对产出持怀疑态度,逐个确认,不能直接拿去当领域模型。
二是用AI做建模反模式扫描。让AI审查你的实体代码,判断是否存在贫血模型、是否在领域层直接引用了技术框架、Service是否过于臃肿。这个用法用起来比较顺手,因为DDD的反模式判断标准相对固定,AI能识别出大部分肉眼疏漏的问题。但它也只适用于低级别的审查,真正对聚合边界和上下文映射的判断,AI目前远达不到可用的程度。
三是生成测试用例和代码骨架。确定建模方案后,可以让AI根据聚合的公开方法生成完整的单元测试,覆盖正常流程、边界条件和异常分支。这个场景我觉得价值很实——模型的公共API就那么几个方法,AI按套路生成的测试基本能覆盖大部分场景,你只需要补上领域特有的分支判断。
我自己测试过让AI直接生成一个带违反"聚合根只能通过ID访问"这种规则的代码库,让它自动识别并修复,它做到了,但耗时比较久。AI给你生成一个DDD的四层项目骨架其实很快,但那个骨架是没有灵魂的——它只是一个空壳,业务规则还是要人来填。
所以我的结论是:AI写DDD,现在与其说"AI能写DDD",不如说"AI是在辅助人进行DDD设计的效率工具"。 如果没有领域专家的判断力来把关,AI生成一堆看似正确的DDD代码,只会让下一次重构变得更快、更灾难性。
10. 什么项目适合上DDD,什么项目真的没必要
写到这里,我得说点劝退的话——不是所有项目都值得用DDD,甚至大多数项目都不需要DDD。
一个项目要不要上DDD,核心判断标准是业务复杂度:业务规则是否复杂到逻辑分散在不同模块之间?领域概念是否频繁变化?团队是否经常因为搞不清某个业务逻辑的归属而改错代码?如果这些答案都是"否",那你用DDD只会自找麻烦——多出来的分层抽象会拖累开发效率,光维护那些实体、值对象、领域事件之间的关联就得花不少精力。
最适合上DDD的场景,通常有几个特征:业务规则密集且沉淀了多年经验;系统要不断演化,新需求高频出现;团队有比较成熟的业务专家在做规则输入。比如金融风控、保险理赔、电商交易、供应链调度这类系统,业务本身够复杂,DDD能真正发挥价值。如果你的项目就是一个简单的To B管理后台,几个表单、几张报表,还是老老实实CRUD + 标准三层架构就好。
另一个角度是团队情况。DDD对团队的门槛其实蛮高的,它需要开发人员对业务有真正的理解和敬畏。如果一个团队的成员更擅长写技术框架代码,对业务一知半解,DDD就会被玩成另一种形式的"表驱动开发"——只是把service里的逻辑挪到实体里,本质还是数据操作,没有任何领域表达。这种走形的DDD比传统的三层架构更难维护。
如果你确定要上DDD,我建议的路径是这样的:先在一个独立的、业务相对复杂的模块上做试点,事件风暴拉上业务人员认真做一轮,把限界上下文、聚合、通用语言定下来;然后设计领域模型、写代码、写测试,找一个迭代周期完整跑一遍;跑通之后复盘哪些方式有效、哪些是过度设计,再决定要不要推广到更多模块。一次把整个系统全面DDD化,风险太大,基本必翻车。
回顾我接触DDD这些年的经历,最大的感悟是:DDD不是一个"代码模板",而是一种思考方式。它逼着开发人员去理解业务、去思考业务的本质边界和变化规律,而不是拿到需求就直接建表、写Service。这种思维方式一旦建立,就算你最终没有全套使用DDD的战术模式,做出来的系统也会比之前清晰不少。
如果以后有人再问我"DDD怎么落地",我大概率不会先教他怎么建聚合,而是会先反问一句:你了解你的业务吗?
