土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】DDD 战略设计:限界上下文与上下文映射

文章导航

分类入口
architecture
标签入口
#DDD#bounded-context#context-mapping#strategic-design#Eric-Evans

目录

一个中型电商系统里,“订单”在交易团队意味着”待支付的购物车快照”,在物流团队意味着”等待拣货的配送单”,在财务团队意味着”一条应收账款记录”。三个团队共用同一张 t_order 表、同一个 OrderService 类,每次迭代都互相踩脚。这种混乱的根源不是代码质量,而是缺少一项最基本的架构决策——限界上下文(Bounded Context) 的划分。

Eric Evans 在 2003 年出版的《领域驱动设计》一书中首次系统化地提出了战略设计(Strategic Design)的概念。二十多年后,这些概念依然是大型系统架构的基石。本文将从限界上下文的本质出发,深入解析上下文映射(Context Mapping)的 9 种模式,并给出可落地的决策框架。

上一篇:推送架构 | 下一篇:DDD 战术模式


一、限界上下文的本质

1.1 它不是”微服务边界”

在当今的技术社区中,限界上下文常被简单等同于”一个微服务”。这种理解只抓住了表象。Evans 原著中对限界上下文的定义是:

A Bounded Context is a boundary within which a particular model is defined and applicable.

核心要素有三个:

  1. 模型(Model):一组概念、关系和规则的集合;
  2. 语言(Ubiquitous Language):团队在该上下文内对概念的统一命名和理解;
  3. 边界(Boundary):模型和语言的适用范围。

限界上下文首先是一个语义边界,而不是一个部署单元。一个限界上下文可以对应一个微服务,也可以对应一个单体中的模块,甚至可以对应一个第三方系统。

1.2 通用语言:上下文的灵魂

通用语言(Ubiquitous Language)是限界上下文的核心驱动力。在同一个上下文内,所有人——开发者、产品经理、领域专家——必须使用完全相同的术语来描述同一个概念。

上下文:交易域
  "订单" = 用户提交的一次购买意图,包含商品快照、价格、优惠信息
  "下单" = 创建订单的行为
  "取消" = 用户主动放弃未支付订单

上下文:物流域
  "订单" = 一条等待拣货和配送的指令
  "下单" = 物流系统接收到配送请求
  "取消" = 配送中止,触发退货流程

同一个词”订单”在不同上下文中有不同含义,这不是问题——这正是限界上下文存在的意义。问题在于把两种含义混在同一个模型里。

1.3 上下文边界的识别信号

以下信号通常意味着你需要拆分上下文:

信号 说明
同一术语在不同团队有不同含义 “订单”在交易和物流中语义不同
一个实体承担过多职责 User 类同时包含认证、画像、社交属性
修改一个功能需要协调多个团队 改价格逻辑需要交易和促销团队同时参与
数据模型中出现大量可选字段 不同场景只用到部分字段
业务规则互相矛盾 交易域要求”订单不可修改”,客服域要求”订单可修改”

二、Evans 的核心概念重新解读

2.1 领域与子域

在 Evans 的体系中,领域(Domain)是整个业务问题空间,子域(Subdomain)是其自然划分。子域有三种类型:

┌──────────────────────────────────────────────┐
│                  业务领域                      │
│                                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │ 核心子域  │  │ 支撑子域  │  │ 通用子域  │   │
│  │ Core     │  │Supporting│  │ Generic  │   │
│  │          │  │          │  │          │   │
│  │ 竞争优势  │  │ 必要但非  │  │ 标准化   │   │
│  │ 需要深度  │  │ 核心差异  │  │ 可外购   │   │
│  │ 领域建模  │  │ 化能力   │  │ 或复用   │   │
│  └──────────┘  └──────────┘  └──────────┘   │
└──────────────────────────────────────────────┘

2.2 子域与限界上下文的关系

子域是问题空间的划分,限界上下文是解决方案空间的划分。二者的关系不是简单的一对一:

