原文发布于 quant67.com,转载请保留出处。
三个月前你的团队决定用 MongoDB 做订单存储。现在新来的工程师问:"为什么不用 PostgreSQL?"没人答得上来。当时拍板的技术负责人已经离职了,会议纪要找不到,Slack 里的讨论淹没在几千条消息里。
你凭记忆说了一些理由------"好像是因为 schema 灵活"------但你自己也不确定这是不是当初的核心考量。另一个同事补充说"好像还考虑了写入性能",第三个人说"我记得是因为前端团队想直接用 JSON"。三个人三个版本。
这就是架构决策的"口头传统"(Oral Tradition)问题。它不是小团队的专利------Spotify 的工程团队在 2014 年前后就撞上了同样的墙,几百个微服务的技术选型散落在各个团队的脑子里,没有人能说清楚整个系统的决策脉络。
这篇文章要回答一个问题:怎样让架构决策变得可追溯、可审计、可质疑?
工具是 ADR------架构决策记录(Architecture Decision Records)。它不是什么新发明,Michael Nygard 在 2011 年就提出了这个格式。但大多数团队要么没听说过,要么试了两周就放弃了。问题不在格式本身,在于没搞清楚"什么值得记、怎么管、什么时候停"。
一、口头决策为什么会失败
先把问题摊开。架构决策不写下来,会在三个地方出问题。这些问题在团队规模小、人员稳定时不太明显,但只要团队开始增长或者有人员流动,就会集中爆发。
上下文丢失
每个技术决策都有前提条件。选 MongoDB 不是因为 MongoDB 绝对好,而是因为"当时团队只有 3 个人、产品需求变化快、schema 没定型、写入量预期不大"。这些前提条件才是决策的真正依据。三个月后条件变了(团队扩到 15 人、需求稳定了、写入量涨了 50 倍),原来的决策可能已经不成立了------但如果没人记录当初的前提,你连"条件变了"这件事都无法判断。
Martin Fowler 在 IEEE Software(2003 年)的文章"Who Needs an Architect?"中指出过一个观点:架构决策的核心特征是难以逆转(hard to reverse)。正因为难以逆转,决策的上下文就格外重要------你不是在选一个可以随时换的工具,你在做一个未来几年都要承担后果的承诺。
重复争论
没有决策记录,同一个问题会被反复讨论。"我们为什么不用 Kafka?"------这个问题在一些团队里每季度都会被提出来,每次都要花半天重新论证一遍,结论还是一样。这不是因为团队里有人健忘,是因为没有一个地方可以让新人自己去翻"这个问题已经讨论过了,结论在这里,当时的理由是这些"。
无法审计和质疑
口头决策没法质疑。因为质疑的前提是你知道决策是什么、理由是什么。如果理由不存在于任何可检索的地方,质疑就变成了"你觉得 vs 我觉得"的意见战。这在团队规模小的时候还能靠信任扛过去,一旦团队超过 10 个人、跨了 3 个以上时区,口头传统就彻底崩溃了。
ThoughtWorks 在 Technology Radar 2016 年的报告中把"Lightweight Architecture Decision Records"列为 Adopt(建议采纳),理由之一就是:缺乏决策记录的团队在代码审查中经常出现"为什么这样做"的争论,而这些争论本该在架构层面就已经有答案。
口头决策的隐性成本
把上面三个问题量化,口头决策的成本主要体现在以下几个方面:
| 成本类型 | 具体表现 | 频率 |
|---|---|---|
| 上下文重建 | 每次有人问"为什么",至少 1-2 小时的考古工作 | 每月 2-5 次 |
| 重复决策 | 同一个技术选型讨论被反复发起 | 每季度 1-3 次 |
| 错误回退 | 不知道当初的约束条件,盲目推翻旧决策导致踩同样的坑 | 偶发但代价极高 |
| onboarding 延迟 | 新成员需要通过"问人"而非"查文档"来理解系统架构 | 每个新人 1-2 周 |
| 决策漂移 | 不同团队成员对同一个决策的理解逐渐分化 | 持续累积 |
这些成本在小团队里感知不强,因为"问一声"就解决了。但团队从 5 人扩到 20 人、从一个服务扩到十个服务时,这些成本会指数级增长。而且口头传统有一个致命弱点:它依赖特定人的记忆和存在。关键人员一旦离职或转岗,知识就永久丢失了。
二、ADR:三种主流格式
ADR 的核心思想很简单:用一份结构化的短文档,记录一个具体的架构决策。不是写设计文档,不是写技术方案,就是回答"我们决定了什么、为什么、代价是什么"。
Nygard 格式:原始模板
Michael Nygard 在 2011 年的博客文章"Documenting Architecture Decisions"中提出了 ADR 的原始格式。这篇文章后来成为 ADR 社区的奠基文献。他的模板只有五个字段:
markdown
# ADR {编号}: {标题}
## Status
{Proposed | Accepted | Deprecated | Superseded by ADR-xxx}
## Context
{描述问题背景和驱动力。什么情况让我们必须做这个决策?
包括技术约束、业务约束、团队约束等。}
## Decision
{我们决定做什么。用主动语态:"We will..."。}
## Consequences
{这个决策带来的后果,包括正面和负面的。
不要只写好处,代价必须写清楚。}
这个格式的设计哲学是极简。Nygard 明确说过,一个 ADR 应该短到"一两页",因为如果写太长,人们就不会写、不会读。
五个字段各有分工:
- Status 跟踪这个决策是否仍然有效。一个被 Superseded 的 ADR 不会被删除,而是标注"已被 ADR-xxx 取代"------这样你能看到决策的演化历史。
- Context 是最容易被忽略也是最重要的部分。写清楚"为什么我们必须做决策"比"我们决定了什么"更有价值。一个好的 Context 应该包括:当前的技术约束、业务约束、团队约束、以及"如果我们什么都不做会怎样"。
- Decision 要求用主动语态("We will use PostgreSQL"),不要写成被动式("PostgreSQL was chosen"),因为主动语态强调团队拥有这个决策。这不是文风偏好,是设计意图------ADR 的目的之一是明确责任归属。
- Consequences 要求同时写正面和负面后果。只写好处的 ADR 不是记录,是推销。Nygard 原文特别强调:正面和负面后果应该在同一个 section 里并列,不要把好处放前面、坏处藏在角落里。
一个常见的误解是:Nygard 格式太简单,不适合复杂决策。但 Nygard 的设计意图正是"简单"------他在原文中写道,如果你的 ADR 需要超过两页,说明你可能在一份 ADR 里塞了不止一个决策。拆开它。
MADR 格式:更结构化的选择
MADR(Markdown Architectural Decision Records)是 Oliver Kopp 在 Nygard 格式基础上扩展的版本,项目托管在 GitHub(adr/madr)。MADR 3.0 的模板增加了几个关键字段:
markdown
# {简短标题,描述已解决的问题和解决方案}
## Context and Problem Statement
{用两三句话描述问题背景。可以用问句形式。}
## Decision Drivers
* {driver 1,例如:团队对技术栈的熟悉程度}
* {driver 2,例如:预期的请求量级}
* ...
## Considered Options
* {option 1}
* {option 2}
* {option 3}
* ...
## Decision Outcome
Chosen option: "{option x}", because {理由}。
### Consequences
* Good, because {正面后果}
* Bad, because {负面后果}
* ...
## Pros and Cons of the Options
### {option 1}
* Good, because {优势}
* Bad, because {劣势}
* ...
### {option 2}
* Good, because {优势}
* Bad, because {劣势}
* ...
MADR 和 Nygard 格式的关键区别在于两个地方:
- Decision Drivers(决策驱动因素)------Nygard 格式把驱动因素混在 Context 里,MADR 单独拆出来。这使得后续审查时能快速判断"当初最看重的是什么"。
- Considered Options(考虑过的选项)------Nygard 格式不要求列出被否决的方案,MADR 要求。被否决的方案和否决理由往往比最终选择更有信息量------它回答的是"为什么不选 Y"。
MADR 的另一个设计选择是把 Consequences 拆成 "Good, because..." 和 "Bad, because..." 的列表格式,而不是 Nygard 那样的自由文本段落。列表格式的好处是写的时候强制你逐条列出,不容易遗漏;代价是有些复杂的后果很难压缩成一行。
MADR 3.0 模板可以从 GitHub 仓库 adr/madr 直接下载,也可以在 VS Code 中通过 snippet 自动生成。如果你的团队用 MADR,建议在仓库的 .github/PULL_REQUEST_TEMPLATE/ 目录下放一个 ADR 提交模板,让 PR 创建者在提交 ADR 时自动看到模板结构。
Y-Statement 格式:一句话决策
Y-Statement 是 Olaf Zimmermann 在"Architectural Decisions --- The Making Of"论文(2012 年)中提出的格式。它把整个决策压缩成一句话:
text
In the context of {use case/user story},
facing {concern},
we decided for {option}
and neglected {other options},
to achieve {quality attribute},
accepting {downside}.
例如:
text
In the context of inter-service communication,
facing the need for low-latency and strong typing,
we decided for gRPC
and neglected REST and GraphQL,
to achieve sub-millisecond serialization and compile-time contract validation,
accepting the loss of human-readable debugging and browser-native support.
Y-Statement 不是用来替代完整 ADR 的。它的定位是索引和摘要------在决策数量多到几十上百条的时候,你需要一种方式快速扫描"我们做了哪些关键决策",Y-Statement 就是那个一行摘要。完整的分析仍然需要 Nygard 或 MADR 格式来承载。
写好 Y-Statement 的关键是 "accepting" 子句------它强制你在一句话里就把代价说清楚。如果你写不出这个子句,说明你还没认真想过这个决策的代价。
三种格式的选择
三种格式不是互斥的。一个团队可以用 Nygard 或 MADR 写完整 ADR,同时在索引文件里为每个 ADR 配一条 Y-Statement 摘要。但正文格式应该统一,不要在同一个 doc/adr/ 目录里混用 Nygard 和 MADR------后面第八节会详细对比三种格式的取舍。
三、怎么写一份好的 ADR
格式只是骨架。一份有价值的 ADR,关键在于 Context 写得够具体、Options 比较得够诚实、Consequences 不回避代价。
下面用两个真实场景来演示。第一个是服务间通信协议的选择------这是微服务架构中最常见的架构决策之一。第二个是数据库选型------每个项目都会碰到,也是最容易产生分歧的决策之一。两个案例分别用 MADR 格式和 Nygard 扩展格式来写,展示不同格式在实际使用中的样子。
案例一:服务间通信选择 REST 还是 gRPC
markdown
# ADR-007: 服务间通信协议选择 gRPC
## Status
Accepted (2025-03-15)
## Context
订单系统重构后拆成 5 个内部服务(用户、商品、订单、支付、物流),
服务之间的调用量预计在 2000-5000 QPS,P99 延迟要求 < 50ms。
当前所有服务间通信走 HTTP/1.1 + JSON,存在三个问题:
1. JSON 序列化/反序列化占请求处理时间的 15%-20%(基于线上火焰图)
2. 接口契约靠 wiki 文档维护,文档和代码经常不一致
3. 缺少流式传输能力,物流轨迹推送只能轮询
团队 8 人,其中 5 人有 Go 经验,2 人有 Java 经验,1 人有 Python 经验。
所有服务部署在同一个 Kubernetes 集群内。
## Decision Drivers
* P99 延迟必须 < 50ms
* 接口契约必须有编译期校验
* 团队学习成本不能超过 2 周
* 服务间无浏览器直接调用需求
## Considered Options
1. **REST (HTTP/1.1 + JSON)**:维持现状
2. **gRPC (HTTP/2 + Protocol Buffers)**
3. **GraphQL**
## Decision
We will use gRPC for all internal service-to-service communication.
面向外部客户端的 API 网关仍然暴露 REST 接口,
通过 grpc-gateway 做协议转换。
## Consequences
### 正面
* Protocol Buffers 的二进制序列化比 JSON 快 5-10 倍
(Google 官方 benchmark,实际收益取决于 payload 大小)
* .proto 文件作为接口契约,编译时校验,
代码生成保证客户端和服务端类型一致
* 原生支持双向流,物流轨迹推送不再需要轮询
* HTTP/2 多路复用减少连接数
### 负面
* 调试变困难:二进制格式无法直接用 curl 测试,
需要引入 grpcurl 或 grpc-web 开发工具
* .proto 文件的版本管理需要建立流程
(字段编号不可复用、删除字段不能改编号)
* 团队需要学习 Protocol Buffers 语法和 gRPC 拦截器模式,
预计 1-2 周
* 浏览器不能直接调用 gRPC,外部 API 必须经过转换层
### 否决方案的理由
* **REST**:维持现状无法解决序列化性能和契约一致性问题
* **GraphQL**:解决了契约问题但引入了查询解析开销,
且服务间调用不需要客户端自定义字段选择的灵活性------
那是面向前端的能力
这份 ADR 有几个值得注意的地方:
- Context 里有数据------"JSON 序列化占 15%-20%"来自线上火焰图,不是拍脑袋。
- Decision Drivers 明确排序------P99 延迟是第一驱动力,学习成本排在后面。
- Consequences 同时写了正面和负面------调试变困难、proto 版本管理是真实代价。
- 否决方案有理由------不是简单说"不选",而是说清楚"为什么不选"。
案例二:数据库选型 PostgreSQL vs MongoDB
markdown
# ADR-012: 用户行为分析系统使用 PostgreSQL
## Status
Accepted (2025-06-02)
## Context
新项目"用户行为分析平台"需要存储用户在 App 内的操作事件
(点击、浏览、购买、搜索等),预期数据特征:
- 日写入量:约 5000 万条事件
- 单条事件大小:200-500 bytes
- 查询模式:按用户 ID 查最近 N 天事件 + 按事件类型聚合统计
- 数据保留:热数据 90 天,冷数据归档到对象存储
- 事件 schema:核心字段固定(user_id, event_type, timestamp),
扩展字段按业务线不同而不同
团队在 PostgreSQL 和 MongoDB 上都有生产经验。
现有基础设施以 PostgreSQL 为主,有成熟的备份、监控和运维流程。
## Decision Drivers
* 查询性能:按用户和时间范围查询必须 < 100ms
* 运维成本:优先使用现有基础设施
* schema 灵活性:扩展字段需要支持不同业务线的差异
* 事务需求:事件写入不需要跨表事务
## Considered Options
1. **PostgreSQL 14+**:利用 JSONB 字段存储扩展属性,
利用分区表按时间范围管理数据
2. **MongoDB 6.0+**:原生文档模型,
灵活 schema,内置分片
3. **ClickHouse**:列式存储,专为分析场景设计
## Decision
We will use PostgreSQL 14 with the following design:
- 按月分区表存储事件数据
- 核心字段使用强类型列,扩展字段使用 JSONB 列
- JSONB 列上建 GIN 索引支持扩展字段查询
- 90 天以上的分区自动 detach 并归档到 S3
## Consequences
### 正面
* 复用现有 PostgreSQL 运维体系,不引入新的数据库运维负担
* JSONB + GIN 索引在扩展字段查询上性能足够
(PostgreSQL 14 的 JSONB 查询在中等数据量下和 MongoDB 差距不大,
Percona 2023 年的对比测试显示在 1 亿条以下差距 < 15%)
* 分区表让数据生命周期管理变得简单------detach 分区比删除文档快几个数量级
* 团队不需要学习新的查询语言和运维工具
### 负面
* 如果未来扩展字段的查询模式变复杂(嵌套文档、数组内查询),
JSONB 的查询语法比 MongoDB 的原生查询更啰嗦
* PostgreSQL 的水平扩展(Citus 或手动分库)比 MongoDB 原生分片更复杂
* 列式聚合不是 PostgreSQL 的强项------
如果分析查询量增长到需要 OLAP 级别,可能需要后续引入 ClickHouse
(此时本 ADR 应被 Supersede)
### 否决方案的理由
* **MongoDB**:性能差距在当前数据量下不显著,
但引入 MongoDB 意味着新增一套运维体系(监控、备份、升级)。
运维成本是第二优先级的 Decision Driver。
* **ClickHouse**:分析能力强但不支持点查(按 user_id 查单用户事件),
不适合同时承担 OLTP 和 OLAP 角色
这份 ADR 的核心判断是:在数据量和查询复杂度还没到需要专用方案的阶段,优先复用现有基础设施,降低运维成本。这是一个工程判断,不是技术优劣比较------MongoDB 在文档查询上确实更原生,但"更原生"不等于"在这个场景下值得引入"。
ADR 里标注了何时应该重新审视这个决策("分析查询量增长到 OLAP 级别时"),这为后续的 Supersede 留了清晰的触发条件。
写好 ADR 的几条原则
从上面两个案例中可以提炼出几条实用原则:
Context 要写约束条件,不要写背景介绍。 "微服务架构是当前流行的架构风格"------这是背景介绍,对决策没有帮助。"团队 8 人,5 人有 Go 经验,所有服务在同一个 K8s 集群内"------这是约束条件,直接影响选择。
Decision Drivers 要区分优先级。 不要平铺。"延迟 < 50ms"和"团队学习成本不超过 2 周"不是同等重要的------把最硬的约束放前面,最软的放后面。决策出现分歧时,优先满足排在前面的 Driver。
Consequences 里的负面后果要具体到行动项。 不要写"可能增加复杂性"这种空话。要写"调试需要引入 grpcurl,团队需要花 2 小时学习"------具体到工具、具体到时间、具体到影响哪些人。
被否决的方案必须给理由。 这一条经常被忽略。三个月后有人问"为什么不用 GraphQL",如果 ADR 里只有"我们选了 gRPC"而没有"为什么不选 GraphQL",这份 ADR 就少了一半的价值。被否决方案的理由和最终选择的理由同等重要。
一份 ADR 不应该超过两页。 如果你写到第三页还没写完,说明你在一份 ADR 里塞了不止一个决策,或者你把 ADR 和设计文档混在了一起。拆分它。
四、ADR 的生命周期
ADR 不是写完就扔的文档,它有自己的状态流转。一个 ADR 从提出到最终被取代或废弃,会经历以下状态:
几个关键规则:
- ADR 一旦 Accepted,正文不再修改。 如果决策需要变更,写一个新的 ADR 来 Supersede 旧的,而不是偷偷改旧文档。这保证了决策历史的完整性。
- Deprecated 和 Superseded 是不同的状态。 Deprecated 表示"这个决策不再适用,但没有新方案取代它"------比如某个服务被下线了,相关的 ADR 就是 Deprecated。Superseded 表示"有更好的方案了"------比如从 REST 切换到 gRPC,旧的 REST ADR 就被新的 gRPC ADR Supersede。
- Rejected 也要保留。 被否决的 ADR 记录了"我们考虑过什么、为什么没选"------这和被采纳的 ADR 一样有价值,它防止同一个提案被反复提出。
Supersede 的具体操作
当一个 Accepted 的 ADR 需要被新决策取代时,操作流程如下:
- 创建新的 ADR,在 Context 中引用旧 ADR 编号,说明"为什么旧决策不再适用"。
- 在新 ADR 的 Status 中写明"Accepted, supersedes ADR-0003"。
- 在旧 ADR 的 Status 中标注"Superseded by ADR-0007"。
- 不修改旧 ADR 的正文------Context、Decision、Consequences 保持原样。
这样做的好处是你能看到完整的决策演化链。假设一个缓存方案经历了三次变更:
text
ADR-0003: Use Redis for session cache [Superseded by ADR-0007]
ADR-0007: Switch to Memcached for session [Superseded by ADR-0015]
ADR-0015: Move session to JWT, drop cache [Accepted]
任何人都能通过这条链看到"我们最初选了 Redis,后来切到 Memcached,最后把 session 管理整体换成了 JWT"。每一步变更的理由和代价都在各自的 ADR 里。
定期审查
ADR 不是"写完就忘"的文档。建议每个季度或每半年做一次 ADR 审查,检查两件事:
- Context 是否仍然成立? 当初选 PostgreSQL 的理由是"日写入量 5000 万"。如果半年后日写入量涨到了 5 亿,这个 ADR 的前提就不成立了,需要考虑是否 Supersede。
- 有没有未记录的重大决策? 如果团队在过去一个季度做了某个影响全局的技术选型但没有写 ADR,补一份。
审查不需要很正式。可以在季度技术回顾会议上花 15 分钟过一遍 ADR 索引表,把状态有变化的标出来。关键是形成节奏,不是走流程。
五、在 Git 仓库中管理 ADR
ADR 最自然的存放位置是代码仓库。它和代码一起版本管理,一起做代码审查,一起被搜索。把 ADR 放在 Confluence 或 Google Docs 里不是不行,但经验表明和代码分离的文档衰减速度更快------半年后就没人更新了。
目录结构
推荐的目录结构:
text
project-root/
├── doc/
│ └── adr/
│ ├── 0001-use-postgresql-for-user-data.md
│ ├── 0002-adopt-grpc-for-internal-services.md
│ ├── 0003-use-redis-for-session-cache.md
│ ├── 0007-switch-to-grpc.md
│ └── README.md # ADR 索引和使用说明
├── src/
└── ...
几个约定:
- 文件名用编号开头(
0001-、0002-),保证排序。编号不一定连续------如果 ADR-0004 被删掉了(虽然不建议删),编号不会被回收。 - 文件名用英文、小写、短横线分隔,概括决策内容。
README.md维护一个索引表,列出所有 ADR 的编号、标题、状态和日期。
索引表示例:
markdown
# Architecture Decision Records
| 编号 | 标题 | 状态 | 日期 |
|------|------|------|------|
| 0001 | Use PostgreSQL for user data | Accepted | 2025-01-10 |
| 0002 | Adopt gRPC for internal services | Accepted | 2025-03-15 |
| 0003 | Use Redis for session cache | Superseded by 0006 | 2025-02-20 |
| ... | ... | ... | ... |
adr-tools CLI
Nat Pryce 的 adr-tools 是一个 Bash 工具集,自动化 ADR 的创建和管理。用 Homebrew 或直接从 GitHub(npryce/adr-tools)安装:
bash
# macOS
brew install adr-tools
# 初始化 ADR 目录
adr init doc/adr
# 创建新 ADR
adr new "Use PostgreSQL for user data"
# 生成 doc/adr/0001-use-postgresql-for-user-data.md
# 创建取代旧 ADR 的新 ADR
adr new -s 3 "Switch session cache from Redis to Memcached"
# 自动在 ADR-0003 中标注 Superseded by 0007
# 生成索引
adr list
adr generate toc
adr-tools 的设计和 ADR 的哲学一致------极简。它只管文件创建、编号递增和状态关联,不做任何复杂的东西。如果你需要更多功能(比如 MADR 模板支持、搜索、可视化),可以看看 Log4brains(thomvaill/log4brains),它能把 ADR 目录渲染成一个可浏览的静态网站。
用 Pull Request 驱动 ADR 评审
ADR 最好通过 Pull Request 提交,和代码变更一起审查。具体流程:
- 作者在分支上创建新的 ADR 文件,状态设为 Proposed。
- 提交 PR,在 PR 描述中说明这个 ADR 要解决什么问题。
- 团队成员在 PR 中评审 ADR 内容------重点审查 Context 是否完整、Options 是否遗漏了关键方案、Consequences 是否诚实。
- 达成共识后合并 PR,同时将状态改为 Accepted。
- 如果这个 ADR 伴随着代码变更(比如引入 gRPC 依赖),代码变更的 PR 中引用这个 ADR 编号。
这种做法的好处是:每个架构决策都有一个可追溯的 Git commit,有评审记录(PR comments),有代码变更的关联(PR references)。三个月后有人问"为什么用 gRPC",你可以指着 ADR-0007 和关联的 PR-234 说:"理由在这里,讨论记录在这里,代码变更在这里。"
将 ADR 链接到代码变更
在代码中引用 ADR 编号,让决策和实现之间有明确的关联:
go
// 使用 gRPC 而非 REST 进行服务间通信
// 决策依据见 doc/adr/0007-switch-to-grpc.md
func NewOrderServiceClient(conn *grpc.ClientConn) OrderServiceClient {
return &orderServiceClient{cc: conn}
}
在 PR 描述中同样引用:
text
## Related ADRs
- Implements ADR-0007: Switch to gRPC for internal services
- Relates to ADR-0012: Use PostgreSQL for user behavior analytics
反过来,ADR 文件中也可以链接到实施它的 PR:
markdown
## Implementation
- PR #234: Add gRPC dependencies and proto definitions
- PR #241: Migrate order-service endpoints to gRPC
- PR #256: Add grpc-gateway for external REST API
这种双向链接在 ADR 数量增长到几十个以后会变得特别有用------你能从任何一个方向追溯"决策 → 讨论 → 实现"的完整链条。
完整的 ADR 工作流
把上面的流程串起来,一个 ADR 从提出到实施的完整工作流如下:
这个流程的关键点在于:ADR 先于代码。先确认决策,再实施。不要写完代码再补 ADR------那样 ADR 就变成了事后合理化,而不是决策记录。
多仓库场景
如果你的系统由多个代码仓库组成(典型的微服务架构),ADR 的存放有两种策略:
- 集中式 :所有 ADR 放在一个专门的
architecture仓库里。好处是全局可搜索;代价是 ADR 和代码分离,容易脱节。 - 分布式 :每个仓库维护自己的
doc/adr/目录,只记录影响本仓库的决策。跨仓库的决策放在一个共享的platform-adr仓库里。
第二种策略更实用。它的原则是:ADR 跟着受影响的代码走。如果一个决策只影响订单服务,ADR 就放在订单服务的仓库里。如果一个决策影响所有服务(比如统一切换通信协议),ADR 放在共享仓库里,各服务仓库的实施 PR 引用共享仓库的 ADR 编号。
六、工业实践:Spotify、Uber 和 ThoughtWorks
Spotify:技术文化中的决策透明
Spotify 的工程团队在多篇博客文章和技术会议演讲中分享过他们的架构决策流程。他们面对的核心问题是:几百个 squad(小组)各自独立选型,整个组织层面没有人知道"我们到底有多少种数据库在生产环境运行"。
Spotify 的做法有几个特征值得注意(来源:Spotify Engineering Blog, "How We Use Golden Paths to Solve Fragmentation", 2020):
- Golden Path(黄金路径):Spotify 不强制所有团队使用同一套技术栈,而是提供"黄金路径"------经过验证的、有完整工具链支持的技术选择。ADR 就是黄金路径的决策依据文档。如果你想偏离黄金路径,可以,但你需要写一份 ADR 说明为什么偏离、你愿意承担什么额外成本。
- 决策在 squad 层面做,透明度在组织层面保证。每个 squad 可以做自己的技术选型,但 ADR 必须存放在组织级别可检索的位置。任何人都可以搜索到任何 squad 的架构决策。
- Tech Radar 与 ADR 互补。Spotify 内部维护自己的 Technology Radar(受 ThoughtWorks Technology Radar 启发),定期更新组织层面的技术推荐和淘汰列表。ADR 记录具体决策,Tech Radar 提供宏观方向。
Spotify 的 Golden Path 模式有一个微妙但重要的设计:它把"偏离标准"的成本显性化了。你不是不能用非标技术,但你要承担更高的运维成本(没有现成的监控模板、没有自动化的升级流程、出问题没有 on-call 支持),而且这些代价必须写在 ADR 里。这种机制让偏离不是"被禁止的"而是"被定价的"------大多数团队一旦看到偏离的真实成本,会自觉选择黄金路径。
这里有一个工程判断:Spotify 的模式适合大型、自治程度高的工程组织。如果你的团队只有 10 个人、只维护一个服务,照搬 Golden Path 的流程反而是过度工程化。
Uber:规模化下的架构治理
Uber 的工程博客在 2020 年发布了一篇关于架构决策的文章("Designing for Failure at Uber",以及内部流程的多次会议分享),描述了他们在微服务数量超过 4000 个之后面对的治理挑战。
Uber 的关键实践包括:
- Domain-Oriented Microservice Architecture (DOMA):Uber 在 2020 年提出 DOMA 架构,其中明确要求每个 Domain 维护自己的架构决策日志。域的边界内,团队自主决策;跨域的决策(比如更换服务间通信协议)需要上升到 Architecture Review Board。
- RFC 流程:Uber 对重大架构变更使用 RFC(Request for Comments)流程,RFC 本质上就是扩展版的 ADR------包含更详细的技术分析、性能预测、迁移计划和回滚方案。RFC 在内部工具上公开评审,任何工程师都可以提出意见。
- Architecture Review Board:对影响多个 Domain 的决策,Uber 有一个 Architecture Review Board 做最终审批。Board 不做决策,而是确保决策过程的完整性------Context 是否完整、Options 是否充分、风险是否评估。
Uber 的模式反映了一个规律:ADR 的管理复杂度随组织规模线性增长 。10 个人的团队用一个 Git 仓库里的 doc/adr/ 目录就够了;4000 个微服务的组织需要 RFC 流程、Review Board 和集中式搜索平台。
下面这张表对比三家公司的决策治理模式:
| 维度 | Spotify | Uber | 典型中小团队 |
|---|---|---|---|
| 组织规模 | 数百 squad | 4000+ 微服务 | 5-50 人 |
| 决策自治度 | 高(squad 自主) | 域内自主,跨域需审批 | 团队内自主 |
| 决策记录形式 | ADR + Golden Path 文档 | RFC(扩展版 ADR) | 标准 ADR |
| 存放位置 | 组织级可搜索平台 | 内部 RFC 工具 | Git 仓库 doc/adr/ |
| 评审流程 | 同行审查 | Architecture Review Board | PR 评审 |
| 补充机制 | 内部 Tech Radar | DOMA 域治理 | 无 |
这张表想说明的是:ADR 的核心机制(写下来、给理由、记代价)在各种规模下都一样,变的只是治理流程的重量级别。不要因为自己是小团队就觉得"我们不需要这些",也不要因为看了大厂的做法就照搬 Review Board。
ThoughtWorks Technology Radar
ThoughtWorks 从 2010 年开始发布 Technology Radar,每半年更新一次,对技术和实践进行四级分类:Adopt(建议采纳)、Trial(值得试验)、Assess(值得关注)、Hold(暂缓采用)。
Technology Radar 在 2016 年 11 月版将 Lightweight Architecture Decision Records 列为 Adopt,评语是:
"Much documentation can be replaced by highly readable code and tests. In a world of evolutionary architecture, however, it's important to record certain design decisions for the benefit of future team members as well as for external oversight."
Technology Radar 本身不是 ADR,但它提供了一种组织层面的决策框架------哪些技术是推荐的、哪些是淘汰的、哪些在试验阶段。团队在写 ADR 时可以引用 Technology Radar 作为决策依据之一。
一个值得注意的做法:有些公司(比如 Zalando)维护自己的内部 Technology Radar,作为 ADR 的"上游输入"。如果某个技术在内部 Radar 上被标记为 Hold,选择它的 ADR 就需要更强的理由和更详细的风险分析。
七、什么时候不要写 ADR
ADR 的反面不是"没有 ADR",而是"ADR 变成了官僚流程"。以下几种情况不需要写 ADR:
可逆的决策
Jeff Bezos 在亚马逊 2015 年致股东信中区分了"单向门"(one-way door)和"双向门"(two-way door)决策。ADR 是为单向门准备的------一旦做了就很难退回的决策。如果一个决策可以在两周内轻松撤销(比如选用哪个日志库、用 tabs 还是 spaces),写 ADR 就是浪费时间。
判断标准:这个决策如果做错了,回退的成本有多高? 如果回退成本低于写 ADR 的成本,就不要写。
行业标准做法
"我们决定用 HTTPS 而不是 HTTP"------这不需要 ADR。"我们决定用 UTF-8 编码"------这也不需要。当一个选择是行业默认做法、不选反而需要理由的时候,不需要写 ADR 来论证"为什么选了默认选项"。
团队内部工具的小改动
把 CI 从 Jenkins 换成 GitHub Actions、把代码格式化工具从 prettier 换成 biome------这些决策影响范围有限、回退成本低,用一条 Slack 消息或一个简短的 PR 描述就够了。不需要为每个工具切换都写一份正式的 ADR。
避免 ADR 通胀
有些团队在推行 ADR 初期会矫枉过正,恨不得每个技术选择都写一份 ADR。结果是 ADR 目录膨胀到上百个,其中大量是无足轻重的决策("使用 Go 1.21 而非 1.20"、"日志格式用 JSON 而非 plaintext"),真正重要的决策反而淹没在噪声中。
一个实用的经验法则:一个服务在它的生命周期里,需要 ADR 的决策通常不超过 10-20 个。如果你一个季度写了 15 个 ADR,大概率其中一半不需要写。
判断边界
下面这张表可以辅助判断:
| 信号 | 需要 ADR | 不需要 ADR |
|---|---|---|
| 回退成本 | 高(数周到数月) | 低(数小时到数天) |
| 影响范围 | 多个服务/多个团队 | 单个服务/单个团队内部 |
| 生命周期 | 预计存活 > 1 年 | 预计 < 半年 |
| 争议程度 | 团队内有不同意见 | 无争议或行业标准 |
| 新人困惑度 | 新人很可能会问"为什么" | 不言自明 |
如果一个决策在五个维度中有三个以上命中"需要 ADR"列,就值得写。不到三个,用 PR 描述或简短的设计笔记就够。
八、ADR 格式对比
三种 ADR 格式各有取舍,下面这张表做一个横向对比:
| 维度 | Nygard 格式 | MADR 格式 | Y-Statement |
|---|---|---|---|
| 字段数量 | 4 个(Status, Context, Decision, Consequences) | 7+ 个(增加 Decision Drivers, Considered Options, Pros/Cons) | 1 句话,6 个子句 |
| 上手成本 | 极低,5 分钟读完模板即可开始写 | 中等,需要理解每个字段的定位 | 低,但压缩信息的功力要求高 |
| 信息密度 | 中等,核心信息都有但被否决方案可能缺失 | 高,明确记录所有选项和比较 | 极高(每句话都是精华),但细节不足 |
| 适合场景 | 小团队、快速决策、ADR 数量 < 30 | 中大型团队、需要标准化流程、ADR 数量 30-200 | 作为索引或摘要,搭配 Nygard/MADR 使用 |
| 被否决方案的处理 | 不强制记录 | 强制记录且逐一分析优劣 | 只用一个"neglected"子句概述 |
| 工具支持 | adr-tools 原生支持 | MADR 有官方 GitHub 模板 | 无专用工具,手动维护 |
| 典型长度 | 半页到一页 | 一到两页 | 一句话 |
| 风险 | 信息不够完整,日后难以追溯否决理由 | 写太长,团队不愿意写或不愿意读 | 信息压缩过度,关键细节丢失 |
选择建议------这是个人判断,不是标准答案:
- 刚开始推行 ADR 的团队,用 Nygard 格式。门槛低,团队容易接受。先养成"写下来"的习惯,格式可以后面再演进。
- 已经有一定 ADR 积累、需要更结构化管理的团队,切换到 MADR。Decision Drivers 和 Considered Options 字段在回顾和审计时价值很大。
- ADR 数量超过 50 个的组织,为每个 ADR 补一条 Y-Statement 摘要,放在索引文件里,方便快速浏览。
不建议在团队内部同时使用多种完整格式。格式不统一比格式不完美更糟糕------混用 Nygard 和 MADR 会让搜索和审计变困难。选一种,坚持用。
九、ADR 实施中的常见问题
写了没人读
这通常是因为 ADR 和日常工作流脱节了。解决方法:
- 把 ADR 链接到相关的 PR 和代码注释里。工程师在读代码时自然会看到。
- 新成员 onboarding 时,把 ADR 目录作为必读材料之一。不需要让新人读完所有 ADR,给一个"推荐阅读"清单,挑出最核心的 5-8 个。
- 在技术决策讨论中直接引用已有 ADR 的编号("这个问题 ADR-012 已经分析过了"),形成引用习惯。
- 在团队 wiki 或 README 中放上 ADR 目录的链接和简要说明。
还有一个容易被忽视的原因:ADR 写得太干,像法律文书一样没有可读性。ADR 虽然是结构化文档,但不妨碍你把 Context 写得生动一点------"线上订单接口 P99 飙到 200ms,排查发现 JSON 序列化吃掉了 15% 的 CPU"比"当前系统存在序列化性能瓶颈"更容易让人读进去。
写太长
ADR 不是设计文档。如果一份 ADR 超过了两页,说明它试图覆盖太多内容。拆分策略:
- 一个 ADR 只记录一个决策。"我们选了 gRPC"是一个决策,"我们选了 gRPC 并且用 Envoy 做 sidecar proxy 并且用 Protobuf v3 语法"是三个决策。
- 如果需要详细的技术分析(benchmark 数据、迁移方案等),写在单独的设计文档里,ADR 中引用链接。
不知道什么时候该写
回到第七节的判断表。另一个实用的规则是:如果你在 PR review 中花了超过 30 分钟讨论一个技术选择,那它大概值得一个 ADR。 因为这个讨论会随着 PR 被合并而沉没,下一次有人问同样的问题,你还得再讨论 30 分钟。
还有一个信号:当你在写代码注释的时候,发现自己在解释"为什么这样做而不是那样做",而且解释超过了三行------这个解释应该从注释里移出来,写成一份 ADR,注释里只留一个链接。代码注释不是放架构决策理由的地方。
团队抵触怎么办
有些团队会抵触 ADR,觉得"写文档浪费时间"。这时候不要强推流程,而是用事实说话:找一个最近发生过"为什么我们当初这样选"争论的例子,花 20 分钟写一份 ADR,下次再有人问同样的问题时直接发链接。当团队成员亲身体验到"有 ADR 和没有 ADR 的区别"之后,推行阻力会大幅降低。
不要试图一步到位地推行完整的 ADR 流程。先从一个人、一个项目开始写,让它自然扩散。强制要求"所有技术决策必须有 ADR"的行政命令通常适得其反。
已有项目怎么补写
不需要追溯补写所有历史决策。挑出当前仍然有效的、最容易被新人质疑的 5-10 个决策,先补这些。后续的新决策正常走 ADR 流程。历史决策如果没人问起,就不需要补。
补写历史 ADR 有一个技巧:从新人的困惑入手。让最近入职的团队成员列出他们最想问"为什么"的 5 个技术选择,按这个列表来补写。新人的困惑就是 ADR 的需求信号。
ADR 和设计文档的关系
ADR 不是设计文档的替代品。两者的定位不同:
| ADR | 设计文档 | |
|---|---|---|
| 回答的问题 | "我们决定了什么,为什么" | "我们打算怎么做" |
| 粒度 | 一个具体的决策 | 一个完整的技术方案 |
| 长度 | 半页到两页 | 几页到几十页 |
| 更新频率 | 写完不改(用新 ADR Supersede) | 实施过程中持续更新 |
| 保质期 | 长期有效(即使被 Supersede 也保留) | 实施完成后逐渐过期 |
实际操作中,一个设计文档可能衍生出多个 ADR。比如一份"订单系统重构设计文档"里可能包含"数据库选型"、"通信协议选择"、"缓存策略"三个独立的架构决策,每个拆成一份 ADR。设计文档记录"怎么做",ADR 记录"为什么这么做"。
十、结论
架构决策记录不是文档负担,是工程资产。它的核心价值不在于格式有多精美,而在于三点:
- 决策上下文被保留------三个月后你还能看到"为什么当时这么选"。
- 被否决的方案被记录------防止同一个提案被反复讨论。
- 决策可以被质疑------因为质疑的对象是白纸黑字的理由,不是某个人的记忆。
从 Nygard 的五字段模板开始,把 ADR 放在代码仓库的 doc/adr/ 目录里,用 PR 流程做评审,坚持"一个 ADR 一个决策"。这套流程不需要任何特殊工具,不需要任何组织变革,一个工程师今天就能在自己的项目里开始做。
复杂的流程------Review Board、RFC 流程、组织级搜索平台------等你的 ADR 积累到几十个、团队扩展到几十人的时候再考虑。先把习惯养成,比选格式更重要。
如果你只从这篇文章里带走一件事,记住这个:架构决策的价值不在于决策本身,而在于决策背后的理由。理由如果只存在于人的脑子里,就一定会丢失。把它写下来,放在代码旁边,用版本管理跟踪。 这就是 ADR 的全部。
下一篇我们讨论架构评估------当你做完了决策,怎么系统性地检验"这个架构方案到底行不行",而不是凭直觉说"应该没问题"。
导航
下一篇:架构评估:ATAM 与 trade-off 分析实战
参考资料
论文与书籍
- Michael Nygard, "Documenting Architecture Decisions", 2011. 博客原文,ADR 概念的起源。
- Olaf Zimmermann, "Architectural Decisions --- The Making Of", 2012. Y-Statement 格式的学术来源。
- Martin Fowler, "Who Needs an Architect?", IEEE Software, 2003. 架构决策"难以逆转"特征的论述。
- Len Bass, Paul Clements, Rick Kazman, "Software Architecture in Practice", 4th Edition, Addison-Wesley, 2021. 架构决策作为架构定义核心要素的系统论述。
工具与模板
- npryce/adr-tools: GitHub 仓库,Bash 实现的 ADR 命令行工具。
- adr/madr: GitHub 仓库,MADR 格式的官方模板和文档。
- thomvaill/log4brains: GitHub 仓库,ADR 可视化和静态站点生成工具。
工业实践
- Spotify Engineering Blog, "How We Use Golden Paths to Solve Fragmentation", 2020. Spotify 的黄金路径和技术选型透明化实践。
- Uber Engineering Blog, "Introducing Domain-Oriented Microservice Architecture", 2020. Uber DOMA 架构中的决策治理方法。
- ThoughtWorks Technology Radar, Volume 15, November 2016. 将 Lightweight Architecture Decision Records 列为 Adopt。
- Jeff Bezos, Amazon 2015 Letter to Shareholders. 单向门/双向门决策框架。
性能数据
- Percona, "PostgreSQL vs MongoDB Performance Benchmark", 2023. JSONB 查询性能对比数据的来源。
- Google Protocol Buffers 官方文档, "Performance" 章节. Protobuf 与 JSON 序列化性能对比的基准数据。