架构决策记录(ADR)最佳实践 - Archyl Blog

我们花了两周时间争论一个一年前就已经做出并否决的数据库选择。那时我发现了ADR,从此它为我们节省了无数时间。

架构决策记录(ADR)最佳实践

那场会议已经进行了两个小时。我们正在讨论是用PostgreSQL还是MongoDB来做新服务。正反论据此起彼伏——关系完整性、灵活的模式、团队熟悉度、运维复杂性。

然后有人提到:"我们去年不是为用户服务讨论过完全相同的问题吗?"

沉默。确实如此。一年前,我们花了类似的时间讨论同样的问题,最终选择了PostgreSQL。但没人记得推理过程。领导那次讨论的工程师已经离开了公司。于是我们从零开始进行完全相同的辩论。

这就是我发现架构决策记录(ADR)的契机,从那以后,它为我们节省了无数重复讨论已定决策的时间。

什么是ADR?

ADR是一份记录单一架构决策的简短文档。不是涵盖系统所有方面的设计文档,只是一个决策:

  • 我们决定了什么
  • 为什么这样决定
  • 考虑了哪些替代方案
  • 结果会怎样

格式刻意保持轻量。一个ADR应该能放在一页纸上。如果更长,你可能在记录不止一个决策。

好的ADR的结构

在撰写了数十个ADR并阅读了数百个之后,我发现最好的ADR遵循一致的结构:

标题和编号

每个ADR都有一个顺序编号和简洁的标题:

ADR-0042: 订单服务使用PostgreSQL

编号很重要。它创建了决策的时间线,使ADR在讨论中易于引用:"正如我们在ADR-42中决定的..."

状态

ADR有生命周期状态:

  • 已提议:仍在讨论中
  • 已接受:决策已做出,我们正在遵循
  • 已弃用:不再相关(新ADR取代了它)
  • 已拒绝:我们考虑了但决定不采用

已拒绝状态特别有价值。有时你想记录为什么_没有_做某事,这样未来的团队就不会提出同样的建议。

背景

这里描述促使做出决策的情况。我们在解决什么问题?有什么约束?谁会受影响?

## 背景

订单服务需要持久化存储来存放订单数据。我们预计最初每天处理50,000个订单,
两年内增长到500,000个。订单有明确定义的结构,但可能需要随时间添加额外的
元数据字段。团队对关系数据库和文档数据库都有经验。

要具体说明约束条件。"我们需要ACID合规性"比"我们需要可靠性"有用得多。未来的读者需要理解塑造决策的力量。

决策

清楚地陈述决策。不是"我们可能考虑"或"我们应该探索"——而是我们实际决定了什么。

## 决策

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

我们选择PostgreSQL因为:

- 金融订单数据需要ACID合规性
- JSON列提供元数据的模式灵活性
- 我们的基础设施团队有PostgreSQL运维经验
- 查询模式适合关系建模

注意这不仅仅是陈述决策,还简要解释了推理。下一节会更详细,但即使是决策部分也应该是自解释的。

考虑的替代方案

这是最被低估的部分。记录你没有选择的方案,往往与记录你选择的方案一样有价值。

## 考虑的替代方案

### MongoDB

优点:

- 原生JSON存储
- 更简单的水平扩展
- 灵活的模式演进

缺点:

- 一致性保证较弱
- 团队对运维不太熟悉
- 需要额外工具支持事务

### DynamoDB

优点:

- 完全托管,运维最少
- 出色的可扩展性

缺点:

- 对AWS的供应商锁定
- 查询模式限于分区键/排序键访问
- 高规模下成本不可预测

当新成员加入团队问"为什么我们不用MongoDB?"时,你就有了答案。不需要安排会议或找到做出原始决策的人。

后果

每个决策都有权衡。对此要诚实。

## 后果

### 积极方面

- 强大的数据完整性保证
- 团队可以利用现有PostgreSQL专业知识
- 成熟的运维特性

### 消极方面

- 模式迁移比文档存储需要更多规划
- 如果超出单节点容量,水平扩展更复杂
- 如果达到扩展限制,需要实现应用层分片

### 风险

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

这一节是关于知识上的诚实。没有完美的决策。承认缺点建立信任,帮助未来的团队理解何时可能需要重新审视决策。

何时写ADR

并非每个技术选择都需要ADR。使用你的判断,但以下是一些指导:

应该写ADR的情况...

  • 决策影响多个团队或服务
  • 决策逆转成本高昂
  • 你在多个可行选项之间选择
  • 未来的团队成员可能质疑这个选择
  • 决策解决了一个重大的技术辩论

