ADR 模板与示例:可直接复制的格式

会议已经开了两个小时。我们在争论一个新服务该用 PostgreSQL 还是 MongoDB。关系完整性、灵活的 schema、团队熟悉度、运维复杂度,各种论点来回交锋。

这时有人说:"去年做用户服务的时候,我们不是就讨论过一模一样的问题吗?"

一片沉默。确实讨论过。一年前,我们在同一个问题上花了同样长的时间,最后选了 PostgreSQL。没有人记得当时的理由,主导那次讨论的工程师也已经离职。于是我们又从零开始,把同样的争论重来了一遍。

从那以后,我们开始写架构决策记录(ADR)。这篇文章是可以直接复制粘贴的部分:三种模板、各自的适用场景,以及我们自己的三个决策的填写示例。

什么是 ADR?

架构决策记录是记录单个架构决策的简短文档。它不是涵盖系统方方面面的设计文档,而只针对一个决策:

  • 我们决定了什么
  • 为什么这样决定
  • 考虑过哪些替代方案
  • 会带来什么后果

一份 ADR 应该控制在一两页之内。如果更长,你很可能记录了不止一个决策。什么时候该写、生命周期,以及团队常犯的错误,完整指南里都有介绍;这篇文章只讲格式。

最小模板(Nygard)

大多数团队至今仍在使用的格式,是 Michael Nygard 在 2011 年的一篇文章中提出的。它由五部分组成:一个简短名词短语构成的标题、状态、背景("起作用的各种力量")、决策("我们对这些力量的回应"),以及后果("应用该决策之后形成的背景")。他定义的状态有 Proposed、Accepted、Deprecated 和 Superseded。

# ADR-NNNN: 简短的名词短语

## 状态

Proposed | Accepted | Deprecated | Superseded by ADR-XXXX

## 背景

有哪些力量在起作用?技术、组织、截止日期、预算。
要具体:数字、约束、受影响的人。

## 决策

我们将……

## 后果

什么变得更容易,什么变得更难,我们今后需要关注什么。

我们在其中加了一个章节:考虑过的替代方案,放在决策和后果之间。日后最省时间的正是这部分,因为一年后你被问到的很少是"我们选了什么?",几乎总是"我们为什么没选 X?"

## 考虑过的替代方案

### 方案 B
- 优点:……
- 缺点:……
- 否决原因:……

MADR

MADR(Markdown Architectural Decision Records)是一种结构化程度更高的模板,维护于 adr.github.io/madr。当前版本是 2024 年 9 月 17 日发布的 4.0.0。它把元数据放在 YAML 前置信息里,并把各个方案及其权衡写成明确的章节,而不是散文。

---
status: proposed | rejected | accepted | deprecated | superseded by ADR-XXXX
date: YYYY-MM-DD
decision-makers: ...
consulted: ...
informed: ...
---

# 概括所解决问题和所选方案的简短标题

## 背景与问题陈述

两三句话,或一个小故事。最好以问题的形式表述。

## 决策驱动因素

* 因素 1
* 因素 2

## 考虑过的方案

* 方案 A
* 方案 B
* 方案 C

## 决策结果

选定方案:"方案 A",因为……

### 后果

* 好,因为……
* 坏,因为……

### 确认方式

我们将如何检查该决策已被落实(评审、测试、适应度函数)。

## 各方案的优缺点

### 方案 A

* 好,因为……
* 坏,因为……

## 更多信息

链接、相关决策、何时重新审视。

在 MADR 中,"考虑过的方案"和"决策结果"之后的所有章节都是可选的。用不上的就删掉,不要留下空标题。

Y-statement

Y-statement 把一个决策装进一句话里。它出自 Uwe Zdun、Olaf Zimmermann 等人(《Sustainable Architectural Design Decisions》,IEEE Software,2013):

在 <用例或组件> 的背景下,
面对 <关注点或需求>,
我们选择了 <方案>
(而不是 <其他方案>),
以实现 <质量或收益>,
并接受 <代价>。

它太短,装不下完整的背景章节,而这正是它的用意。可以把它用于不值得写一整页的决策、放在较长 ADR 顶部作为摘要行,或者作为决策日志的条目格式。

用哪种格式

格式 适用场景 注意事项
Nygard(加替代方案) 刚开始实践,或大多数决策都有一个明显胜出的方案和几个被否决的方案 背景章节膨胀成长篇大论
MADR 决策有争议、需要多人签字确认,或希望不同 ADR 之间的方案和驱动因素可以对比 出于习惯把每个可选章节都填满
Y-statement 小决策、决策日志,或长 ADR 顶部的一句话摘要 把它用在需要真正背景章节的决策上

每个仓库选定一种并坚持下去。格式混杂的记录,读起来比用任何一种格式写起来都费劲。

示例 1:ADR-0042,订单服务使用 PostgreSQL

这是开头那个故事里的决策,用 Nygard 格式加上我们的"考虑过的替代方案"章节写成。

# ADR-0042: 订单服务使用 PostgreSQL

## 状态

Accepted

## 背景

订单服务需要持久化存储订单数据。我们预计初期每天处理 50,000 笔订单,
两年内增长到 500,000 笔。订单结构定义清晰,但随着时间推移可能需要
增加元数据字段。团队对关系型数据库和文档型数据库都有经验。

