← 返回文章列表

DDD 实战(五):账号、组织、关系、群组的边界取舍

2026年6月26日架构设计
DDD限界上下文IM架构取舍

DDD 实战(五):账号、组织、关系、群组的边界取舍

推荐 BeWild 代充 Codex ,工作猛猛提效。

DDD 讨论中最有价值的部分,往往不是画出一张漂亮的领域图,而是在争议点上说清楚取舍。

账号、组织、好友关系、群聊关系,就是协同办公 IM 里最容易产生争议的一组边界。

有人会说:账号、好友、群成员都是“人和人之间的关系”,应该放在一个用户中心里。

也有人会说:组织成员和好友关系是两种完全不同的关系,必须拆开。

还有人会说:群聊是消息的一部分,群成员关系应该归消息域。

这些说法都不是完全错误。DDD 没有一个脱离业务阶段和组织约束的标准答案。真正重要的是:每种划分方案解决了什么问题,又制造了什么成本。

1. 先把几个词拆开

开始讨论边界之前,先把常见词拆开。

Account:可登录系统的账号。它关注登录、凭证、设备、安全策略。

Member:某个账号在某个企业或租户下的组织身份。它关注部门、岗位、角色、在职状态、通讯录可见性。

Contact:某个主体可联系的对象。它关注联系人列表、备注、关系来源、可见范围。

Friend:一种双向或经过确认的关系。它关注好友申请、通过、拒绝、删除、拉黑。

ExternalContact:企业外部的联系人,可能是客户、供应商、合作伙伴。

GroupMember:某个成员在某个群里的身份。它关注群昵称、群角色、禁言状态、入群方式。

这些词背后都可能指向同一个自然人,但它们属于不同业务语境。

如果团队不能先在语言层面拆开,代码层面的拆分一定会吵不清楚。

2. 方案一:全部放进用户中心

最简单的方案,是把账号、组织成员、好友关系、群成员都放进一个用户中心。

这种方案通常长这样:

user-center
├── account
├── member
├── friend
├── contact
└── group-member

它的优点很明显:

  • 早期实现快。
  • 查询链路短。
  • 事务简单。
  • 团队理解成本低。
  • 一个接口就能拿到“用户全部信息”。

对于早期产品,或者团队规模很小、业务规则很简单的系统,这个方案并不荒唐。

问题在于,它的长期代价也很明显:

  • 用户中心会持续膨胀。
  • 任何关系规则变化都会修改用户中心。
  • 组织、好友、群成员的生命周期纠缠在一起。
  • 下游所有模块都依赖用户中心。
  • 用户中心故障会影响登录、组织、消息、群聊和联系人。

这类模块最后很容易变成“上帝模块”。它不是因为设计者水平差,而是因为早期为了简单把太多语义塞进了一个边界。

3. 方案二:账号、组织、关系、群组分开

更符合中大型协同办公 IM 长期演进的方案,是拆成四个上下文:

Identity & Access
Tenant & Organization
Contact & Relationship
Group & Conversation

各自职责如下:

上下文拥有的事实不拥有的事实
Identity & Access账号、凭证、设备、登录会话部门、好友、群成员
Tenant & Organization企业、部门、成员、组织角色登录凭证、好友申请、群治理
Contact & Relationship好友、联系人、外部联系人、黑名单账号安全、部门树、群公告
Group & Conversation群、群成员、群角色、会话设置消息正文、组织主数据、好友主关系

这个方案的好处是边界更清晰:

  • 账号安全策略变化,不影响好友模型。
  • 组织架构调整,不直接修改群成员模型。
  • 群治理增强,不把消息域拖复杂。
  • 关系模型可以独立支持好友、客户、外部联系人。

代价也很明确:

  • 查询需要跨上下文组合。
  • 需要事件同步或缓存快照。
  • 一些流程从单事务变成最终一致。
  • 开发团队必须理解上下文之间的边界。

这个方案适合中长期产品,因为它把变化点拆开了。但如果团队没有工程纪律,只是把表拆到不同服务里,调用又互相穿透,那它只会增加复杂度。

4. 方案三:抽象成统一关系图谱

还有一种方案,是建立一个统一的 Relationship Graph,把组织关系、好友关系、群成员关系都抽象成“主体之间的边”。

例如:

Subject --[relation_type]--> Subject

关系类型可以是:

  • belongs_to_department
  • reports_to
  • friend_with
  • blocks
  • member_of_group
  • admin_of_group
  • external_contact_of

这个方案在模型上很诱人。它适合这些场景:

  • 产品高度依赖关系查询。
  • 权限判断可以抽象成关系图遍历。
  • 需要统一表达组织、社交、客户、群组等关系。
  • 团队有能力维护图模型和查询引擎。

但它的风险也很大:

  • 抽象过早。
  • 业务语言被技术图模型吞掉。
  • 不同关系的生命周期差异被掩盖。
  • 群治理、组织管理、好友申请的规则很难统一表达。
  • 普通业务开发理解成本高。

