1 为什么先学「架构决策记录 ADR」
零售应用准备上线订单 API。用户点击提交订单,系统需要核对商品价格、预留库存、保存订单,并通知后续履约。讨论很快出现分歧:库存检查放在哪里,通知是否必须当场完成,调用超时后客户端能否重试。每种做法都能画出看似合理的架构图,但架构图通常没有回答另一个问题:在当时的约束下,团队为什么选择这条路?
架构决策记录,英文 Architecture Decision Record,简称 ADR,就是保存这类选择及其理由的短文档。它把问题、约束、候选方案、最终选择和后果放在同一个位置。几个月后维护者看到订单通知采用异步发送,就能查到当时是在保护下单响应时间,并且接受了通知存在短暂延迟的后果,而不必猜测原作者的意图。
没有记录时,团队容易把历史选择误认为技术定律。例如,“订单接口一直这样重试”可能源于早期流量很低,并不表示促销期间继续重试仍然合适。人员变化、需求变化和故障发生以后,口头解释很难完整传递。ADR 的价值在于保留判断所依赖的条件,使后来的人能区分仍然有效的约束与已经过期的假设。
本课采用一个贯穿场景:零售系统提供 POST /orders,客户端可能因为网络中断重复提交,库存服务可能变慢,通知服务可能暂时不可用。示例团队期望正常下单的服务端响应 p95 不超过 800 毫秒;p95 表示统计窗口内约 95% 的请求耗时不高于该值。这是教学场景中的目标,不是所有订单系统都应采用的标准。
先学 ADR,是为了把“用什么组件”转成“解决什么问题、如何证明有效”。初学者不需要先熟悉复杂框架,也可以练习比较同步与异步、直接重试与查询结果等基本选择。只要能够说明谁受到影响、失败时发生什么、怎样发现偏差,就已经在进行有依据的架构判断。
ADR 也降低协作成本。开发者据此理解接口约定,测试者据此设计异常场景,值班人员据此判断系统行为是否符合预期。文档无法保证选择永远正确,却能让错误被发现、被解释、被修正。它的主要读者是未来需要作出下一次判断的人,包括未来的自己。

