3. 基于DDD的限界上下文划分

限界上下文(Bounded Context)是领域驱动设计(DDD)中最核心的战略设计工具,它直接回答了“某个领域概念应该由哪个模块负责”这一经典问题。本章将围绕子领域识别、上下文映射、聚合与实体边界、共享内核四个环节,给出可操作的划分步骤与权衡策略。

核心原则:每个限界上下文拥有独立的通用语言、数据存储与演化节奏,避免一个概念在不同上下文中的含义冲突。

3.1 子领域识别:从业务域到模型边界

子领域(Subdomain)是识别限界上下文的前置步骤。核心子领域决定系统竞争力,支撑子领域承载通用能力,通用子领域可直接采购或复用。通过事件风暴(Event Storming)或业务流分析,将整个业务域拆解为多个互不重叠的子领域,每个子领域对应一个潜在上下文。

子领域类型特征上下文划分策略
核心复杂、高商业价值、差异性强专属上下文,封闭演进,高质量团队投入
支撑必要但非核心,可内部开发可独立上下文,也可与主上下文共享部分模型
通用标准化,如认证、通知采购或开源产品,通过防腐层隔离
操作建议:在子领域识别阶段,先列出所有业务用例,按核心竞争力排序,再收敛到最终子领域列表。不要在前期追求完美边界,留出10%的调整空间。

3.2 上下文映射图:明确协作关系

上下文映射图(Context Map)描述了限界上下文之间的通信方式与信任等级。常见关系包括:合作关系(Partnership)、共享内核(Shared Kernel)、客户-供应商(Customer-Supplier)、防腐层(Anti-Corruption Layer)等。选择关系类型取决于两个上下文之间的耦合度与演化自由度。

  • 合作关系:两个团队同步计划,频繁集成,适用于早期探索阶段。
  • 客户-供应商:上游定义接口,下游按需实现,适用于稳定依赖场景。
  • 防腐层:通过适配器隔离外部上下文的模型变化,适用于集成遗留系统或第三方。
  • 分离方式:无直接调用,通过事件异步通信,最松散耦合。
常见陷阱:过度使用共享内核会导致上下文边界模糊,最终变成大泥球。共享内核只应用于高度稳定、变更极少的领域模型。

3.3 聚合与实体边界:划出内聚单元

聚合(Aggregate)是限界上下文内部的数据修改单元。每个聚合由根实体(Aggregate Root)控制所有内部实体的访问与持久化,外部只能通过根进行操作。聚合边界决定了事务边界与一致性范围。

划分聚合时遵循三条规则:
1. 根实体全局唯一:使用UUID或前缀+ID,不在不同上下文中混用。
2. 一次事务只修改一个聚合:跨聚合改动用最终一致性(事件驱动)。
3. 聚合尽量小:理想状态下包含1~3个实体,避免加载大量无关数据。

边界冲突信号:当两个实体在不同业务流程中被频繁联合修改,或一个实体的生命周期依赖另一实体的状态时,说明它们可能属于同一个聚合。

3.4 共享内核:高收益与高风险并存

共享内核(Shared Kernel)指两个上下文共享一部分领域模型与代码。这是DDD中耦合度最高的一种关系,适用于“两个上下文必须使用完全相同的概念定义”的场景,如订单上下文与支付上下文共享金额对象。

共享内核的落地要求:
- 双方团队共同维护共享模块,变更需双方评审。
- 共享部分必须使用独立模块或仓库,不允许直接复制代码。
- 尽可能将共享内容限制为值对象与接口,避免共享实体生命周期。

// 共享内核示例:金额值对象(放在shared-kernel模块中)
public record Money(Currency currency, BigDecimal amount) {
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new DomainException("货币不匹配");
        }
        return new Money(this.currency, this.amount.add(other.amount));
    }
}

// 订单上下文中使用
public class OrderItem {
    private Money unitPrice;
    private int quantity;
    public Money subtotal() {
        return unitPrice.multiply(quantity);
    }
}

// 支付上下文也使用同一Money对象
public class Payment {
    private Money totalAmount;
    public boolean canProcess(Money balance) {
        return balance.amount().compareTo(totalAmount.amount()) >= 0;
    }
}

3.5 落地步骤与验收标准

落地五步法

  1. 事件风暴产出事件流:与业务方一起梳理核心业务流程,标记每个事件的归属子领域。
  2. 映射上下文:将子领域分组为候选上下文,用上下文映射图记录关系。
  3. 识别聚合边界:对每个上下文,从根实体出发逐步内聚,直到实体间的非事务依赖被消除。
  4. 抽取共享内核:确认哪些模型必须完全一致,放入独立模块,并制定同步计划。
  5. 定义接口契约:使用OpenAPI或Protobuf明确定义上下文间的数据交换。

验收标准

维度通过条件
边界清晰度每个上下文拥有唯一的通用语言词汇表,无二义性条目
独立部署修改一个上下文的代码无需同时部署其他上下文
事务一致性同一聚合内的数据修改在一个事务内完成,跨聚合通过事件最终一致
数据库独立不同上下文的数据表或模式完全隔离
共享内核稳定性共享模块的接口变更频率低于每个月1次,且变更前双方预演
风险提示:如果验收时发现某个上下文的数据库表被其他上下文的定时任务直接修改,或者两个上下文使用同一个消息队列主题消费事件,说明边界已被破坏,需要立即执行重构。
限界上下文划分总体框架 子领域识别 事件风暴产出业务事件 按核心/支撑/通用分类 收敛到候选上下文列表 上下文映射图 定义协作关系类型 客户-供应商 / 防腐层 合作关系 / 分离方式 聚合与实体边界 根实体全局唯一 一次事务只改一个聚合 聚合尽量小(1~3实体) 共享内核 值对象与接口共享 独立模块 / 共同维护 变更需双方评审 落地五步法 ①事件风暴 → ②映射上下文 → ③聚合边界 → ④共享内核 → ⑤接口契约 验收标准 独立部署 | 事务一致 | 数据库隔离 | 通用语言无歧义 | 共享内核变更≤1次/月 ⚠️ 避免过度共享 共享内核只限稳定值对象

3.6 选型权衡与风险防范

在限界上下文划分过程中,以下权衡点直接影响最终架构质量:

决策项推荐方案备选方案适用场景
上下文粒度按业务能力拆分(一个上下文对应一组完整用例)按数据实体拆分(每个聚合独立上下文)核心业务用能力拆分,通用业务用实体拆分
通信模式事件驱动(RabbitMQ/Kafka)Feign REST同步调用跨上下文查询用REST,命令用事件
共享模块组织独立Git仓库 + 私有NPM/Maven包Monorepo中共享目录团队协作频繁用独立仓库,小型项目用Monorepo
数据库隔离每上下文独立Schema + 独立数据库实例同一实例不同Schema合规要求高用实例隔离,成本敏感用Schema
实践策略:第一次划分不要追求完美。先按照业务能力粗分,运行2~3个迭代后,根据实际出现的耦合问题(如跨上下文事务、数据冗余更新)反向调整边界。每次调整只改动一个上下文,验证通过后再迁移下一个。