3168 字
约 10 分钟
16
S18-X09 · 架构决策记录:把为什么留在仓库里

三个月后有人问"为什么网关鉴权放在 Filter 而不是 Service 里",翻遍代码只能看到结果,看不到原因。Slack 里搜不到,开会的人已经走了两个。最后要么重做一遍已经否定过的方案,要么在错误的路上继续加固。

架构决策记录(ADR)就是为这个问题设计的:一条决策一个文件,写在仓库里,跟代码一起 review、一起演进。


1. ADR 解决什么问题

症状 没有 ADR 的结果
新人问"为什么不用 X" 靠老人记忆,老人走了答案就没了
有人提"我们换成 Y 吧" 无法判断半年前是否评估过 Y、为什么否掉
代码里两种风格并存 不知道哪一种是"当时决定的",哪一种是"遗留的"
上线后出问题复盘 只知道"这样做会出问题",不知道"当时为什么接受这个风险"

ADR 不替代设计文档、不替代注释。它只记录有替代方案、且影响面大的选择——以及被否掉的那些。


2. 一份 ADR 的结构

模板本身要短到有人愿意写:

# 0007 订单状态回写改用 outbox,不再依赖事务内双写

- 状态:已接受
- 日期:2025-03-14
- 决策者:后端组
- 影响范围:order-service、payment-service

## 背景
支付回调需要在同一逻辑里更新订单状态并发送通知。目前两者都在本地事务内,
消息发送失败会让整个事务回滚,导致第 3 方回调重试,重复回调需要人工核对。

## 决策
订单状态变更后写入 outbox 表,由独立投递器读取并发送消息,发送成功再标记已投递。

## 后果
- 正面:消息发送失败不再回滚业务事务;重试集中在一处,便于观测。
- 负面:通知延迟从"毫秒级"变成"最长 1s";需要新增 outbox 表与清理任务。
- 需要接受:投递器故障时通知会积压,属于可接受范围。

## 被否决的替代方案
1. 分布式事务(Seata):引入新组件与运维成本,延迟更高,收益不匹配当前量级。
2. 事务提交后直接发消息,失败只记日志:会丢消息,且没有任何补偿路径。
3. 把通知改成定时全量扫描:对 DB 压力大,且难以判定"该发没发"。

每个字段都有必须回答的问题:

字段 必须回答 不写会怎样
标题 一句话说清"决定了什么" 文件名 decision-2025.md,半年后没人想点开
状态 现在还有效吗 读者把废弃决策当现状
背景 什么约束、什么数据促成了这次选择 决策像是拍脑袋
决策 具体做什么,可执行 写成一堆原则,没有动作
后果 好消息 + 坏消息 + 需要接受什么 变成宣传材料,没人信
替代方案 每个方案被否的具体理由 下次有人重新提一遍

"被否决的替代方案"是 ADR 里最值钱的一段。 只写结论的 ADR,等于告诉未来的读者"别问,就这么办"。


3. 编号与存放位置

放在代码仓库里,跟代码一起走:

order-service/
├── docs/
│   └── adr/
│       ├── README.md              # 索引 + 状态一览
│       ├── 0001-use-outbox-for-notify.md
│       ├── 0002-jwt-short-lived-token.md
│       └── 0007-order-state-outbox.md
└── src/
项目 约定 理由
目录 docs/adr/(单服务)或 docs/adr/{服务名}/(多服务) 离代码近,review 时一起看
文件名 NNNN-kebab-case-title.md,4 位补零 排序即时间序,改动标题不改编号
编号 全仓库单调递增,不复用、不回收 被取代的 ADR 仍要能按号找到
索引 README.md 列出编号、标题、状态 不用翻目录就能看全貌
语言 团队内部语言 决策是给人看的,不是给机器

编号原则只有一条:一旦发布不再改号。0007 被 0012 取代后,文件名不动,只在状态里写"被 0012 取代"。

git mv docs/adr/0007-order-state-double-write.md docs/adr/0007-order-state-outbox.md
git commit -m "docs(adr): 补充 0007 的替代方案与被否决理由"

改名也要留痕:只允许改标题部分,编号前缀保持。用 git mv 而不是删了重建,git log --follow 才能追到全部讨论。


4. 状态流转

状态 含义 谁能改 文件是否还改内容
提议(proposed) 正在评审,未定 作者 可以,改动直接提交
已接受(accepted) 是当前的实现依据 作者 + 一位复核者 只能勘误,观点改动要开新 ADR
已废弃(deprecated) 不再推荐,但代码可能还在 任何维护者 不再改内容,只改状态
被取代(superseded by 00XX) 有新决策替代它 新 ADR 的作者 冻结,只保留链接

状态本身可以在代码里建模,省得各处拼字符串:

public enum AdrStatus { PROPOSED, ACCEPTED, DEPRECATED, SUPERSEDED }

关键约束:已接受的 ADR 不改内容,改状态或开新条目。

- 状态:被取代(by 0012)
- 取代日期:2025-09-02

理由很实际:允许改正文,文档就变成随波逐流的说明,读者无法判断"当时是怎么想的"。表达"改主意了"的正确做法是新开一条并在背景里引用旧号——决策历史因此是一条链,而不是不断被覆盖的当前值。