关系 场景
一个子域 → 一个上下文 理想情况
一个子域 → 多个上下文 子域复杂度高,需要进一步拆分
多个子域 → 一个上下文 遗留系统,子域边界模糊

2.3 限界上下文的内部结构

一个限界上下文内部通常包含:

┌─────────────────────────────────────┐
│        限界上下文(Bounded Context)  │
│                                     │
│  ┌───────────┐  ┌───────────┐      │
│  │ 领域层     │  │ 应用层    │       │
│  │ - 聚合    │  │ - 应用服务 │      │
│  │ - 实体    │  │ - 命令处理 │      │
│  │ - 值对象  │  │ - 查询处理 │      │
│  │ - 领域服务│  │           │       │
│  │ - 领域事件│  │           │       │
│  └───────────┘  └───────────┘      │
│                                     │
│  ┌───────────┐  ┌───────────┐      │
│  │ 基础设施层 │  │ 接口层    │       │
│  │ - 仓储实现│  │ - API     │      │
│  │ - 消息发布│  │ - 事件消费 │      │
│  │ - 持久化  │  │ - 转换器  │       │
│  └───────────┘  └───────────┘      │
└─────────────────────────────────────┘

三、上下文映射的 9 种模式

上下文映射(Context Mapping)描述的是两个限界上下文之间的关系。这种关系既包含技术层面的集成方式,也包含组织层面的团队协作模式。

3.1 全景图

graph TB
    subgraph "上下文映射模式"
        SK["共享内核<br/>Shared Kernel"]
        CS["客户-供应商<br/>Customer-Supplier"]
        CF["遵从者<br/>Conformist"]
        ACL["防腐层<br/>Anti-Corruption Layer"]
        OHS["开放主机服务<br/>Open Host Service"]
        PL["发布语言<br/>Published Language"]
        PT["合作关系<br/>Partnership"]
        SW["各行其道<br/>Separate Ways"]
        BBM["大泥球<br/>Big Ball of Mud"]
    end

    subgraph "按团队协作分类"
        direction TB
        M1["互利对等"] --> PT
        M1 --> SK
        M2["上下游依赖"] --> CS
        M2 --> CF
        M2 --> ACL
        M3["公共接口"] --> OHS
        M3 --> PL
        M4["无协作"] --> SW
        M4 --> BBM
    end

3.2 共享内核(Shared Kernel)

两个上下文共享一小部分模型(代码、数据库 schema 或消息格式)。

// shared-kernel 模块:两个上下文都依赖
package com.example.shared.kernel;

public class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        if (amount == null || currency == null) {
            throw new IllegalArgumentException("金额和币种不能为空");
        }
        this.amount = amount;
        this.currency = currency;
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("币种不一致,无法直接相加");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }

    // equals 和 hashCode 基于值比较
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return amount.compareTo(money.amount) == 0
            && currency.equals(money.currency);
    }
}

3.3 客户-供应商(Customer-Supplier)

上游上下文是供应商,下游上下文是客户。下游的需求会影响上游的优先级。

上游(订单)                    下游(物流)
┌───────────┐   需求反馈   ┌───────────┐
│ OrderService│ ←────────── │ ShippingService│
│           │   提供 API   │           │
│           │ ──────────→  │           │
└───────────┘              └───────────┘

3.4 遵从者(Conformist)

下游上下文完全接受上游的模型,不做任何转换。

3.5 防腐层(Anti-Corruption Layer)

下游在自己与上游之间建立一个翻译层,将上游模型转换为自己的领域模型。

// 防腐层示例:将遗留 ERP 的订单模型转换为本域模型
type ERPOrder struct {
    OrdNo     string  `json:"ord_no"`
    CustCode  string  `json:"cust_code"`
    TotalAmt  float64 `json:"total_amt"`
    StatusFlg int     `json:"status_flg"`
}

