ADR(Architecture Decision Record)是一份简短文档,用来保存一个重要架构决策及其理由。多份 ADR 按时间组成项目的决策日志。
代码能告诉后来者“系统现在怎样实现”,却不一定能解释“为什么这样实现”。ADR 补的是这个缺口:当约束、参与者和技术环境发生变化时,未来的开发者可以判断当前决定仍然成立,还是应该被新决定取代。
ADR 记录什么
一份 ADR 的中心是决策,而不是技术知识或讨论过程。它至少要让读者看懂三件事:
- 当时有哪些事实、约束或力量需要平衡;
- 项目选择了什么,以及为什么;
- 这个选择接受了哪些成本或后果。
例如:
公开文章阅读和直接搜索不依赖 AI。模型服务能增强数字分身体验,但成本与上游可用性不可控,因此模型或额度不可用时降级为直接搜索,而不是让博客整体不可用。
这段话同时包含背景、决定和权衡,已经是一份有效的 ADR。表格、流程图和长模板都不是必要条件。
什么时候值得写
不要为每次技术选择都写 ADR。一个决定同时满足以下三个条件时,才值得进入决策日志:
- 难以撤销:未来改变它会产生明显迁移成本、兼容风险或组织影响。
- 缺少背景会令人意外:后来者只看代码,很可能误以为当前设计是疏忽并试图“修正”。
- 存在真实权衡:至少有一个合理替代方案,而项目基于具体约束作出了选择。
常见候选包括:
- 单体、模块化单体还是微服务;
- 数据由哪个业务边界拥有;
- 同步调用还是异步事件;
- 数据库、身份提供商或部署平台等高迁移成本选择;
- 公共 API 的兼容和弃用策略;
- 不容易从代码看出的合规、隐私或性能约束;
- 有意偏离团队惯例的方案。
通常不需要 ADR 的内容包括:
- 普通缺陷修复和局部重构;
- 容易替换的小型工具库;
- 已由编码规范明确规定的常规写法;
- 没有合理替代方案的必然选择;
- 具体任务拆分和实施进度。
改动行数不是判断标准。一个只有几行代码的数据所有权边界,可能比一次大规模但机械的重命名更值得记录。
ADR 与其他文档的边界
| 文档 | 主要回答的问题 |
|---|---|
| README | 如何了解、运行或使用项目 |
| 架构说明 | 系统当前由哪些部分组成,它们如何协作 |
| CONTEXT / 术语表 | 项目中的业务词语分别是什么意思 |
| ADR | 为什么选择当前方案,并接受了什么代价 |
| Issue / 计划 | 接下来由谁完成哪些工作 |
| Runbook | 运行故障发生时怎样处理 |
ADR 不是会议纪要。讨论记录可以作为附件,但 ADR 应提炼最终决定,而不是保存所有发言。
ADR 也不是规范的替代品。假如“金额以最小货币单位存储”已经形成系统契约,ADR 解释为什么选择它;数据模型和 API 文档仍要准确规定字段单位与校验规则。
从最小模板开始
最轻量的 ADR 只需要标题和一段完整的话:
# 使用模块化单体与独立索引 Worker
第一版由模块化单体承载 Web、认证和数字分身入口,并使用独立
Worker 执行知识摄取与索引任务。该形态保留模块边界和异步任务
隔离,同时避免在产品边界尚未稳定时承担微服务的网络契约、部署
和故障处理成本。当一段话不足以消除歧义时,再增加真正有价值的字段:
---
status: proposed
---
# 实测某供应商作为模型候选
[背景、决定和理由]
## Considered Options
[只有被拒绝方案将来可能被再次提出时才保留]
## Consequences
[只有非显而易见的正面、负面或中性影响才单列]常见状态可以包括:
proposed:尚待责任人接受;accepted:当前有效;deprecated:不再推荐,但未必存在直接替代;superseded by ADR-NNNN:已被指定的新决定取代。
状态不是必填字段。若仓库只保存已接受决定,省略状态往往比每份文件重复写 accepted 更清楚。
怎样写得短而有用
标题直接表达选择
使用“使用 PostgreSQL 保存持久任务”而不是“数据库讨论”。读者在目录中就应知道决定是什么。
背景写事实与约束
说明为什么现在需要决定,以及哪些力量相互冲突。不要为了证明结论而把偏好伪装成事实。
决定使用主动语态
“我们将……”比“建议考虑……”更明确。若尚未接受,用 proposed 表达状态,而不是模糊决策句。
理由必须与项目有关
“业界流行”“性能更好”通常不够。写清当前规模、预算、团队能力、隐私边界、兼容要求或测量结果。
后果同时包含所得与所失
真实决策一定有代价。只写优点的文档更像方案推销,而不是决策记录。
链接详细材料,不复制它们
基准测试、调研表和长设计稿可以链接到 ADR。这样既保留证据,又不让决策本身淹没在材料中。
文件位置与编号
常见做法是在代码仓库中建立 docs/adr/,使用单调递增的编号:
docs/adr/
├── 0001-separate-public-and-owner-knowledge.md
├── 0002-git-content-is-the-article-source.md
└── 0003-reading-and-search-do-not-depend-on-ai.md编号只表达记录顺序,不代表优先级,也不应复用。文件名中的短语应稳定描述决定,避免依赖临时 Issue 标题。
仓库不是唯一选择,但 ADR 应靠近其约束的实现,并进入版本控制和正常评审流程。跨多个仓库的系统级决定,则需要一个所有受影响团队都能发现的权威位置。
决策变化时不要抹掉历史
上下文变化并不表示旧 ADR 写错了。若已接受并实施的决定发生变化:
- 保留旧记录;
- 创建新 ADR,说明新上下文和新决定;
- 将旧记录标记为被新 ADR 取代;
- 在新旧记录之间建立双向链接。
这样可以区分“当时基于旧约束作出的合理选择”与“当前有效选择”。拼写修正和链接维护可以直接修改,但不应静默改写历史决定,让它看起来像从未变化。
ADR 如何进入开发流程
- 在高影响方案尚可调整时起草,而不是实现完成后补写胜利叙事。
- 让受影响边界的责任人审查事实、替代方案和后果。
- 决定接受后,与相关实现一起或在其之前合并。
- 在架构说明、模块文档或 Pull Request 中链接 ADR,不在代码注释里复制全文。
- 当约束变化时,重新评估并用新 ADR 取代旧决定。
CI 最多检查编号重复、链接和格式。它无法判断一项权衡是否真实,也不能代替责任人接受高影响决定。
与 AI Agent 协作
ADR 对 Agent 的价值不是增加更多上下文,而是提供代码中看不见的意图。Agent 在修改模块边界、数据所有权、安全策略或基础设施前,应先检索相关 ADR。
Agent 可以协助:
- 从代码、配置和讨论材料中整理事实;
- 找出与现有 ADR 的冲突;
- 提出替代方案和反例;
- 起草背景、决定与后果;
- 检查实现是否符合已接受决定。
Agent 不应未经授权接受高影响决定,也不应因为新方案看起来更现代就绕过现有 ADR。若代码与 ADR 不一致,应先确认是实现偏离、文档过时,还是上下文已经变化。
发布前检查
- 是否只记录一个核心决定?
- 三个门槛是否同时成立?
- 标题能否直接看出选择?
- 背景是否包含项目特有的事实和约束?
- 是否写清选择理由与至少一个真实代价?
- 是否避免复制教程、会议纪要和实施清单?
- 若取代旧决定,是否保留历史并互相链接?
- 实现、架构说明和 ADR 是否一致?
一句话总结:ADR 不是架构文档的重量级模板,而是写给未来开发者的一份最小解释——我们面对什么约束、选择了什么、为什么,以及为此接受了什么。
参考资料:Michael Nygard:Documenting Architecture Decisions、ADR GitHub Organization:Motivation and Definitions、ADR Templates