2 核心概念与边界
适合记录为 ADR 的决定通常具有持续影响、修改成本或跨团队影响。例如,订单如何识别重复提交、创建成功的业务边界在哪里、跨服务失败怎样补偿,都值得保留理由。局部变量命名、临时日志措辞等容易修改且影响范围很小的选择,通常不需要独立记录。判断重点是未来是否有人需要知道“为什么不能随便改”。
1 份入门 ADR 可以包含编号与标题、状态、背景、决策、备选方案、后果和验证方式。编号便于稳定引用,标题直接写选择,例如“订单创建按商户与请求键去重”。背景写明触发决策的问题;决策写系统将执行什么;后果同时描述收益、代价和新增责任;验证方式说明用哪些证据检查假设是否成立。
状态用来区分讨论与承诺。常见状态有“提议”“已接受”“已废弃”“被替代”。提议表示仍在讨论,已接受表示团队认可这个方向,但并不表示代码已经上线。可以另写实施状态与发布日期。被替代表示后续记录接替了原决定,旧记录仍应保留,并链接到新记录,便于还原演变过程。
ADR 与需求文档、接口文档、故障复盘有不同侧重。需求文档解释用户需要什么;接口文档定义字段、返回码与调用约定;ADR 解释影响结构的选择及取舍;复盘解释某次事件如何发生。它们可以互相引用。例如,ADR 决定采用幂等请求键,接口文档再精确定义请求头名称、有效期和错误响应。
“幂等”在这里指同一个业务请求重复执行时,不产生额外的业务效果。对于创建订单,需要明确怎样识别“同一个请求”,以及重复提交返回什么结果。仅在文档中写“支持幂等”并不充分;如果客户端每次重试都生成新键,服务端就可能把它们视为不同购买意图。技术名词必须落到可观察的约定上。
边界同样重要。ADR 不替代完整设计,不应塞入全部数据库字段和每次会议对话,也不能把尚未验证的猜测写成事实。可以写“预计通知延迟可被业务接受,待业务负责人确认”,并指定确认人和截止条件。记录不确定性有助于安排后续工作;隐藏不确定性则会让后续实现建立在错误承诺上。
3 关键机制(可迁移)
第 1 个机制是从决策问题出发,而不是先写偏好的技术。一个可讨论的问题应包含对象、范围和矛盾:“在客户端可能重试的情况下,订单创建如何避免重复扣减,同时让超时请求能够查到结果?”这个表述比“是否引入缓存”更稳定,因为即使候选技术变化,需要解决的问题仍然明确。
第 2 个机制是区分硬约束与偏好。不得出现跨商户数据混用,是必须满足的约束;希望下单路径更短,是可以权衡的目标。还应标明证据来源,例如来自业务规则、已有接口承诺或压测结果。对于“改造时间只有 2 周”这样的约束,要写明它适用于本次发布,避免它在以后被当成永久限制。
第 3 个机制是让备选方案可比较。可以比较同步创建全部关联结果、订单持久化后异步通知、收到请求即排队后再创建订单。它们的成功语义并不相同,因此应先统一比较问题:客户端什么时候知道订单已成立,依赖故障时能否继续接单,谁负责清理积压。不能只比较平均耗时,再忽略失败时的业务差异。
第 4 个机制是把取舍写成因果关系。采用异步通知,会缩短下单对通知服务的等待,但需要可靠保存待发送任务、处理重试并监控积压。收益与代价来自同一个边界变化。这样的后果描述能直接导出实现和运维任务,比“高性能、高可靠、易扩展”更容易检查,也更容易发现遗漏。
第 5 个机制是设置验证与复审条件。可写“上线后连续观察 7 天,检查重复订单率、通知最老等待时间及人工处理量”。复审条件应关注原假设何时失效,例如库存预留超时明显增多、通知延迟持续超过业务目标,或者需要接入新的履约流程。一次异常通常先触发调查,是否改变架构仍要根据影响与证据判断。
比较时可以使用评分帮助讨论,但不要让总分掩盖硬约束。若某个方案无法隔离商户,即使响应很快,也不能靠其他指标加分抵消。评分权重应公开,并说明数据是测量值、估计值还是尚未知晓。ADR 保存的是有条件的判断:在当前目标和证据下选择某方案,并承担已识别的成本。
4 关键机制续:落到订单链路
现在把这些方法落到订单 API。示例决定把“创建成功”定义为订单和库存预留关联已可靠保存,同时保存待通知任务;通知实际送达可以稍后完成。这里的可靠保存应根据部署方式落实,例如使用数据库事务提交。客户端收到成功后能查询订单,通知服务暂时不可用不会把已经成立的订单重新判为创建失败。
订单数据库与库存服务分属不同系统,无法因为应用里按顺序调用,就假设它们天然具有同一个事务。示例选择先向库存服务申请有期限的预留,再在订单数据库的同一个本地事务中保存订单和待通知任务。库存预留必须支持按请求标识查询,并有明确的释放或到期策略。否则,订单保存失败后库存可能被长期占用。
在这个流程中,请求键表示购买意图,关联标识帮助追踪预留。服务端对商户与请求键建立唯一约束,并保存规范化请求内容的摘要,用于判断重复键是否对应相同内容。相同键且内容相同的请求可以返回已有结果;相同键但商品或数量不同,应返回明确冲突,避免把用户新的购买内容误解释为旧订单。
并发请求还需要处理“处理中”的状态。不能让 2 个工作进程都先查询“没有订单”,随后各自创建。唯一约束与处理权获取应由可靠的原子操作保护;库存侧也要按关联标识幂等预留。若处理进程中途退出,后续请求或恢复任务应能够查询预留与订单状态,再决定继续、返回结果或进入人工处理。