type DomainOrder struct {
    OrderID    OrderID
    CustomerID CustomerID
    Total      Money
    Status     OrderStatus
}

// Translator:负责模型转换
func translateERPOrder(erp ERPOrder) (DomainOrder, error) {
    status, err := mapERPStatus(erp.StatusFlg)
    if err != nil {
        return DomainOrder{}, fmt.Errorf("未知的 ERP 状态码: %d", erp.StatusFlg)
    }
    return DomainOrder{
        OrderID:    NewOrderID(erp.OrdNo),
        CustomerID: NewCustomerID(erp.CustCode),
        Total:      NewMoney(erp.TotalAmt, CNY),
        Status:     status,
    }, nil
}

详细实现请参见 防腐层与开放主机服务

3.6 开放主机服务(Open Host Service)

上游定义一组公开协议(API),供多个下游使用。

              ┌───────────────┐
              │ Open Host     │
              │ Service       │
              │ (标准化 API)   │
              └───────┬───────┘
                      │
          ┌───────────┼───────────┐
          │           │           │
    ┌─────┴─────┐┌────┴────┐┌────┴────┐
    │ 下游 A    ││ 下游 B  ││ 下游 C  │
    └───────────┘└─────────┘└─────────┘

3.7 发布语言(Published Language)

上下文之间通过一种公共的、文档化的数据交换格式进行通信。

// published_language/order_events.proto
syntax = "proto3";
package order.events;

message OrderCreatedEvent {
    string order_id = 1;
    string customer_id = 2;
    repeated LineItem items = 3;
    MoneyValue total = 4;
    google.protobuf.Timestamp created_at = 5;
}

message LineItem {
    string product_id = 1;
    int32 quantity = 2;
    MoneyValue unit_price = 3;
}

message MoneyValue {
    string amount = 1; // 使用字符串避免浮点精度问题
    string currency = 2;
}

3.8 合作关系(Partnership)

两个上下文的团队建立一种对等的合作关系,共同规划和协调开发。

3.9 各行其道(Separate Ways)

两个上下文之间没有任何集成关系,各自独立发展。

3.10 大泥球(Big Ball of Mud)

这不是一种推荐的模式,而是对现实的描述——系统中没有清晰的上下文边界,各种概念混杂在一起。


四、上下文映射模式的决策树

面对两个需要集成的上下文,如何选择合适的映射模式?以下决策树提供了一个系统化的判断路径:

flowchart TD
    START["两个上下文需要集成"] --> Q1{"两个团队能否<br/>紧密协作?"}
    Q1 -->|是| Q2{"需要共享<br/>部分模型?"}
    Q1 -->|否| Q3{"下游能否影响<br/>上游的接口?"}

    Q2 -->|是| SK["共享内核<br/>Shared Kernel"]
    Q2 -->|否| PT["合作关系<br/>Partnership"]

    Q3 -->|是| CS["客户-供应商<br/>Customer-Supplier"]
    Q3 -->|否| Q4{"上游模型是否<br/>可以接受?"}

    Q4 -->|是| CF["遵从者<br/>Conformist"]
    Q4 -->|否| Q5{"集成是否<br/>值得投入?"}

    Q5 -->|是| ACL["防腐层<br/>ACL"]
    Q5 -->|否| SW["各行其道<br/>Separate Ways"]

    CS --> Q6{"上游需要服务<br/>多个下游?"}
    Q6 -->|是| OHS["开放主机服务<br/>+ 发布语言"]
    Q6 -->|否| CS2["保持客户-供应商"]

4.1 Shared Kernel vs ACL 的关键决策因素

这两种模式是最容易混淆的,以下对比帮助做出判断:

