好的设计文档不是把脑子里的东西倒出来,而是帮下一个打开文件的人,在最短时间里建立同一幅图:要解决什么、为什么这样选、系统怎么拆、风险在哪、下一步做什么。
「漂亮」在这里不是排版花哨,而是层次一眼能看见、重点不用翻三页、图文对得上。「容易理解」则是:读者不用猜你的省略,也不用先读完整个仓库才能读懂这一篇。
1. 先问三个问题
动手写之前,先能口头答完这三句。答不清,文档一定会散。
| 问题 | 答不清楚时的后果 | 写清楚后的效果 |
|---|---|---|
| 给谁看? | 对架构师讲实现细节,对新人堆术语 | 深度和用语对得上读者 |
| 要他读完后做什么? | 读完仍不知道该评审、该实现还是该反对 | 每节都指向一个决定或动作 |
| 不写会怎样? | 口头对齐三次还是各做各的 | 只写「不写就会歪」的那些部分 |
常见读者不是同一种人,一篇文档里要标明主读者:
- 评审者:关心取舍、边界、风险,不关心函数名。
- 实现者:关心模块怎么拆、接口长什么样、失败怎么处理。
- 后来的维护者:关心「当时为什么不选更简单的路」。
- 协作方(产品、测试、运维):关心影响面、发布顺序、回滚。
主读者超过两种时,宁可拆成「一页决策 + 一篇实现说明」,也不要揉成七十页百科。
2. 先选对文档类型
很多「难看」的文档,病根是类型错了:把会议纪要写成架构,把接口清单写成愿景。
| 类型 | 解决什么 | 篇幅建议 | 不该写什么 |
|---|---|---|---|
| 一页概述 | 新同学 10 分钟看懂系统 | 1~2 页 | 算法细节、票号流水账 |
| RFC / 设计提案 | 还没拍板,征求反对意见 | 3~8 页 | 假装已经定了的实现 |
| ADR(架构决策记录) | 记下「选了 A 而不是 B」 | 1~2 页 | 整系统说明书 |
| 高层设计(HLD) | 模块、数据流、部署、依赖 | 5~15 页 | 每个类的字段 |
| 详细设计(LLD) | 关键路径、状态机、错误码 | 按模块拆 | 重复 HLD 的背景 |
| 接口约定 | 请求响应、鉴权、兼容策略 | 以表格为主 | 业务抒情 |
一条经验:决策用 ADR,结构用 HLD,细节用 LLD,对外契约用接口文档。 混在一处,读者会在「为什么」和「怎么写代码」之间反复迷路。
ADR 可以短到这种程度:
# ADR-0012:订单状态存在独立状态机服务
- 状态:已采纳
- 日期:2026-09-01
- 背景:订单状态原先散落在三个服务的 if/else 里,对账经常不一致。
- 决策:抽成独立状态机,其它服务只发命令、订事件。
- 备选:继续分散;或塞进现有订单服务。
- 后果:多一次网络调用;换来状态可审计、可回放。
短,但后人能看见当时的约束。这比空喊「微服务更好」有用得多。
3. 一份设计文档的推荐骨架
下面这套目录适合大多数中等复杂度的后端 / 平台设计。可以删节,不建议打乱顺序:读者习惯先背景后方案。
# 标题:一句话说清「给谁做什么」
1. 摘要(半页,可单独转发)
2. 背景与问题
3. 目标与非目标
4. 名词与范围
5. 方案概述(先给图)
6. 关键设计(数据、接口、状态、失败)
7. 取舍与备选
8. 风险、观测与发布
9. 开放问题
10. 附录(长表、错误码、会议链接)
3.1 标题和摘要
差标题:订单模块优化
好标题:订单履约:把状态机从三个服务收拢到一处,并保证对账可回放
摘要用五句话就够,建议固定句式:
- 现在怎样,痛在哪。
- 我们打算怎样改。
- 明确不做什么。
- 最大的风险是什么。
- 需要谁在何时拍板。
读者若只读摘要就能决定「要不要往下看」,这篇文档就已经赢了一半。
3.2 背景与问题:写事实,不写情绪
把问题写成可检验的句子。
差:现在订单很乱,经常出 bug。
好:近 30 天对账失败 47 次,其中 31 次是「支付成功、履约仍为待支付」,根因是支付回调与库存扣减没有同一套状态源。
配一个「当前链路」简图,比再写三段文字更省事。背景里只放迫使你现在做决定的信息,公司创业史请删。
3.3 目标与非目标:最能让文档变薄的两栏
| 目标(做到算出成功) | 非目标(这次坚决不做) |
|---|---|
| 支付成功后 5 秒内履约状态一致 | 不重做结算与分账 |
| 任意一笔订单可回放状态变迁 | 不更换支付渠道 |
| 对账差异从周均 10+ 降到 1 以下 | 不在本迭代上跨城多活 |
非目标不是偷懒,是防止评审时被拖进无边需求。写下来,比会上说「那个以后再说」有效。
3.4 名词表:消灭同词异义
团队里最贵的误解,常常是同一个词。花半页列清楚:
| 名词 | 本文含义 | 不要理解成 |
|---|---|---|
| 订单 | 用户一次下单生成的履约单 | 购物车、支付单 |
| 已支付 | 支付渠道回调成功且本地落库 | 用户看见「付款成功」弹窗 |
| 冻结库存 | 预占,可释放 | 已出库 |
后文凡是用这些词,都按表走。比在段落里反复括号解释更干净。
4. 先画图,再写字
人脑对结构的理解,图快于字。漂亮的文档几乎都是图在前、字解释图,而不是字写完再补一张装饰图。
4.1 选对图,不要为了「看起来专业」堆图
| 你想说清的事 | 用什么图 | 常见滥用 |
|---|---|---|
| 系统由哪些块组成、谁依赖谁 | 模块图 / C4 容器图 | 把每个 jar 都画成六边形 |
| 代码怎么分层、依赖朝哪边 | 分层架构图 | 和部署图画在一张上,或画成没有方向的方块墙 |
| 一次请求怎么走 | 时序图 | 把重试、日志、监控全画进去 |
| 对象生命周期 | 状态机 | 用流程图冒充状态机(缺事件名) |
| 数据从哪来、经过谁、落到哪 | 数据流图 | 线上不写数据名,或画成主从拓扑 |
| 进程跑在哪、几份、谁连谁 | 部署图 | 和分层图、类图画在同一张上 |
| 上线先后顺序 | 步骤泳道 | 和部署拓扑画在同一张上 |
分层架构图回答的是谁可以调用谁,不是机房拓扑。四层横条叠起来,箭头只朝下;哪一层放规则、哪一层碰 SQL,一眼能看出来。
接入层HTTP / 鉴权 / 参数校验
把请求收成命令。不写「能不能批」这种业务 if。
应用层用例编排
一个用例一个服务:开事务、调领域、发通知。不拼 SQL。
领域层规则与状态机
业务不变量住在这里。仓储接口在本层定义,实现不在本层。
基础设施数据库 / 消息 / 外部系统
SQL、SMTP、SDK 只出现在这一层。换库不改上面三层。
部署图回答的是跑在哪、几份、连哪台,不是包名怎么分层。机房或 VPC 画成框,进程写实例数,边上标端口和主从。
公网
接入 VPC
应用 VPC
应用不落本地盘。配置来自环境变量,开关 deviceloan.enabled。
数据 VPC
一张图只回答一个问题。一张图上既有机房、又有类、又有产品路线图,一定难看。每种图的框和线各自有习惯,底稿见 4.5。
4.2 时序图要写出失败,而不仅是成功
成功路径人人会画。真正让实现者和测试省事的,是超时、重复、乱序:
用户 -> 网关: 提交订单
网关 -> 订单: CreateOrder
订单 -> 库存: Freeze
库存 --x 订单: 超时
订单 -> 订单: 标记 pending_freeze,进入补偿
订单 --> 网关: 202 + 查询号
旁注三行即可:超时阈值、是否可重入、补偿谁来触发。比再写一页「我们会考虑异常情况」实在。
4.3 状态机把事件写在箭头上
状态是圆圈,事件是箭头上的字。只写「待支付 → 已支付」而不写「收到 PaymentSucceeded」,实现时一定会各写各的 if。
同时写明:
- 哪些转移非法(要返回明确错误,而不是默默忽略)。
- 是否允许乱序事件(先到履约、后到支付)。
- 终态能不能再出来(已取消能否复活)。
4.4 图的可读性细则
- 同一概念同一名字,图里叫
FreezeStock,正文不要改成「锁库存」。 - 箭头有方向、有含义;不要用无标签细线把所有框连起来。
- 颜色最多两三种:例如「已有 / 新增 / 废弃」。四色以上像节日贺卡。
- 图放在被解释的那一节正上方,不要全部堆到文末。
- 能用 mermaid / PlantUML 进仓库的,就不要只贴一张过期截图。
4.5 形状与连线:每种图一张底稿
选对图之后,还要约定框是什么、线是什么。下面七张是底稿:形状本身带语义,线上写含义。每张图下方有图例,和正文里的形状一一对应。写正文时只替换名字,不要改形状用法——圆圈忽然变成「服务器」,读者就得重新猜。
模块图 / C4 容器
分层架构图
时序图
状态机
数据流图
部署图
步骤泳道
5. 把「关键设计」写成别人能实现的规格
概述解决「长什么样」,关键设计解决「碰到硬问题时怎么走」。下面四块几乎每篇中等设计都用得上。
5.1 数据:谁是源,谁是副本
写清:
- 主键与唯一约束(业务幂等键往往比自增 id 重要)。
- 写路径:谁先落库,谁后更新缓存。
- 读路径:能脏读多久,要不要读自己的写。
- 删除是软删还是硬删,审计留多久。
差:订单和支付记录存在数据库里,用 Redis 加速。
好:订单主状态以orders表为准;Redis 只缓存只读展示,TTL 30s,缓存失败直接回表。支付回调以payment_id幂等,重复回调返回 200 且不推进状态。
5.2 接口:给例子,不要只给形容词
每一个对外接口至少具备:路径、调用方、成功示例、失败码、超时与重试、兼容策略。
POST /v1/orders/{id}/freeze-stock
Idempotency-Key: 8f2a...
# 成功
{ "status": "frozen", "expire_at": "2026-09-09T12:00:00+08:00" }
# 失败:库存不足
{ "error": "STOCK_INSUFFICIENT", "sku": "A01", "want": 2, "left": 0 }
「接口要优雅」「错误要友好」这类句子可以删。读者需要的是字段和码。
版本策略也要写死一句,例如:/v1 只追加字段,不改语义;破坏性变更走 /v2,旧版至少保留两个发布周期。
5.3 一致性:说人话,并落到机制
「我们保证最终一致」太空。改写成机制:
- 以哪张表 / 哪个主题为权威。
- 失败如何补偿(定时扫、发件箱、人工工单)。
- 用户看到中间态时,页面怎么写(「支付确认中」而不是假成功)。
- 对账任务的周期、差异阈值、谁值守。
5.4 安全与权限:点名,不空喊
写清:谁能调、凭什么、敏感字段怎么脱敏、审计日志记什么。涉及钱、库存、隐私的设计,缺这一节就不该过审。
6. 语言:清楚比华丽重要
漂亮的中文技术文档,通常有这些习惯。
短句,主动语态。
差:库存的扣减将会在支付完成之后被进行。
好:支付成功后,订单服务调用库存冻结。
先结论后解释。 每节第一句就是答案,细节放后面。评审者时间少,实现者才会往下翻。
一次只引入一个新名字。 同一段里同时冒出「编排器、协调者、网关、门面」,没有人分得清。
数字要有单位和对比。 QPS 会很高 不如 高峰 1.2k QPS,现网峰值的 3 倍。
少用程度副词。 「非常」「尽可能」「妥善」在设计里等于没写。改成阈值、超时、重试次数。
术语第一次出现给定义,之后保持稳定。 不要前一节 SLA、后一节 服务承诺、附录又变 可用性指标。
删除正确的废话。 例如「随着业务发展,系统复杂度提升,我们需要更好的架构」。读者已经在读设计文档了,不用再被说服「设计有意义」。
可用的自检:把一节读给没参加讨论的同事听,他若不断问「这是什么意思」,那一节就还没写完。
7. 正反例:同一件事的两种写法
反例(常见病)
为了提升用户体验并赋能业务高质量发展,我们将对订单中台进行全面升级。新架构采用领域驱动与微服务理念,充分解耦,灵活扩展,后续可支撑各类创新场景。具体实现由各小组根据实际情况推进,遇事沟通。
读完不知道:改什么、不改什么、谁先上、失败怎么办。形容词很多,动词没有。
正例(同一主题)
决策: 把订单状态从支付、履约、库存三处,收拢到订单服务内的状态机;其它服务只发命令、订
order.events。
不做什么: 不拆分结算,不换支付渠道。
成功标准: 支付成功后 5 秒内状态一致;对账差异周均 ≤ 1。
发布: 先双写一周,对账无差异再切读。回滚开关:order.state_machine.enabled。
字数更少,能评审、能实现、能验收。
再看一处接口描述的对比。
差:前端调用后端获取数据,注意性能和安全。
好:
| 项 | 约定 |
|---|---|
| 调用 | GET /v1/orders/{id},登录用户且为下单人 |
| 热点 | 详情页允许 50 QPS / 实例,超限 429 |
| 缓存 | CDN 不缓存;网关缓存 0 |
| 脱敏 | 手机号中间四位 *,仅客服角色可见全文 |
| 失败 | 订单不存在 404;无权限 403,不泄露是否存在他人订单 |
8. 版式:少装饰,多对齐
「漂亮」多半来自克制。
- 标题是大纲。 读者只扫
h2/h3应能复述结构。不要出现「其它」「补充说明」这种垃圾桶标题。 - 一段一个意思。 超过八行就考虑拆,或改成列表。
- 能用表就不要用散文。 对比、字段、错误码、发布步骤,表比段落干净。
- 代码和配置用等宽块。 不要把 JSON 塞进一句话的引号里。
- 强调不超过一种。 加粗或引用,选一个。满屏加粗等于没有重点。
- 留白。 节与节之间空一行;图下写一句图注:
图 2 支付成功后的补偿路径。 - 链接写人话。 用「见 ADR-0012」而不是「点击这里」。
Markdown 足够写绝大多数设计文档。先把结构写对,再考虑 Wiki 皮肤和封面。
9. 评审:文档是用来被反对的
写完先自己当反对者,再发给别人。评审不是找错别字,是找没写出来的决定。
请评审者只回答这几个问题:
- 问题是否真的存在?有没有更小的改法?
- 非目标是否被偷偷做大了?
- 失败路径有没有空洞?
- 数据以谁为准?冲突时听谁的?
- 发布和回滚能否在半夜独立做完?
- 三个月后,一个新人只读这一篇,能不能动手?
收集意见时,把「已采纳 / 不采纳 + 理由」写回文档或 ADR,不要只留在聊天记录。反对被记录下来,文档才算闭环。
评审前把文档冻一版(日期 + 链接)。边评边改到面目全非,参会的人会对着不同文本说话。
10. 维护:过期的漂亮文档比没有更危险
设计文档的寿命往往短于系统。要让它继续「容易理解」,得约定谁改、何时改。
- 代码行为变了,先改文档再合入,或在 PR 里附文档 diff。
- 每个 HLD 顶部加一行:
最后核对:2026-09-09,与 main@a1b2c3 一致。 - 废弃的方案写
已废弃并指向新文档,不要直接删——后人会搜到旧链接。 - 季修一次:删掉已落地的「开放问题」,把临时决策收成 ADR。
文档和代码冲突时,以代码为准、并记一笔债。假装文档永远正确,是最常见的信任破产。
11. 样例:一份带分层架构的设计文档
下面整节就是一篇可以拿去评审的设计,主题是内部设备借用系统 DeviceLoan。核心不是堆接口,而是把软件拆成四层:接入、应用、领域、基础设施。依赖只允许从上往下。
主读者:实现者 · 评审者 状态:待拍板 核对:2026-09-09
11.1 摘要
行政现在用表格登记示波器、开发板、镜头的借还,近 90 天出现 11 次「账上在库、柜子是空的」。做一套内部借用系统:员工申请,管理员审批,借出与归还改库存,逾期发通知。
做: 申请、审批、借出、归还、逾期提醒。不做: 采购、折旧、对外租赁。架构: 四层,领域规则不进 Controller,SQL 不进领域对象。拍板: 本周五前行政与平台各一人签字。
11.2 背景与问题
设备 86 件,分散在 3 个柜子。登记本与即时通讯群并行,对不上时只能翻聊天记录。没有审批痕迹,也没有「谁持有」的唯一事实源。
11.3 目标与非目标
| 目标(可验收) | 非目标(本次不做) |
|---|---|
| 任意时刻,一件设备至多一张「已借出」单据 | 不接财务、不算出资产原值 |
| 申请到审批的接口 P99 < 300ms | 不做移动端原生 App、不扫码入库 |
| 逾期 T+1 个工作日发出通知,漏发率周均 < 1% | 不支持跨园区调拨、不对接门禁 |
11.4 名词
| 名词 | 本文含义 | 不要理解成 |
|---|---|---|
| 设备 | 可单独出借的一件实物,有唯一 asset_no | 设备型号、配件包 |
| 借单 | 一次申请生成的聚合根 LoanOrder | 聊天里的口头答应 |
| 在库 | 领域状态 available,可被新申请占用 | 柜子里看得到但单据未还 |
11.5 分层架构
进程内四层。箭头只准向下:上面可以调下面的接口,下面不准认识 HTTP、不准倒依赖 Controller。换数据库或换通知渠道,只改基础设施。
L1接入层 Interface
HTTP 路由、登录态、参数校验、错误码映射。不写业务 if。组件:LoanController、AuthFilter。
L2应用层 Application
一个用例一个服务:ApplyLoan、ApproveLoan、CheckoutLoan、ReturnLoan。编排事务与通知,不塞领域规则。
L3领域层 Domain
聚合 LoanOrder、Device;规则「在库才能批准」「已借出才能归还」。状态机只活在这里。仓储接口在本层定义,实现不在本层。
L4基础设施层 Infrastructure
PgLoanRepository、PgDeviceRepository、MailNotifier、StaffDirectoryClient。SQL、SMTP、LDAP 只出现在这一层。
包结构按层落盘,避免「一个 utils 兜住所有」:
deviceloan/
interface/http/ # L1
application/ # L2 用例
domain/
model/ # L3 聚合、值对象
service/ # L3 跨聚合规则(如库存占用)
port/ # L3 Repository / Notifier 接口
infrastructure/
persistence/ # L4
notify/ # L4
directory/ # L4
层间禁令: Controller 不得 new 出 SQL;LoanOrder 不得引用 Spring / Gin 注解以外的 Web 类型;通知失败不能回滚已经成立的领域状态——由应用层记补偿任务。
11.6 请求如何穿过各层
以「管理员批准借单」为例。成功与失败都要看见停在哪一层。
失败不写在 Controller 里「再查一次库存」。领域返回 DeviceNotAvailable,接入层翻译成 HTTP。这样换 gRPC 时,规则不用搬家。
11.7 领域状态机
借单状态只由领域方法推进。应用层调用 approve / checkout / giveBack,不得直接 SET status=。
设备侧另有两态:available ↔ on_loan。批准时占用,归还时释放。两边必须同一事务提交,避免「单过了、库存没占上」。
11.8 数据与接口
权威在 PostgreSQL。devices.asset_no 唯一;loan_orders 与 loan_items 表达一张单可借多件(本期限制 1 件,表仍按多件留)。无 Redis 热点,读走主库。
| 接口(接入层) | 用例(应用层) | 失败 |
|---|---|---|
POST /v1/loans 登录员工{"asset_no":"EQ-012","days":3} |
ApplyLoan |
设备不存在 404;已占用 409 DEVICE_ON_LOAN |
POST /v1/loans/{id}/approve 管理员 |
ApproveLoan |
非 submitted 409;库存被别人截胡 409 |
POST /v1/loans/{id}/return 管理员 |
ReturnLoan |
非 borrowed 409 |
幂等:ApplyLoan 使用 Idempotency-Key,同一员工 24 小时内相同 Key 返回同一借单。审批不幂等重放成第二次占用——重复 Approve 对已 approved 返回 200 且不改数据。
11.9 取舍、风险与发布
为什么分层而不是单文件 CRUD? 审批规则已经出现「周末是否计入租期」「管理员不能批给自己」——这些会涨。放进 Controller 三个月后就会复制到脚本和定时任务里各写一份。
为什么不先上工作流引擎? 现在只有单级审批。引擎引入状态外置,领域图反而看不清。第二级审批出现时再抽 ApprovalPolicy。
风险: 批准与占库的竞态——同一设备两张 submitted 单,用设备行 SELECT FOR UPDATE 串行化。通知失败: 领域提交成功后应用层写 outbox,定时投递,不回滚借单。
观测: loan_approve_total{result}、device_on_loan_gauge、overdue_notice_failed。逾期任务连续失败 3 次告警。
发布: 先行政 3 人试用一周,开关 deviceloan.enabled。关闭后写入接口 503,已借出单据仍可归还。表格并行双写三天,对账无误再停表。
开放问题: 逾期是否自动续期 1 天——行政 @王五 本周四前回复。答案进来后只改领域规则,不改接入层。
对照本文前面的标准:读者能画出四层、能指出规则在领域、能按用例写第一行代码。图负责结构,表负责失败码,禁令负责防止分层塌回大泥球。
12. 动笔前的检查清单
写完或评审前,按表勾一遍。未勾的项,就是读者会卡住的地方。
- [ ] 标题能单独转发,不依赖文件夹名字
- [ ] 摘要五句话:现状、方案、非目标、风险、谁来拍板
- [ ] 主读者写明了,深度配得上
- [ ] 目标可验收,非目标能挡住加塞
- [ ] 名词表覆盖易混词
- [ ] 至少一张结构图、一张关键路径图(含失败)
- [ ] 图与正文用同一套名字
- [ ] 数据源、幂等、超时、重试、回滚都有数字
- [ ] 对外接口有成功 / 失败例子
- [ ] 取舍写了「为什么不选更简单的」
- [ ] 观测指标和告警阈值具体
- [ ] 开放问题有负责人,不是修辞
- [ ] 长表和历史讨论进附录,正文能一口气读完
- [ ] 自己出声读一遍,没有接不上的代词
13. 常见跑偏,以及对一下
| 跑偏 | 看起来像 | 改法 |
|---|---|---|
| 小说 | 从项目成立写到今天 | 背景最多一节,其余进附录 |
| 代码说明书 | 大段伪代码,没有决策 | 决策上移,代码放到 LLD |
| 幻灯片 | 全是标题和形容词 | 每节补上数字、接口、失败 |
| 日记 | 按讨论时间罗列发言 | 按问题重组,发言进会议纪要 |
| 百科 | 一篇覆盖构建、部署、UI、薪酬 | 拆文档,互相链接 |
| 秘密 | 关键数字只在会上说 | 写进正文,否则等于没设计 |
收束
写出漂亮、好懂的设计文档,靠的不是文采,而是对读者的体贴:先告诉他要做什么决定,再给他刚好够用的结构、图和数字;把形容词换成机制,把情绪换成取舍,把「以后再说」写成非目标或开放问题。
你可以记住一句更短的标准:
一个没参加讨论的同事,只读这一篇,能画出系统、能指出风险、能开始写第一行代码——这才叫写完了。
从下一篇起,先写摘要和非目标,再画一张失败路径图。文档多半会立刻好看一截。