## 决策

我们将使用 PostgreSQL 15 作为订单服务的主数据库。

选择 PostgreSQL 的原因:

- 涉及资金的订单数据必须满足 ACID
- JSON 列为元数据提供了 schema 灵活性
- 基础设施团队有 PostgreSQL 的运维经验
- 查询模式非常适合关系建模

## 考虑过的替代方案

### MongoDB

- 优点:原生 JSON 存储、更简单的水平扩展、灵活的 schema 演进
- 缺点:一致性保证较弱,团队运维经验不足,
  事务需要额外的工具

### DynamoDB

- 优点:全托管、运维量极小、出色的可扩展性
- 缺点:被 AWS 锁定,查询模式受限于分区/排序
  键访问,大规模时成本难以预测

## 后果

### 正面

- 强有力的数据完整性保证
- 团队可以复用现有的 PostgreSQL 专业经验
- 运维特性众所周知

### 负面

- schema 迁移比文档型存储需要更多规划
- 超出单节点容量后,水平扩展更复杂
- 触及扩展上限时需要应用层分片

### 风险

- 如果订单量超过每天 100 万笔,可能需要重新审视
- JSON 列查询的效率低于原生文档型存储

## 相关决策

- 我们为何接受最终一致性,见 ADR-0007
- 取代推荐使用 MongoDB 的 ADR-0003

背景要写得具体。"我们需要满足 ACID"比"我们需要可靠性"对下一位读者有用得多。后果要写得诚实:没有哪个决策是免费的,把缺点写下来,就能告诉未来的团队什么时候该重新审视。

示例 2:ADR-0007,事件驱动通信,用 MADR 写

Kafka 这个决策摆在桌上的方案更多,参与的人也更多,这正是 MADR 显式列出驱动因素和方案的价值所在。

---
status: accepted
---

# 服务之间的异步通信使用 Kafka

## 背景与问题陈述

我们的微服务通过 HTTP 同步通信。这让它们紧密耦合,
一个服务不可用就会向上游引发连锁故障。
当调用方不需要立即得到回复时,服务之间应该如何通信?

## 决策驱动因素

* 韧性:一个服务宕机不能把其他服务一起拖垮
* 服务之间更松的耦合

## 考虑过的方案

* 保持同步 HTTP
* 基于 Apache Kafka 的事件驱动通信

## 决策结果

选定方案:"基于 Apache Kafka 的事件驱动通信",因为它
消除了导致连锁故障的运行时耦合。

### 后果

* 好,因为服务对彼此的故障更有韧性
* 坏,因为我们从强一致性转向了最终一致性
* 坏,因为我们现在要运维一个 Kafka 集群
* 坏,因为团队需要接受事件驱动模式的培训

状态和标题沿用原记录;日期和决策者字段在这里省略了,因为原记录没有保留它们。

示例 3:ADR-0015,前端单体仓库,用 Y-statement 写

单体仓库这个决策一句话就能写完,而这一句话足以回答"为什么所有东西都在一个仓库里?"

在我们有 5 个前端应用分散在各自仓库中的背景下,
面对版本不一致以及为共享代码而发布包的开销,
我们选择了用 Nx 管理的单一单体仓库
(而不是改进包管理的独立仓库方案,也不是不用 Nx 的单体仓库),
以实现简单的代码共享,并能用一个拉取请求同时更新共享代码及其所有使用方,
并接受更大的仓库需要构建缓存、各应用之间可能意外耦合。

被否决方案的理由保留在完整记录中:独立仓库仍有协调开销,而不用 Nx 的单体仓库会让构建时间变得无法接受。

放在哪里

把 ADR 放在它所描述的那个仓库里,路径为 docs/adr/ 或 docs/decisions/(Nygard 建议的是 doc/arch/adr-NNN.md)。它们会与代码一起进行版本管理,会出现在代码评审中,也不会在 wiki 里走丢。

按顺序编号,永远不要复用编号。决策发生变化时,不要改写历史,而是写一条新的 ADR 取代旧的,并更新旧 ADR 的状态。

大多数团队跳过的一步,是把 ADR 和它所影响的系统部分联系起来。在 Archyl 中,一条 ADR 带有 Nygard 的字段(状态、背景、决策、后果),外加日期和"被取代"链接,并挂接到它所塑造的系统、容器和组件上。在 C4 图中查看订单服务的人,会在它旁边看到 ADR-0042:"这个服务为什么用 PostgreSQL?"的答案只有一次点击之遥。

常见的坑(事后补写 ADR、让它膨胀到五页、在第一个忙碌的季度之后就放弃这一实践)在完整指南中有介绍。

从一条开始

那场关于 PostgreSQL 还是 MongoDB 的两小时会议呢?下一次同样的问题再被提起时,有人找到了 ADR-0042,会议十分钟就结束了。约束、替代方案和理由都在里面。我们确认原来的决策依然适用,然后继续往下推进。

从上面的模板中挑一个,粘贴到 docs/adr/0001-record-architecture-decisions.md。你的第一条 ADR,可以就是"决定写 ADR"这个决策。


了解更多关于架构文档的内容:C4 模型指南 | 为什么文档很重要 | 记录用户流程