维度 Shared Kernel Anti-Corruption Layer
团队信任度 高,愿意共同维护 低或不可控
模型一致性 共享部分的模型完全一致 两侧模型可以完全独立
变更协调成本 每次修改需双方同意 各自独立修改
耦合度 高(共享代码/schema) 低(通过翻译层隔离)
适用团队规模 小团队(2-3 个上下文) 任意规模
实现成本 低(直接复用) 高(需要建设翻译层)
维护成本 高(协调成本) 中(翻译层维护)
典型场景 同一产品线的核心概念 对接遗留系统或外部服务

4.2 选择建议的总结表

模式 适用的组织关系 适用的技术场景 主要风险
Shared Kernel 紧密协作的小团队 核心概念确实相同 耦合扩散
Partnership 对等协作的团队 需要同步演进的上下文 协调成本高
Customer-Supplier 上下游关系明确 下游有合理需求 上游优先级冲突
Conformist 下游无谈判余地 上游模型基本可接受 模型污染
ACL 下游需要保护模型 对接遗留/外部系统 翻译层维护成本
OHS + PL 一对多服务 提供标准化接口 版本管理复杂度
Separate Ways 无协作关系 集成价值低于成本 数据不一致
Big Ball of Mud 遗留系统 无边界的混沌系统 所有风险

五、上下文边界与团队边界的对齐

5.1 康威定律的启示

梅尔文·康威(Melvin Conway)在 1968 年提出的康威定律(Conway’s Law)指出:

设计系统的组织,其产出的系统架构必然是该组织沟通结构的映射。

这意味着限界上下文的边界最终会趋向于团队的边界。反过来,如果你的架构边界与团队边界不一致,两者之间必然产生摩擦。

5.2 反向康威策略

反向康威策略(Inverse Conway Maneuver)是指主动调整组织结构来匹配目标架构。如果你希望系统由三个限界上下文组成,那就组建三个团队,每个团队负责一个上下文。

目标架构:
┌──────────┐  ┌──────────┐  ┌──────────┐
│ 交易上下文 │  │ 物流上下文 │  │ 支付上下文 │
└──────────┘  └──────────┘  └──────────┘

组织结构(对齐后):
┌──────────┐  ┌──────────┐  ┌──────────┐
│ 交易团队  │  │ 物流团队  │  │ 支付团队  │
│ 5-8 人   │  │ 5-8 人   │  │ 3-5 人   │
└──────────┘  └──────────┘  └──────────┘

5.3 Team Topologies 的四种团队类型

Matthew Skelton 和 Manuel Pais 在《Team Topologies》一书中定义了四种基础团队类型,与限界上下文的分类可以形成映射:

团队类型 说明 对应的子域类型
流对齐团队(Stream-aligned) 面向业务价值流交付 核心子域
使能团队(Enabling) 帮助其他团队克服技术障碍 跨多个子域
复杂子系统团队(Complicated-subsystem) 负责需要深度专业知识的组件 技术复杂的支撑子域
平台团队(Platform) 提供底层基础设施能力 通用子域

5.4 一个团队应该负责多少个上下文?

核心原则是认知负载(Cognitive Load)——一个团队能有效理解和维护的领域复杂度是有限的。

经验法则: - 一个 5-8 人的团队通常负责 1-2 个限界上下文; - 如果一个团队负责 3 个以上的上下文,说明上下文粒度可能太细,或团队需要拆分; - 如果一个上下文需要 15 人以上的团队,说明上下文粒度可能太粗。


六、工程案例:电商系统的上下文映射

6.1 背景

某电商平台在快速发展阶段,系统从单体演进为微服务。初期的服务划分按功能模块拆分(用户服务、商品服务、订单服务……),但随着业务复杂度增长,出现了以下问题:

  1. “订单服务”承担了交易、支付、物流等多个领域的逻辑;
  2. 修改价格计算规则需要同时改动促销服务和订单服务;
  3. 用户服务同时包含了认证、会员、画像三种不同的业务关注点。

6.2 重新建模

团队采用事件风暴(参见 领域事件与事件风暴)重新梳理业务领域,识别出以下限界上下文:

