80.1 C4 模型
一句话
C4 用四个缩放层级 (Context / Container / Component / Code) 画同一个系统,每一层只回答一类人的一类问题;实践中 90% 的场景只需要前两层,第四层几乎不用手画。
四层:同一个系统的四个缩放级别
| 层级 | 画什么 | 元素 | 读者 | 元素数上限 | 什么时候画 |
|---|---|---|---|---|---|
| C1 System Context | 系统、使用者、外部系统 | 人、软件系统 | 所有人(含非技术) | ≤ 15 | 立项、方案评审开场 |
| C2 Container | 可独立运行 / 部署的单元 + 数据存储 | 应用、服务、DB、队列、前端 | 开发、运维、架构 | ≤ 20 | 技术方案、交付文档 |
| C3 Component | 一个容器内部的组件与依赖 | 组件(一组类 / 模块) | 该容器的开发者 | ≤ 25 | 复杂容器的内部设计 |
| C4 Code | 类、接口、方法 | 类图 | 极少(接手者) | — | 关键算法,通常由工具生成 |
🔑 缩放的关键是同一套元素、不同粒度:C1 里一个框是 “交易系统”,C2 里它展开成 4 个容器,C3 里其中一个容器再展开成组件。四层不是四个系统,是四个放大倍数。
“Container” 不是 Docker 容器
这是 C4 被误解最多的一点:
| 误解 | 事实 |
|---|---|
| Container = Docker / K8s Pod | Container = 可独立运行或独立存储数据的单元:一个进程、一个数据库、一个消息队列、一个浏览器里的 SPA、一个文件系统 |
| 一个 Pod 就是一个 Container | 一个容器可能由多个 Pod 组成(同一服务的多副本),也可能一个 Pod 里跑多个容器 |
| 前端不算容器 | 浏览器里跑的 SPA 是容器(它独立运行、独立部署) |
判定标准两条,满足其一即可进 C2:
- 能独立运行、独立部署 (独立进程 / 独立发布单元);
- 独立持有数据 (数据库、对象存储、缓存、队列)。
各层的判定规则
| 层 | 该画 | 不该画 |
|---|---|---|
| C1 | 你的系统(1 个框)、外部角色、外部系统、交互目的 | 内部组件、技术栈、数据库 |
| C2 | 容器、容器间的调用关系、协议、数据存储 | 类、方法、代码细节、容器内部结构 |
| C3 | 组件、组件间依赖方向、对外接口 | 部署细节(节点、副本数、可用区) |
| C4 | 关键类与接口 | 全部类(那是 IDE 的活) |
符号规范:形状本身要说话
| 形状 | 含义 | 备注 |
|---|---|---|
| 人形 | 角色 / 使用者 | 写角色(「买家」),不写具体人 |
| 实线方框 | 本系统的容器 | 边框统一,颜色可标识技术类型 |
| 灰色方框 | 外部系统 | 视觉上必须能一眼区分「我管的」和「别人管的」 |
| 圆柱 | 数据存储(DB / 对象存储) | C4-PlantUML 里是 ContainerDb |
| 队列符号 | 消息中间件 | ContainerQueue |
| 箭头 | 调用 / 数据流,带方向 | 标签写「动词 + 协议」:创建订单 / gRPC |
| 虚线框 | 系统边界 | 标出「哪些是你负责的」 |
每张图必须有:图名 + 层级 + 版本 + 日期 + 范围。没有版本的架构图,半年后没人敢用。
代码:C4-PlantUML (可直接渲染)
C1 与 C2 写在同一份源文件里 (依赖 C4-PlantUML 标准库,PlantUML ≥ 1.2020 已内置):
@startuml
!include <C4/C4_Container>
title 交易系统 · C2 容器图 · v1.2 · 2026-10-24 · 范围:下单与支付
Person(buyer, "买家", "浏览商品、下单、支付")
System_Boundary(shop, "交易系统") {
Container(web, "Web 应用", "Go / HTTP", "对外提供 H5 与 OpenAPI")
Container(order, "订单服务", "Go / gRPC", "订单生命周期与状态机")
ContainerDb(db, "订单库", "MySQL 8", "订单、支付记录")
ContainerQueue(mq, "消息队列", "Kafka", "订单事件")
}
System_Ext(pay, "支付网关", "第三方支付渠道")
Rel(buyer, web, "下单", "HTTPS")
Rel(web, order, "创建订单", "gRPC")
Rel(order, db, "读写", "TCP")
Rel(order, mq, "发布 OrderPaid", "Kafka 协议")
Rel(order, pay, "发起支付", "HTTPS")
@enduml
同一套元素画 C1,只需要 “收起” 系统内部的容器:
@startuml
!include <C4/C4_Context>
title 交易系统 · C1 上下文图 · v1.2 · 2026-10-24
Person(buyer, "买家", "浏览商品、下单、支付")
Person(ops, "运营", "配置商品与营销活动")
System(shop, "交易系统", "承接下单、支付、履约")
System_Ext(pay, "支付网关", "第三方支付渠道")
System_Ext(sms, "短信平台", "订单通知")
Rel(buyer, shop, "下单 / 查订单", "HTTPS")
Rel(ops, shop, "配置商品", "HTTPS")
Rel(shop, pay, "发起支付", "HTTPS")
Rel(shop, sms, "发送通知", "HTTPS")
@enduml
💡 C1 里没有数据库。数据库是内部实现细节,出现在上下文图里说明作者在混层级——这是最容易被评审抓住的错误。
什么时候不用 C4
| 你想表达 | 该用什么 |
|---|---|
| 数据怎么流动、怎么加工 | 数据流图(DFD)/ 数据架构图(46.1 数据建模) |
| 部署在哪些节点、可用区、网络分区 | 部署图(C4 有 Deployment 图作为补充) |
| 一次请求的时序 | 时序图(见 80.3 架构设计文档模板) |
| 状态流转 | 状态机图(见 80.3 架构设计文档模板) |
| 单个脚本 / 单服务小工具 | 一张上下文图足够,别硬凑四层 |
反模式
| 反模式 | 后果 |
|---|---|
| 把 C2 画成类图 | 读者是来做技术决策的,看到 60 个类直接放弃 |
| 一张图混多层(上下文里塞数据库和缓存) | 抽象层级混乱,谁看都不对 |
| 元素超过 20 个还硬塞一张 | 认知负荷超限,读者只能「看个大概」 |
| 图里只写技术,不写职责 | 读者知道「用了 Kafka」,不知道「为什么需要它」 |
| 没有版本 / 日期 / 图例 | 半年后无法判断是否过期,于是没人敢信 |
| 手画 C4 代码层(C4) | 与代码不同步,一周就过期;应由工具生成 |
| 用截图代替源文件 | 无法 diff、无法多人改、无法批量更新 |
| 图与代码两套事实 | 新人按图理解,上线踩坑 |
自检
Q1 评审会上有人问 “为什么 C1 里没有数据库?我们明明有 MySQL”。你怎么回答,并说明如果加上会破坏什么?
▸ 答案:C1 的读者包括非技术方,它只回答 “这个系统为谁解决什么问题、依赖哪些外部系统”。数据库是系统内部的实现选择,对 “系统边界” 这一层没有信息量。加上它会破坏两件事:① 抽象层级一致性——一旦出现一个内部元素,读者就会期待看到全部内部元素,C1 会退化成一张不完整的 C2;② 边界表达——C1 的核心是 “哪些是我负责的、哪些是别人负责的”,数据库并不改变系统边界。数据库出现在 C2。
Q2 团队用 C4 画了四层图,但每次发版后 C3 和 C4 都过期,新人照着图找不到代码。问题出在哪一层?怎么改?
▸ 答案:问题不在 C4,在把 “易变的层” 当成 “手工维护的文档”。C3/C4 的粒度已经接近代码结构,天然随重构变化,手画必然过期。改法:① C1/C2 手工维护 (它们本来就该稳定,半年才改一次);② C3 只在 “要讲清某个复杂容器的设计” 时画,并标注版本与对应 commit;③ C4 不手画,交给 IDE / 工具从代码生成。原则:离代码越近的图,越必须从代码生成。
Q3 一个系统有 3 个服务、1 个数据库、1 个缓存、1 个网关、2 个外部系统、2 类用户。你画几张图?
▸ 答案:两张。C1 一张 (系统 1 个框 + 2 类用户 + 2 个外部系统 = 5 个元素),C2 一张 (3 服务 + 1 DB + 1 缓存 + 1 网关 = 6 个容器,加 2 个外部系统 = 8 个元素)。C3 只在某个服务内部真的复杂 (例如订单状态机跨 4 个组件) 时补一张,C4 不画。判据是读者的决策需要,不是 “C4 有四层所以要画四张”——图的数量由问题数量决定,不由模型层数决定。