DDD 实战(五):账号、组织、关系、群组的边界取舍
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 的现实主义在这里:模型边界要比部署边界先出现。
下一篇我们继续看更复杂的跨域协作:消息、通知、搜索和开放平台如何通过领域事件协同。