ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

架构决策记录(ADR)实战:从Uber实践到团队落地指南

2026/8/30 4:13:39 拓冰建站 浏览量
架构决策记录(ADR)实战:从Uber实践到团队落地指南 团队里讨论技术方案经常是会议室聊一整天结论最后落在某个人的备忘录里。三个月后新人入职问“当时为什么选这个方案”大家只能凭记忆补充甚至已经没人记得完整理由。这不是执行力的问题而是技术决策缺少一种结构化沉淀方式。Uber 很早就遇到过类似问题并在内部广泛使用一种轻量级方法来解决这就是 ADR。本文会围绕 Uber 的 ADR 实践展开讲清楚架构决策记录的概念、文档格式、落地流程和团队协作方式并提供一个完整案例帮助你在自己的项目中直接落地这套思路。1. ADR 是什么为什么连 Uber 都在用1.1 ADR 的基本概念ADR 是 Architecture Decision Record 的缩写中文常翻译为“架构决策记录”。它本质上是一种用文档记录技术决策的方式当团队做出一个重要技术选择时把背景、方案对比、最终结论、影响范围等内容写成一篇文章存放到代码仓库中长期保存并持续维护。在 ADR 出现之前技术决策通常只存在于两种地方一是会议纪要二是架构师的脑子里。这两种方式都有明显问题会议纪要往往只记录结论不记录理由架构师的记忆又会随着人员流动而丢失。ADR 把决策从“一次性讨论产物”变成了“可追溯的工程资产”。这个概念最早由 Michael Nygard 在 2011 年前后提出核心思想并不复杂架构决策不能只靠口头沟通应该像代码一样被管理、被评审、被版本化。每一条 ADR 都是独立的文档描述一个具体的架构决策并按照固定模板记录上下文、决策和后果。1.2 ADR 到底解决了什么问题大多数开发团队都会遇到下面几类场景业务发展后旧技术选型不再适合但不清楚当初为什么选它。新成员加入后需要花大量时间了解系统的设计背景。两个团队选择了不同的方案但各自都有道理缺少统一决策记录。重构时想换掉某个组件但担心影响面太大不知道当初的约束条件。技术评审后结论没有落实到文档后续实现偏离了决策方向。ADR 解决的核心问题就是“决策的上下文丢失”。它不要求你写长篇大论而是强制你回答三个问题当时面临什么情况我们决定了怎么做这个决定带来的影响是什么当这三件事被清晰记录后团队就不再依赖个人记忆来理解系统。1.3 Uber 的 ADR 文化Uber 在技术管理上非常强调“用文档驱动协作”。在 Uber 工程团队公开分享的内容中ADR 是他们技术决策体系里的重要组成部分。Uber 的工程师会把 ADR 放在统一目录中通过代码评审流程来审批并允许任何人基于 ADR 提出更新。Uber 的做法的特殊之处在于它不是把 ADR 当作“事后补文档”而是把它融入到开发流程中。每个重大技术改动都会先有一条对应的 ADR 进入评审评审通过后再开始实现。这样做保证了代码库中的每一个关键设计都有据可查。对我们普通团队来说不一定需要完全复刻 Uber 的规模但完全可以借鉴它的思路让技术决策走上“提案—评审—定稿—维护”的完整闭环。2. ADR 核心概念与文档结构2.1 ADR 的基本组成一条标准 ADR 通常包含以下部分字段作用说明标题该条决策的核心主题例如“订单服务引入消息队列”状态当前生命周期如草案、已接受、已废弃上下文为什么需要做这个决策存在哪些约束条件决策最终确定的方案是什么包含关键细节后果这个决策带来的正面影响和负面影响其中“上下文”是最容易被忽略但最重要的部分。它记录了决策发生时的事实包括业务背景、技术约束、团队情况、候选方案等。三个月后再回头看只有上下文写清楚了决策才真正具有参考价值。下面是一个最基础的 ADR 模板# ADR-001订单服务引入 Redis 缓存 ## 状态 草案 ## 上下文 订单服务商家信息接口平均响应时间为 800ms 数据库连接池在高峰期接近上限需要降低数据库压力。 ## 决策 引入 Redis 作为查询缓存缓存 key 按 merchant:info:{merchantId} 设计过期时间 10 分钟。 ## 后果 正面数据库 QPS 下降接口响应时间预计降至 100ms 以内。 负面商家信息修改后需要主动删除缓存存在短暂不一致窗口。2.2 ADR 的状态流转ADR 不是写完就固定不变的它和代码一样有生命周期。常见的状态有Proposed草案表示这个决策正在被讨论。Accepted已接受表示团队认可这个方案可以开始实现。Superseded已替代表示这个决策被新的 ADR 取代。Deprecated已废弃表示该决策不再推荐使用。状态流转是 ADR 实践中最容易忽略的环节。很多团队写了一条 ADR 就再也没更新过最终文档和系统实际情况完全脱节。正确做法是每当系统架构发生变化先检查关联的 ADR 是否还有效如果不再有效不要删除原文档而是新增一条 ADR 声明废弃原因并指向新的决策。这样历史记录才能完整保留。2.3 ADR 与设计文档、技术方案的区别很多团队会问“我们已经有技术方案文档了还需要 ADR 吗”两者定位其实不同。设计文档通常描述“怎么做”比如模块划分、接口定义、数据表设计内容往往很长ADR 则描述“为什么这么做”更关注决策动机和权衡过程。一次架构设计中可能有多个关键决策一个设计文档对应多个 ADR。举个例子设计一个订单服务时设计文档会包含服务拆分、接口协议、数据库表结构而 ADR 则单独描述“为什么用 MySQL 而不是 PostgreSQL”“为什么用同步调用而不是异步消息”。前者回答 How后者回答 Why。3. 环境准备与项目落地方式ADR 本身不依赖任何特定技术栈只要团队有代码仓库就可以落地。但为了让 ADR 管理得更规范通常会结合 Git、Markdown 和自动化检查工具。3.1 目录规划建议在仓库根目录下创建docs/adr目录专门存放 ADR 文档。如果仓库内部有多个子项目也可以在每个子项目下建立独立目录。一个典型的目录结构如下my-project/ ├── docs/ │ └── adr/ │ ├── README.md │ ├── 0001-use-redis-for-cache.md │ ├── 0002-introduce-message-queue.md │ └── 0003-use-kafka-replace-rabbitmq.md └── src/README.md用来说明 ADR 的管理规范包括编号规则、模板地址、评审流程等。0001这种四位数字编号可以保证文件在排序时按照创建时间展示。3.2 版本管理每条 ADR 文件都应该纳入 Git 管理并保留完整的提交历史。当 ADR 内容发生更新时不要直接改写历史记录而是新增提交并在 PR 描述中说明变更原因。这样可以清晰看到一次决策从草案到定稿的演进过程。如果团队使用 GitHub、GitLab 或 Gitea建议把 ADR 的评审直接绑定到 PR 流程中。提交者创建一条 ADR 分支发起 Pull Request评审人在 PR 中讨论合并后视为 ADR 被接受。3.3 自动化检查随着 ADR 数量增加人工维护一致性会变困难。可以在 CI 中加入简单的检查脚本比如校验 ADR 文件是否包含必填字段、文件名编号是否连续、状态字段是否合法。下面是一个简单的 Python 检查脚本用于扫描 ADR 文件中的必填字段import pathlib import re ADR_DIR pathlib.Path(docs/adr) REQUIRED_SECTIONS [状态, 上下文, 决策, 后果] def check_adr_file(file_path: pathlib.Path) - list[str]: errors [] content file_path.read_text(encodingutf-8) for section in REQUIRED_SECTIONS: if not re.search(rf^##\s{section}\s*$, content, re.MULTILINE): errors.append(f{file_path.name}: 缺少「{section}」小节) file_name file_path.stem if not re.match(r^\d{4}-., file_name): errors.append(f{file_path.name}: 文件名不符合编号规则) return errors def main() - None: all_errors [] for adr_file in ADR_DIR.glob(*.md): all_errors.extend(check_adr_file(adr_file)) if all_errors: print(ADR 检查未通过) for err in all_errors: print(f - {err}) raise SystemExit(1) print(ADR 检查通过) if __name__ __main__: main()这个脚本要求 ADR 文件同时包含“状态、上下文、决策、后果”四个小节并要求文件名以四位数字开头。团队可以根据自己的模板调整脚本规则。4. 完整实战案例为订单服务引入两级缓存下面通过一个虚拟业务场景演示从“技术讨论”到“ADR 落地”的完整过程。这个例子尽量贴近真实项目方便直接参考。4.1 背景与候选方案假设订单服务中的商家信息查询接口出现了性能问题。商家信息是订单列表页的基础数据每次下单和查询订单都需要读取QPS 持续上涨后数据库连接池频繁告警接口 P99 延迟从 200ms 上升到了 800ms。团队提出了三个候选方案方案 A优化数据库查询给热点表增加索引调整 SQL 语句。 方案 B引入 Redis 作为分布式缓存所有服务共享一份缓存数据。 方案 C在订单服务本地引入进程内缓存直接减少网络请求。三个方案各有优缺点方案优点缺点A改动小不引入新组件数据库压力仍然存在无法根本上降低 QPSB缓存共享容量可扩展引入新中间件运维成本增加C性能最好无网络开销多实例数据不一致缓存更新复杂最终团队决定采用“Redis 本地缓存”的两级缓存方案先查本地缓存未命中再查 Redis仍未命中才回源数据库。4.2 编写 ADR-001根据上面的背景创建文件docs/adr/0001-introduce-two-level-cache.md内容如下# ADR-001订单服务引入两级缓存 ## 状态 已接受 ## 上下文 订单服务的商家信息查询接口在高峰期 QPS 达到上万级别 数据库连接池频繁告警接口 P99 延迟从 200ms 上升至 800ms。 商家信息更新频率低但读取频率极高适合使用缓存加速。 需要同时考虑降低数据库压力与保证数据最终一致性。 候选方案包括 1. 优化数据库 SQL 与索引 2. 只引入 Redis 分布式缓存 3. Redis 与本地进程缓存结合。 ## 决策 采用 Redis Caffeine 两级缓存方案 - 第一级本地 Caffeine 缓存过期时间 5 分钟适合高并发热点数据。 - 第二级Redis 分布式缓存过期时间 10 分钟解决多实例本地缓存不一致问题。 - 数据更新时先更新数据库再删除 Redis 缓存同时通过消息队列通知其他实例删除本地缓存。 缓存 key 设计 merchant:info:{merchantId} ## 后果 正面 - 数据库 QPS 大幅下降接口 P99 延迟预计降到 100ms 以内。 - 两级缓存减少了对 Redis 的网络请求Redis 压力可控。 负面 - 系统复杂度增加需要维护缓存一致性。 - 本地缓存存在多实例短暂不一致窗口业务上需要容忍。 - 需要额外监控缓存命中率避免缓存穿透。 ## 替代方案 曾考虑只使用 Redis 缓存但由于 Redis 读取仍有网络开销 在超大规模 QPS 下不如本地缓存性能好最终选择两级缓存。4.3 评审与状态流转ADR-001 编写完成后提交 PR 到团队仓库。评审人会关注几个关键点上下文是否真实反映了当时的业务压力和数据指标。决策是否足够具体缓存 key、过期时间、更新策略是否明确。后果是否完整尤其是否分析了负面问题。是否存在合理但没被考虑的候选方案。如果评审通过状态改为“已接受”PR 完成合并。如果后续发现方案有问题可以新增 ADR-002 来替代 ADR-001而不是直接修改历史内容。4.4 在代码库中沉淀ADR 合并后建议在 README 中建立索引。docs/adr/README.md示例# 架构决策记录 本仓库使用 ADR 记录所有重要技术决策。 ## 目录 - [ADR-001订单服务引入两级缓存](./0001-introduce-two-level-cache.md) ## 规范 - 每条 ADR 使用四位数字编号。 - 必须包含状态、上下文、决策、后果四个小节。 - 状态变更时更新文档并注明变更原因。这样团队成员进入项目时先看 ADR 目录就能快速理解系统的关键设计脉络。5. Uber 风格 ADR 实践要点5.1 编号与命名Uber 的工程文化中非常重视“可发现性”。ADR 的编号和命名应该让人一眼看出主题。文件命名建议使用“编号-简短主题”例如0001-use-kafka-as-event-bus.md0002-replace-monolith-with-microservices.md0003-adopt-grpc-for-internal-api.md不建议使用过于笼统的名称比如001-decision.md。一个好的文件名本身就是搜索入口。5.2 上下文要“薄”很多团队的 ADR 失败是因为写得像需求文档上下文部分塞满了大段背景描述。实际情况是ADR 的上下文只需要把决策的必要条件写清楚即可控制在 200 字以内最好。关键信息包括当前系统的痛点或瓶颈是什么这次决策要解决的核心问题是什么有哪些候选方案如果业务背景太长可以单独写一篇业务背景文档ADR 只保留必要信息。5.3 决策要明确ADR 中最忌讳的写法是“方案 A 与方案 B 各有优劣团队将根据实际情况灵活选择”。这种表述等于没有决策。决策部分应该具备可执行性选择了什么技术不选什么关键的参数是什么边界条件是什么只有决策足够具体后续实现才不会被反复讨论。5.4 后果要可追踪“后果”不是简单的优缺点列表而是需要说明这个决策对未来的影响。在 Uber 的实践里一条好的 ADR 会明确指出哪部分代码、哪个业务模块受该决策影响甚至给出后续需要关注的信号。例如在缓存 ADR 中后果不仅写“性能提升”还应该写如果缓存命中率低于 90%需要考虑容量规划。如果本地缓存版本不一致频繁出现应考虑切回纯 Redis。每季度回顾一次该决策的有效性。这样 ADR 才能继续指导未来的技术演进。6. 常见问题与排查思路在推广 ADR 的过程中团队会遇到各种各样的问题。下面整理了一张高频问题表供参考问题现象常见原因解决思路ADR 写了没人看文档放在私有空间和代码隔离将 ADR 放入代码仓库绑定 PR 评审流程内容写得太长像论文过度解释背景缺少决策重点限定模板字段控制上下文篇幅状态永远不更新没有负责人和过期检查机制指定每条 ADR 的 owner定期 Review决策后实现偏差大决策不够具体无法指导开发在决策部分写清楚技术选型和关键参数新方案替换旧方案历史丢失直接改写旧文档新增 ADR 声明替代关系保留旧记录ADR 数量太多维护成本高每条 ADR 粒度太小只对重大、跨模块、影响面广的决策建立 ADR如果团队刚引入 ADR 时推进困难建议从“先跑通一个完整流程”开始而不是一开始就要求所有决策都写 ADR。先选择当前正在讨论的一个真实技术方案按模板写一条 ADR走一次 PR 评审让团队看到实际价值后再逐步扩大范围。7. 最佳实践与工程建议7.1 让 ADR 贴近代码第一条建议是ADR 必须放在代码仓库里而不是放在 Wiki 或在线文档中。放在代码仓库意味着它和代码一样有版本历史、需要评审、能被搜索到。很多团队在 Wiki 上写决策文档最后都因为无人维护而废弃原因就在于 Wiki 和开发流程完全分离。7.2 建立轻量评审机制ADR 的评审不需要像架构评审那样正式。如果团队使用 GitLab 或 GitHub直接在 MR/PR 中评审即可。评审关注点应放在上下文是否真实、决策是否明确、后果是否考虑全面而不是纠结格式细节。评审人会随项目演化而变化建议每条 ADR 至少由一位对该技术领域熟悉的资深工程师以及一位不了解背景的同事共同评审。后者负责检查“只看这篇文章能否理解整个决策”。7.3 自动化检查与统计在 CI 中运行前面提供的检查脚本可以在合并前拦住格式不完整的 ADR。随着数量增长还可以收集一些数据例如当前处于提案状态的 ADR 有多少。已接受但超过一年未更新的 ADR 有多少。被替代的决策数量判断系统架构演变趋势。这些数据能帮助技术负责人及时发现架构治理盲区。7.4 避免的坑不要把 ADR 变成形式主义。如果团队已经从实践中形成了有效决策记录方式不一定要强推 ADR。不要一条 ADR 记录多个决策。每个 ADR 保持单点聚焦否则后续更新会非常混乱。不要只写决策不写状态。没有状态的 ADR 只是一篇博客文章无法指导工程实践。不要让 ADR 成为少数人的文档。理想状态下任何工程师都应该能提出 ADR这种自下而上的机制更利于发现系统问题。8. 总结与学习路线本文围绕 Uber 的 ADR 实践系统介绍了架构决策记录的核心概念、文档结构、状态流转和落地流程。通过一个完整的缓存方案案例演示了从编写模板到评审合并的全过程。与普通的会议纪要和设计文档不同ADR 强调记录“Why”而不是“How”强调把决策变成代码库的一部分并持续维护其状态。如果你所在团队正在经历技术决策反复讨论、新人理解成本高、系统历史理由不清晰这类问题可以考虑从今天开始做这件事把最近一次技术讨论的结论整理成一条 ADR 文档放进仓库走一次评审流程感受一下“文档驱动决策”的差异。如果本文对你有帮助可以收藏备用后续需要时直接对照落地。如果你对 ADR 的自动检查、Uber 其他工程实践或者如何在不同团队规模下调整 ADR 流程感兴趣欢迎在评论区交流。从一条 ADR 开始逐步建立属于你自己的技术决策资产你会发现良好的架构治理并不需要复杂工具只需要把每个关键决策记录下来然后保持更新。