如何写出高效的软件设计文档
好文翻译,原文链接
https://refactoringenglish.com/excerpts/write-an-effective-design-doc/
一份优秀的设计文档(design doc)能为你节省数年的开发时间。撰写设计文档会倒逼你在错误的实现方案上浪费时间、或是把自己逼进死胡同之前,就把重要决策都想透彻。它也是团队内部、跨合作团队之间协调设计决策的最佳方式。
我曾以开发者的身份在谷歌(Google)、微软(Microsoft)任职,也在自己创立的公司里写过设计文档。不同公司的具体做法各有差异,但核心原则始终不变:设计文档应当清晰阐述你正在解决的核心难题,同时帮助团队成员给你提供有效反馈。
接下来,我会分享自己撰写高效设计文档的方法,说明哪些内容该放进设计文档、哪些不该放。
-
• 设计文档示例 -
• 什么时候该写设计文档? -
• 该在设计文档上投入多少精力? -
• 设计文档应该包含什么? -
◦ 决策失误的代价是什么? -
• 设计文档的组成部分 -
◦ 标题 -
◦ 元数据(Metadata) -
◦ 目标 -
◦ 背景 -
◦ 相关文档 -
◦ 目标成果 -
◦ 非目标(Non-goals) -
◦ 场景 -
◦ 图示 -
◦ 术语表 -
◦ 约束条件 -
◦ 服务等级目标(Service Level Objectives, SLOs) -
◦ 监控与告警 -
◦ 时间线 -
◦ 接口 -
◦ 依赖与基础设施 -
◦ 安全 -
◦ 隐私 -
◦ 法律考量 -
◦ 日志 -
◦ 待解决问题 -
◦ 已解决问题 -
◦ 已考量的替代方案 -
• 推动设计文档通过评审
设计文档示例
关于设计文档,大家问我最多的问题就是:去哪里能找到一份优质的参考范例?我从没见过哪份公开的设计文档能称得上高质量。我自己写过的所有设计文档,都归属于付我薪水的各家公司,没有对外公开。
因此,我基于本文分享的原则,从零写了一份设计文档,用来规划我正在开发的一款真实网页应用。
我在写任何代码之前就完成了这份设计文档,并且在开发应用的过程中始终遵循这份设计。
-
• 《Little Moments 设计文档》
这份设计的详尽程度,超出了我平时给个人业余项目写文档的标准;但如果是在专业项目里和其他人协作开发,文档的篇幅和深度大概就是这个水平。
什么时候该写设计文档?
项目越复杂、风险越高,写设计文档的价值就越大。
你可以问问自己这几个问题:
-
• 是否需要多人协作配合来实现这套设计? -
• 项目的全职开发工作量是否超过三个月? -
• 这套实现方案是否会在生产环境(production)里运行数年? -
• 项目是否涉及跨团队协作? -
• 项目的目标和需求是否不够明确? -
• 是否存在一些在设计阶段就能规避的灾难性风险(比如安全漏洞、法律风险)?
如果以上任意一个问题你的答案是“是”,那写设计文档大概率是值得的。如果有两个及以上的问题答案为“是”,那写设计文档几乎一定能带来正向收益。
该在设计文档上投入多少精力?
设计文档可以是简单的一页纸,也可以是长达50页、需要五个不同团队签字确认的正式文档。你需要根据实际情况判断写到多详细才合理。
就像没有通用规则规定代码要写多少测试一样,也没有规则规定你该花多久写设计文档。合适的投入程度取决于团队的目标、风险、截止日期和文化氛围。有些时候,最合适的投入就是零——也就是完全不写设计文档。
设计文档应该包含什么?
如果你把所有可能的细节都写进设计文档,那本质上就是在设计阶段把实现代码都写完了,这完全违背了设计文档的初衷。
有一条简单的经验法则,可以帮你判断一项决策该不该放进设计文档:如果做错了,代价有多大?
决策失误的代价是什么?
不是所有设计决策的重要性都一样。有些方案的调整灵活度,要远高于另一些。
举个例子:如果你用C++开发了一款网页应用,写了20万行代码之后才发现Ruby on Rails才是更合适的选择,那你就陷入困境了。从零开始重写基本不现实;哪怕你后续用Rails写新功能,也要长期背负着维护两种差异极大的语言代码的负担。
另一些设计决策则无足轻重。比如你的应用要展示1000篇文章的列表,是一次性全部展示,还是每次显示20篇、用户点“加载更多”再看下一批?
这根本不重要。
“加载更多”按钮不属于设计层面的考量。哪怕你选了一种方案,用户反馈证明你选错了,花几个小时就能改好。你不需要在设计文档里详述整套思考过程,更不该浪费评审环节的时间去争论这种细节。
设计文档的组成部分
下面列出了设计文档里常见的章节。通常你不需要每份文档都用上所有章节,选适合你项目的部分即可。
标题
项目首先需要一个标题,这是大家日常沟通时指代项目的方式。好的标题要满足以下特点:
-
• 简短 :念起来顺口 -
• 辨识度高 :能清晰对应到具体项目 -
• 有表意性 :能从概念上体现项目的作用
比如,如果你要在应用服务器和数据库服务器之间加一层缓存层(caching layer),“RecencyBank”就是个好名字——好读,也能体现项目用途。“飞银马项目”就是个坏名字,又长又不知所云。
元数据(Metadata)
元数据看起来枯燥但很实用,能帮读者快速了解文档的基础背景:
-
• 作者是谁?(姓名+邮箱) -
• 文档创建时间? -
• 官方访问地址是什么? -
◦ 尤其当公司内部使用短链跳转(比如 http://go/recency-bank)的时候
元数据
• 作者 :Michael Lynch(michael@refactoringenglish.com) • 状态 :待评审 • 创建时间 :2026-06-22 • 地址 :http://go/recency-bank-design
目标
目标是用一句话说明项目的用途。它应该放在文档的第一页,用所有相关方都能看懂的平实语言来写。
目标在Trogdor网页服务器与Postgres数据库之间增加一层缓存,提升应用性能。
背景
背景章节说明项目的上下文和发起动机,需要回答这些问题:
-
• 团队为什么要做这个项目? -
• 这个项目解决了什么问题? -
• 之前有没有人尝试过解决这个问题?
背景2023年Trogdor网页应用上线时,页面加载耗时通常在100毫秒以内。三年后,页面加载的中位时长已经飙升到600毫秒,用户会明显觉得应用卡顿。
我们调研了变慢的原因,发现数据库查询占了页面加载耗时的80%。随着存储的数据量越来越大,数据库查询的速度也越来越慢。
我们还发现,95%的数据库查询都集中在3%的数据行上。这种访问模式非常适合用内存缓存来优化:缓存可以更快地返回高频访问的数据,同时降低数据库的负载,让其他查询也能跑得更快。
你的设计文档脱离额外背景能看懂吗?试想一下,在同事或合作团队读设计文档之前,你需要跟他们交代哪些前置信息。 要知道,有些读者在看到文档之前,不会听到你任何的口头说明。因此,所有理解文档必需的信息,都应该放在文档的第一页。
相关文档
如果这个项目和其他文档有关联,附上链接,方便读者查阅。包括:
-
• 项目经理或测试同事产出的项目相关文档(比如测试计划、功能规格说明) -
• 相关系统的设计文档 -
• 本项目之前版本的设计文档
相关文档
• 测试计划 :http://go/recency-bank-test-plan • Trogdor性能报告 :http://go/trogdor-perf-2026
目标成果
目标章节描述项目的高层目标。它应当和背景部分逻辑呼应,说明项目完成之后,会带来什么样的改变。
不要用实现细节来定目标。目标应当体现项目能给用户、团队或是公司带来什么价值。
反面例子:用内部实现细节定目标
-
• 给我们的基础设施增加Kubernetes
正面例子:用实际影响定目标
-
• 减少应用新版本部署导致的服务中断
目标成果
• 提升Trogdor网页应用的用户感知响应速度 • 降低数据库服务器的负载
非目标(Non-goals)
目标定义了项目的范围之内有什么,非目标则明确划定了范围之外的内容。
有没有哪些目标是读者可能误以为属于项目范围的?如果有,就把它们明确写进非目标里。
非目标
• 打造一套通用、可复用的缓存系统
◦ 本次给Trogdor网页应用加的缓存层,只会做针对本应用的优化。把这套缓存复用到其他系统不在本次范围内。 • 地理位置感知缓存
◦ 未来可能会支持在离终端用户更近的地理位置部署缓存来降低延迟,但这不在第一版的范围内。
场景
如果你的目标是“给图表增加‘通过链接分享’按钮”这类功能,读者可能没法直观想象出实际效果。
场景章节可以帮读者具象化地理解:完整的系统在真实场景里是怎么运行的。
场景:通过链接分享报告
1. Bob在自己的KeyMetrics仪表盘里创建了一份自定义报告 2. Bob点开菜单栏,点击“分享 > 生成链接” 3. Bob把链接通过邮件发给了同事Charlie 4. Charlie点开链接,看到了和Bob的报告完全一致的只读版本
图示
图示的价值非常大,尽管很多人没意识到。
作为设计的撰写者,你脑子里自然清楚方案里各个部分是怎么组合的,能想象出完整的架构。但评审者没有这个认知,帮他们建立认知最快的方式,就是画一张图。
简单网页应用的架构示例图
如果你不确定图里该画什么,可以思考这些问题:
-
• 数据在系统里是怎么流转的? -
• 系统的各个组件是怎么组合到一起的? -
• 系统和它的依赖、下游客户端之间是怎么交互的? -
• 系统定义了哪些通信协议?
选一款方便编辑的绘图工具。我见过很多开发者在白板上画了漂亮的图,拍张照放进设计文档里。初稿看起来特别惊艳,但之后就再也改不了了——要改就得全部重画,没法直接编辑照片。
Excalidraw、draw.io和Google Drawings都是常用的、方便修改的绘图工具。也有Mermaid、D2、Graphviz这类语言工具,可以用代码生成图示。我自己用大语言模型(LLM)生成绘图代码的体验就很好。记得附上绘图的源文件或代码链接,方便同事后续复现和修改。
术语表
术语表用来解释读者可能不认识的名词。
多想想文档的潜在读者,尤其是新入职的同事、以及你直属团队之外的人。他们能看懂文档里提到的内部工具、内部系统的名字吗?
尽可能用读者一眼就能懂的词汇。在术语表里定义名词,总比不定义强;但最好的方式是用通用词汇,或是在行文中直接解释,不用读者来回翻找。
术语表
• Apposaurus :团队内部的压测工具。我们用它模拟大量用户同时访问Trogdor网页应用的场景,验证应用在预期负载下能否正常运行。 • Baba-o-styley :内部代码风格检查工具(code linter),用来强制统一公司的代码编写规范。
约束条件
如果预算、客户要求、基础设施、依赖项等给你的设计施加了硬性限制,把这些约束写出来,让读者理解你设计选型的背景。
约束条件我们的服务器全部是RISC-V架构,因此所有代码和依赖都必须能在RISC-V架构上运行。
服务等级目标(Service Level Objectives, SLOs)
SLO是服务向其客户或用户承诺的可量化目标。你可能听过服务等级协议(Service Level Agreements, SLAs)——SLA就是加上了违约赔偿条款的SLO。
在公司内部,我们一般不会因为同事犯错就罚钱,因此设计文档里通常定义SLO而非SLA。
SLO给系统性能设定了可衡量、客观的指标。你的经理可能会说“应用在移动端要流畅”,但这太模糊了。你总不想等代码写完了才发现,经理眼里的“流畅”是延迟低于2毫秒。定义清晰的SLO可以避免这种歧义,把目标用具体、客观的数值表达出来。
SLO通常要考虑这几个维度:
-
• 正常运行时间/可用性 :系统有多少比例的时间是可用的? -
• 延迟 :服务处理完请求需要多久? -
• 承载规模 :系统能处理多大的工作量?
服务等级目标
• Trogdor面向用户的HTTP请求,第50百分位延迟 ≤ 200ms • Postgres查询的第50百分位延迟 ≤ 80ms
监控与告警
确定了SLO之后,接下来就要考虑怎么在生产环境里衡量这些指标。
验证是否达成SLO最简单的方式是手动测试。但随着团队发展,应该实现自动化监控,第一时间发现SLO不达标。
制定监控策略时,可以问问自己这些问题:
-
• 如果服务挂了,你怎么第一时间知道? -
• 如果服务性能慢了100倍,你怎么发现? -
• 还有哪些事件应该触发告警? -
◦ 比如CPU使用率飙升、认证失败、系统报错等
监控发生以下事件时,会给值班工程师发告警通知:
• Trogdor面向用户的HTTP请求,第95百分位延迟 ≥ 3秒 • Postgres服务器过去2分钟的平均CPU使用率 ≥ 90%
时间线
时间线章节把项目拆成多个里程碑,明确各个阶段相关方能拿到什么交付成果。
选里程碑的时候,要尽量产出对相关方有价值的中间产物。比如先做一个展示模拟数据的界面,先给客户看。如果发现你理解错了客户需求,用模拟数据就能早发现,不用等你把对接生产数据的整套底层逻辑都写完了才踩坑。
如果你不知道怎么估算项目时间,我非常推荐Joel Spolsky的《轻松制定软件项目时间表》一文。这篇文章已经有25年历史,但依然是我最喜欢的软件估算方法。
时间线
• 里程碑1(2026-07-01) :RecencyBank在测试环境上线,内置硬编码的缓存数据(暂不读取Postgres) • 里程碑2(2026-07-17) :RecencyBank在测试环境上线,可缓存来自Postgres的真实数据 • 里程碑3(2026-08-03) :RecencyBank在测试环境上线,实现缓存淘汰和生命周期管理规则 • 里程碑4(2026-08-22) :RecencyBank全部开发完成,部署到生产环境
接口
你的项目是服务于人或是其他软件系统的,那这些交互具体是什么样的?
-
• 对于图形界面系统,用户界面长什么样? -
◦ 只需要简单的草图就行,不用纠结精确的UI细节 -
• 对于软件接口,API或命令行界面(CLI)的语义是什么? -
• 对于文件类接口,文件格式是什么样的?
接口目前Trogdor的
Server结构体直接依赖一个Go语言的PostgresDB结构体,代码如下:
> type Server struct { db PostgresDB }>
PostgresDB对外暴露以下方法:
> GetUser(id UserID) (User, error)> ListUsers() ([]User, error)> ...>
我们会创建一个Go的
interface(接口)类型,拥有和PostgresDB完全一致的API范围:
> type Store interface {> GetUser(id UserID) (User, error)> ListUsers() ([]User, error)> }>
我们会实现一个RecencyBank缓存类型,它实现了上述
interface,并封装了底层的PostgresDB结构体。RecencyBank的实现会缓存从Postgres读取的数据;当请求会修改数据,或是请求的数据不在缓存中时,就把请求转发给Postgres。
Server实现里唯一的改动,就是把其中一个成员的类型替换成新的接口:
> type Server struct { db store.Store }>
依赖与基础设施
依赖章节需要回答这类问题:
-
• 会使用哪些编程语言? -
• 代码运行在什么硬件或服务上? -
• 持久化数据存在哪里?
这个章节很容易被忽略,但编程语言、依赖库、基础设施的选型,对系统的复杂度和长期维护成本影响极大。
要重点考虑那些实现之后很难更换的依赖,轻易就能替换的不用太纠结。换编程语言、换存储后端是很难的;但如果你不满意用来发邮件的第三方服务,一下午就能换掉。
依赖
• 语言 :Go
◦ 我们已经在广泛使用Go,它也非常适合处理高并发的服务场景。 • 第三方包
◦ bbolt:一款应用广泛的键值存储(key-value store)实现,具备RecencyBank需要的很多功能特性。
安全
要开发安全的软件,开发者必须把安全融入软件的整个生命周期,从设计阶段就要开始。
安全章节需要回答这类问题:
-
• 你考虑了哪些威胁? -
◦ 比如攻击者暴力破解密码怎么办?用户上传带恶意软件的PDF怎么办? -
• 系统的攻击面(attack surface)有多大? -
◦ 也就是系统在哪些地方会处理可能有恶意的数据 -
• 信任边界在哪里? -
◦ 数据在哪个节点会从低权限系统流入高权限系统? -
◦ 比如在网页应用里,来自用户浏览器的请求就跨越了信任边界——服务器不能默认浏览器发来的输入是安全的。
哪怕你觉得系统里不太可能出现安全威胁,把你的判断理由写下来也有好处。你的说明可能会帮评审者发现你漏掉的风险。
安全RecencyBank不做任何访问控制,因此绝对不能直接接收来自公网的请求。
RecencyBank会运行在隔离网络中,只接收来自Trogdor网页服务器的入站请求,也只能向Postgres服务器池发起出站请求。
隐私
隐私章节可以帮你梳理系统处理的敏感数据,以及你会采取哪些保护措施。它需要回答这些问题:
-
• 系统处理哪些敏感数据? -
• 数据会保留多久? -
• 谁有权访问这些数据? -
• 怎么保护数据? -
◦ 比如数据在存储和传输过程中是否加密?
隐私RecencyBank里存储的敏感用户数据和Postgres数据库里的一致,因此沿用我们Postgres系统的隐私政策。具体来说,工程师访问生产环境的RecencyBank系统时,必须关联对应的问题编号;工程师访问的用户数据,必须严格限定在排查问题所需的最小范围内。
法律考量
如果你的系统运行在金融、医疗这类强监管领域,法律章节可以帮你确保符合相关法规要求。
就算不在监管领域,也可以想想:如果出了问题,系统会不会触犯法律?说明你会怎么规避可能给公司或客户带来风险的违法行为。
如果你要开源代码,也要说明你选了什么开源许可证,以及为什么选它。
FizzleCorp合同合规我们和FizzleCorp的合同严格限制了我们复制其专有FizzlePerfect™用户生物特征数据的行为。
好在法务团队审核了合同条款后确认,缓存层属于现有定义中的“存储层”范畴,因此我们可以在RecencyBank里缓存FizzlePerfect™的数据,不需要重新谈判合同。
日志
在排查bug、性能问题或是安全事件时,日志的价值非常大。如果在设计阶段就规划好高效的日志方案,能大幅降低系统长期维护的难度。
考虑日志方案时,可以想想这些问题:
-
• 服务会记录哪些关键事件? -
• 是否有不同的日志级别? -
◦ 比如信息、警告、错误、严重 -
• 系统把日志存在哪里? -
• 日志保留多久? -
• 谁有权查看日志? -
• 有没有不能写进日志的敏感数据?
日志RecencyBank会记录以下事件:
• 初始化时,记录RecencyBank的启动参数,以及宿主机的内存容量和使用情况 • 内存写入值失败的事件 • Postgres里的数据修改后,缓存失效失败的事件
待解决问题
写设计文档的过程中,你大概率会遇到至少一种情况:
-
• 设计里有个缺陷,但你还没想好怎么解决 -
• 在好几个方案之间纠结,拿不定主意 -
• 设计还有缺口,需要收集更多信息才能补上
在设计文档里加一个叫“待解决问题”的附录,把这些悬而未决的问题都记下来。
待解决问题的每一条都要说明:
-
• 是什么问题需要进一步处理? -
• 你觉得有哪些解决方向? -
• 解决这个问题的下一步动作是什么?
待解决问题:缓存的内存容量选型我们需要确定给缓存层分配多大内存。加内存能提升性能,但内存成本很高,而且内存加到一定程度后,性能收益会边际递减。
理论上,我们可以搭建测试环境、跑多组模拟测试,找到能让缓存系统+数据库的基础设施总成本最低的最优内存值。但跑这些模拟会占用开发人力。
我估算,搭建测试环境并跑一轮模拟需要3个开发人天。环境搭好之后,每多跑一轮模拟大概需要0.75个开发人天。
提议方案 :不做测试,直接选128GB内存。这个数值大概率接近最优值,而且开发人力的成本远高于内存本身的成本。
下一步 :请技术负责人拍板。
已解决问题
当你解决了一个待解决问题后,把决策总结一下,从“待解决问题”移到“已解决问题”章节里。完整的讨论过程保留下来,方便后续追溯。
已解决问题:缓存的内存容量选型决策 :给缓存层配置128GB内存。如果后续没达到性能目标,且瓶颈确实在内存不足,届时再加内存也不迟。花开发人力去测试算出“完美内存容量”的成本,远高于多买一点内存的成本。
我们需要确定……[原待解决问题的完整内容放在此处]
已考量的替代方案
如果你预料到读者会问“为什么不做X方案?”,那最好在“已考量的替代方案”章节里主动回答。这个章节也可以用来解释你否决的选项,尤其是那些乍一看很有吸引力、或是你花了不少精力调研过的方案。
我知道有些开发者会事无巨细地记下每一个被否决的设计想法,但我觉得没必要。不管是作为读者还是作者,我觉得这个章节只要用短短几行,说明几个有竞争力的替代方案,以及为什么没选它们就够了。
已考量的替代方案
• Google Cloud Firestore(持久化存储)
◦ 它的持久性和可靠性很有吸引力,但我不喜欢平台绑定的问题,而且本地测试也不方便。
推动设计文档通过评审
写完设计文档之后,下一步就是分享给团队,收集反馈了。
夜雨聆风