待通知任务与订单在同一个事务中保存,是为了避免“订单成功但没有任何发送记录”的空隙。这种做法常被称为事务发件箱:事务提交后,后台发送器读取任务并投递通知。发送器可能在成功发送后、标记完成前退出,因此仍可能再次发送。接收方需要依据事件标识去重,不能把这个模式理解为消息永远只会到达 1 次。
这份 ADR 应明确剩余风险:预留成功但响应丢失时怎样查询,订单事务失败时怎样释放预留,发送任务积压时谁告警。还要说明接近预留到期时怎样阻止订单错误成立,并由库存服务验证预留有效性。入门设计无需展开所有分布式算法,但必须承认跨系统存在中间状态,并给每个中间状态安排出口。
5 动手与命令/控制台直觉
动手时先写出可验证的承诺,再观察接口。准备隔离的练习环境、测试商品与商户,把订单查询接口、库存预留查询和发送任务列表放在一起。练习范围是模拟创建、重复提交和依赖失败,不需要真实支付或向真实顾客发送消息。所有地址与标识都应替换为练习环境提供的值。
下面用 curl 展示调用直觉。localhost:8080 代表本地练习服务;请求头与路径属于示例约定。Idempotency-Key 由客户端生成并在同一次购买的重试中复用,X-Merchant-Id 在实际系统中还需要与认证身份对应,不能单凭客户端填写的商户编号决定访问权限。
curl -i -X POST http://localhost:8080/orders \
-H 'Content-Type: application/json' \
-H 'X-Merchant-Id: demo-shop' \
-H 'Idempotency-Key: purchase-demo-001' \
-d '{"items":[{"sku":"sku-demo-01","quantity":1}]}'
先保留响应中的订单编号和请求标识,再原样重复调用。期望只产生 1 个订单与 1 份有效库存预留,响应能够指向同一个订单。随后保持请求键不变,把数量改为 2,检查是否返回文档约定的冲突。不能只看到 2 次响应都成功就断定幂等有效,还要检查实际业务效果与请求内容是否一致。
下一步通过练习环境提供的故障开关暂停通知发送器,再提交新的请求。订单应按约定成立,待通知任务应可见。恢复发送器后,检查任务是否最终完成,并观察重复投递是否产生重复通知效果。故障开关应作用于明确的测试范围,避免把修改配置与业务流量混在一起,导致证据无法解释。
对于库存调用超时,重点练习“结果未知”。超时说明调用方没有及时收到确定响应,不等于库存侧没有预留。此时依据关联标识查询结果,或进入有期限的恢复流程。可以在控制台观察处理中记录的年龄、预留状态和最终订单状态,再把这些观察结果链接到 ADR 的验证部分。
把实验结果写成简短表格:场景、期望、实际结果、证据链接与结论。若发现相同键导致重复预留,先判断是实现偏离约定,还是原决策根本没有覆盖库存侧幂等。前者通常修复实现,后者需要补充或替代决策。不要为了让实验通过,悄悄修改原来已经接受的成功语义。
6 观测:看哪些字段与信号
ADR 中的验证方式需要落到具体字段。一次创建请求可以记录 request_id、trace_id、商户内部标识、请求键指纹、order_id、预留关联标识、处理阶段、耗时和结果类别。请求标识定位单次调用,追踪标识连接关联调用,请求键指纹关联同一次购买的多次尝试。它们的用途不同,应在字段说明中区分。
日志中的结果类别应区分业务拒绝、依赖超时、重复命中、内容冲突和内部错误。例如库存不足是确定的业务结果;库存调用超时可能是未知结果。把它们都写成“创建失败”,会掩盖恢复方式的差异。对于处理中状态,还应记录首次进入时间与最近处理时间,以发现已经没有执行者推进的请求。
指标适合回答总体趋势。可以观察订单创建成功率、p95 耗时、重复请求比例、相同键内容冲突数、库存未知结果数,以及通知任务的数量与最老等待时间。成功率要注明分母是否包含库存不足等正常业务拒绝;没有口径的百分比很难用于判断 ADR 中的目标是否达成。

