架构决策记录 ADR:把为什么留下来
接手项目时最怕的一句话
每个开发者都经历过这样的场景:接手一个运行了两三年的系统,看到某个模块用了让人意外的技术方案。你问同事「这里为什么不用 X?」,对方说「之前定的,具体原因我也不清楚」。
代码能告诉后来者系统现在怎么做,却很难说明当初为什么这样做。版本历史里只有 diff,没有当时的业务压力、团队能力限制和技术债权衡。
架构决策记录(Architecture Decision Record,ADR)就是用来解决这个问题的。它不是厚重文档,而是一份短小、具体、可追溯的单页记录,把关键选择背后的约束、备选方案和取舍留下来。
ADR 的来历与核心原则
Michael Nygard 在 2011 年首次提出 ADR 这个概念。他在 Cognitect 工作时注意到,团队反复在架构讨论中做出决定,但这些决定的理由随着人员流动迅速丢失。于是他提出了一种轻量级文档模板,只记录一件事:我们在什么背景下、选择了什么方案、放弃了什么方案、为什么。
Martin Fowler 后来在自己的博客中进一步推广了这个概念。他认为 ADR 有两个作用:一是给后来者提供决策的历史记录,二是撰写过程本身能帮助团队暴露分歧、理清思路、推动讨论收敛。
如今 ADR 已经成为主流的工程实践。AWS、Google Cloud、Microsoft Azure 的架构指南中都把 ADR 列为推荐做法。Spotify 在 2020 年公开分享了自己的 ADR 实践,强调「每当做出有重大影响的决策时就应该写一份 ADR」。
ADR 的核心原则可以归结为四条:
- 一次只记录一个决策,不把多个无关决定混在一起
- 重点是判断过程而非结论包装,备选方案和取舍比最终选择更有价值
- 存放在代码仓库中,和源码一起版本控制,跟着项目一起演进
- 可以被更新但不可篡改,旧决策过时后创建新 ADR 将其取代,不直接删除
ADR 到底记什么
一份有效的 ADR 通常包含以下部分:
- 标题:一句话概括决策内容
- 状态:提议中(Proposed)、已接受(Accepted)、已废弃(Deprecated)、已取代(Superseded)
- 背景:当时遇到了什么问题,有什么约束条件
- 备选方案:至少列出被认真考虑过的两个以上选项
- 决策:最终选择了什么,为什么
- 影响:带来哪些收益、代价和后续工作
Martin Fowler 的模板还增加了「置信度」和「重新评估条件」两个字段——前者记录决策时的确定程度,后者说明什么情况下应该重新审视这个决策。
三个真实的决策场景
案例一:消息队列选型——Kafka 还是 RabbitMQ
场景:电商系统的订单服务需要和库存服务、通知服务解耦,团队要选一个消息队列。
翻车:之前的做法是在周会上讨论了十分钟,最后技术负责人拍板「用 Kafka」。三个月后团队来了新人,问为什么不用 RabbitMQ,没人能说出具体理由。又过了半年,业务量增长,发现订单消息体量根本不需要 Kafka 的吞吐量,反而运维成本高。
正确做法:写一份 ADR,把当时的约束、候选方案和取舍写清楚。
# ADR-0012: 订单服务消息队列选型
状态:已接受(2026-03-15)
## 背景
订单服务需要异步通知库存服务和通知服务。
约束:
- 日消息量约 50 万条,峰值不超过 200 条/秒
- 团队 5 人,无专职运维
- 需要至少 3 天消息回溯能力
## 备选方案
1. **Apache Kafka**:高吞吐、持久化强,但运维复杂,需要 ZooKeeper/KRaft
2. **RabbitMQ**:成熟稳定、运维简单,支持死信队列和延迟消息
3. **Redis Streams**:轻量,适合简单场景,但持久化可靠性不如前两者
## 决策
选择 RabbitMQ。
理由:日消息量 50 万条,RabbitMQ 完全能承载。团队没有 Kafka 运维经验,
引入 Kafka 的运维学习成本远超业务需求。RabbitMQ 的死信队列能直接支持
消费失败重试,减少自研逻辑。
## 影响
- 正面:上手快,运维成本低,死信队列开箱即用
- 负面:如果日消息量增长到亿级,需要迁移到 Kafka
- 后续工作:监控消息堆积量,超过 100 万条/天时启动迁移评估
## 重新评估条件
日消息量超过 1000 万条,或需要多消费者组广播消费时,重新评估 Kafka。反面示例:很多团队只做决定不写理由。
# ❌ 不推荐的 ADR 写法
# ADR-0012: 使用 Kafka
## 决策
经团队讨论,决定使用 Apache Kafka 作为消息队列。这份记录没有背景、没有备选方案、没有取舍。三个月后看这份文档的人和没看一样。
案例二:前端状态管理——全量 Redux 还是按需拆分
场景:一个 B 端管理后台,最初用了 Redux 管理所有状态,包括表单、分页、模态框开关。两年后代码里到处是 useSelector、useDispatch,改一个按钮的 loading 状态要改三个文件。
问题:团队逐渐没人知道哪些状态在 Redux 里、哪些在组件内部。新人问「为什么这个状态放 Redux」,得到的回答是「一直就这样」。
修复方案:写一份 ADR 重新划定状态管理边界,把决策理由固化下来。
# ADR-0023: 前端状态管理分层策略
状态:已接受(2026-04-20)
取代:ADR-0008(全局 Redux 方案)
## 背景
项目使用 Redux 管理所有前端状态,导致:
- 简单 UI 状态(表单、弹窗)也需要写 action/reducer
- 单个功能变更需要修改 3-4 个文件
- 新人学习成本高,不理解状态分层的边界
## 备选方案
1. **保持 Redux 全量管理**:统一,但样板代码多
2. **Zustand 替换 Redux**:轻量,但生态不如 Redux
3. **分层策略**:服务端状态用 React Query,全局 UI 状态用 Zustand,
表单状态用 React Hook Form,Redux 只保留跨模块共享状态
## 决策
选择方案 3:分层策略。
理由:80% 的 Redux 使用场景是服务端数据缓存和表单状态,这两类
已有更专业的工具。Redux 只保留真正需要跨模块共享的状态
(用户权限、全局配置、多步骤流程中间态)。
## 影响
- 正面:新功能开发减少 60% 样板代码,表单和列表页改动不再牵连全局
- 负面:团队需要学习三种工具,需要写状态分层指南
- 后续工作:逐步迁移现有 Redux slice,新功能不再新增非必要 slice案例三:数据库选型——PostgreSQL 还是 MongoDB
场景:一个内容管理系统,早期为了快速迭代选了 MongoDB。随着业务增长,内容之间的关联查询越来越多(文章→标签→作者→分类),MongoDB 的嵌套文档和手动 join 让查询越来越复杂。
问题:每次产品加一个新的筛选维度,后端就要改一批查询逻辑。有人提议迁移到 PostgreSQL,但团队对 MongoDB 有经验,担心迁移风险。
修复方案:先写一份 ADR 记录当初选 MongoDB 的原因,再写一份新的 ADR 记录迁移决策。
# ADR-0031: 内容服务数据层迁移至 PostgreSQL
状态:已接受(2026-05-10)
取代:ADR-0005(使用 MongoDB 存储内容数据)
## 背景
CMS 系统当前使用 MongoDB,存储文章、标签、作者、分类数据。
问题:
- 关联查询需要多次 find + 手动拼装,平均查询耗时 > 200ms
- 数据一致性靠应用层保证,偶发脏数据
- 全文搜索需要额外引入 Elasticsearch,增加运维复杂度
约束:
- 团队 8 人,3 人熟悉 PostgreSQL,2 人熟悉 MongoDB
- 迁移期间不能停服,需要双写过渡
- 现有数据量约 500 万条文档
## 备选方案
1. **继续使用 MongoDB,引入 $lookup 聚合**:改动小,但关联查询
性能提升有限,复杂查询仍依赖应用层拼装
2. **迁移到 PostgreSQL**:关系模型天然适合关联查询,内置全文搜索,
JSONB 支持半结构化数据
3. **双数据库方案**:读走 Elasticsearch,写走 MongoDB:运维成本最高
## 决策
选择 PostgreSQL。
理由:核心痛点是关联查询,这是关系数据库的主场。PostgreSQL 的
JSONB 可以兼容现有的半结构化字段,迁移不需要彻底重构数据模型。
内置的 tsvector 全文搜索能覆盖 80% 的搜索需求,减少一个组件。
## 影响
- 正面:关联查询性能预计提升 5-10 倍,减少 Elasticsearch 运维成本
- 负面:迁移需要 2-3 个迭代,双写期间有数据一致性风险
- 后续工作:
1. 设计 PostgreSQL schema,兼容现有数据结构
2. 实现双写同步,灰度验证数据一致性
3. 切换读路径,观察两周后关闭 MongoDB 写入
## 重新评估条件
如果内容类型变为纯日志/时序数据,考虑迁移到 ClickHouse。ADR 的生命周期
一份 ADR 从提出到废弃,经历以下阶段:
关键规则:ADR 一旦标记为 Accepted,就不应该再修改内容。如果决策需要调整,创建一份新的 ADR,把旧的那份标记为 Superseded,并在新 ADR 中说明为什么改变。这样做的目的是保留决策演进的历史轨迹。
ADR、RFC 和设计文档的区别
很多团队同时有 ADR、RFC 和设计文档,容易混淆。它们解决的问题不同:
| 维度 | ADR | RFC | 设计文档 |
|---|---|---|---|
| 目的 | 记录已做出的决策 | 征集团队意见 | 详细描述方案设计 |
| 阶段 | 决策之后 | 决策之前 | 决策之前或之中 |
| 篇幅 | 短,1-2 页 | 中等,可附原型 | 长,包含详细设计 |
| 可变性 | 接受后不可修改 | 收集反馈后关闭 | 可能迭代多版 |
| 回答什么 | 选了什么、为什么 | 大家觉得怎么样 | 具体怎么做 |
简单的理解方式:RFC 是讨论稿,设计文档是方案稿,ADR 是结论稿。RFC 和设计文档是过程产物,ADR 是最终记录。
实践中,一份 ADR 可以引用对应的 RFC 或设计文档作为链接,自己只保留结论和关键取舍。
四种 ADR 模板对比
不同团队可以选择不同轻量级的模板。以下是四种常见选择:
| 维度 | Nygard 原版 | MADR | Y-Statement | Google Cloud 扩展 |
|---|---|---|---|---|
| 字段数 | 5 | 13+ | 1 句话 | 10+ |
| 适用场景 | 通用 | 复杂决策 | 轻量快速 | 企业级项目 |
| 包含状态 | ✅ | ✅ | ❌ | ✅ |
| 包含备选方案 | ✅ | ✅(结构化) | ❌ | ✅ |
| 包含置信度 | ❌ | ✅ | ❌ | ❌ |
| 工具支持 | adr-tools | log4brains | 手动 | 手动 |
| 学习成本 | 低 | 中 | 极低 | 中 |
Nygard 原版是最经典的格式,五个字段(标题、状态、背景、决策、影响)覆盖了核心需求。MADR(Markdown ADR)在 Nygard 基础上增加了置信度、决策者、日志等字段,适合需要更多审计信息的团队。Y-Statement 是极端精简版,一句话概括决策,适合小团队快速对齐。Google Cloud 扩展版增加了贡献者、触发条件、受影响用户旅程等字段,适合跨团队协作。
我的建议是从 Nygard 原版开始。如果团队发现信息不够用再往上加。模板太重反而没人愿意写。
ADR 管理工具对比
ADR 写出来之后需要管理和检索。以下是三种常见工具:
| 维度 | adr-tools | log4brains | 手动管理 |
|---|---|---|---|
| 类型 | CLI 工具 | CLI + 静态站点生成 | 纯文件 |
| 创建命令 | adr new | log4brains new | 手动新建 .md |
| 浏览方式 | 文件系统 / README 索引 | 静态网站,可搜索 | 文件系统 / 文档站点 |
| Git 集成 | ✅ 自动编号 | ✅ 自动编号 | 手动 |
| CI/CD 集成 | ❌ | ✅ 自动构建发布 | 手动 |
| 适用团队 | 3-10 人 | 5-50 人 | 不限 |
| 额外依赖 | 无 | Node.js | 无 |
对于大多数团队,adr-tools 够用——一条命令创建新 ADR,自动编号,自动在目录里生成索引。如果团队超过 10 人或需要非技术人员查看 ADR,log4brains 更合适,它能把所有 ADR 编译成一个可搜索的静态网站,可以在 CI 中自动构建和部署。
代码级ADR:Y-Statement 的用法
有些决策不需要一份完整的 ADR 文档。比如一个函数内部选择了某种算法、一个模块选择了某种数据结构。这时候 Y-Statement 派上用场:
We decided to use [方案] for [问题], because [理由],
even though [备选方案] has [优势] but we chose not to
because [排除原因].// ❌ 没有解释为什么
const cache = new Map()
// ✅ Y-Statement 写清了决策理由
// 我们选择 Map 而不是 LRU 缓存库,因为缓存条目数不超过 100 且
// 不需要淘汰策略,引入第三方库的收益不抵依赖成本。
const cache = new Map()# ❌ 没有解释为什么选这个序列化方案
data = json.dumps(obj)
# ✅ Y-Statement 写清了决策理由
# 我们选择 JSON 而不是 Protobuf,因为数据需要被外部系统直接读取,
# Protobuf 需要共享 .proto 文件,跨组织协作成本过高。
data = json.dumps(obj)Y-Statement 适合写在代码注释、PR 描述或 commit message 里。它是完整 ADR 的微型版,核心是把「为什么」留下来。
反面案例:ADR 常见的坑
坑一:写完就扔
# ❌ 只写不维护
ADR-0001: 使用微服务架构
状态:Accepted(2023-01-10)
(之后再也没人看过,团队已经变成单体部署)ADR 不是写完就存档的文档。当约束变化、决策过时时,需要更新状态或创建新的 ADR 取代。一份 Superseded 的 ADR 比一份被删除的 ADR 有价值得多——它能告诉后来者系统是怎么演进的。
坑二:决策不写备选方案
# ❌ 没有备选方案的 ADR
ADR-0015: 引入 Redis 作为缓存层
## 背景
系统响应慢,需要缓存。
## 决策
使用 Redis。
## 影响
缓存命中率提升,响应时间降低。为什么不用 Memcached?为什么不用本地缓存(Caffeine / lru-cache)?为什么不用 CDN 缓存?这些问题不写清楚,半年后新人会问一模一样的问题。
坑三:一个 ADR 塞太多决策
# ❌ 多个决策混在一起
ADR-0020: 技术栈升级方案
## 决策
1. Node.js 升级到 22
2. 数据库从 MySQL 迁移到 PostgreSQL
3. 前端从 Webpack 迁移到 Vite
4. CI 从 Jenkins 迁移到 GitHub Actions一个 ADR 只记一个决策。多个决策混在一起,评审时很难聚焦,日后检索也很困难。上面四个决策应该拆成四份独立的 ADR。
坑四:状态不更新
# ❌ 状态与实际不符
ADR-0008: 使用 Redux 管理全局状态
状态:Accepted(2024-06-01)
(实际上团队已经在用 Zustand 了,但这份 ADR 还是 Accepted)ADR 的状态必须反映真实情况。如果团队已经改了方案,就应该创建新 ADR 并标记旧 ADR 为 Superseded。
什么时候该写 ADR
不是每个小改动都需要 ADR。以下是判断标准:
| 场景 | 是否需要 ADR | 理由 |
|---|---|---|
| 选择数据库类型 | ✅ | 影响数据模型、查询方式、运维成本 |
| 选择消息队列 | ✅ | 影响系统解耦方式、吞吐量、运维复杂度 |
| 选择认证方案 | ✅ | 影响安全模型、用户体验、合规性 |
| 选择前端框架 | ✅ | 影响开发效率、招聘、生态选择 |
| 修改一个 API 字段名 | ❌ | 影响范围小,PR 描述足够 |
| 升级依赖版本 | ❌ | 除非是 breaking change 的大版本迁移 |
| 选择函数命名风格 | ❌ | 团队规范或 lint 规则覆盖 |
| 引入新的跨模块通信方式 | ✅ | 影响模块边界和系统拓扑 |
核心判断标准:如果半年后团队会问「为什么不用另一种方案」,现在就该写一份 ADR。
ADR 检查清单
写之前
- 这个决策影响范围是否超过单个模块或单次 PR?
- 是否有至少两个被认真考虑过的备选方案?
- 是否能清晰描述当前的约束条件(时间、团队、成本、性能)?
- 是否了解这个决策的下游影响(其他团队、其他系统)?
写的时候
- 标题是否能一句话说清决策内容?
- 背景是否足够具体,让不了解项目的人也能理解?
- 备选方案是否列出了各自的优缺点,而不是只列名字?
- 决策理由是否和约束条件对应,而不是凭直觉?
- 影响是否同时包含正面收益和负面代价?
- 是否标注了状态和日期?
写之后
- ADR 是否已提交到代码仓库的
docs/adr/或doc/adr/目录? - 是否通知了相关的团队成员和跨团队干系人?
- 当约束发生变化时,是否及时更新状态或创建新 ADR?
- 是否定期(比如每季度)review 一次现有 ADR 的状态是否仍然准确?
参考资料
- Michael Nygard - Documenting Architecture Decisions — ADR 概念的原始论文,2011 年发表
- Martin Fowler - Architecture Decision Record — Fowler 对 ADR 的系统阐述和最佳实践
- AWS - Master Architecture Decision Records (ADRs) — AWS 架构博客,ADRs 最佳实践指南
- Google Cloud - 架构决策记录概览 — Google Cloud 架构中心 ADR 指南
- Microsoft Azure - 维护体系结构决策记录 — Azure Well-Architected Framework 中的 ADR 章节
- Spotify - When Should I Write an Architecture Decision Record — Spotify 工程团队的 ADR 实践
- adr-tools (GitHub) — 轻量级 ADR 命令行工具
- log4brains (GitHub) — ADR 管理 + 静态站点生成工具
- ADR GitHub Organization — ADR 资源汇总站