不需要写ADR的情况...

  • 选择是显而易见且无争议的
  • 决策只影响一个人或一个文件
  • 无需重大成本即可轻松逆转
  • 这是团队一直遵循的标准模式

例如:"应该用哪个JSON库?"可能不需要ADR。"我们的公共API应该用GraphQL还是REST?"绝对需要。

我们项目中的真实ADR

以下是我们写过的一些实际ADR(为这篇文章做了简化):

ADR-0007: 服务间的事件驱动通信

背景:我们的微服务目前通过HTTP同步通信。这造成了紧耦合,当服务不可用时会产生级联故障。

决策:我们将采用事件驱动架构,使用Apache Kafka进行服务间的异步通信。

后果

  • 服务对故障的容错性提高
  • 最终一致性代替强一致性
  • 运维复杂性增加(Kafka集群管理)
  • 团队需要事件溯源模式的培训

ADR-0015: 前端应用使用Monorepo

背景:我们有5个前端应用在不同的仓库中。共享代码需要发布包。版本不匹配导致开发体验差。

决策:使用Nx将所有前端应用合并到单个monorepo中。

拒绝的替代方案

  • 保持独立仓库加强包管理:拒绝,因为协调开销仍然存在
  • 不使用Nx的单一仓库:拒绝,因为构建时间会过长

后果

  • 代码共享简化,一致性提高
  • 单个PR可以更新共享代码和所有消费者
  • 更大的仓库需要更好的构建缓存工具
  • 应用间可能存在意外耦合

我犯过的常见错误

错误1:事后才写ADR

写ADR的最佳时机是在决策过程中,而不是几周后。事后写的话,你会忘记细微之处、考虑过的替代方案和具体的约束条件。

现在我们将ADR草稿作为决策过程的一部分来撰写。讨论发生在ADR文档中,而不是在消失的Slack线程中。

错误2:写得太长

如果你的ADR超过一页,你可能是:

  • 记录了多个决策(拆分为多个ADR)
  • 包含了实现细节(留给设计文档)
  • 过度解释了显而易见的背景

简洁的纪律迫使你保持清晰。

错误3:不链接相关ADR

决策很少是孤立存在的。当我们选择Kafka进行事件驱动通信(ADR-0007)时,这影响了我们的数据库选择(ADR-0042),因为我们可以接受最终一致性。

交叉引用相关ADR:

## 相关决策

- 参见ADR-0007了解我们为什么接受最终一致性
- 取代了推荐MongoDB的ADR-0003

错误4:放弃这个实践

ADR随时间提供价值。一两个ADR帮助不大。经过多年积累的50多个ADR语料库变得极其有价值。它是架构演进的可搜索历史。

忙碌时停止写ADR是很大的诱惑。要抵抗它。花十分钟写一个ADR会在以后节省数小时的会议时间。

让ADR成为工作流的一部分

最难的部分不是写ADR——而是让它成为习惯。以下是有效的方法:

将它们存放在代码旁边

把ADR放在你的代码仓库中,通常在docs/adr/docs/decisions/中。这样:

  • 它们与描述的代码一起版本化
  • 它们出现在代码审查中
  • 它们不会在某个wiki中成为孤儿

模板化

创建一个大家都使用的markdown模板。这减少了摩擦并确保一致性:

# ADR-NNNN: 标题

## 状态

已提议 | 已接受 | 已弃用 | 已拒绝

## 背景

[促使做出这个决策的问题是什么?]

## 决策

[我们提议和/或正在做的变更是什么?]

## 考虑的替代方案

[我们考虑了哪些其他选项?]

## 后果

[由于这个变更,什么变得更容易或更困难?]

每季度审查ADR

设置日历提醒每季度审查你的ADR:

  • 是否有决策不再相关?
  • 情况是否发生了变化,使我们的假设无效?
  • 是否有未记录的决策应该记录?

将ADR链接到架构图

这就是Archyl等工具发挥作用的地方。当你将ADR链接到C4图中的特定组件时,查看该组件的任何人都可以立即看到塑造它的决策。

"为什么这个服务和Kafka通信而不是直接连数据库?"点击关联的ADR就能找到答案。

总结

架构决策记录是那种看起来像额外工作直到你需要它们的实践之一。然后它们变得无价。

那场关于PostgreSQL和MongoDB的两小时会议?在有人找到原始ADR后十分钟就结束了。所有的背景都在那里——约束、替代方案、推理。我们很快确认了原始决策仍然适用,然后继续前进。

从小处开始。下次你做架构决策时,花十五分钟写一个ADR。把它放在你的仓库中。六个月后,当有人问"我们为什么那样做?"时,你就有了答案。


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