开头
提到DDD(领域驱动设计),做后端的人第一反应通常是两件事:概念很美好,落地很痛苦。我见过太多团队在启动会上把事件风暴、聚合划分、限界上下文讲得头头是道,结果一写代码就原形毕露——Service层胖成山,实体变成纯数据容器,值对象被塞成Map,防腐层直接消失。这些不是团队不努力,而是DDD对人的要求实在太高:既要懂业务建模,又要懂代码设计,还得在任何一次需求变更中坚持架构约束。脑力一跟不上,架构马上就漂移了。
最近我在折腾AI编程工作流的时候,接触到一个很有意思的东西:cleanddd-skills。说白了,它是一套给AI Agent用的"DDD技能包",让大模型在写代码时不再是机械地生成CRUD,而是被规则约束着,按DDD的战术模式去产出代码。这篇文章就把我对它的理解、实际折腾过程和踩过的坑完整写出来,希望对那些想用AI辅助做领域建模、又怕AI把代码写成一坨屎的团队有帮助。
这个思路解决的是实打实的痛点:AI写代码确实快,但它没有架构贞操感,你如果不约束它,它默认就会生成Controller + Service + Mapper三件套,跟你聊再多DDD也没用。cleanddd-skills做的事,就是给AI装上一套"DDD岗位职责说明书"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1. DDD落地难的本质:不是概念不行,而是缺少可执行的工程约束
1.1 建模会议开完,代码是另一套
很多人把DDD落地难归咎于概念抽象,比如实体和值对象的边界如何界定、聚合多大算合适、事件风暴怎么组织。但根据我的观察,真正的崩塌点不在这里,而是在"建模结论迁往代码"的这个过程。事件风暴产出了一大面便利贴,领域专家拍板了核心子域怎么划分,架构师画好了限界上下文关系图——然后呢?回到工位上,从Controller开始写,写着写着就回到老路了。
原因在于,DDD的建模产物和工程代码之间存在一条巨大的翻译鸿沟。领域专家表达的是"订单可以包含多个商品项,每个商品项有自己的快照价格和数量",但Java代码要用什么组件承载这个结论?是Order聚合根里放List<OrderItem>,还是直接在订单Service里用List<Map<String, Object>>传参?如果没有人持续做这种"翻译决策",开发人员靠直觉写代码的结果,绝大多数情况下就是贫血模型+万能Service。
贫血模型不是说不能跑,而是在业务复杂到一定程度后,业务规则散落在各个Service方法中,同一个规则在不同地方有不同实现,改一个校验逻辑要翻遍五六个类。我见过最夸张的例子,一个订单取消的状态校验逻辑被复制到了三个不同的Service里,其中两处已经因为业务迭代而语义不一致了。架构腐化从来不是一夜之间发生的,它就是每次"先这样写,下次再重构"累积出来的。
1.2 传统代码生成器为什么解决不了
既然人是不可靠的,那用工具来约束行不行?很多人已经试过代码生成器。MyBatis Generator、JHipster、以及各色低代码平台,我都见过团队尝试。它们能解决的是"通用CRUD层的机械劳动问题",但解决不了"业务规则在不同层之间的正确分配问题"。
打个比方,代码生成器像是一台只能做指定款式家具的木工机床——你调好参数,它能又快又准确地切出抽屉和挡板。但如果这批次家具需要根据每个用户的房型定制,尺寸和结构每次都不一样,机床就无能为力了。DDD恰恰是这种高度定制化的活儿:每个聚合的边界、仓储接口的方法语义、应用服务对事务边界的组织方式,都依赖具体业务场景,不可能靠一套固定模板穷举。
更麻烦的是,代码生成器产出的代码是一次性的。项目一旦进入持续迭代,需求变了,结构自然跟着改。生成器没有办法感知你后面的手工改动,于是它生成的代码和手工代码逐渐分叉,最后没人敢再碰那个生成按钮。DDD需要的是"持续的结构守护",而不是"一次性的代码生成"。
1.3 AI编程时代,问题的解法变了一个维度
大模型编程工具的出现,让这个死局出现了松动。AI写代码的本质不是模板填充,而是"在给定上下文条件下进行概率化生成"。它完全可以生成符合DDD风格的分层代码,前提是它的上下文里被注入了足够强的DDD约束规则。
但没有约束的AI,默认写出来的东西极其"泥石流"。你让它实现一个订单创建功能,它会非常贴心地生成OrderController、OrderService、OrderMapper、OrderDO,然后在OrderService里完成所有的状态流转和金额计算。如果你在提示词里写下"请用DDD实现",它确实会给出domain、application、infrastructure这样的包名,但你细看代码就会发现——它只是把Service改名成了ApplicationService,业务规则仍然写在一个个方法里,实体依然没有行为。这就是典型的"挂着DDD的羊头,卖着三层架构的狗肉"。
cleanddd-skills这个技能包的思路,恰好是针对这个现象来的。它想解决的问题不是"提高AI代码生成速度",而是"保证AI在一套指定代码架构约束下进行生成"。
2. cleanddd-skills到底是什么:给AI塞进一份"DDD岗位说明书"
2.1 它不是框架,不是模板,是一种Agent技能定义
先说清楚一个容易混淆的概念:cleanddd-skills不是一个可以引入的Maven依赖,也不是什么新的Java框架,更不是低代码平台。你可以把它理解成一组专门给AI编码Agent使用的"技能文件"。熟悉Agent Skills机制的话,你应该了解这类技能的本质结构:一个包含说明文档、规则清单和可执行材料的目录。AI Agent在运行前会加载这个目录,把里面的规则作为自身行为约束的一部分。
我当下的做法是把这个技能目录放到Agent技能仓库里,让它在每次编码会话开始时自动读取。读取之后,AI对自己身份的认知就不再是"一个通用的编程助手",而更像是"一个遵循Clean Architecture与DDD战术建模规则的资深后端开发者"。这套"身份设定"看起来只是措辞变化,实际效果差别非常大,因为大模型的输出风格高度依赖系统提示里对角色和规则的描述强度。
2.2 核心模块:分层规则、战术模式与代码生成哨兵
从我自己在实操中整理的目录结构来看,一个可用的cleanddd-skills技能包至少包含这样几层内容:
分层规则文件定义了标准的四层边界(如果按Clean Architecture视角也可以拆为interface/application/domain/infrastructure),由外向内依赖。文件里不仅写"依赖方向必须向内",还给了具体判例——比如Application层能不能直接引用Infrastructure层的类、Domain层是否可以出现Spring注解等。这些判例极为重要,因为大模型靠泛泛描述理解规则会产生非常大的歧义,它需要正例和反例来锁定边界。
战术模式卡片是技能包的核心资产。实体卡、值对象卡、聚合根卡、领域服务卡、应用服务卡、仓储接口卡,每张卡片都包含:模式定义的通俗解释、适用场景、代码骨架示例、以及什么情况下不该用这个模式。价值对象尤其需要强化,因为它是新手最容易忽略的部分。技能包里会明确告诉AI,金额、编码、时间段这类"有格式约束且不可变、有内聚行为"的字段值应当封装为值对象,而不是放任业务代码里到处使用BigDecimal或String裸传。
编码哨兵规则是我称呼它的一种东西,也是这套技能机制比较巧妙的部分。它要求AI在生成完一段代码之后进行一轮"自检",把生成的代码和一套DDD破戒清单逐条比对。如果发现自己把业务校验逻辑写进了应用服务,或者让领域实体直接调用了MyBatis的Mapper,就要主动停下来修正。这相当于把架构评审变成一个模型可执行的自检步骤。
2.3 名称里的Clean:不只是DDD,还叠加了整洁架构
这个技能名里隐藏的另一个关键词是Clean Architecture。为什么DDD落地还要扯上Clean Architecture?因为它们解决的问题并不完全相同。DDD侧重于为复杂业务找到合适的领域模型,主要是从业务需求推演出聚合、实体、值对象等模型结构。Clean Architecture则侧重于系统层次和依赖规则的组织,让外层的不稳定机制(数据库、Web框架、MQ)不污染内层稳定的业务核心。
cleanddd-skills把两者合并处理,实际工程意义不小。你单纯用DDD很容易做出一套与Spring深度耦合的"领域模型",但基础设施替换时痛苦不堪。加上Clean Architecture的依赖规则后,领域层就是一个百分之百纯Java的模块,不知道什么是MyBatis接口,也不知道什么是RestController,它只被应用服务调用,并通过仓储接口反向依赖由基础设施层实现。如果AI严格遵守了这套约束,那么换数据库本质上就是换一个基础设施模块的事,业务核心一行代码都不动。
3. 实操记录:让AI按cleanddd-skills产出一个订单域模块
3.1 先把架子搭好:Spring Boot项目 + 技能包加载
我建议第一次尝试时不要直接拿生产项目做实验,容易劝退。找一个干净的托盘工程,基于Spring Boot 3.x,用Maven做依赖管理。我自己的实验工程划分了三个Maven模块,分别是order-api(interface层)、order-app(application层)、order-domain(domain层)和order-infra(infrastructure层),再由一个装配模块order-boot负责启动和依赖注入装配。
然后就是把cleanddd-skills技能目录放到Agent的工作目录之下。如果你使用支持Skills机制的编码AI工具,通常只需要注意目录命名和SKILL说明文件的格式。技能说明文件要写得足够清晰,让AI一眼明白这个技能在什么时候该被触发——理想情况下,只要用户提的需求里有"新增一个业务模块"或"设计一个聚合"的字样,Agent就会自动加载这套规则,而不是等你手动切换。
3.2 我到底给AI下发了一个什么任务
实验目标我选择了一个非常经典的例子:订单创建。完整的需求描述是这样一段话:
"用户能够创建一个订单,订单中包含多个商品项,每个商品项记录商品ID、购买数量和成交单价。创建订单时需要校验商品是否可以销售,订单总额需要根据成交单价和数量计算。订单创建后默认处于待支付状态。"
这个描述非常朴素,几乎没有任何DDD暗示。如果直接丢给没有技能约束的AI,我预测它会立刻输出一堆DTO和Mapper。但有了cleanddd-skills,AI的第一反应应当不是去建表,而是先做领域分析。
3.3 AI先产出的竟然是模型文章,而不是代码
这是让我印象最深的一步。AI在加载了技能包后写出的第一份产物不是代码,而是一份模型说明:它先指出Order是一个聚合根,具有创建订单这个工厂方法;OrderItem是值对象,因为在这个上下文里它没有独立生命周期,跟随Order创建和消亡;然后它分析"商品是否可以销售"属于商品上下文的业务规则,不应该让订单聚合去直接查询商品状态,应该由应用服务协调两个领域完成,或者引入防腐层。
我没有教它这些,是技能包里的聚合识别判断规则起作用了。如果换作平时代码生成,它根本不会关心这些。看到这个产出,我当时的感觉是——DDD落地最难的模型设计环节,终于出现了可以被AI代劳的可能。
3.4 代码生成不是一把梭,而是分步落实
后续AI分步产出了以下代码:
领域层代码典型结构:
java复制public class Order {
private OrderId id;
private OrderStatus status;
private List<OrderItem> items;
private Money totalAmount;
public Order(OrderId id, List<OrderItem> items) {
this.id = id;
this.status = OrderStatus.PENDING_PAYMENT;
this.items = new ArrayList<>(items);
this.totalAmount = configurable items aggregate
// 这里由领域方法计算总额并快照
this.totalAmount = calculateTotalAmount();
}
public void markPaid() {
if (!this.status.canTransitTo(OrderStatus.PAID)) {
throw new IllegalStateException("当前状态不能变更为已支付");
}
this.status = OrderStatus.PAID;
}
private Money calculateTotalAmount() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
值对象OrderItem的代码:
java复制public record OrderItem(ProductId productId, int quantity, Money unitPrice) {
public OrderItem {
if (quantity <= 0) {
throw new IllegalArgumentException("商品数量必须为正数");
}
Objects.requireNonNull(productId, "商品ID不能为空");
}
public Money getSubtotal() {
return unitPrice.multiply(quantity);
}
}
看到Money被单独建模成值对象而非BigDecimal直传,我知道这套约束是真的在起作用。AI还在注释里自己解释了一句——金额相关的计算都被收拢到Money值对象里,业务代码只需要做表达式编排,不需要关心单位溢出和舍入规则。
应用服务层代码:
java复制@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final ProductAvailabilityChecker productAvailabilityChecker;
public OrderId createOrder(CreateOrderCommand command) {
// 应用服务负责跨领域协调与事务边界控制,不写业务规则
List<OrderItem> items = command.items().stream()
.map(item -> new OrderItem(
new ProductId(item.productId()),
item.quantity(),
new Money(item.unitPrice())
))
.toList();
productAvailabilityChecker.ensureAllAvailable(items);
Order order = new Order(new OrderId(UUID.randomUUID().toString()), items);
orderRepository.save(order);
return order.id();
}
}
注意,库存/可用性检查被放到了一个叫做ProductAvailabilityChecker的接口中,这个接口在领域层定义,基础设施层实现。AI没有直接在应用服务里写死"查询商品表status字段=1来判断可用",这正是防腐层思想的体现。
3.5 追问式迭代:让AI修正自己的越界行为
实操中最有价值的时刻来了一次质疑。生成初版之后,我故意给AI提了一个问题:"这个订单总额的计算逻辑是不是写得太分散了?是否要放一个策略模式让它能支持促销活动?"
这是我埋的一个"诱导式需求"。实际上在这类简单需求中引入策略模式是过度设计。结果AI的回答很克制,它拒绝了引入策略模式,说当前的业务并发复杂度使用简单计算,配合快照语义已经足够支撑,过早引入策略模式反而会破坏聚合内聚性。它还主动指出,如果将来促销活动真的要进入领域规则,第一改动点应该在Order内部而不是应用层。
这种"有原则的拒绝"让我比较惊讶。普通提示词下的AI基本上是有求必应,你说加策略模式它一定给你写一堆接口和实现类。但加载了技能包后的AI,倾向于在DDD的原则框架内评判需求合理性,这已经有点接近一个DDD实践者的思维方式了。
4. 常见翻车现场与排查技巧
4.1 症状:包名是domain,代码是三层架构
这是最常见的情况。AI把类放到domain包下面,却没有真正的领域行为。Order类里全是getter和setter,业务逻辑全部写在ApplicationService中,聚合根形同虚设。会让这种问题产生的触因有几种,一个是技能包规则文件的约束强度不够,Prompt上下文被大量对话冲淡;另一个是AI可能被旧代码风格污染了——它从上下文里看到了项目里已有的三层架构代码,会倾向模仿那种风格。
排查方式看一个核心点:Order类里有没有封装业务不变式,比如状态流转规则、金额计算逻辑。如果Order里只有状态字段和一堆get/set,基本可以判定位贫血模型。纠偏做法是把"行为必须内聚到聚合内部"这类规则,写到自检清单的前三条,并且在技能包示例代码里给出正例和反例对照。
4.2 症状:值对象被当成普通DTO用
AI没有把OrderItem定义成record或不可变类,而是给了一个可变的class,里面有一堆setter。或者更常见的情况——它根本不会创建OrderItem这个类型,直接用一个List<Map<String, Object>>来承载商品项数据。这种情况多半是因为模型对值对象这个概念的理解还不够到位,它不知道在DDD里一个业务概念承载的格式和行为约束越高,越值得被建模成一个独立值对象。
我通常要求技能包给一个强制规则:在领域层,业务字段永远不能使用List<Map>、String传JSON这类无类型结构;所有具有格式约束或计算规则的数据载体都应当建模为record类值对象。把这条规则放进负面代码示例里,比在正面说明部分反复强调有用得多。
4.3 症状:聚合直接调用其他聚合的仓储
让AI实现"订单创建时扣减库存",稍微没约束住,它就会在订单聚合内部直接调用一个商品仓储的接口,通过商品对象来修改库存数量。这在DDD里是明确的红线——一个聚合不能通过另外一个聚合的仓储直接修改其内部状态。
正确的处理方式是命令或调用事件:订单聚合可以发出QtyReservedRequest(应用层协调)或者领域事件"OrderCreated"由商品上下文侧的订阅者进行库存预扣。实操中,我会特别要求AI把跨聚合状态变更拆为三个候选方案之一去实现:应用服务链路编排、领域事件的异步消费、领域服务的多聚合协调。AI必须显式说明自己选了哪个,不能绕过去。
4.4 症状:领域服务被滥用,聚合重新失血
还有一部分翻车表现为AI把很多操作放进了DomainService,OrderDomainService里面写满了与订单状态流转相关的业务方法。适当使用领域服务没有错,比如跨多个聚合的转账操作。但如果你发现同一个聚合内的高内聚业务行为也被塞进领域服务,那聚合根就退化成数据壳了。
这条问题我用一个"责任归属检查"让AI自查:一段业务逻辑在描述时主语如果是"订单状态",那它就应该被放进Order的方法里;主语如果是"跨订单与账本的金额划转",才应该放进领域服务。这个方法对AI特别有效,因为大模型对自然语言主语的理解能力很强,它能通过这个启发式判断做出正确的职责归置。
4.5 持久化方案的纠结
AI生成领域模型后,到了基础设施层往往纠结:用JPA还是MyBatis?要不要把JPA注解直接打在Order类上?如果在领域实体类上直接标@Table和@Column,这个类就和JPA绑死了,领域层被迫依赖持久化框架,污染了纯洁性。
我的实践中比较顺手的做法是:领域层定义聚合和仓储接口,基础设施层单独建立数据库映射对象(可能叫OrderPO之类),由仓储实现类完成聚合与PO的转换,然后在仓储内部做数据组装。这个映射代码确实繁琐,但对模型隔离意义重大。如果团队使用了JPA的自定义构造函数查询或ResultTransformer,也能减少部分手工映射量。另外MyBatis的ResultMap可以处理一部分复杂的嵌套关系生成,AI在技能包的引导下能比较好地根据目录约束完成自动选择——你在技能包的工程说明文件里写明底层持久化使用MyBatis-Plus或JPA,它就不会在领域实体上加注解。
4.6 常见问题与排查速查表
| 症状表现 | 根因判断 | 处理建议 |
|---|---|---|
| domain包里全是DTO风格类 | 规则约束失效/上下文污染 | 强化自检清单,清理对话上下文后重试 |
| 值对象可变、带setter | 建模规则未落实 | 提供正面示例record OrderItem(ProductId, quantity, unitPrice) |
| 应用服务里写了大量if-else业务规则 | 分层边界被模糊 | 应用服务只做编排与事务,业务规则收拢到实体方法 |
| 聚合调其他聚合的仓储 | 聚合边界概念不清 | 引导应用服务协调或领域事件驱动 |
| 领域层依赖Spring/JPA注解 | 依赖规则文件未生效 | 在技能包加入依赖方向正反例,领域层引用Spring包时自动报错 |
| AI拒绝使用DDD,仍然CRUD | 任务描述与技能触发条件未配对 | 明确需求文本,加入"领域建模"关键词 |
5. 用一套技能约束AI编码,这件事本身值得想清楚什么
5.1 Skill封装的不是代码模板,而是项目守则
我越来越觉得,做一套类似的DDD技能包,本质上是在做项目级架构守则的显性化编码工作。传统上,这些守则存在于架构师的Review意见里、存在于团队wiki的最佳实践里,甚至存在于老员工脑子里。新来的同事为何架构代码写得漂移?不是因为他笨,而是没有一个人把他的每一步编码都拉回到守则的轨道上。
一个设计良好的技能文件,等于把架构守则翻译成了模型可执行的偏好和禁忌。你反复让AI遵守DDD分层原则的过程,本质上和给新人做架构培训是一样的——只是这个新人学习速度极快,一次就是几百行代码,但忘性也大,让它在输出之前先做一轮自检,是弥补这个缺陷的好办法。
5.2 人应当以什么身份留在这个工作流里
有人担心AI加规则之后,开发人员会不会被替代。我实际的体会是:替代的不是开发人员本身,而是开发人员的"无脑打字时间"。人被替代出来的精力应该放到更有价值的环节——领域建模评审、业务规则梳理、架构风险识别。
我使用cleanddd-skills跑完一个模块后,不会直接采纳AI产出的代码。我会先看它的领域模型设计文档,判断聚合划分是否准确,看它有没有把值的边界划错。确认建模没有问题后再把代码落到本地,然后只检查几个关键节点:聚合行为是否完整、应用服务事务边界是否合理、仓储接口是否够用。其余大部分代码我基本不逐行审阅。人从"代码审查者"变成了"建模审查者",这个转变发生得很自然。
5.3 团队引入的路径与顺序
如果你想在真实项目里引入这个过程,我的建议是不要全面铺开,也不要在遗留老项目里做噩梦级别的重构。找两个相对独立的新业务模块先跑通试点。试点过程中让架构师先把技能包按当前项目的技术栈细节调好,以MyBatis为例,就得把SQL文件的生成规则、Mapper接口所在模块的依赖限制写进技能包的工程说明里,否则AI生成出来的基础设施代码几乎肯定要给你惹麻烦。
同时需要一个清晰的评审闭环。AI生成的东西必须过人工评审,哪怕只是领域模型评审。我给团队定的底线是:领域层的实体和值对象,每一行都要有人看过;应用服务层做代码抽样评审;基础设施层主要看SQL和事务逻辑。运转两三周之后,团队成员会对这套技能包的优劣形成自己的修改意见,再迭代技能文件本身。某种意义上,你维护的不是一份代码文件,而是一份活的架构规范。
最后再分享一个让我感触很深的细节
有一次AI在生成Order类时,主动生成了一个私有方法applyEvent并写了注释:该方法用于对OrderApprovedEvent做状态投影,未来如果引入事件溯源机制可以无缝对接。我看完代码笑着说这个设计在当前场景用不上,但还是保留了它。因为它让我意识到,一个加载了DDD技能的Agent,已经不再是一个机械的代码搬运工,它会按照领域模式的惯性去思考半步之后的事情——这恰好是DDD希望领域专家和开发者都能做到的思考方式。
如果你最近也在尝试让AI写业务代码,同时又觉得它在架构上让你不放心,强烈建议你去搞清楚cleanddd-skills这类技能方案。你不一定要直接复刻它的实现,但可以把这套设计思路抄走:先把你们团队内部的DDD约束写成一份机器可读的标准化文件,再喂给AI Agent,让它从第一行代码开始就站在架构边界之内。这门手艺,将来大概率会成为业务开发团队的一项新基本功。
