用 Spring 思维理解 DDD —— 以 Kratos 为参照
在此前的学习工作中,使用的开发框架一直都是 SpringBoot,对 MVC 架构几乎是肌肉记忆:Controller 接请求,Service 写业务逻辑,Mapper 操作数据库,这套套路早已深入骨髓。
最近开始学习 Go,为了写微服务,接触到了 Kratos 框架,也顺带深入了解了 DDD(领域驱动设计)。一开始,我的第一反应是——“这不就是 MVC 换个说法吗?”
但越学越发现,虽然 DDD 和我们熟悉的 Spring MVC 分层在形状上很像,但它对“业务逻辑该放哪、数据访问该放哪”划分得更严格,还用工程化的方式强制执行这些规则。
这篇文章,我就用自己熟悉的 Java Spring 思维,把 DDD 的分层思想翻译成 Spring 语言,再对照 Kratos 在 Go 里的实现,帮你快速搞懂它们的异同。
往期博客
Go语言新手村:轻松理解变量、常量和枚举用法
Go 语言中的结构体、切片与映射:构建高效数据模型的基石
1. Spring 的常见分层
在 Java 项目中,最典型的分层是:
Controller → Service → Mapper → DB
- Controller:接收请求、参数校验、调用 Service
- Service:执行业务逻辑(有时混合数据访问)
- Mapper:访问数据库(MyBatis Mapper / JPA Repository)
这种分层没错,而且配合团队自律,也能写出很干净的项目结构。但现实是:
- Service 往往既写业务规则,又写 SQL 条件拼接;
- 业务规则分散在 Service、Mapper 甚至 Controller 中;
- 一旦底层数据访问方式变化(换数据库、换 RPC),改动会影响大量上层代码。
2. DDD 的目标:边界清晰、职责单一
DDD 要解决的,就是让业务逻辑与技术实现彻底解耦,做到:
- 业务规则集中在领域模型中,贴着数据维护不变式;
- 数据访问细节被隔离在仓储实现中,随时可替换;
- 跨聚合的编排逻辑集中在应用服务(Usecase)中,清晰可测。
DDD 的典型分层:
接口层(Controller / API)
→ 应用层(Usecase / Application Service)
→ 领域层(Entity / Aggregate / Domain Service / Repository 接口)
→ 基础设施层(Repository 实现 / 外部服务实现)
3. 用 Spring 语言对照 DDD 分层
DDD 层次 | Kratos 对应 | Spring 对应 | 职责 |
---|---|---|---|
接口层(API) | service | Controller | 参数校验、鉴权、DTO ↔ Domain 转换 |
应用层(Usecase) | biz | Service(理想状态) | 编排业务流程、事务控制、调用多个领域对象或外部服务 |
领域层 | repo | 实体类、领域服务接口 | 维护业务不变式、暴露行为方法、定义仓储接口 |
基础设施层 | data | Mapper / Feign 实现层 | 数据持久化、调用远程服务、模型映射(PO ↔ Domain) |
4. 核心理念对比
4.1 Repository
- Spring 常见写法:Mapper/Repository 接口直接返回 PO(数据库模型),业务层可能直接用它判断。
- DDD 写法:仓储接口定义在领域层,返回的是领域对象(封装了业务行为的方法),由基础设施层实现。
4.2 业务规则的位置
- 常见误区:在 Service 里写
if (user.getEnabled() == 0) throw ...
。 - DDD 方式:在领域对象中提供
ensureActive()
,领域对象自己决定什么是可用。
4.3 应用服务(Usecase)
- 职责:一次完整业务流程的编排、事务边界、调用多个仓储接口、发布领域事件。
- 不做的事:不直接写 SQL,不去判断
user.getEnabled()
,不实现底层细节。
5. 案例:锁单流程
下面的锁单方法只是为了演示 DDD 分层思路,并不具备生产可用性。在真实系统中,锁单流程往往要面对并发控制、一致性等复杂问题。
5.1 常见 Spring 写法(简化版)
@Service
@AllArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final UserMapper userMapper;
private final StockMapper stockMapper;
private final WalletMapper walletMapper;
@Transactional
public String lockOrder(String userId, String sku, int count) {
// 校验账户是否可用
if (!userMapper.ensureActive(userId)) {
throw new BizException("用户不可用");
}
// 校验库存
if (!stockMapper.hasStock(sku, count)){
throw new BizException("商品库存不足");
}
// 检查余额
if (!walletMapper.hasBalance(userId, count * price)){
throw new BizException("余额不足");
}
// 预减库存
stockMapper.reserveStock(...);
// 冻结余额
walletMapper.freezeBalance(...);
// 添加一条订单记录
orderMapper.insertOrder(...);
return orderId;
}
}
缺点:业务判断和数据访问混杂
5.2 DDD 写法(Spring 风格)
领域层(实体类 + 仓储接口)
// 聚合根:用户
public class User {
public void ensureActive() { /* 校验用户有效性 */ }
}
// 聚合根:库存
public class Stock {
public void reserve(int count) { /* 校验库存并预留 */ }
}
// 聚合根:钱包
public class Wallet {
public void freeze(double amount) { /* 校验余额并冻结 */ }
}
// 仓储接口
public interface UserRepo { User load(String id); void save(User u); }
public interface StockRepo { Stock load(String sku); void save(Stock s); }
public interface WalletRepo { Wallet load(String uid); void save(Wallet w); }
public interface OrderRepo { void save(Order o); }
应用层(用例编排)
@Service
@AllArgsConstructor
public class LockOrderUsecase {
private final UserRepo userRepo;
private final StockRepo stockRepo;
private final WalletRepo walletRepo;
private final OrderRepo orderRepo;
public void lock(String userId, String sku, int qty, double price) {
// 调用仓储接口,获取领域对象
User user = userRepo.load(userId);
Stock stock = stockRepo.load(sku);
Wallet wallet = walletRepo.load(userId);
// 业务编排
user.ensureActive();
stock.reserve(qty);
wallet.freeze(qty * price);
// 持久化数据
orderRepo.save(new Order());
stockRepo.save(stock);
walletRepo.save(wallet);
}
}
- 业务逻辑在领域对象:例如
ensureActive
、reserve
、freeze
方法,他们只关心业务实现,不关心数据是怎么获取的 - 数据访问集中在仓储实现:Repo 不做业务判断,取出来交给实体自己判断
- 应用层清晰编排流程:用 Repo 加载实体 → 调用实体方法做判断/修改 → 再通过 Repo 保存变更
6. Kratos 如何落地
Kratos 在 Go 里用目录结构 + wire 静态注入强制执行这种依赖方向:
service(接口层) → biz(用例+仓储接口) → data(仓储实现) → DB/远程服务
biz
中不能 import ORM/HTTP 客户端等具体库;data
中实现所有仓储接口,负责 PO ↔ Domain 映射;service
只负责接收请求、调用 Usecase。
这跟 Spring 在理想状态下的分层几乎一致,但 Kratos 用工程手段物理防止越层,减少团队自律成本。
7. 总结
用 Spring 开发者的眼光看:
- DDD 并不是要你放弃 Controller/Service/Mapper,而是让 Service 变成 应用服务,专注业务编排;
- 业务判断应该写在领域对象中,不应该在 Mapper 或 Service 里直接写;
- Repository 接口定义在领域层,实现放在基础设施层;
领域模型对外暴露的是业务语义,数据访问实现细节被封装在仓储里,上层业务不感知底层变化。