graph LR
    subgraph "核心子域"
        TC["交易上下文<br/>Trading Context"]
        PC["定价上下文<br/>Pricing Context"]
    end

    subgraph "支撑子域"
        SC["物流上下文<br/>Shipping Context"]
        PAY["支付上下文<br/>Payment Context"]
        INV["库存上下文<br/>Inventory Context"]
    end

    subgraph "通用子域"
        AUTH["认证上下文<br/>Auth Context"]
        NOTIFY["通知上下文<br/>Notification Context"]
    end

    TC -->|"Customer-Supplier"| SC
    TC -->|"Partnership"| PC
    TC -->|"Customer-Supplier"| PAY
    TC -->|"Customer-Supplier"| INV
    TC -->|"OHS + PL"| NOTIFY
    AUTH -->|"OHS + PL"| TC
    AUTH -->|"OHS + PL"| SC

6.3 上下文映射决策

上游 下游 映射模式 决策理由
交易 物流 Customer-Supplier 物流需求明确,交易团队愿意配合
交易 定价 Partnership 两者紧耦合,需要同步演进
交易 支付 Customer-Supplier 支付需求标准化程度高
认证 全部 OHS + PL 认证提供标准化 JWT 接口
交易 通知 OHS + PL 通知是通用能力
交易 遗留 ERP ACL ERP 模型老旧,不允许修改

6.4 效果

重新划分后,团队对齐到各自的限界上下文:

交付效率在三个月内提升约 40%,跨团队沟通冲突减少约 60%。


七、上下文映射的可视化工具

7.1 上下文地图

上下文地图(Context Map)是战略设计最重要的交付物之一。它用可视化的方式展示系统中所有限界上下文及其关系。

一个完整的上下文地图应该包含:

  1. 所有已识别的限界上下文;
  2. 上下文之间的映射关系(用标准化的缩写标注);
  3. 上下文的负责团队;
  4. 上下文的核心聚合。

7.2 标准缩写

缩写 模式
SK Shared Kernel
CS / U-D Customer-Supplier / Upstream-Downstream
CF Conformist
ACL Anti-Corruption Layer
OHS Open Host Service
PL Published Language
PT Partnership
SW Separate Ways
BBM Big Ball of Mud

7.3 工具推荐

工具 特点 适用阶段
白板 + 便利贴 最灵活,适合初期探索 事件风暴工作坊
Miro / FigJam 远程协作友好 远程事件风暴
Context Mapper DSL 代码化的上下文地图 设计文档化
PlantUML / Mermaid 文本化绘图 技术文档
Structurizr C4 模型集成 架构决策记录

八、战略设计的常见误区

8.1 误区一:限界上下文等于微服务

限界上下文是逻辑边界,微服务是物理边界。一个限界上下文可以是一个单体中的模块,一个微服务中的包,甚至可以跨越多个部署单元。先划上下文,再决定部署策略。

8.2 误区二:先定技术架构再识别上下文

正确的顺序是:理解业务 → 识别子域 → 划分限界上下文 → 选择映射模式 → 决定部署架构。技术架构应该服务于业务架构,而不是相反。

8.3 误区三:上下文边界一旦确定就不变

限界上下文是演进式的。随着业务发展和团队对领域理解的深入,上下文边界应该持续调整。常见的演进模式包括:

8.4 误区四:追求”完美”的上下文划分

不存在唯一正确的划分方案。两个不同的团队面对同一个业务,可能画出不同但都合理的上下文边界。关键不在于”正确”,而在于一致性和可演进性

8.5 误区五:忽视组织因素

纯技术视角的上下文划分注定失败。一个上下文如果跨越三个不同部门的职责范围,那它在组织现实中很难作为一个整体运作。


九、实施战略设计的步骤

9.1 第一步:识别核心子域

与领域专家一起,明确哪些是企业的核心竞争力。这些子域值得最深入的建模和最好的团队资源。

9.2 第二步:绘制现状上下文地图