队列长度与最老等待时间要结合看。任务数量不多,但最老任务已等待很久,可能意味着某些任务持续失败;任务数量暂时增加但等待时间稳定,也可能只是促销期间的正常波动。指标只能提示问题方向,具体原因仍需结合任务状态、错误类别和依赖服务信号检查。
日志与指标还存在不同的成本约束。订单编号、请求键等取值非常多,不宜直接作为每条时序指标的标签,否则会产生大量序列。它们更适合受控的日志或追踪查询。记录请求内容时避免保存支付信息和完整个人信息;用于内容比较的摘要也应控制访问权限,不能默认任何摘要都没有隐私风险。
最后把信号与行动联系起来。若发现重复订单,值班人员需要沿商户、请求键与订单关联检查;若通知最老等待时间超标,需要查看发送器、接收方错误及重试计划。ADR 可以链接运行手册,并说明哪些信号触发调查、哪些持续变化触发架构复审,让监控结果能够回到原来的判断过程。
7 常见坑与反模式
常见问题是只记结论,没有记录条件。例如“决定采用异步通知”无法说明业务是否允许延迟,也没有交代消息如何可靠保存。可以补写当时的响应目标、通知允许延迟的业务依据,以及新增的补发责任。记录应使后来的维护者能够判断:当前需求变化是否已经超出原来的适用范围。
另一个问题是把备选方案写成陪衬。若选定方案描述得很完整,其他方案只有“性能差”“太复杂”,读者无法理解比较过程。应在相同场景下说明差异,例如同步通知会把通知依赖的延迟带入请求路径,却可能让用户立即知道通知发送结果。即使最终没有选择,也应保留它适用的条件。
过度承诺同样危险。“彻底解决重复”“保证永不丢失”隐藏了系统边界。示例可以保证在约定的键作用域与保留期内识别重复,但客户端更换请求键、去重记录过期、恢复流程缺失,都需要另外处理。写出边界比增加绝对措辞更能帮助实现者,边界外的行为也应有明确约定。
有些团队在上线后补写全部理由,并把当前理解当成当时事实。这会让 ADR 失去历史证据价值。可以补录,但应标明补录日期、信息来源和无法确认的部分。已接受记录中的实质选择发生变化时,建立新记录并链接旧记录;文字纠错可以直接修正,同时保持正常版本历史。
还有一种问题是记录过细,任何改动都要走同样漫长的流程。初学团队可以先抓住会改变业务成功语义、故障责任或跨团队约定的决定。评审者关注问题是否清楚、约束是否真实、后果是否有人负责。文档篇幅应随风险与复杂度调整,模板中没有意义的栏目不必靠空话填满。
最后是接受以后无人落实。ADR 写了幂等与恢复流程,代码却没有唯一约束、库存查询和告警,这时文档只形成了期待。为关键后果关联实现任务、验证证据和负责人,并在发布前检查。决策状态、实施状态与运行结果是不同信息,分别记录才能避免“已接受”等同于“已经生效”的误解。
8 案例:订单系统相关故障
某零售系统在促销期间出现重复订单。最初版本同步等待库存与通知,客户端超时后自动重试,并在每次重试时生成新请求键。服务端把新键视为新的购买意图。与此同时,通知变慢拉长了响应时间,让更多客户端进入重试。问题涉及客户端约定、成功边界和依赖延迟,不能只靠增加服务实例解释。
团队先保存故障窗口内的请求、订单和库存关联证据,确认重复订单来自同一个客户端购买流程,而不是用户主动购买多次。随后核对已有 ADR:记录只写了“服务端按请求键去重”,没有写客户端必须复用键,也没有覆盖通知失败后订单已经保存的情况。实现确实存在缺口,决策边界本身也不完整。
修复后的决定要求客户端在同一次购买中持久保存请求键;服务端以商户与键作为作用域,检查请求内容;订单与待通知任务一起提交,通知由后台发送。客户端没有收到创建响应时,使用原键恢复查询或重试。用户明确开始新的购买时才生成新键,避免把合法的新订单错误合并到旧订单。
异步通知上线后又暴露了新的运维责任。接收方某段时间拒绝请求,订单仍然正常成立,但发送任务逐渐积压。如果团队只看订单 API 成功率,就会误认为业务链路完全正常。增加最老等待时间与错误类别监控后,值班人员才能识别通知延迟,并在接收方恢复后观察积压是否下降。
恢复过程中,发送器采用有上限的重试与间隔,避免持续快速调用已经故障的依赖。达到自动处理边界的任务进入可查询的人工处理状态。接收方按事件标识去重,因此发送器在不确定上次投递结果时可以再次尝试。具体间隔与期限依据业务要求另行确定,ADR 记录选择原则和责任边界。
这次故障不能简单归结为“同步差、异步好”。新的选择保护了下单响应,却引入了任务积压、重复投递和恢复管理。记录应说明为什么这些代价在当前业务中可接受,并保留上线后的验证结果。未来如果业务要求通知确认后订单才成立,就必须重新评估成功语义,而不是沿用旧结论。
9 排查案例:证据链怎么写
假设值班人员收到反馈:“客户端显示提交超时,刷新后出现 2 个订单。”证据链从现象与范围开始:记录发生时间、受影响商户、客户端版本和反馈来源,并注明时间使用的时区。用户反馈能够提出调查方向,但不能单独证明重复的原因,也不能证明所有超时请求都受影响。
接着关联请求与业务结果。利用客户端购买标识、请求键指纹、追踪标识、订单编号与预留关联标识,建立调用到订单的对应关系。若 2 个订单分别来自不同请求键,需要继续检查客户端是否在重试时换键;若相同作用域内同一个键生成了 2 个订单,就应检查唯一约束、数据分区或实现路径是否绕过去重。

