大多数团队的”架构图”是一张白板照片:方框和箭头没有定义,混着服务器、模块、类和人,三个月后就没人看得懂。C4 模型试图解决的正是这个问题——它不是一种画图工具,而是一套让架构图具备统一语义、可以分层缩放的方法论。
一、为什么需要 C4
C4 由 Simon Brown 在 2011 年前后提出(正式成型于其著作《Software Architecture for Developers》),起点是他在大量咨询和培训中观察到的一个现象:软件行业在架构可视化上没有共同语言。
UML 曾经试图成为这个共同语言,但在敏捷运动之后基本被抛弃了——它太重、太面向工具、和”工作的软件高于详尽的文档”的价值观冲突。结果是行业退回到了”随手画方框”的状态,而随手画的图普遍存在几个问题:
- 抽象层次混乱。一张图里既有”支付系统”这种系统级的框,又有
OrderController这种类级的框,读者无法建立一致的心智模型。 - 符号没有语义。虚线和实线有什么区别?圆角和直角有什么区别?画的人自己也说不清。
- 缺少受众意识。给 CTO 看的图和给新入职工程师看的图混在一起,结果谁都看不舒服。
- 无法维护。图是一次性产物,和代码脱节,很快过时。
C4 的回应是一个非常朴素的隐喻:架构图应该像地图一样支持缩放。你查看世界地图、国家地图、城市地图、街道图时,期待的细节层次完全不同,且每一层都有清晰的边界和一致的图例。软件架构图也应该如此。
二、四个 C:核心层级
C4 = Context、Container、Component、Code,四个由粗到细的缩放层级。理解它们的关键是记住每一层的抽象单位和目标受众。
Level 1:System Context(系统上下文图)
抽象单位:软件系统(Software System)和人(Person)。
这是最高层视图。你要描述的系统是画面中央的一个框,周围是使用它的角色(用户、管理员、运营人员)和它依赖或被依赖的外部系统(支付网关、邮件服务、上游 ERP)。
这一层刻意不包含任何技术细节——没有数据库、没有协议、没有框架名。它回答的问题是:
- 这个系统是干什么的?
- 谁在用它?
- 它和周围世界的哪些系统打交道?
受众:所有人,包括完全非技术的业务方、产品经理、新入职的任何角色。这张图应该可以贴在团队墙上,任何路过的人看十秒就能明白你们在做什么。
一个电商系统的 Context 图大致是:中央是”电商平台”,左边连着”顾客”和”客服人员”两个角色,右边连着”支付网关(Stripe)""物流系统""邮件服务(SendGrid)“三个外部系统,每条连线标注交互目的,比如”顾客 → 电商平台:浏览商品、下单”。
Level 2:Container(容器图)
抽象单位:容器(Container)——一个可独立部署或运行的单元。
这是最容易被误解的一层:这里的 Container 与 Docker 无关(C4 提出时 Docker 还不存在)。C4 中的容器指的是”应用程序或数据存储”——任何需要独立运行起来、崩溃时会独立崩溃的东西:
- 服务端应用:API 服务、后台任务处理器、定时任务
- 客户端应用:单页应用(SPA)、移动 App、桌面客户端
- 数据存储:关系数据库、文档数据库、缓存、对象存储
- 中间件:消息队列、搜索引擎
Container 图把 Level 1 中央那个框放大,展示系统内部由哪些容器组成、每个容器用什么技术、容器之间如何通信。每个容器必须标注技术栈,例如:
- Web 应用 [Container: Next.js]
- API 服务 [Container: Node.js / Express]
- 数据库 [Container: PostgreSQL 16]
- 消息队列 [Container: RabbitMQ]
连线也必须标注协议和目的:“Web 应用 → API 服务:JSON/HTTPS 调用 REST API”。
受众:团队内外的技术人员,包括运维和安全。这是日常最有用的一层——新人入职、跨团队对接、容量规划、安全评审,看的基本都是这张图。Simon Brown 本人的说法是:如果只画一张图,就画 Container 图。
Level 3:Component(组件图)
抽象单位:组件(Component)——容器内部的一组相关功能,通常对应一个模块、包或一组类,隐藏在良好定义的接口后面。
选择某一个容器(通常是 API 服务这类逻辑最重的),放大展示它的内部结构。比如一个 API 服务可能拆成:
- 认证组件(处理登录、JWT 签发)
- 订单组件(订单生命周期管理)
- 支付适配组件(封装对 Stripe 的调用)
- 通知组件(封装邮件/短信发送)
组件的边界应该对应代码中真实存在的结构(包、命名空间、模块目录),而不是想象出来的逻辑分组——否则图和代码就脱节了。
受众:本容器的开发者。这一层已经不需要给团队之外的人看。
C4 官方的建议是:Component 图按需画,且只给复杂的容器画。很多团队画到 Level 2 就够了。
Level 4:Code(代码图)
抽象单位:类、接口、函数。
UML 类图级别的细节。C4 对这一层的官方态度非常明确:不建议手画。理由是这一层信息密度最高、过时最快,而 IDE 和工具(IntelliJ 的类图生成、各语言的文档工具)可以随时按需自动生成。手工维护一张类图的成本远高于收益。
把 Code 列为第四层更多是为了让缩放模型完整——它标记了”再往下就交给代码本身”的边界。
三、层级之外的三张补充图
C4 还定义了三种不属于缩放序列、按需绘制的图:
System Landscape(系统全景图):比 Context 还高一层。当企业内部有多个系统时,画一张全景图展示所有系统和角色的关系。适合平台团队、架构组做治理时使用。
Deployment(部署图):回答”容器跑在哪”。把 Level 2 的容器映射到基础设施节点上——哪个容器部署在哪台 EC2、哪个 K8s 集群的哪个 namespace、哪个是直接用的 SaaS。同一套容器在开发/预发/生产环境可能有不同的部署图。做容灾评审、成本核算时这张图非常有用。
Dynamic(动态图):类似简化版时序图,用编号箭头展示某个具体用例(比如”用户下单”)下容器或组件之间的调用顺序。当静态结构图无法说清”事情按什么顺序发生”时补充这张。如果交互复杂,直接用真正的时序图(比如 Mermaid sequenceDiagram)也完全可以。
四、表示法约定:比图形本身更重要的部分
C4 刻意不规定视觉符号——你可以用任何形状、颜色、工具。它规定的是信息完整性约定,这部分才是 C4 的精髓:
每个元素都要有名称、类型和技术标注。不是画一个框写 “API”,而是:
API Application[Container: Node.js / Express]向前端提供订单和商品的 JSON API框的第一行是名字,第二行方括号里是抽象类型和技术选型,第三行是一句话职责描述。这个三段式让图不需要口头解释就能自解释。
每条连线都要有方向和语义。一条裸线是无效信息。正确的写法是”A → B:通过 HTTPS 调用 REST API 获取订单数据”——包含方向、协议、目的三要素。
每张图都要有标题和图例。标题标明这是哪一层、描述的是什么范围(例如”Container 图:电商平台”);图例解释你用到的所有视觉约定(颜色、线型、形状分别代表什么)。
一张图内不混用抽象层次。系统的框和类的框绝不出现在同一张图里。发现自己想混着画的时候,通常意味着应该拆成两张不同层级的图。
这套约定的检验标准是 Simon Brown 常说的一句话:把图发给一个不在场的人,如果对方不需要你在旁边解说就能正确理解,这张图才算合格。
五、工具链落地
C4 是方法论,不绑定工具,落地大致分三个档次。
档次一:通用画图工具 + C4 约定
draw.io(有 C4 形状库)、Excalidraw、甚至 PPT,手工按 C4 约定画。门槛最低,适合一次性沟通,缺点是无法版本管理、容易漂移。
档次二:Diagram-as-code
把图写成文本,进 Git,随代码评审和演进。
C4-PlantUML 是目前最流行的方案,在 PlantUML 之上提供了一组 C4 语义的宏:
@startuml!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
Person(customer, "顾客", "在线购物的用户")System_Boundary(shop, "电商平台") { Container(spa, "Web 应用", "Next.js", "商品浏览与下单界面") Container(api, "API 服务", "Node.js/Express", "业务逻辑与数据访问") ContainerDb(db, "数据库", "PostgreSQL", "订单、商品、用户数据")}System_Ext(stripe, "Stripe", "支付网关")
Rel(customer, spa, "使用", "HTTPS")Rel(spa, api, "调用", "JSON/HTTPS")Rel(api, db, "读写", "SQL/TCP")Rel(api, stripe, "发起支付", "HTTPS")@enduml宏直接对应 C4 概念(Person、Container、System_Ext、Rel),生成的图自动带标准配色和图例。缺点是渲染依赖 Java 或 Kroki 服务。
Mermaid 有实验性的 C4Context 语法,但成熟度低、布局差,社区普遍不推荐。用 Mermaid 画 C4 的务实做法是放弃专用语法,用普通 flowchart + subgraph 手工遵循 C4 约定。
D2 没有 C4 专用语法,但它的嵌套容器是一等公民、布局引擎(ELK/TALA)远强于 Mermaid,手工按 C4 约定写 Container 图效果很好:
customer: 顾客 {shape: person}
shop: 电商平台 { spa: "Web 应用\n[Next.js]" api: "API 服务\n[Node.js/Express]" db: "数据库\n[PostgreSQL]" {shape: cylinder}}
stripe: "Stripe\n[外部系统]"
customer -> shop.spa: HTTPSshop.spa -> shop.api: JSON/HTTPSshop.api -> shop.db: SQLshop.api -> stripe: 发起支付档次三:模型化——Structurizr
Simon Brown 自己做的工具,代表了 C4 落地的”完全体”思路:先定义模型,再声明视图,图从模型生成。
workspace { model { customer = person "顾客" shop = softwareSystem "电商平台" { spa = container "Web 应用" "商品浏览与下单" "Next.js" api = container "API 服务" "业务逻辑" "Node.js" db = container "数据库" "持久化存储" "PostgreSQL" } stripe = softwareSystem "Stripe" "支付网关" "External"
customer -> spa "使用" spa -> api "调用 API" "JSON/HTTPS" api -> db "读写" api -> stripe "发起支付" } views { systemContext shop { include * ; autolayout lr } container shop { include * ; autolayout lr } }}关键区别在于:元素和关系只定义一次,Context 图和 Container 图都从同一份模型渲染。改一处模型,所有视图同步更新——彻底消除”Container 图删了一个服务但 Context 图忘了改”这类漂移。这也是”画图”和”建模”的分水岭。
开源的 Structurizr Lite 是单个 Docker 容器,很适合自托管;DSL 文件进 Git,CI 里导出图片。
渲染服务的统一入口:Kroki
如果团队里 PlantUML、Mermaid、D2、Graphviz 混用,自托管一个 Kroki 可以统一渲染入口——一个 Docker 容器提供 HTTP API,POST 图源码返回 SVG,支持二十多种格式。对 CI 集成和 AI agent 出图场景都很友好。
六、实践建议与常见反模式
从 Level 1 和 Level 2 开始,且很可能到此为止。 Context 图 + Container 图覆盖了 80% 的沟通场景。Component 图只给真正复杂的容器画,Code 图不画。
图例不是可选项。 没有图例的图,颜色和线型就是噪音。
技术标注要具体。 “[Container: 数据库]“是废话,“[Container: PostgreSQL 16, RDS]“才有信息量。
图和文档同源存储。 图源码嵌在 Markdown/ADR 里而不是贴 PNG,这样架构演进时图能跟着代码一起被 review 和更新。过时的架构图比没有架构图更有害。
反模式方面,最常见的有三个。其一是”一张图打天下”:试图在一张图里同时表达系统边界、部署拓扑和模块划分,结果三个受众都看不懂——这正是 C4 分层要根治的病。其二是”Docker 容器混淆”:把 Container 图画成 docker-compose 的可视化,塞进 sidecar、init container 这类部署细节——部署细节属于 Deployment 图。其三是”为画图而画图”:给一个三人团队的单体应用画满四层加三张补充图。C4 的原则始终是按需绘制,图是沟通工具,不是交付物本身。
七、批评与局限
公平起见,C4 也有几个常被讨论的局限。
它偏静态结构,对事件驱动、编排复杂的系统表达力有限——大量异步消息流用静态的框线图画出来并不直观,这类系统往往需要补充事件风暴(Event Storming)产出物或真正的时序图。
Component 层在实践中最尴尬:微服务架构下每个服务本身很小,Container 层已经够细;而在大单体里 Component 图又极易和代码脱节。很多团队事实上跳过了这一层,这也符合官方”按需”的态度。
另外 C4 解决的是”如何画”,不解决”如何决策”——它不替代 ADR(架构决策记录)、不表达质量属性和权衡。成熟团队通常是 C4 图 + ADR 搭配使用:图说明现状结构,ADR 说明为什么变成这样。
八、C4 与 AI 时代的一点延伸
最后是一个当下很实际的观察:C4 的分层思想意外地契合 LLM 生成架构图的工程约束。
让 LLM 一次性生成一张大而全的架构图,几乎必然遭遇布局失控——节点一多,Mermaid/D2 的自动布局就会崩坏,模型也容易漏画关系。而 C4 天然提供了分而治之的框架:agent 先出一张 Context 图(五个框以内,不可能画崩),用户追问哪个部分,再针对性下钻出那一层的 Container/Component 图。每张图的复杂度都被层级约束住了。
更进一步,Structurizr 的”模型 → 视图”思路对 agent 更友好:让模型维护一份结构化的架构模型(DSL 文本),视图按需渲染。相比让模型直接”画图”,让它”改模型”的出错率低得多,也天然获得了版本管理和一致性。
C4 在 2011 年为了解决人与人之间的架构沟通而生,十几年后它约束复杂度、分层缩放的思想,恰好也是人与 AI 之间架构沟通的正确姿势。这大概是好的方法论共有的特征:它约束的是问题的本质复杂度,所以不随工具时代变迁而失效。
参考资料
- 官方站点:c4model.com(内容不长,一小时可通读)
- Simon Brown, Software Architecture for Developers(Leanpub)
- C4-PlantUML:github.com/plantuml-stdlib/C4-PlantUML
- Structurizr DSL:docs.structurizr.com/dsl
- Kroki 统一渲染服务:kroki.io