描述系统当前的上下文关系,包括那些不理想的 Big Ball of Mud。诚实面对现状是改进的前提。

9.3 第三步:设计目标上下文地图

基于业务发展方向和组织规划,设计目标状态的上下文地图。

9.4 第四步:制定演进路径

从现状到目标不是一步到位的。需要制定分阶段的演进计划,每个阶段都应该能独立交付业务价值。

阶段 1:在大泥球外围建立 ACL,隔离核心域
阶段 2:从大泥球中剥离第一个限界上下文
阶段 3:建立 OHS,为新上下文提供标准接口
阶段 4:逐步迁移剩余功能到独立上下文

9.5 第五步:持续验证和调整

每个 Sprint 或迭代周期结束后,回顾上下文边界是否仍然合理,映射模式是否需要调整。


十、战略设计的综合权衡

决策维度 细粒度上下文 粗粒度上下文
团队自治 高:每个团队独立决策 低:需要内部协调
模型纯净度 高:每个上下文语义清晰 低:可能混合多种语义
集成复杂度 高:上下文间交互多 低:内部调用
数据一致性 难:需要跨上下文最终一致 易:事务边界内强一致
部署独立性 高:各自独立部署 低:需要协调部署
认知负载 低:每个上下文容易理解 高:需要理解多个概念
运维成本 高:更多的服务和基础设施 低:更少的运维对象
性能 可能受跨服务调用影响 内部调用效率高
适合阶段 成熟业务、大团队 创业期、小团队

十一、从战略到战术

战略设计回答的是”系统应该分成哪几块”的问题,而战术设计回答的是”每一块内部应该怎么组织”的问题。战略设计的输出——限界上下文和上下文映射——是战术设计的输入。

在下一篇文章中,我们将深入限界上下文的内部,探讨聚合(Aggregate)、实体(Entity)、值对象(Value Object)等战术模式的设计原则和实践方法。

下一篇:DDD 战术模式:聚合、实体与值对象


参考资料

  1. Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
  2. Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013.
  3. Brandolini, Alberto. Introducing EventStorming. Leanpub, 2021.
  4. Skelton, Matthew; Pais, Manuel. Team Topologies. IT Revolution, 2019.
  5. Context Mapper DSL 官方文档:https://contextmapper.org/
  6. Conway, Melvin. “How Do Committees Invent?” Datamation, 1968.
  7. Fowler, Martin. “Bounded Context.” martinfowler.com.
  8. 张逸.《解构领域驱动设计》. 人民邮电出版社, 2021.

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略

2026-04-13 · architecture

【系统架构设计百科】DDD 战术模式:聚合、实体与值对象

某团队在实施领域驱动设计时,把整个"订单"建模为一个聚合根(Aggregate Root),其中包含订单基本信息、所有订单行、配送信息、支付记录、物流轨迹、评价数据。结果这个聚合加载一次需要从 7 张表联查,保存一次需要锁定整个订单树。并发下单高峰期,数据库锁等待飙升至秒级。这就是典型的"大聚合"反模式——聚合的边界画…

2026-04-13 · architecture

【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案

某金融科技公司正在构建新一代交易系统。新系统使用领域驱动设计,模型清晰、代码整洁。然而它必须对接一套运行了 15 年的核心银行系统(Core Banking System)——这套系统的接口返回 COBOL 风格的定长字段,状态码用两位数字表示("01"正常、"02"冻结、"99"未知),金额用"分"而非"元"为单位。…

2026-04-13 · architecture

【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界

某电商团队按数据库表拆分微服务——用户服务管 tuser,商品服务管 tproduct,订单服务管 torder。看起来边界清晰,实际运行中却发现:下单需要同步调用商品服务查价格、调用库存服务检查库存、调用优惠服务算折扣、调用用户服务查地址,一个下单请求扇出 4 次 RPC,任意一个服务超时整条链路就失败。这种"一实体…


By .