所以我不建议把统一关系图谱作为第一阶段主模型。它可以作为搜索、推荐、权限分析、组织洞察等派生能力存在,但不应该替代各个业务上下文的事实模型。

5. 账号和组织的关键取舍

账号和组织最容易被合并,因为它们都围绕“人”。

但在企业协作产品里,账号和成员的生命周期不一致。

账号可能长期存在,成员身份可能随企业变化:

  • 一个账号加入多个企业。
  • 一个账号从企业 A 离职,但仍在企业 B 有成员身份。
  • 一个成员被禁用,但账号仍可登录个人空间。
  • 一个成员转岗,只影响组织上下文。
  • 一个账号设备异常,只影响身份安全上下文。

因此,我更倾向于:

  • Account 属于 Identity & Access。
  • Member 属于 Tenant & Organization。
  • Account 和 Member 用稳定 ID 关联。
  • 组织上下文不直接修改登录凭证。
  • 身份上下文不直接维护部门树。

这样切的代价是多了一层关联和同步,但收益是长期模型更稳定。

6. 组织和关系的关键取舍

组织关系和好友关系也容易混在一起,因为它们都可以出现在通讯录里。

但组织通讯录和好友关系的业务含义不同。

组织通讯录来自企业管理。它通常由管理员、HR 系统或组织同步流程维护。成员之间是否可见,取决于组织策略、部门权限、字段脱敏规则。

好友关系来自个人或业务互动。它可能需要申请、同意、拒绝、删除、拉黑、备注、分组。

如果把组织成员和好友关系都放在通讯录模块里,通讯录会变得很难定义。它到底是组织事实源,还是个人关系事实源,还是搜索视图?

更好的方式是:

  • Tenant & Organization 拥有组织成员事实。
  • Contact & Relationship 拥有好友、联系人、外部联系人事实。
  • 通讯录页面只是一个组合视图。
  • 搜索上下文可以建立联系人索引,但不拥有关系事实。

这样一来,“通讯录”不再是一个大领域,而是多个领域在用户界面上的聚合入口。

7. 群组和消息的关键取舍

群组和消息也容易混在一起。

如果从用户体验看,群就是一个聊天窗口。但从领域模型看,群至少有两部分:

  • 群组事实:群资料、群成员、群角色、入群规则、群公告、群治理。
  • 消息事实:消息内容、发送状态、撤回、已读、转发、引用。

群组上下文应该回答:

  • 这个群是否存在?
  • 这个成员是否在群里?
  • 这个成员是否能发言?
  • 这个成员是否能邀请别人?
  • 谁可以修改群公告?

消息上下文应该回答:

  • 这条消息是否已写入?
  • 这条消息是否已撤回?
  • 哪些成员已读?
  • 客户端如何补拉?
  • 消息如何投递到多端?

两者协作时,消息发送可以调用群组上下文校验发送资格,也可以消费群组上下文的成员快照。但消息不应该直接维护群成员主数据。

8. 边界会随着业务变化而变化

今天合理的领域划分,几年后可能变得不合理。这不是 DDD 失败,而是业务演进的结果。

出现这些信号时,就应该重新审视边界:

  • 某个上下文经常因为另一个上下文的需求而修改。
  • 一个模型里的字段大多只服务少数场景。
  • 事务边界越来越难维持。
  • 模块之间出现大量互相读写数据库。
  • 团队职责和代码边界不一致。
  • 某个模块的发布频率明显高于其他模块。
  • 某个模块的故障影响范围远超它的业务职责。

比如早期好友关系放在账号模块里没有问题。但当好友申请、外部联系人、客户关系、黑名单、跨企业协作都出现后,就应该把关系上下文独立出来。

再比如早期群组和消息在一个模块里没有问题。但当群治理、群机器人、超大群、群权限、群文件、群公告都变复杂后,就应该重新拆分群组上下文。

9. 我的推荐基线

如果目标是中大型协同办公 IM,我会选择一个相对稳健的基线:

Identity & Access
Tenant & Organization
Contact & Relationship
Group & Conversation
Messaging

其中:

  • Account 不等于 Member。
  • Member 不等于 Contact。
  • Contact 不等于 GroupMember。
  • Group 不等于 Conversation。
  • Conversation 不等于 Message。

这不是唯一答案,但它能给长期演进留出空间。

对于工程落地,可以先不拆成五个服务。可以先在一个代码仓库里拆成五个模块,建立清晰的包边界、接口边界和数据所有权。等吞吐、团队、发布频率和故障隔离真的需要,再拆部署。

DDD 的现实主义在这里:模型边界要比部署边界先出现。

下一篇我们继续看更复杂的跨域协作:消息、通知、搜索和开放平台如何通过领域事件协同。

返回博客列表