DDD 实战(七):从领域模型到部署模块
DDD 实战(七):从领域模型到部署模块
推荐 BeWild 代充 Codex ,工作猛猛提效。
领域边界确定以后,很多团队会立刻问:那是不是要拆微服务?
答案通常是:不一定。
DDD 可以帮助识别微服务边界,但 DDD 不等于微服务。领域边界、代码边界、数据库边界、部署边界、团队边界,是几件相关但不同的事。
Microsoft 的领域分析文档也把领域分析作为微服务设计的重要输入,但它并没有把“发现一个限界上下文”直接等同于“立刻拆一个独立服务”。Microsoft: Domain analysis for microservices
这篇文章讨论的是:协同办公 IM 这种复杂系统,如何从领域模型走向部署模块,以及每一步的 trade off。
1. 五种边界不要混为一谈
先把几个边界拆开。
领域边界:业务模型在哪个上下文内成立。比如账号、组织、关系、群组、消息是不同上下文。
代码边界:代码如何分包、分模块、定义依赖方向。比如 identity 包不能直接 import messaging/internal/mysql。
数据边界:谁拥有哪类数据事实。比如组织上下文拥有成员事实,搜索上下文只拥有索引视图。
部署边界:哪些模块独立构建、发布、扩容、回滚。比如消息服务可以独立部署,搜索索引服务也可以独立部署。
团队边界:哪个团队负责哪个上下文。比如开放平台团队负责应用、机器人、事件订阅,IM 团队负责消息主链路。
这五种边界可以一致,也可以不一致。
早期系统可以有清晰领域边界和代码边界,但仍然部署成一个单体。成熟系统可以让高吞吐、高变化、高隔离要求的上下文独立部署。不要一上来就把所有边界都强行对齐。
2. 第一阶段:普通单体
很多系统最早都是普通单体:
app
├── controller
├── service
├── dao
└── model
它的优点很明确:
- 开发快。
- 本地调试简单。
- 部署简单。
- 事务处理容易。
- 团队协作成本低。
如果产品还在探索期,需求变化大,团队规模小,普通单体是合理选择。
但当协同办公 IM 的能力不断扩展,普通单体会出现明显问题:
- 所有业务规则堆在 service。
- 账号、组织、群组、消息互相读表。
- 消息主链路被搜索、通知、开放平台拖慢。
- 发布一个审批改动,也要带着消息模块一起发布。
- 故障隔离差,一个低优先级模块拖垮核心链路。
单体的问题不是“它是单体”,而是没有内部边界。
3. 第二阶段:模块化单体
模块化单体是 DDD 很推荐的中间形态。
它仍然是一个进程、一个部署包,但代码按限界上下文组织:
internal
├── identity
├── organization
├── relationship
├── group
├── messaging
├── notification
├── openplatform
├── search
└── governance
模块化单体的目标是:
- 先建立业务边界。
- 先明确数据所有权。
- 先让依赖方向变清楚。
- 先让领域事件和接口抽象成型。
- 暂时不承担分布式系统成本。
它的优点:
- 比普通单体更可维护。
- 比微服务更容易调试和部署。
- 可以使用本地事务。
- 适合在业务边界还不稳定时快速调整。
- 为后续拆服务保留路径。
它的缺点:
- 运行时仍然共享进程。
- 故障隔离有限。
- 不同模块不能独立扩容。
- 如果团队纪律差,仍然可能互相 import 内部实现。
模块化单体的关键,不是目录拆开,而是依赖规则要立住。比如 Messaging 可以调用 Group 暴露的接口校验会话权限,但不能直接查 Group 的表,也不能 import Group 的基础设施实现。
4. 第三阶段:选择性拆服务
当业务发展到一定规模,某些上下文会出现独立部署的需求。
协同办公 IM 中,通常优先考虑拆出的模块包括:
| 上下文 | 拆分原因 | 拆分风险 |
|---|---|---|
| Messaging | 高频核心链路,需要独立扩容和故障隔离 | 对组织、群组、风控依赖处理不好会拖慢发送 |
| Notification | 推送、提醒、红点适合异步处理 | 未读数和消息状态边界容易混淆 |
| Search | 索引构建和查询负载特殊 | 权限过滤和索引一致性复杂 |
| Open Platform | 外部应用、机器人、Webhook 有独立流量和安全模型 | 外部 API 版本和内部模型容易互相污染 |
| Governance | 风控、审计、合规横跨多个域 | 容易变成横向侵入所有模块 |
选择性拆服务的原则是:先拆变化率、负载特征、故障隔离需求明显不同的上下文。
不要因为领域图上有十个上下文,就一次拆十个微服务。那通常会把业务复杂度变成分布式复杂度。
5. 微服务的收益和代价
微服务的收益主要有:
- 独立发布。
- 独立扩容。
- 故障隔离。
- 技术栈隔离。
- 团队自治。
- 数据所有权更容易落实。
但代价也很重:
- 本地调试变复杂。
- 调用链变长。
- 数据一致性从本地事务变成分布式协作。
- 灰度、监控、链路追踪、告警都要跟上。
- 接口版本管理变重要。
- 测试成本上升。
- 小团队容易被运维复杂度拖垮。
协同办公 IM 特别容易掉进“过早微服务”的坑。比如账号、组织、关系、群组、消息一开始就拆成五个服务,看起来边界很清晰,但每次发消息都要同步调用多个服务。只要任何一个服务抖动,消息链路就变慢。
这时你会被迫引入缓存、事件、降级、幂等、补偿、链路追踪。结果业务还没复杂到那个程度,基础设施先复杂了。
6. 从模块化单体到微服务的迁移路径
比较稳妥的路径是:
第一步,在单体内按领域上下文重组代码。
messaging
├── domain
├── application
├── adapter
└── infrastructure
第二步,禁止跨模块直接访问数据库。所有跨上下文访问都走接口或事件。
第三步,把领域事件先在进程内跑通,事件模型稳定后再接 MQ。
第四步,把搜索、通知、开放平台这类天然异步上下文拆成独立进程。
第五步,再拆消息主链路或组织上下文这类核心服务。
第六步,根据团队和流量继续拆分。
这条路径的好处是每一步都能带来收益,而不是为了“微服务化”一次性重写系统。
7. Monorepo 适合什么阶段
monorepo 不是微服务的反义词。它只是代码仓库组织方式。
协同办公 IM 如果有多个服务,仍然可以用 monorepo:
apps
├── im-api
├── im-worker
├── open-platform-api
└── search-worker
domains
├── identity
├── organization
├── group
├── messaging
└── notification
pkg
├── observability
├── config
└── errors
monorepo 的优点:
- 跨服务重构更容易。
- 统一工具链和代码规范。
- 公共包版本一致。
- 领域模型演进时影响面更容易发现。
monorepo 的风险:
- 容易滥用公共包。
- 边界不清时,所有服务互相 import。
- 构建和 CI 需要按变更范围优化。
- 权限和发布流程要设计好。
如果团队采用 monorepo,必须明确依赖规则。公共包只能放真正通用的技术能力,不能把业务模型随便放进 pkg/common。
common 往往是边界崩坏的开始。
8. 部署拆分的判断信号
什么时候应该把一个模块拆成独立服务?
可以看这些信号:
- 它的发布频率明显不同。
- 它的流量特征明显不同。
- 它需要独立扩容。
- 它故障时不应该拖垮其他模块。
- 它的数据所有权已经清晰。
- 它和其他模块的交互已经稳定。
- 它有明确负责人或团队。
- 它的运维复杂度能被团队承受。
比如搜索上下文通常适合较早拆分,因为它的索引构建、查询负载、存储引擎和延迟容忍度都和消息主链路不同。
开放平台也适合独立,因为它面对外部开发者,需要配额、签名、回调、审计、事件版本管理,安全边界和内部业务不同。
但组织上下文未必一开始就要拆服务。如果团队还在频繁调整组织、成员、角色模型,先保持模块化单体可能更合适。
9. 推荐的演进路线
对于一个中大型协同办公 IM,我会推荐这条路线:
- 先做模块化单体。 按 Identity、Organization、Relationship、Group、Messaging、Notification、Open Platform、Search、Governance 建立代码边界。
- 建立数据所有权。 每个上下文明确拥有的表和聚合,禁止其他上下文直接写。
- 建立领域事件。 先用进程内事件或 outbox 表跑通事件模型。
- 拆异步支撑能力。 优先拆 Notification、Search、Open Platform Worker、Audit Worker。
- 拆高频核心链路。 在边界稳定后拆 Messaging。
- 按团队和流量继续拆。 不按领域图机械拆服务。
这条路线比较朴素,但它尊重一个现实:业务模型稳定之前,服务拆得越细,返工成本越高。
10. 领域模型最终要落到工程纪律
领域边界能否落地,不取决于架构图,而取决于工程纪律:
- 包依赖是否单向。
- 模块能否隐藏内部实现。
- 数据库表是否有明确所有者。
- 跨上下文是否通过接口或事件。
- CI 是否能发现非法依赖。
- 代码评审是否坚持边界。
- 故障和发布是否按上下文复盘。
如果这些纪律没有建立,微服务也救不了系统。它只会把原来的单体泥球变成分布式泥球。
下一篇我们落到 Go 项目,看看在单模块和 monorepo 下,DDD 的代码、文件、包应该怎么组织。