架构决策记录 ADR:把为什么留下来

0阅读11分钟

接手项目时最怕的一句话

每个开发者都经历过这样的场景:接手一个运行了两三年的系统,看到某个模块用了让人意外的技术方案。你问同事「这里为什么不用 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 管理所有状态,包括表单、分页、模态框开关。两年后代码里到处是 useSelectoruseDispatch,改一个按钮的 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 从提出到废弃,经历以下阶段:

流程图画布 · 115%
Mermaid 流程图加载中...

关键规则:ADR 一旦标记为 Accepted,就不应该再修改内容。如果决策需要调整,创建一份新的 ADR,把旧的那份标记为 Superseded,并在新 ADR 中说明为什么改变。这样做的目的是保留决策演进的历史轨迹。

ADR、RFC 和设计文档的区别

很多团队同时有 ADR、RFC 和设计文档,容易混淆。它们解决的问题不同:

维度ADRRFC设计文档
目的记录已做出的决策征集团队意见详细描述方案设计
阶段决策之后决策之前决策之前或之中
篇幅短,1-2 页中等,可附原型长,包含详细设计
可变性接受后不可修改收集反馈后关闭可能迭代多版
回答什么选了什么、为什么大家觉得怎么样具体怎么做

简单的理解方式:RFC 是讨论稿,设计文档是方案稿,ADR 是结论稿。RFC 和设计文档是过程产物,ADR 是最终记录。

实践中,一份 ADR 可以引用对应的 RFC 或设计文档作为链接,自己只保留结论和关键取舍。

四种 ADR 模板对比

不同团队可以选择不同轻量级的模板。以下是四种常见选择:

维度Nygard 原版MADRY-StatementGoogle Cloud 扩展
字段数513+1 句话10+
适用场景通用复杂决策轻量快速企业级项目
包含状态
包含备选方案✅(结构化)
包含置信度
工具支持adr-toolslog4brains手动手动
学习成本极低

Nygard 原版是最经典的格式,五个字段(标题、状态、背景、决策、影响)覆盖了核心需求。MADR(Markdown ADR)在 Nygard 基础上增加了置信度、决策者、日志等字段,适合需要更多审计信息的团队。Y-Statement 是极端精简版,一句话概括决策,适合小团队快速对齐。Google Cloud 扩展版增加了贡献者、触发条件、受影响用户旅程等字段,适合跨团队协作。

我的建议是从 Nygard 原版开始。如果团队发现信息不够用再往上加。模板太重反而没人愿意写。

ADR 管理工具对比

ADR 写出来之后需要管理和检索。以下是三种常见工具:

维度adr-toolslog4brains手动管理
类型CLI 工具CLI + 静态站点生成纯文件
创建命令adr newlog4brains 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 的状态是否仍然准确?

参考资料

评论 0

0 / 1000