架构决策记录(ADR)已经成为很多团队的标配——选了什么技术、为什么选、谁拍板,一条条写清楚。但 ADR 有一个隐含的盲区:它只记录决策那一刻的状态,却很少追问"这个决定以后还能改吗?改起来要花多大代价?"
Pierre Pureur 和 Kurt Bittner 提出的 架构变更案例(Architectural Change Cases) 正是针对这个盲区。变更案例不是替代 ADR,而是在 ADR 之上追加一层"未来视角"——把隐藏假设摊开,把变更的可逆性和成本提前估出来。
ADR 记了什么,漏了什么
一个典型的 ADR 大概长这样:
# ADR-007: 使用 Redis 作为会话缓存
## 状态
已采纳
## 背景
当前会话存储在 MySQL 中,读写延迟随用户量上升显著增加。
## 决策
采用 Redis 单节点缓存会话数据,MySQL 作为持久化后备。
## 理由
- Redis 单线程模型满足当前 QPS 预估(峰值 5k)
- 团队已有 Redis 运维经验
- 迁移成本可控,可逐步切换
看起来很完整。但仔细想:如果半年后 QPS 涨到 50k,单节点扛不住怎么办?如果业务要求会话跨区域同步,Redis 单节点方案还能撑多久?从 Redis 换成另一种缓存,数据迁移要多久?
ADR 不回答这些问题——它默认决策是"终点"。而现实里,决策只是"起点",后续几乎一定会变。
变更案例:给每个决策加上"如果以后要改"
变更案例的核心动作很简单:对每个架构决策,追问一组结构化的"变更假设"——
- 触发条件:什么业务或技术变化会让当前决策不再适用?
- 变更路径:如果要换方案,大致怎么走?
- 可逆性评估:改回去的难度是"改个配置"还是"重写半个系统"?
- 成本估算:人力、时间、数据迁移、下游依赖改造等。
把这些写下来,就构成了一个变更案例。它和 ADR 的关系不是替代,而是配对——每个 ADR 至少挂一个变更案例。
下面是一个和 ADR-007 配对的变更案例示例,用 YAML 格式存储,方便脚本化检索:
# change-case-007-a.yaml
adr_ref: "ADR-007"
title: "Redis 单节点无法满足流量增长时切换至 Redis Cluster"
trigger:
condition: "单节点 QPS 持续超过 20k,或延迟 p99 > 50ms"
likelihood: "高——业务增长曲线显示 6-9 个月内可能触及"
change_path:
steps:
- "将单节点扩展为 3 主 3 从 Cluster"
- "修改客户端连接配置,启用 cluster-aware 模式"
- "验证会话 key 的 hash tag 分布是否均匀"
- "灰度切换:先对新注册用户走 Cluster,老用户逐步迁移"
reversibility:
level: "中等"
rationale: "Cluster 可缩回单节点,但需重新 slot 迁移,约 2-4 小时窗口"
cost_estimate:
effort: "2 人 × 3 天(含测试与灰度观察)"
data_migration: "Redis slot 迁移工具自动完成,无需离线"
downstream_impact: "客户端需升级驱动版本;3 个消费会话事件的服务需验证兼容性"
assumptions_exposed:
- "当前 QPS 峰值 5k 不会短期翻 4 倍——实际增长曲线未建模"
- "会话 key 无跨 slot 聚合查询需求——若后续引入按用户维度聚合,Cluster 方案需重新设计"
一个决策可以挂多个变更案例——比如 ADR-007 还可以再挂一个"如果业务要求跨区域会话同步"的变更案例,评估从 Redis 换成 DynamoDB 或 CockroachDB 的路径和代价。
用脚本把变更案例纳入日常评审
变更案例写了不看等于没写。一个实用的做法是:在架构评审或迭代规划时,自动拉出和当前决策相关的变更案例,检查触发条件是否已经逼近。
下面是一个 Python 小脚本,扫描所有变更案例文件,输出"触发条件可能已激活"的清单。你可以把它放进 CI 或定期任务里:
"""
scan_change_cases.py
扫描变更案例 YAML 文件,输出触发条件中 likelihood 为"高"的条目,
提醒团队关注即将逼近的架构变更节点。
依赖:pip install pyyaml
运行:python scan_change_cases.py --dir ./change_cases
"""
import argparse
import glob
import yaml
from pathlib import Path
def load_change_cases(directory: str) -> list[dict]:
cases = []
for path in glob.glob(str(Path(directory) / "*.yaml")):
with open(path, "r", encoding="utf-8") as f:
doc = yaml.safe_load(f)
if doc and "adr_ref" in doc:
doc["_source"] = path
cases.append(doc)
return cases
def filter_high_likelihood(cases: list[dict]) -> list[dict]:
return [
c for c in cases
if c.get("trigger", {}).get("likelihood", "").startswith("高")
]
def print_report(cases: list[dict]):
if not cases:
print("✅ 当前无高概率触发变更案例,架构决策短期稳定。")
return
print("⚠️ 以下变更案例触发概率为"高",建议在下次迭代规划中复核:\n")
for c in cases:
t = c["trigger"]
print(f" ADR: {c['adr_ref']}")
print(f" 变更案例: {c['title']}")
print(f" 触发条件: {t.get('condition', '未填写')}")
print(f" 可逆性: {c.get('reversibility', {}).get('level', '未评估')}")
print(f" 预估代价: {c.get('cost_estimate', {}).get('effort', '未估算')}")
print(f" 来源文件: {c['_source']}")
print()
def main():
parser = argparse.ArgumentParser(description="扫描架构变更案例")
parser.add_argument("--dir", default="./change_cases", help="变更案例 YAML 目录")
args = parser.parse_args()
cases = load_change_cases(args.dir)
high = filter_high_likelihood(cases)
print_report(high)
if __name__ == "__main__":
main()
运行效果示例:
$ python scan_change_cases.py --dir ./change_cases
⚠️ 以下变更案例触发概率为"高",建议在下次迭代规划中复核:
ADR: ADR-007
变更案例: Redis 单节点无法满足流量增长时切换至 Redis Cluster
触发条件: 单节点 QPS 持续超过 20k,或延迟 p99 > 50ms
可逆性: 中等
预估代价: 2 人 × 3 天(含测试与灰度观察)
来源文件: change_cases/change-case-007-a.yaml
团队看到这条输出,就可以在规划会上直接讨论:当前 QPS 是多少?离 20k 还有多远?要不要提前启动 Cluster 方案的预研?
变更案例暴露的隐藏假设
变更案例最有价值的地方,往往不是变更路径本身,而是 assumptions_exposed 字段——它逼着决策者把"当时没说出来的默认前提"写下来。
上面 Redis 示例里暴露了两条假设:
- "QPS 不会短期翻 4 倍"——但实际增长曲线没有建模,只是凭感觉。一旦业务侧搞了个大促,这条假设立刻失效。
- "会话 key 无跨 slot 聚合需求"——如果后续产品要求"同一用户的所有会话可批量查询",Redis Cluster 的 hash 分布就成了障碍。
这些假设在写 ADR 时往往不会被提及,因为"当时觉得不会发生"。变更案例的作用就是把这些"觉得不会发生"的事情显式化,让团队在假设被打破时不用从零开始慌乱评估。
落地建议与取舍
变更案例是个轻量实践,但也不是零成本。几点实际取舍:
- 不必每个 ADR 都挂变更案例。优先覆盖"替换成本高、业务变化快"的决策——比如存储选型、核心通信协议、身份认证方案。那些低风险、易回滚的小决策,写一个简短 ADR 就够了。
- 变更案例的粒度别太细。一个决策挂 2-3 个变更案例已经足够;如果写了 10 个,说明决策本身可能过于模糊,应该回到 ADR 重新厘清。
- 定期复核比写更重要。建议每 2-3 个迭代跑一次扫描脚本,或在架构评审会上花 10 分钟过一遍高概率触发条目。写完就归档的变更案例等于浪费。
- 变更案例不是变更计划。它评估的是"如果要变的代价和路径",不是"我们下个月就要变"。别把变更案例当成排期任务——它是风险雷达,不是项目计划。
一个简单的落地检查清单:
- [ ] 团队是否对 QPS 最高、替换成本最大的 3-5 个架构决策写了变更案例?
- [ ] 每个变更案例是否至少包含触发条件、可逆性评估和成本估算?
- [ ] 是否有定期机制(脚本扫描或评审议程)复核高概率触发条目?
- [ ]
assumptions_exposed字段是否写出了至少一条"当时默认但没验证"的前提?
变更案例不会让架构变更变得更少,但它能让变更发生时团队不再措手不及——因为你已经提前知道哪里会断、断了大致怎么接、接起来要花多少时间。这比事后翻 ADR 猜"当时怎么想的"要靠谱得多。