时间线应列事实及证据来源。例如,14:02:01 服务端提交订单,14:02:02 开始等待通知,14:02:04 客户端等待超时,14:02:05 新请求抵达。不同设备时钟可能有偏差,应优先使用服务端关联记录与追踪关系,不要仅根据跨设备时间戳推断精确先后。缺失日志的地方应明确标为未知。
然后逐项检验假设。通知变慢是否发生在订单提交之后?新请求是否携带不同键?订单数据库是否实际存在 2 条业务记录?库存是否对应 2 份有效预留?每个判断都附查询条件、时间范围和证据位置。仅凭“当时数据库很忙”不能推出数据库导致重复,因为资源紧张与重复创建之间还缺少因果环节。
证据链可以写成“观察—解释—限制—行动”。观察是旧请求已提交且重试更换键;解释是服务端把重试识别为新的购买;限制是尚未覆盖缺少客户端日志的样本;行动是修正客户端键生命周期、补齐服务端冲突检测,并开展故障演练。这样读者既知道结论,也知道结论能覆盖到哪里。
最后把故障结论反馈到 ADR。若原决策已明确要求复用键,而某版本没有执行,关联修复任务与回归证据即可;若原记录没有定义键的生命周期,应建立补充或替代记录,注明影响的接口与迁移安排。发布后继续观察重复订单与未知结果,只有证据支持改善,才能确认修复达到预期。
放进方案里
在零售订单方案中,可以设置“关键决策与验证”段落,并引用独立 ADR。示例写法:本期订单 API 以订单和待通知任务在本地事务中提交成功作为创建成功边界;库存通过可查询、有期限的预留接口协作;通知后台发送。这样能够缩短通知依赖对下单响应的影响,同时承担重复投递处理、积压监控与失败恢复责任。
背景写清本期目标与范围:客户端可能因网络中断重试,正常创建响应 p95 目标为 800 毫秒,业务允许通知稍后送达。上述目标需要业务确认并通过演练验证。备选方案包括同步等待通知,以及请求排队后再创建订单;分别说明响应等待、成功语义和恢复复杂度,避免只列技术名称。
决策段明确商户与请求键的作用域、相同键内容冲突的处理、客户端键的生命周期,以及未知结果的查询入口。库存预留成功而订单未提交时,由恢复流程查询并释放或等待到期;订单已提交时,通知任务持续追踪至完成或人工处理。接口字段与细节链接接口文档,操作步骤链接运行手册。
验证段安排相同键并发提交、响应丢失、库存超时和通知暂停等场景,检查订单数量、有效预留、任务恢复与查询结果。上线后观察重复订单、未知结果数量、响应耗时和通知最老等待时间。每项关键后果指定负责人,并将实施任务与证据链接回记录,使方案中的承诺能够被检查。
记录保留编号、状态、接受日期、实施状态与复审条件。当通知时效要求改变、预留期限不再适用或恢复工作量持续增加时,启动复审。新的决定使用新编号并链接旧记录,说明替代范围与迁移安排。这样方案既能解释当前选择,也能支持后续团队依据变化作出下一次判断。
10 小结:检查清单
评审 1 份入门 ADR 时,可以逐项检查:
- 问题是否包含对象、范围与真正需要权衡的矛盾?
- 硬约束、目标、假设是否区分,并说明依据?
- 备选方案是否在相同业务语义下比较?
- 决策是否明确成功边界、重复请求和未知结果的处理?
- 收益、代价与新增责任是否形成清楚的因果关系?
- 是否指定实现任务、负责人、验证证据和复审条件?
- 状态是否准确,替代记录能否追溯到旧记录?
可以用订单场景自测:通知服务停止工作时,用户是否仍能创建订单?请求超时后,为什么不能立即断定创建失败?如果客户端每次重试都更换请求键,服务端去重为什么可能失效?能够依据记录回答这些问题,说明 ADR 已经开始承担连接设计、实现与运行证据的作用。
下一课转向 P9 架构图表达:把本章验收过的结论带进后续主题,写成可检查的方案约束。