← 返回文章列表

DDD 实战(七):从领域模型到部署模块

2026年6月28日架构设计
DDD微服务模块化单体Monorepo

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,我会推荐这条路线:

  1. 先做模块化单体。 按 Identity、Organization、Relationship、Group、Messaging、Notification、Open Platform、Search、Governance 建立代码边界。
  2. 建立数据所有权。 每个上下文明确拥有的表和聚合,禁止其他上下文直接写。
  3. 建立领域事件。 先用进程内事件或 outbox 表跑通事件模型。
  4. 拆异步支撑能力。 优先拆 Notification、Search、Open Platform Worker、Audit Worker。
  5. 拆高频核心链路。 在边界稳定后拆 Messaging。
  6. 按团队和流量继续拆。 不按领域图机械拆服务。

这条路线比较朴素,但它尊重一个现实:业务模型稳定之前,服务拆得越细,返工成本越高。

10. 领域模型最终要落到工程纪律

领域边界能否落地,不取决于架构图,而取决于工程纪律:

  • 包依赖是否单向。
  • 模块能否隐藏内部实现。
  • 数据库表是否有明确所有者。
  • 跨上下文是否通过接口或事件。
  • CI 是否能发现非法依赖。
  • 代码评审是否坚持边界。
  • 故障和发布是否按上下文复盘。

如果这些纪律没有建立,微服务也救不了系统。它只会把原来的单体泥球变成分布式泥球。

下一篇我们落到 Go 项目,看看在单模块和 monorepo 下,DDD 的代码、文件、包应该怎么组织。

返回博客列表