
Design Doc 的价值不在于留下多少页文档，而在于把重要决策提前到实现之前讨论。

如果一种选择写错后只需要几小时就能修改，它通常不值得占用设计评审时间；如果选错后会影响接口、数据、权限、存储或未来数年的维护成本，就应该在写代码前把约束、方案和代价说清楚。

本文主要参考 Michael Lynch 的 [How to Write an Effective Software Design Document](https://refactoringenglish.com/excerpts/write-an-effective-design-doc/)，结合实际工程工作整理出一套可直接用于编写和评审 Design Doc 的方法，并非原文的逐段翻译。

<!--more-->

## Design Doc 要解决什么问题

实现复杂项目或功能时，团队经常在写代码后才发现以下问题：

- 不同参与者理解的目标并不一致。
- 某个接口已经被其他系统依赖，无法轻易修改。
- 存储、权限或部署方案遗漏了关键约束。
- 方案可以完成正常流程，但没有定义故障行为。
- 评审一直讨论局部实现，影响架构的决策反而没有结论。

Design Doc 要做的是把这些问题提前暴露。它应该让没有参与过前期讨论的人也能理解：

1. 为什么要做这个项目或功能。
2. 谁会使用它，当前遇到了什么问题。
3. 完成后要达到什么结果。
4. 哪些内容明确不在范围内。
5. 系统在正常、异常和并发场景下如何工作。
6. 如何兼容已有客户端和数据，如何发布与回退。
7. 哪些决策最难修改，为什么这样选择。
8. 如何验证功能和非功能目标。
9. 还有哪些问题尚未解决。

Design Doc 位于需求和实现之间，用来承载需要提前评审的技术决策。它不替代需求文档，也不是实现完成后的代码说明。

## 什么时候值得写

不是每个改动都需要 Design Doc。一个字段改名、一个按钮位置调整或一段局部重构，用 Issue 或 PR 描述通常就足够了。

可以用以下问题判断是否值得投入：

- 是否需要多人协作实现？
- 是否涉及其他团队或外部调用方？
- 是否需要持续数月开发？
- 是否会在生产环境运行很长时间？
- 目标和需求是否仍有明显歧义？
- 是否存在安全、隐私、法律或数据迁移风险？
- 核心方案如果选错，是否很难回退？

这些问题不是机械的打分表。它们共同衡量的是两件事：协调成本和错误成本。参与者越多、系统寿命越长、失败影响越大，Design Doc 越有价值。

低风险改动可能只需要一页说明，也可能完全不写；复杂项目或功能则需要更完整的文档。篇幅应与风险匹配，而不是与关注度匹配。

## 用“选错的代价”控制内容

Design Doc 不应该提前写完所有实现细节。否则设计阶段会变成另一种形式的编码，而且评审者很难从大量细节中找到关键问题。

判断一项内容是否应该写入，可以问：

> 如果现在判断错误，等实现完成后再修改，需要付出多大代价？

通常值得提前决定的内容包括：

- 对外 API、CLI 或文件格式。
- 数据模型、存储后端和迁移方式。
- 身份认证、权限模型和信任边界。
- 跨服务通信协议和故障语义。
- 部署拓扑、基础设施和难以替换的依赖。
- 向后兼容策略和旧数据处理方式。
- 可用性、延迟、容量和成本边界。

下面这些内容通常可以留到实现阶段：

- 局部变量或内部函数命名。
- 容易替换的第三方工具。
- 不影响公共行为的代码拆分。
- 可以低成本调整的 UI 细节。
- 普通的机械实现步骤。

Design Doc 不必覆盖所有细节，只需让高代价决策获得充分讨论。

## 第一页先讲清背景和范围

读者不应该先参加一场会议，才能看懂 Design Doc。文档第一页至少要回答 Objective、Background、Goals 和 Non-goals。

### Objective

Objective 用一句话说明项目或功能要解决的问题。它应该使用普通语言，并让非当前团队的读者也能理解。

例如：

```text
通过在应用服务与 Postgres 之间增加缓存层，降低用户请求延迟和数据库读取压力。
```

Objective 不需要解释具体实现，但必须说明系统或用户将得到什么变化。

### Background

Background 说明为什么现在需要做这件事。好的背景应包含事实，而不是只有“性能不好”“维护困难”之类的判断。

可以回答：

- 当前观察到了什么现象？
- 哪些指标、日志、代码路径或事故支持这个判断？
- 问题影响哪些用户或系统？
- 之前是否尝试过其他方案？
- 为什么现有方案已经不够？

例如，与其写“数据库查询很慢”，不如说明页面延迟从多少增长到多少、数据库耗时占请求时间的比例，以及请求是否集中访问少量热点数据。

### Goals

Goals 描述项目或功能完成后的结果，不要写成实现动作。

```text
不合适：在生产环境中部署 Kubernetes。

更合适：减少发布新版本时的服务中断，并支持失败后自动恢复。
```

技术选型只是达到目标的手段。把手段写成目标，会让评审者难以判断是否存在更简单的方案。

### Non-goals

Non-goals 用来阻止范围膨胀。它不需要列出世界上所有不做的事情，只记录读者可能合理误解为当前范围的内容。

例如，一个为单个业务实现的缓存层，可以明确：

- 不建设通用缓存平台。
- 第一版不支持跨地域部署。
- 不改变现有数据库写入模型。

有了 Non-goals，评审可以围绕当前版本作出决策，而不是不断把未来需求加入第一版。

## 对外功能和 API 先做同类调研

所有面向外部用户的功能和 API，发布后都会形成用户习惯和兼容性承诺，修改成本通常高于内部实现。Design Doc 在进入接口设计之前，应先说明目标受众、使用场景和现有解决方案。

对于已有同类产品或友商实现的能力，至少调查：

- 它面向哪些用户，在什么场景下使用？
- 它解决的具体问题是什么，用户原来如何处理？
- 功能入口或 API 的核心对象、操作和默认行为是什么？
- 如何处理认证、权限、错误、分页、幂等、版本和废弃？
- 当前方案有哪些限制，哪些约定已经成为用户预期？

调研的目的不是照搬友商接口，而是确认问题是否普遍存在、用户已经形成了哪些使用习惯，以及当前设计为什么需要相同或不同的选择。Design Doc 应记录可核验的公开文档、实际产品行为或用户反馈，避免只写“行业通常如此”。

如果没有同类实现，不能把“友商没有”直接当成建设理由。文档需要进一步回答：

1. 目标受众是谁，规模或使用频率是否足以支撑投入？
2. 用户现在遇到了什么问题，有哪些反馈、数据或失败案例？
3. 现有功能、第三方产品或人工流程为什么无法解决？
4. 为什么现在需要解决，继续维持现状的代价是什么？
5. 发布后用什么结果判断问题已经得到改善？

如果这些问题没有证据支持，应先补用户访谈、数据分析或原型验证，而不是直接进入完整设计和实现。

## 用场景、接口和图说明系统行为

目标只能说明要到哪里，Scenarios 和 Interfaces 才能说明系统完成后如何被使用。

### Scenarios

场景使用具体参与者和步骤描述系统行为。例如“支持通过 URL 分享报表”仍然比较抽象，可以展开成：

1. 用户创建一份报表。
2. 用户生成只读分享链接。
3. 接收者打开链接。
4. 接收者看到生成链接时的报表内容，但不能修改原始配置。

场景可以暴露需求描述里没有出现的问题：链接是否过期、数据是否实时、谁有权限访问、原作者删除报表后会怎样。

### Interfaces

接口部分说明系统与人或其他系统之间的边界：

- API 的请求、响应和错误语义。
- CLI 的参数、输出和退出码。
- 配置文件或数据文件的格式。
- 用户界面的关键流程和简单草图。

这里应该把兼容性和失败行为写清楚，但不需要展开所有内部实现。

### Diagrams

架构图适合表达文字难以快速说明的关系：

- 数据如何流动。
- 组件如何连接。
- 请求经过哪些服务。
- 系统依赖哪些外部资源。
- 哪些位置跨越了权限或信任边界。

图应该保留可编辑源文件。随着设计变化，能够修改的 Mermaid、D2、Graphviz 或绘图源文件，比一张无法维护的截图更有价值。

跨团队文档还需要处理术语问题。优先使用读者熟悉的名称；无法避免内部术语时，在首次出现的位置解释，术语较多时再单独增加 Glossary。

## 把 API 设计写成行为契约

API 设计不能停在路径、方法和字段列表。调用方还需要知道每个输入会触发什么行为，以及什么时候可以认为操作已经完成。

对外 API 至少要写清：

- 字段缺省、显式传空值和传入零值是否具有不同含义。
- 默认值、允许范围、互斥字段和非法组合。
- 资源 ID、租户、区域等归属关系如何校验。
- 创建、更新和删除是否幂等，重复请求如何识别。
- 列表接口的过滤、排序和分页结果是否稳定。
- HTTP 状态码与业务错误码分别表达什么，哪些错误允许重试。
- 批量操作是全部成功才提交，还是逐项返回成功与失败。

异步操作还需要单独定义状态模型。文档应列出所有状态、允许的状态迁移、负责推进状态的组件，以及每个状态对用户意味着什么。特别要区分以下时间点：

1. 请求已经受理。
2. 状态已经持久化。
3. 后台任务已经执行。
4. 数据面或外部系统已经生效。

如果 API 返回成功只代表“已受理”，就应说明调用方如何查询最终状态、失败后是否自动重试、卡住多久需要告警，以及删除中的资源何时可以重新使用。

## 把并发、一致性和失败恢复写进主流程

一个操作同时修改数据库、缓存、消息系统或外部资源时，正常流程只能说明功能可以运行，不能说明数据最终会保持一致。Design Doc 需要回答：

- 哪个系统是事实来源？
- 哪些写入必须位于同一个事务中？
- 哪些操作依赖唯一约束、版本号、锁或幂等键？
- 两个请求同时修改同一资源时，后写覆盖、拒绝冲突还是合并？
- 中间步骤失败后，系统选择回滚、补偿、重试还是等待人工处理？
- 定时对账或 reconciliation 如何发现并修复状态漂移？

配额检查、库存分配、删除前的引用检查尤其容易出现竞态。只写“先检查再写入”通常不够，还要说明检查和写入之间如何保持原子性，以及数据库约束如何作为最后一道防线。

错误也应按处理方式分类。参数错误不应重试；依赖暂时不可用可以退避重试；同一资源出现不一致配置时，可能需要停止自动推进并触发告警。批量操作允许部分成功时，响应必须能定位每一项的结果，避免调用方重复执行已经成功的部分。

## 写清约束和难以替换的依赖

一个方案看起来不够理想，可能是因为它受到预算、硬件、历史接口或组织能力限制。如果不写约束，评审者会不断提出实际上无法采用的方案。

Constraints 可以记录：

- 必须兼容的旧客户端或历史数据。
- 只能使用的硬件架构或运行环境。
- 成本、交付时间和人员限制。
- 上下游系统提供的接口边界。
- 不能中断的迁移或发布要求。

Dependencies / Infrastructure 则说明语言、存储、运行位置和第三方依赖。重点仍然是难以改变的部分：编程语言和存储后端通常比邮件发送服务更难替换，因此需要更充分的理由。

## 把兼容、迁移和发布作为设计的一部分

兼容性不能只写成“保持兼容”。文档需要列出具体对象和行为：

- 旧客户端调用新服务时，缺少的新字段采用什么默认值。
- 新客户端调用尚未升级的服务时，如何识别能力是否可用。
- 历史数据中的旧枚举、旧字段和缺失字段如何读取。
- 新旧服务实例同时运行时，是否会写出彼此无法理解的数据。
- 旧入口保留多久，新请求是否仍允许使用，响应是否继续返回。

涉及数据或存储切换时，还要选择迁移方式。在线迁移可能需要回填、双写、读流量切换和一致性核对；停机迁移流程更短，但必须说明不可用窗口和恢复步骤。无论采用哪种方式，都要写出切换条件、验证方法、回滚路径，以及旧字段、旧索引或旧服务由谁在什么时候清理。

发布计划应说明功能是否需要按用户、租户、区域或环境逐步开放，开关的默认值是什么，配置需要同步到哪些环境。回滚不能只写“关闭开关”：如果新版本已经写入新格式数据或创建外部资源，还要确认旧版本能否读取和清理这些状态。

## 列出影响面和验收矩阵

功能跨越多个模块时，按调用链列出影响面比只列代码目录更有用。常见检查项包括：

- 对外 API、内部 API、API 文档、客户端或 SDK。
- 用户界面、管理界面和权限控制。
- Service、后台任务、存储模型、索引和数据迁移。
- 计费、订单、配额、库存和资源生命周期。
- 配置默认值、功能开关和多环境配置。
- 审计日志、指标、告警、调试工具和 Runbook。

验收标准应从 Goals 和行为契约反推，不要只覆盖正常流程。测试矩阵至少考虑：

- 正常输入、边界值、缺省值和非法组合。
- 每条状态迁移、失败重试、重复请求和删除回收。
- 不同权限、租户、区域或环境下的行为。
- 旧客户端、历史数据和新旧版本并存。
- 并发写入、配额竞争、部分失败和补偿流程。
- 发布开关、回滚路径以及监控是否能发现异常。

每个关键目标都应对应一个可观察的验证结果。这样，评审者可以在实现前发现无法验收的目标，开发完成后也能按同一份契约确认行为。

## 把生产要求放进设计阶段

能够运行不等于能够长期维护。面向生产环境的 Design Doc 还需要覆盖可靠性和可观测性。

### SLO

“高性能”“高可用”无法用于验收。SLO 应使用可以测量的指标，例如：

- 服务可用性。
- 请求的 P50、P95 或 P99 延迟。
- 每秒请求数或任务吞吐量。
- 支持的数据规模和并发数量。

具体数值应来自业务要求、现有基线或容量测试，不能为了让文档看起来完整而随意填写。

### Monitoring 和 Alerting

有了 SLO，还要说明如何发现系统没有达到目标：

- 服务不可用时由什么指标发现？
- 延迟上升时如何区分应用、数据库和外部依赖问题？
- 哪些错误需要告警，哪些只需要记录？
- 告警由谁处理，是否有对应 Runbook？

### Logging

日志设计需要同时考虑排障价值和数据风险：

- 哪些关键状态变化必须记录？
- 日志保存在哪里、保留多久？
- 谁可以访问生产日志？
- 哪些用户数据、密钥或凭证不能写入日志？
- 是否需要通过请求 ID 或资源 ID 串联调用链？

这些问题如果等到事故发生后再补，通常已经无法还原当时的状态。

## 在设计阶段处理安全、隐私和法律约束

只要系统处理外部输入、权限或敏感数据，就应该在设计阶段说明安全边界。

Security 至少应考虑：

- 系统的攻击面在哪里？
- 哪些输入不可信？
- 数据在哪些位置跨越信任边界？
- 服务是否对公网开放？
- 如果凭证泄漏，攻击者能访问哪些资源？

Privacy 应说明系统处理哪些敏感数据、保留多久、谁能访问，以及传输和静态存储如何保护。

涉及金融、医疗、合同数据或开源发布时，还需要明确法律与许可证要求。即使判断某项风险不适用，也可以写出理由，让评审者有机会指出遗漏。

## 记录未决问题和替代方案

Design Doc 不需要假装所有问题都已经解决。相反，Open issues 往往是最值得评审的部分。

每个 Open issue 至少包含：

1. 需要解决的问题。
2. 当前已知的候选方案。
3. 各方案的主要代价。
4. 下一步动作和负责人。

只有“待确认”而没有下一步的问题，很容易长期留在文档中。下一步可以是补实验、收集指标、请安全团队评审，或者由明确的决策者选择方案。

问题解决后，不要删除原讨论。将它移到 Resolved issues，并在开头写清最终决定及其理由。这样未来维护者可以知道当时为什么选择当前方案。

Alternatives considered 不需要记录每个短暂出现过的想法。保留最有竞争力、最可能被再次提出的替代方案，并用几句话说明没有采用的原因即可。

## 如何推进评审

写完 Design Doc 只是评审的起点。评审要让高风险问题在实现前得到反馈和结论。

在实际工作中，可以按下面的顺序推进：

1. 先让最熟悉当前系统的人检查背景事实和约束。
2. 再让接口调用方、运维、安全或数据负责人检查各自边界。
3. 把争议整理成 Open issues，而不是散落在评论中。
4. 为每个问题明确下一步和决策者。
5. 决策完成后更新正文，并保留 Resolved issues。
6. 实现过程中如果关键设计变化，同步更新文档。

评审不应该把大量时间花在容易修改的实现细节上。作者可以在文档开头明确本次最希望评审的决策，帮助读者把注意力放到正确的位置。

## 一份可直接使用的精简模板

下面的模板不是固定规范。根据项目或功能的风险选择需要的章节，低风险改动可以继续删减。

```markdown
# 项目或功能名称

## 元数据

- Author:
- Created:
- Status: Draft / In Review / Approved
- Reviewers:
- Related documents:

## 目标概述

用一句话说明项目或功能要解决的问题。

## 背景

说明当前现象、数据、原因、影响范围和已有尝试。

## 目标受众与现有方案

- 目标受众是谁，在哪些场景使用？
- 当前问题和已有处理方式是什么？
- 同类产品或友商如何提供这项功能或 API？
- 当前设计与它们有哪些相同和不同的选择，原因是什么？
- 如果没有同类实现，为什么仍值得建设，如何验证需求？

## 目标成果

- 完成后用户、团队或系统获得什么结果？

## 非目标

- 哪些容易被误解为当前范围的内容明确不做？

## 使用场景

使用具体参与者和步骤说明系统完成后的行为。

## 接口

描述 API、CLI、文件格式、错误语义和兼容性边界。

## 行为契约与状态模型

说明默认值、非法组合、幂等性、错误分类、状态迁移，以及同步或异步完成语义。

## 设计方案

说明组件、数据流和关键状态变化，附可编辑架构图。

## 一致性与失败处理

说明事实来源、事务边界、并发控制、重试、补偿和状态对账。

## 约束与依赖

列出硬约束、基础设施和难以替换的依赖。

## 兼容、迁移与发布

说明旧客户端与历史数据兼容、迁移步骤、灰度范围、配置默认值、回滚和清理计划。

## 影响范围

列出 API、客户端、界面、存储、计费、权限、配置、监控和文档等受影响部分。

## 可靠性与可观测性

说明 SLO、监控、告警、日志、容量和故障恢复。

## 安全与隐私

说明攻击面、信任边界、权限和敏感数据处理。

## 验证方案

根据目标成果和行为契约列出正常、异常、并发、兼容、迁移和回滚场景。

## 未决问题

记录问题、候选方案、代价、下一步和负责人。

## 备选方案

记录主要替代方案及未采用原因。

## 时间线

按可以交付和验证的中间产物拆分里程碑。
```

## 常见问题

### 把 Design Doc 写成实现清单

大量类名、函数名和代码步骤会掩盖需要评审的决策。实现细节只有在影响公共接口、兼容性或关键约束时才值得提前固定。

### 背景只有结论，没有证据

“系统很慢”无法帮助评审者判断方案是否合理。应补充指标、调用链、日志或真实失败案例。

### 对外功能没有同类调研

只描述自己的设计，无法判断它是否符合用户已有习惯，也无法解释为什么需要重新定义一套接口。应补充同类产品或友商的公开功能和 API；如果没有同类实现，则说明目标受众、现有问题、替代方式、建设理由和验证标准。

### API 只有字段定义，没有行为契约

字段列表无法说明缺省值、重复请求、异步生效和失败重试。应补充默认行为、非法组合、状态迁移、错误分类和幂等语义。

### 把兼容和迁移留到实现阶段

如果设计没有说明旧客户端、历史数据、新旧版本并存和回滚后的行为，实现阶段很容易被迫修改已经评审过的接口与数据模型。

### Goals 全是技术名词

“引入 Redis”“迁移到 Kubernetes”描述的是方案，不是目标。先写希望改善的用户体验或系统行为，再讨论技术选型。

### 没有 Non-goals

没有边界的文档会在评审中不断吸收新需求，最终既无法决策，也无法按期实现。

### Open issues 没有下一步

只列问题不会让问题自动解决。需要明确补充什么证据、由谁判断，以及什么时候重新评审。

### 为了完整而填写虚假数字

没有依据的 SLO 和时间线不会提升设计质量。如果当前无法确定，应明确写成 Open issue，并说明获取数据的方式。

### 测试只覆盖正常流程

正常请求成功不能证明设计能够处理重复请求、并发修改、部分失败、历史数据和回滚。测试矩阵应直接覆盖行为契约与发布计划中的边界。

## 总结

有效的 Design Doc 集中回答三类问题：为什么要做、哪些决策最难修改、还有哪些风险需要在实现前解决。对于需要长期维护的功能，还要把 API 行为、状态迁移、兼容路径和失败恢复写成可以验证的契约。

开始写之前，先判断项目或功能的复杂度和风险；写作时，用“选错的代价”筛选内容；评审时，把注意力放在接口、数据、权限、可靠性和兼容性等高代价决策上。文档的价值由它促成的决策决定，而不是篇幅和章节数量。