5. 什么该写,什么不值得写

判断标准三条同时成立才写:有多个可行替代方案、影响面超过单个模块、三个月后有人会问为什么。

该写 不值得写
引入新的中间件/存储(Redis、MQ、ES) 加一个 Controller 接口
改变数据一致性策略(双写 → outbox) 变量重命名、格式化
接口协议与版本策略(REST vs gRPC、版本号方案) 单元测试怎么写
认证鉴权方案(会话 vs JWT、网关校验 vs 服务内) 依赖包小版本升级
跨服务的边界划分与职责归属 单个类内部的重构
明确"我们决定接受某个已知缺陷" 遵循框架默认约定的普通实现

最后一条常被忽略:接受一个已知问题也是决策。比如"先不做幂等,接受少量重复,因为业务侧可人工撤销",这个判断如果不写下来,半年后有人修好了,反而可能破坏当时的业务假设。

写 ADR 的成本控制在 20–40 分钟;要半天的 ADR,团队写到第三篇就停了。


6. 与设计文档、commit message 的分工

载体 回答什么 生命周期 典型长度
ADR 为什么这么选、否了什么 长期,冻结后不改 半页到一页
设计文档 完整方案怎么做(表结构、接口、时序) 到实现完成 数页
commit message 这次改动做了什么 永久但零散 一行标题 + 若干正文
代码注释 这段代码为什么这么写(局部技巧) 跟代码一起改 几行

分工的实操口径:

  • 决定"用 outbox 而不是分布式事务" → ADR;
  • outbox 表的字段、投递器的重试曲线 → 设计文档;
  • "补投递器的指数退避" → commit message;
  • "这里 sleep 50ms 是为了等索引可见" → 代码注释。

同一个内容不要写三遍:ADR 链接设计文档、设计文档引用 ADR 编号,但都不复述。


7. 评审流程:轻量到有人愿意做

1. 起草:作者开 PR,只加一个 ADR 文件,状态=提议
2. 复核:指定 1 位熟悉该模块的人复核(不是开会,是 PR comment)
3. 定稿:复核通过后改状态为已接受,合并
4. 索引:同一 PR 里更新 docs/adr/README.md 的状态列

PR 上挂一个极简检查清单:

# .github/pull_request_template.md 片段
- [ ] 背景里写清了触发这次决策的具体问题或数据
- [ ] 决策是可执行的(不是"我们要重视可维护性")
- [ ] 后果里包含负面与需要接受的部分
- [ ] 至少列出一个被否决的替代方案及其理由
- [ ] 已更新 docs/adr/README.md 索引
团队规模 合适的评审方式
3–8 人 一人起草、一人复核,PR 合并即生效
8–20 人 起草 + 两位领域相关者复核,超过 3 天未表态视为默认通过
跨团队接口相关 强制加一位对端团队复核者
20 人以上 不要全员评审单篇 ADR,改为按季度汇总状态回顾

不要做成"架构委员会审批制"。ADR 的作用是留痕,不是把关;把关交给代码评审与发布流程。


8. 常见坑

现象 原因 处理
ADR 写成事后总结 决策做完了才补文档,只写结论 决策前起草、决策后改状态
只有结论没有替代方案 作者觉得"显然" 强制填"被否决的替代方案"字段
状态永远停在"提议" 忘了改状态 索引加过期提醒,或复核通过即由复核者改
ADR 与代码脱节 实现时走了另一条路,没回写 PR 模板加"是否需要新增/更新 ADR"
目录里 60 条,全是同一类 粒度太细,把每个依赖升级都写成 ADR 只写影响面大的,聚合同类记录
内容重复三处 ADR、设计文档、README 各抄一遍 ADR 只留决策与理由,其余链接

第四条最常见的表现是:ADR 说"用 outbox",代码里其实还是双写,因为实现时发现 outbox 投递器没排期。这时要么补一条新 ADR 说明临时偏差,要么改回原 ADR 的状态为"已废弃"——不能放着不管,一份与代码不符的 ADR 比没有 ADR 更坏。


9. 边界:什么情况不该用 ADR

场景 替代做法 理由
一次性原型、验证性编码 不留档,直接删 决策寿命短于文档寿命
合规/审计要求的正式评审 正式设计评审记录 需要签字与流程,ADR 承载不了
纯运维操作(扩容、参数调整) 变更单 / Runbook 不是架构决策
团队还没有代码仓库规范 先建立 PR 流程 ADR 依赖 review 文化
需要对外承诺的接口变更 接口变更公告 + 版本策略文档 受众是外部方,不是本团队

务实起点:只写"引入新组件"和"改变一致性/鉴权策略"两类,攒到 10 条再扩范围,空目录里立一份 8 页规范等于不写。


10. 自测

  1. ADR 与设计文档的分工边界在哪?
  2. 一份 ADR 里最容易被省略、却最有价值的是哪一段?
  3. 已经"已接受"的 ADR 为什么不允许改正文?
  4. 判断"这次决策要不要写 ADR"的三条标准?
  5. 写完 ADR 但实现走了另一条路,该怎么处理?
  6. 为什么"接受某个已知缺陷"也值得写成 ADR?
S18-X09 · 架构决策记录:把为什么留在仓库里
https://www.clxhxhhr.top/posts/5724/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。