四、限界上下文(Bounded Context):如何识别和划分限界上下文

说实话,限界上下文这个概念,是 DDD 里最容易被误解、也最值得反复琢磨的一个。我见过太多团队,一上来就画个大而全的领域模型,结果代码里全是「万能对象」,改一个功能要动十几个服务。嗯,说白了,限界上下文就是帮你划清边界、避免这种混乱的。

4.1 什么是限界上下文?

先别急着背定义。我习惯这么理解:每个限界上下文,就是一个独立的「小世界」。在这个世界里,每个术语都有明确的含义,每个模型只服务于这个世界的业务逻辑。

举个例子。在电商系统里,「订单」这个词,在「下单上下文」和「物流上下文」里含义完全不同。下单时,订单包含商品、价格、优惠券;物流时,订单只关心地址、重量、配送状态。你想想看,如果强行用一个 Order 对象包打天下,那这个类迟早变成「上帝类」——什么都有,什么都改不动。

核心要点:限界上下文 = 模型边界 + 语言边界 + 实现边界。三者必须一致。

4.2 如何识别限界上下文?

我总结了三个实战中最好用的方法,每个都踩过坑,你直接拿去用。

4.2.1 从业务事件出发

别一开始就盯着数据库表。先问自己:这个系统里,发生了哪些关键业务事件?

  • 用户提交了订单 → 触发「订单上下文」
  • 仓库完成了拣货 → 触发「库存上下文」
  • 财务确认了收款 → 触发「支付上下文」

每个事件,往往对应一个独立的上下文。我在项目中遇到过,团队把「订单创建」和「订单支付」混在一个上下文里,结果支付逻辑改了,订单查询也跟着崩。后来拆开,各自独立演进,清爽多了。

4.2.2 从术语歧义入手

这是个很妙的技巧。你让业务专家和开发坐在一起,把核心术语列出来。如果同一个词,不同人理解不一样——恭喜你,这里大概率需要拆成两个上下文。

术语 上下文A的理解 上下文B的理解
客户 下了单的人,有收货地址 注册了账号的人,有登录密码
商品 有库存、有价格 有图片、有详情描述
订单 待支付、待发货 已完成、已退款

你看,同一个词,在不同上下文里,属性、行为、生命周期都不一样。强行统一,只会让模型变得臃肿不堪。

4.2.3 从变化频率判断

这是我个人最常用的方法。问一个简单问题:这部分业务,多久改一次?

  • 如果「商品详情页」每周都在调样式、改推荐算法 → 它应该独立
  • 如果「用户登录」半年都不动一次 → 它也可以独立,但没必要拆太细
  • 如果「订单状态机」经常因为促销活动加新状态 → 它必须独立

我曾经接手过一个项目,所有业务逻辑都写在一个服务里。商品展示改了,结果订单流程也跟着重新部署。你想想看,这多危险。后来按变化频率拆成三个上下文,各自独立发布,再也没出过这种问题。

4.3 划分限界上下文的实战原则

识别出来之后,怎么划?我给出四条铁律,每条都是血泪教训换来的。

4.3.1 一个上下文,一个数据库

这不是说物理上必须独立数据库,但逻辑上必须隔离。两个上下文不能直接操作对方的表。我见过最坑的写法:订单上下文直接去查库存表。结果库存改了字段名,订单查询直接报错。嗯,这种耦合,早晚出事。

// ❌ 错误示范:订单上下文直接查询库存表
public class OrderService {
    public void createOrder(Order order) {
        // 直接查库存表 —— 耦合!
        int stock = jdbcTemplate.queryForObject(
            "SELECT quantity FROM inventory WHERE product_id = ?", 
            Integer.class, order.getProductId()
        );
    }
}

// ✅ 正确做法:通过接口或事件通信
public class OrderService {
    private final InventoryClient inventoryClient;
    
    public void createOrder(Order order) {
        // 调用库存上下文的接口
        StockResult result = inventoryClient.checkStock(order.getProductId());
    }
}

4.3.2 上下文之间,只通过接口通信

不管是 RPC、消息队列还是 REST,总之不能直接共享内部模型。每个上下文对外暴露的,应该是自己定义的「防腐层」。

我的习惯:每个上下文维护一个独立的 API 模块,里面只放对外暴露的 DTO 和接口定义。内部实现随便改,只要接口不变,其他上下文就不受影响。

4.3.3 上下文大小要适中

太小了,变成微服务灾难(一个服务就一个 CRUD 接口);太大了,又回到单体老路。我一般用「两个 pizza 团队」原则:一个上下文,应该可以由一个 5-9 人的团队独立维护。

4.3.4 允许上下文之间有「共享内核」

有些东西,比如通用的值对象(金额、地址、时间范围),可以在多个上下文里共享。但注意:共享的是定义,不是实现。我建议把这些放到一个独立的「公共库」里,所有上下文依赖它,但公共库本身几乎不变。

4.4 一个完整的识别案例

拿一个真实的「在线教育平台」来说。我参与过这个项目,当时我们是这样划分的:

  1. 课程上下文:管理课程基本信息、章节、课件。变化频率中等,主要受教研团队影响。
  2. 学习上下文:记录用户学习进度、完成情况、考试成绩。变化频率低,但数据量大。
  3. 支付上下文:处理订单、退款、发票。变化频率高,经常因为财务合规要求调整。
  4. 用户上下文:账号、权限、个人设置。变化频率极低,但安全性要求最高。
  5. 推荐上下文:根据用户行为推荐课程。变化频率极高,算法团队每周都在迭代。

每个上下文都有自己的数据库、自己的部署单元、自己的团队。你想想看,如果把这些全揉在一起,改推荐算法要重新部署整个平台,那得多痛苦。

注意:划分不是一次性的。随着业务发展,一个上下文可能会膨胀,这时候需要再次拆分。我见过一个「订单上下文」后来拆成了「下单上下文」「售后上下文」「对账上下文」三个。别怕重构,怕的是不敢动。

4.5 避坑指南

最后,分享几个我踩过的坑,你遇到了可以绕开走。

  • 别把「微服务」和「限界上下文」划等号。一个上下文可以拆成多个微服务,也可以合并成一个。关键看团队规模和变化频率。
  • 别过度设计。刚开始,3-5 个上下文就够了。等业务跑起来,再根据痛点逐步拆分。我见过一上来就拆 20 个上下文的项目,结果光服务间通信就把团队搞崩溃了。
  • 别忘了「共享内核」的版本管理。公共库一旦改了,所有上下文都得跟着升级。我建议公共库的变更走严格的评审流程,最好半年才发一个新版本。

好了,限界上下文的识别和划分,说白了就是「找到业务边界,守住模型纯洁」。你按我说的三个方法去试,基本不会跑偏。下一章,我们聊聊上下文之间的映射关系——那才是真正考验架构设计能力的地方。