DDD 中的代码组织:按技术层分 vs 按领域模块分,哪种才是正解?
前言
在实践领域驱动设计(DDD)时,你可能见过两种截然不同的代码组织方式:一种是传统的按技术层划分文件夹,另一种是按业务模块划分文件夹。两种写法的人都声称自己在做 DDD,那到底哪种更合理?本文来聊聊这个问题。
一、两种风格长什么样?
风格 A:按技术层划分
src/ ├── controllers/ │ ├── OrderController.java │ ├── UserController.java │ └── ProductController.java ├── services/ │ ├── OrderService.java │ ├── UserService.java │ └── ProductService.java ├── repositories/ │ ├── OrderRepository.java │ ├── UserRepository.java │ └── ProductRepository.java └── entities/ ├── Order.java ├── User.java └── Product.java特点:所有 Controller 放一起,所有 Service 放一起,所有 Repository 放一起。代码按"技术职责"归类。
风格 B:按领域模块划分
src/ ├── order/ │ ├── entity/ │ ├── service/ │ ├── repository/ │ └── event/ ├── user/ │ ├── entity/ │ ├── service/ │ ├── repository/ │ └── event/ └── product/ ├── entity/ ├── service/ └── repository/特点:每个业务领域独占一个文件夹,内部再按技术层细分。代码按"业务归属"归类。
二、两种都算 DDD 吗?
严格来说,两种都可以算 DDD。
DDD 的核心在于战略设计(限界上下文、通用语言)和战术设计(聚合、实体、值对象、领域事件等),并没有强制规定文件夹必须怎么组织。只要你的代码遵循了聚合隔离、分层架构等原则,文件夹结构属于实现细节。
但——
按领域模块划分,才是 DDD 社区推荐的做法。原因往下看。
三、为什么按模块分更优?
| 维度 | 按技术层分 | 按领域模块分 |
|---|---|---|
| 可读性 | 找一个功能需要跨 3-4 个文件夹 | 打开一个文件夹就能看到完整业务 |
| 维护性 | 修改一个功能,改动散落各处 | 改动集中在一个目录内 |
| 扩展性 | 新增模块的文件分散在各层 | 新建一个文件夹即可 |
| 拆分微服务 | 需要大量重构抽离 | 直接把文件夹拎出去 |
| 聚合隔离 | 容易跨聚合乱引用 | 天然形成物理隔离边界 |
| 团队协作 | 多人改同一文件夹,冲突频繁 | 各团队各守一个模块目录 |
核心原因:与 DDD 的限界上下文天然契合
DDD 强调:
聚合之间在代码上应当完全隔离,聚合之间通过应用层协调。
按模块划分文件夹,让限界上下文在物理结构上可见。一个文件夹就是一个限界上下文(或聚合),边界清清楚楚。而按技术层划分时,这个边界是隐性的,全靠开发者自觉——时间一长,跨模块调用必然泛滥。
四、为什么还有人按技术层分?
几个常见原因:
MVC 惯性:从 Spring MVC / Rails 等框架入门,习惯了 controller-service-dao 三层结构,迁移到 DDD 时直接照搬。
项目规模小:只有 2-3 个实体时,按模块分反而显得过度设计,按层分更简洁直观。
“伪 DDD”:口头上说在做 DDD,实际思维还是面向数据库的 CRUD,时间一长项目会退化成"披着 DDD 外衣的 MVC"。
框架脚手架默认生成:很多脚手架默认生成的就是按层分的结构,开发者没有主动调整。
五、推荐的项目结构
结合 DDD 四层架构 + 按模块划分,推荐如下结构:
src/ ├── interfaces/ # 用户接口层(对外暴露) │ ├── rest/ │ │ ├── OrderController.java │ │ └── UserController.java │ └── dto/ │ ├── application/ # 应用层(用例编排,不含业务逻辑) │ ├── order/ │ │ └── OrderApplicationService.java │ └── user/ │ └── UserApplicationService.java │ ├── domain/ # 领域层(核心业务逻辑) │ ├── order/ # 订单限界上下文 │ │ ├── entity/ │ │ │ ├── Order.java │ │ │ └── OrderItem.java │ │ ├── valueobject/ │ │ │ └── Money.java │ │ ├── event/ │ │ │ └── OrderCreatedEvent.java │ │ ├── repository/ │ │ │ └── OrderRepository.java # 接口定义 │ │ └── service/ │ │ └── OrderDomainService.java │ │ │ └── user/ # 用户限界上下文 │ ├── entity/ │ ├── valueobject/ │ ├── repository/ │ └── service/ │ └── infrastructure/ # 基础设施层(技术实现) ├── persistence/ │ ├── OrderRepositoryImpl.java │ └── UserRepositoryImpl.java ├── messaging/ └── config/这个结构的好处
- 领域层是绝对核心,不依赖任何外部框架
- 每个限界上下文一个文件夹,边界清晰
- 架构演进友好:将来拆微服务时,
domain/order/整个目录迁移即可 - 新人友好:看目录结构就能理解业务划分
六、什么时候用哪种?
| 场景 | 建议 |
|---|---|
| 项目只有 1-3 个实体,且不会扩展 | 按技术层分就够了,别过度设计 |
| 中大型项目,多个业务领域 | 按模块分,必须的 |
| 团队多人协作 | 按模块分,减少代码冲突 |
| 预期未来要拆微服务 | 按模块分,提前做好物理隔离 |
| 学习 DDD 初期练手 | 直接用按模块分,养成好习惯 |
七、总结
| 结论 | 说明 |
|---|---|
| 两种都"算" DDD | DDD 不强制规定文件夹结构 |
| 但按模块分是推荐做法 | 让限界上下文在代码物理结构上可见 |
| 按技术层分的本质问题 | 业务边界隐性化,长期维护成本高 |
| 一句话原则 | 代码结构应该反映业务结构,而不是技术结构 |
如果打开你的项目目录,一个不懂技术的产品经理都能大概看出"这里是订单、这里是用户、这里是商品",那你的代码组织就对了。
如果你正在从传统 MVC 项目向 DDD 转型,第一步不妨就从调整文件夹结构开始——把散落在各技术层的同一业务代码,收拢到同一个模块目录下。这一步虽然简单,但对团队理解"领域边界"的帮助是立竿见影的。
