技术架构文档的写作规范ADRs(Architecture Decision Records)从原理到实践一、架构知识流失——为什么口口相传的架构决策必然失败技术团队的架构知识流失是一种慢性病。一个关键的架构决策——为什么选择了消息队列A而非B、为什么数据库表设计采用反范式化、为什么微服务拆分以这个边界为准——如果只存在于当事人的头脑中或某次会议的纪要里随着人员流动和时间推移这些决策的为什么将彻底消失。后果是双重的新人困惑新成员看到代码中的非直觉设计无法判断这是刻意的架构权衡还是历史的遗留债务。前者需要维护后者需要重构。决策反复当原有决策的条件发生变化如流量增长10倍、数据量突破阈值团队缺乏上下文来判断原决策的前提是否依然成立导致在同样的问题上重复讨论。ADRsArchitecture Decision Records正是为解决这一问题而生的轻量级文档范式。它不追求架构文档的系统性和完整性而是聚焦于记录单个决策的上下文、方案对比、选择理由和后果。一个ADRs文件短则一页长则三四页但信息密度极高——读者能在5分钟内理解一个关键决策的来龙去脉。二、ADRs的核心结构——从Michael Nygard模板到团队自定义规范ADRs 的经典模板由 Michael Nygard 在2011年提出包含五个核心章节。一个良好的 ADR 文档具有极高的信息密度和自包含特性。标题规范采用ADR-NNNN: 决策的简要描述格式。编号使用自增序列不重新编排。标题应当概括做什么决策如ADR-0003: 采用PostgreSQL的JSONB列存储非结构化元数据而非关于数据库选型的讨论。状态机是ADR的生命周期管理核心。状态流转规则Draft → Proposed初稿完成提交团队评审Proposed → Accepted团队达成共识ADR成为正式记录Accepted → Superseded新决策取代旧决策ADR头部引用新ADR编号Accepted → Deprecated决策因架构演进自然过期如微服务下线背景章节要回答为什么现在做这个决策和如果不做决定会怎样。不是描述技术方案的细节而是描述面临的具体技术约束和业务需求。比如当前的MySQL单表已增长到5000万行按月分区策略导致跨月查询需要扫描所有分区p99延迟已恶化到3秒。决策章节需要阐述我们决定做什么以及为什么是这个方案。关键不是罗列方案特性而是说明权衡trade-off的逻辑——为什么接受A的成本换取B的收益。如果列出了备选方案需要逐一说明为什么不选。后果章节是ADR最有价值的部分。它分为两个方向正向哪些问题被解决了什么变得更容易负向引入了什么新成本什么变得更困难需要注意哪些风险三、ADR管理工具——模板驱动的规范化生成与检索以下代码实现了一套ADR生命周期管理工具确保团队以统一格式生成和管理架构决策记录 ADRArchitecture Decision Records管理工具 支持模板生成、状态管理、关联检索和Markdown渲染 from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Optional from enum import Enum class ADRStatus(str, Enum): ADR状态枚举 DRAFT draft PROPOSED proposed ACCEPTED accepted SUPERSEDED superseded DEPRECATED deprecated property def label(self) - str: labels { draft: 草案, proposed: 提议中, accepted: 已接受, superseded: 已取代, deprecated: 已废弃, } return labels[self.value] dataclass class ADR: ADR数据模型 id: int title: str status: ADRStatus context: str # 背景 decision: str # 决策 consequences: str # 后果 alternatives: list[str] field(default_factorylist) superseded_by: Optional[int] None supersedes: Optional[int] None created_at: datetime field(default_factorydatetime.now) updated_at: datetime field(default_factorydatetime.now) tags: list[str] field(default_factorylist) def filename(self) - str: 生成标准文件名 slug ( self.title.lower() .replace( , -) .replace((, ) .replace(), ) ) return fADR-{self.id:04d}-{slug}.md class ADRGenerator: ADR文档生成器从数据模型生成Markdown格式文档 TEMPLATE # {title} - **状态**: {status} - **日期**: {date} - **标签**: {tags} {superseded_section}{supersedes_section} ## 背景与上下文 {context} ## 决策 {decision} ## 备选方案 {alternatives} ## 后果与影响 {consequences} def generate(self, adr: ADR) - str: tags_str , .join(adr.tags) if adr.tags else — superseded if adr.superseded_by: superseded f- **已被取代**: ADR-{adr.superseded_by:04d}\n supersedes if adr.supersedes: supersedes f- **取代**: ADR-{adr.supersedes:04d}\n alternatives_text if adr.alternatives: items [ f- **备选{i1}**: {alt} for i, alt in enumerate(adr.alternatives) ] alternatives_text \n.join(items) else: alternatives_text 未记录备选方案 return self.TEMPLATE.format( titleadr.title, statusadr.status.label, dateadr.updated_at.strftime(%Y-%m-%d), tagstags_str, superseded_sectionsuperseded, supersedes_sectionsupersedes, contextadr.context.strip(), decisionadr.decision.strip(), alternativesalternatives_text, consequencesadr.consequences.strip(), ) class ADRRepository: ADR仓库管理器 负责ADR的CRUD操作、索引管理和关联查询 INDEX_HEADER # 架构决策记录索引 | 编号 | 标题 | 状态 | 日期 | |------|------|------|------| def __init__(self, root_dir: str docs/adr): self.root Path(root_dir) self.root.mkdir(parentsTrue, exist_okTrue) self.generator ADRGenerator() self._adrs: dict[int, ADR] {} self._load_existing() def create(self, adr: ADR) - str: 创建新的ADR if adr.id in self._adrs: raise ValueError(fADR-{adr.id:04d} 已存在) # 分配编号 if adr.id 0: adr.id max(self._adrs.keys(), default0) 1 content self.generator.generate(adr) filepath self.root / adr.filename() filepath.write_text(content, encodingutf-8) self._adrs[adr.id] adr self._rebuild_index() return str(filepath) def supersede(self, old_id: int, new_adr: ADR) - tuple[str, str]: 用新ADR取代旧ADR 自动更新旧ADR的状态和引用 if old_id not in self._adrs: raise ValueError(fADR-{old_id:04d} 不存在) old_adr self._adrs[old_id] old_adr.status ADRStatus.SUPERSEDED old_adr.superseded_by new_adr.id new_adr.supersedes old_id # 重新生成旧ADR self._save_adr(old_adr) # 创建新ADR new_path self.create(new_adr) self._rebuild_index() return (str(self.root / old_adr.filename()), new_path) def get_active_adrs(self) - list[ADR]: 获取所有活跃已接受的ADR return [a for a in self._adrs.values() if a.status ADRStatus.ACCEPTED] def search_by_tag(self, tag: str) - list[ADR]: 按标签检索ADR return [a for a in self._adrs.values() if tag in a.tags] def get_decision_chain(self, start_id: int) - list[ADR]: 追踪决策链从一个ADR出发找出所有取代它的后续ADR chain [] current_id start_id visited set() while current_id and current_id not in visited: visited.add(current_id) current self._adrs.get(current_id) if not current: break chain.append(current) current_id current.superseded_by return chain def _load_existing(self) - None: 加载已有ADR文件 for md_file in sorted(self.root.glob(ADR-*.md)): content md_file.read_text(encodingutf-8) adr self._parse_adr(content, md_file.stem) if adr: self._adrs[adr.id] adr def _parse_adr(self, content: str, filename: str) - Optional[ADR]: 从Markdown内容解析ADR lines content.split(\n) if not lines: return None title_line lines[0].strip(# ) adr_id 0 try: parts filename.split(-) if len(parts) 2: adr_id int(parts[1]) except ValueError: pass return ADR( idadr_id, titletitle_line, statusADRStatus.ACCEPTED, context, decision, consequences, ) def _save_adr(self, adr: ADR) - str: 更新已有ADR文件 adr.updated_at datetime.now() content self.generator.generate(adr) filepath self.root / adr.filename() filepath.write_text(content, encodingutf-8) self._adrs[adr.id] adr return str(filepath) def _rebuild_index(self) - None: 重建README索引 rows [] for adr in sorted(self._adrs.values(), keylambda a: a.id): date_str adr.updated_at.strftime(%Y-%m-%d) rows.append(f| ADR-{adr.id:04d} | {adr.title} | {adr.status.label} | {date_str} |) index_content self.INDEX_HEADER \n.join(rows) \n (self.root / README.md).write_text(index_content, encodingutf-8) # 使用示例 if __name__ __main__: repo ADRRepository(root_dir./docs/adr) # 创建第一条ADR adr1 ADR( id0, # 自动分配 titleADR-0001: 采用PostgreSQL的JSONB列存储非结构化元数据, statusADRStatus.ACCEPTED, context( 产品中的每个订单有10-50个动态扩展属性如定制参数 传统EAV模式需要3张表联表查询查询复杂度高。 业务要求单次查询延迟 200ms。 ), decision( 使用PostgreSQL 14的JSONB数据类型存储动态扩展属性 配合GIN索引加速JSON路径查询 使用CHECK约束确保JSON Schema合规。 ), alternatives[ EAV实体-属性-值模式灵活但查询复杂不满足延迟要求, MongoDB需要引入新数据库组件运维成本过高, MySQL 8.0 JSON类型支持不完善性能弱于PG, ], consequences( 正向单表查询即可获取订单全量数据延迟满足要求。\n 负向JSONB索引占用较多存储空间约原始数据的30% 需要建立JSON Schema治理规范 开发团队需了解PG的JSON操作符。 ), tags[database, postgresql, data-modeling], ) path1 repo.create(adr1) print(f已创建: {path1}) # 获取所有活跃ADR active repo.get_active_adrs() print(f活跃ADR数量: {len(active)}) # 检索标签 db_adrs repo.search_by_tag(database) print(f数据库类ADR数量: {len(db_adrs)})四、ADR实践中的反模式——什么不算ADR什么不该记录为ADR不要将ADR写成架构文档的替代品。ADR记录的是决策不是系统全景图。架构文档C4 Model等描述系统是什么样ADR描述为什么是这个样。两者互补不是竞争关系。不要记录所有技术决策。只有以下类型的决策值得写入ADR影响系统外部接口或行为API契约、协议选择产生显著的技术债务或运维成本引入新技术栈、数据存储方案会约束未来的架构选择微服务拆分边界、Event Schema存在多个合理的方案且团队有分歧不要在ADR中堆砌无关细节。一个ADR应该在300-800字内完成核心内容的阐述。如果超过1500字说明决策本身需要进一步拆解为多个更小的决策。不要忘记更新ADR的状态。ADR不是一次性的交付物。当架构演进导致原有决策不再适用时必须通过superseded或deprecated状态显式标记确保读者看到的是当前有效的约定而非历史的考古记录。五、总结ADRs是一种轻量级架构决策记录格式通过背景-决策-后果的三段式结构将关键架构决策的为什么固化到代码仓库中。其核心价值不在于文档本身而在于迫使团队在做出决策时显式记录上下文、评估方案、声明后果——这个过程本身就是架构决策质量的保障机制。落地要点从零开始选最近一次有分歧的技术决策写出第一篇ADR与代码同行ADR存放在代码仓库的docs/adr/目录中随PR提交轻量渐进不追求全覆盖每月新增1-2条高质量的ADR比一次性补齐所有历史决策更有价值状态维护定期回顾已接受的ADR评估其是否依然适用ADRs解决的不是文档怎么写的问题而是团队如何积累和传承架构判断力的问题——这是技术团队从短期生存走向长期演进的必经之路。
技术架构文档的写作规范:ADRs(Architecture Decision Records)从原理到实践
技术架构文档的写作规范ADRs(Architecture Decision Records)从原理到实践一、架构知识流失——为什么口口相传的架构决策必然失败技术团队的架构知识流失是一种慢性病。一个关键的架构决策——为什么选择了消息队列A而非B、为什么数据库表设计采用反范式化、为什么微服务拆分以这个边界为准——如果只存在于当事人的头脑中或某次会议的纪要里随着人员流动和时间推移这些决策的为什么将彻底消失。后果是双重的新人困惑新成员看到代码中的非直觉设计无法判断这是刻意的架构权衡还是历史的遗留债务。前者需要维护后者需要重构。决策反复当原有决策的条件发生变化如流量增长10倍、数据量突破阈值团队缺乏上下文来判断原决策的前提是否依然成立导致在同样的问题上重复讨论。ADRsArchitecture Decision Records正是为解决这一问题而生的轻量级文档范式。它不追求架构文档的系统性和完整性而是聚焦于记录单个决策的上下文、方案对比、选择理由和后果。一个ADRs文件短则一页长则三四页但信息密度极高——读者能在5分钟内理解一个关键决策的来龙去脉。二、ADRs的核心结构——从Michael Nygard模板到团队自定义规范ADRs 的经典模板由 Michael Nygard 在2011年提出包含五个核心章节。一个良好的 ADR 文档具有极高的信息密度和自包含特性。标题规范采用ADR-NNNN: 决策的简要描述格式。编号使用自增序列不重新编排。标题应当概括做什么决策如ADR-0003: 采用PostgreSQL的JSONB列存储非结构化元数据而非关于数据库选型的讨论。状态机是ADR的生命周期管理核心。状态流转规则Draft → Proposed初稿完成提交团队评审Proposed → Accepted团队达成共识ADR成为正式记录Accepted → Superseded新决策取代旧决策ADR头部引用新ADR编号Accepted → Deprecated决策因架构演进自然过期如微服务下线背景章节要回答为什么现在做这个决策和如果不做决定会怎样。不是描述技术方案的细节而是描述面临的具体技术约束和业务需求。比如当前的MySQL单表已增长到5000万行按月分区策略导致跨月查询需要扫描所有分区p99延迟已恶化到3秒。决策章节需要阐述我们决定做什么以及为什么是这个方案。关键不是罗列方案特性而是说明权衡trade-off的逻辑——为什么接受A的成本换取B的收益。如果列出了备选方案需要逐一说明为什么不选。后果章节是ADR最有价值的部分。它分为两个方向正向哪些问题被解决了什么变得更容易负向引入了什么新成本什么变得更困难需要注意哪些风险三、ADR管理工具——模板驱动的规范化生成与检索以下代码实现了一套ADR生命周期管理工具确保团队以统一格式生成和管理架构决策记录 ADRArchitecture Decision Records管理工具 支持模板生成、状态管理、关联检索和Markdown渲染 from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Optional from enum import Enum class ADRStatus(str, Enum): ADR状态枚举 DRAFT draft PROPOSED proposed ACCEPTED accepted SUPERSEDED superseded DEPRECATED deprecated property def label(self) - str: labels { draft: 草案, proposed: 提议中, accepted: 已接受, superseded: 已取代, deprecated: 已废弃, } return labels[self.value] dataclass class ADR: ADR数据模型 id: int title: str status: ADRStatus context: str # 背景 decision: str # 决策 consequences: str # 后果 alternatives: list[str] field(default_factorylist) superseded_by: Optional[int] None supersedes: Optional[int] None created_at: datetime field(default_factorydatetime.now) updated_at: datetime field(default_factorydatetime.now) tags: list[str] field(default_factorylist) def filename(self) - str: 生成标准文件名 slug ( self.title.lower() .replace( , -) .replace((, ) .replace(), ) ) return fADR-{self.id:04d}-{slug}.md class ADRGenerator: ADR文档生成器从数据模型生成Markdown格式文档 TEMPLATE # {title} - **状态**: {status} - **日期**: {date} - **标签**: {tags} {superseded_section}{supersedes_section} ## 背景与上下文 {context} ## 决策 {decision} ## 备选方案 {alternatives} ## 后果与影响 {consequences} def generate(self, adr: ADR) - str: tags_str , .join(adr.tags) if adr.tags else — superseded if adr.superseded_by: superseded f- **已被取代**: ADR-{adr.superseded_by:04d}\n supersedes if adr.supersedes: supersedes f- **取代**: ADR-{adr.supersedes:04d}\n alternatives_text if adr.alternatives: items [ f- **备选{i1}**: {alt} for i, alt in enumerate(adr.alternatives) ] alternatives_text \n.join(items) else: alternatives_text 未记录备选方案 return self.TEMPLATE.format( titleadr.title, statusadr.status.label, dateadr.updated_at.strftime(%Y-%m-%d), tagstags_str, superseded_sectionsuperseded, supersedes_sectionsupersedes, contextadr.context.strip(), decisionadr.decision.strip(), alternativesalternatives_text, consequencesadr.consequences.strip(), ) class ADRRepository: ADR仓库管理器 负责ADR的CRUD操作、索引管理和关联查询 INDEX_HEADER # 架构决策记录索引 | 编号 | 标题 | 状态 | 日期 | |------|------|------|------| def __init__(self, root_dir: str docs/adr): self.root Path(root_dir) self.root.mkdir(parentsTrue, exist_okTrue) self.generator ADRGenerator() self._adrs: dict[int, ADR] {} self._load_existing() def create(self, adr: ADR) - str: 创建新的ADR if adr.id in self._adrs: raise ValueError(fADR-{adr.id:04d} 已存在) # 分配编号 if adr.id 0: adr.id max(self._adrs.keys(), default0) 1 content self.generator.generate(adr) filepath self.root / adr.filename() filepath.write_text(content, encodingutf-8) self._adrs[adr.id] adr self._rebuild_index() return str(filepath) def supersede(self, old_id: int, new_adr: ADR) - tuple[str, str]: 用新ADR取代旧ADR 自动更新旧ADR的状态和引用 if old_id not in self._adrs: raise ValueError(fADR-{old_id:04d} 不存在) old_adr self._adrs[old_id] old_adr.status ADRStatus.SUPERSEDED old_adr.superseded_by new_adr.id new_adr.supersedes old_id # 重新生成旧ADR self._save_adr(old_adr) # 创建新ADR new_path self.create(new_adr) self._rebuild_index() return (str(self.root / old_adr.filename()), new_path) def get_active_adrs(self) - list[ADR]: 获取所有活跃已接受的ADR return [a for a in self._adrs.values() if a.status ADRStatus.ACCEPTED] def search_by_tag(self, tag: str) - list[ADR]: 按标签检索ADR return [a for a in self._adrs.values() if tag in a.tags] def get_decision_chain(self, start_id: int) - list[ADR]: 追踪决策链从一个ADR出发找出所有取代它的后续ADR chain [] current_id start_id visited set() while current_id and current_id not in visited: visited.add(current_id) current self._adrs.get(current_id) if not current: break chain.append(current) current_id current.superseded_by return chain def _load_existing(self) - None: 加载已有ADR文件 for md_file in sorted(self.root.glob(ADR-*.md)): content md_file.read_text(encodingutf-8) adr self._parse_adr(content, md_file.stem) if adr: self._adrs[adr.id] adr def _parse_adr(self, content: str, filename: str) - Optional[ADR]: 从Markdown内容解析ADR lines content.split(\n) if not lines: return None title_line lines[0].strip(# ) adr_id 0 try: parts filename.split(-) if len(parts) 2: adr_id int(parts[1]) except ValueError: pass return ADR( idadr_id, titletitle_line, statusADRStatus.ACCEPTED, context, decision, consequences, ) def _save_adr(self, adr: ADR) - str: 更新已有ADR文件 adr.updated_at datetime.now() content self.generator.generate(adr) filepath self.root / adr.filename() filepath.write_text(content, encodingutf-8) self._adrs[adr.id] adr return str(filepath) def _rebuild_index(self) - None: 重建README索引 rows [] for adr in sorted(self._adrs.values(), keylambda a: a.id): date_str adr.updated_at.strftime(%Y-%m-%d) rows.append(f| ADR-{adr.id:04d} | {adr.title} | {adr.status.label} | {date_str} |) index_content self.INDEX_HEADER \n.join(rows) \n (self.root / README.md).write_text(index_content, encodingutf-8) # 使用示例 if __name__ __main__: repo ADRRepository(root_dir./docs/adr) # 创建第一条ADR adr1 ADR( id0, # 自动分配 titleADR-0001: 采用PostgreSQL的JSONB列存储非结构化元数据, statusADRStatus.ACCEPTED, context( 产品中的每个订单有10-50个动态扩展属性如定制参数 传统EAV模式需要3张表联表查询查询复杂度高。 业务要求单次查询延迟 200ms。 ), decision( 使用PostgreSQL 14的JSONB数据类型存储动态扩展属性 配合GIN索引加速JSON路径查询 使用CHECK约束确保JSON Schema合规。 ), alternatives[ EAV实体-属性-值模式灵活但查询复杂不满足延迟要求, MongoDB需要引入新数据库组件运维成本过高, MySQL 8.0 JSON类型支持不完善性能弱于PG, ], consequences( 正向单表查询即可获取订单全量数据延迟满足要求。\n 负向JSONB索引占用较多存储空间约原始数据的30% 需要建立JSON Schema治理规范 开发团队需了解PG的JSON操作符。 ), tags[database, postgresql, data-modeling], ) path1 repo.create(adr1) print(f已创建: {path1}) # 获取所有活跃ADR active repo.get_active_adrs() print(f活跃ADR数量: {len(active)}) # 检索标签 db_adrs repo.search_by_tag(database) print(f数据库类ADR数量: {len(db_adrs)})四、ADR实践中的反模式——什么不算ADR什么不该记录为ADR不要将ADR写成架构文档的替代品。ADR记录的是决策不是系统全景图。架构文档C4 Model等描述系统是什么样ADR描述为什么是这个样。两者互补不是竞争关系。不要记录所有技术决策。只有以下类型的决策值得写入ADR影响系统外部接口或行为API契约、协议选择产生显著的技术债务或运维成本引入新技术栈、数据存储方案会约束未来的架构选择微服务拆分边界、Event Schema存在多个合理的方案且团队有分歧不要在ADR中堆砌无关细节。一个ADR应该在300-800字内完成核心内容的阐述。如果超过1500字说明决策本身需要进一步拆解为多个更小的决策。不要忘记更新ADR的状态。ADR不是一次性的交付物。当架构演进导致原有决策不再适用时必须通过superseded或deprecated状态显式标记确保读者看到的是当前有效的约定而非历史的考古记录。五、总结ADRs是一种轻量级架构决策记录格式通过背景-决策-后果的三段式结构将关键架构决策的为什么固化到代码仓库中。其核心价值不在于文档本身而在于迫使团队在做出决策时显式记录上下文、评估方案、声明后果——这个过程本身就是架构决策质量的保障机制。落地要点从零开始选最近一次有分歧的技术决策写出第一篇ADR与代码同行ADR存放在代码仓库的docs/adr/目录中随PR提交轻量渐进不追求全覆盖每月新增1-2条高质量的ADR比一次性补齐所有历史决策更有价值状态维护定期回顾已接受的ADR评估其是否依然适用ADRs解决的不是文档怎么写的问题而是团队如何积累和传承架构判断力的问题——这是技术团队从短期生存走向长期演进的必经之路。