许多在发布前才发现的Bug都有固定模式:前端按10%计算折扣,后端却按15%计算;文档写着某字段必填,但实际API中已经没有它。两边代码本身都没问题。问题只是相同信息存在于两个地方,而只有一边被修改了。
从结构上避免这类事故的原则,就是Single Source of Truth,简称SSOT。名字听起来很宏大,但内容只有一句话:每个信息片段都必须且只能有一个权威原始来源。
核心总结。
- SSOT是“信息只有一个原始来源”的原则,不意味着只能有一个存储库
- 重复的信息必然会不一致。真正的问题不是发生不一致的那一刻,而是没人知道哪一份才正确的那一刻
- 需要复制时,就把它做成“派生物”。不要手写两遍,而是从原始来源自动生成
- 缓存和副本不违反SSOT。只要明确原始来源在哪里,以及更新方向即可
为什么重复必然导致不一致
一旦把信息放在两个地方,保持两者一致的责任就转移给了人,因为工具和编译器并不知道这个约定。每次修改都要记住“这个值还在哪里来着”,只要忘记一次,两个值就会走向不同的方向。
更麻烦的是不一致发生之后。当代码两处的常量出现不同值时,光看代码无法判断哪一个正确。接下来要翻查git历史、寻找当时的负责人、确认规划文档,一场考古就开始了。SSOT崩溃的真正成本不是修Bug,而是判断“哪个才是真相”所花的时间。
来看几个常见案例。
- 常量重复:最大上传大小10MB分别硬编码在前端校验、后端校验和错误消息中
- 校验逻辑重复:客户端和服务器使用不同的正则表达式校验邮箱格式
- 文档与代码:API规范文档和实际实现各自维护。几个月后,文档就成了小说
- 数据库与缓存:更新原始来源后忘记使缓存失效,旧数据持续被提供
- 设计与代码:设计稿中的颜色值与应用里硬编码的颜色值存在细微差异
形式不同,但结构相同:原始来源有两个,同步依赖人工操作。
原则不是“禁止复制”,而是“派生”
误解SSOT时,常会把它理解成“信息只能存放在一个地方”。这样一来,缓存、只读副本和构建产物似乎全都违反原则。实际原则不同:信息可以存在于多个地方,但原始来源只有一个,其余都必须从原始来源派生。
创建派生物的典型方法是代码生成。
- 从Schema生成类型:从OpenAPI规范生成客户端类型和服务器Stub后,规范成为原始来源,文档与代码便没有产生不一致的空间
- 设计令牌:在一个令牌文件中定义颜色和排版,再分别生成iOS、Android和Web代码
- 共享常量模块:让前端和后端从同一个包import常量,从根本上阻止硬编码复制
- 从代码提取文档:从注释和类型生成API文档,文档就会始终跟随代码
共同点是把“人要写两遍”的地方改成“机器生成一次”的地方。同步责任从人的记忆转移到构建流水线,不一致发生时,CI会先发现。
缓存和副本也可以用同一框架整理。只要声明原始来源的位置,更新只沿原始来源→副本单向流动,并定义副本可能过期的时间(TTL和失效策略),就遵守了SSOT。违反发生的时刻不是“存在副本”时,而是“开始直接修改副本”时。
同一原则也适用于组织
SSOT不只是代码问题。在微服务设计中确定“哪个服务拥有这份数据”,以及从散落在内部Wiki、Notion和Slack中的政策文档里指定“正式版本”,本质上都是同一个问题。
文档尤其容易比代码更快产生不一致,因为没有编译器也没有测试。因此,组织层面的SSOT首先需要共识,其次才是工具。例如规定“入职流程的原始来源只有Wiki这一页,其他地方只放链接”。用链接代替复制,就是文档世界里的派生。
实践检查清单
整理成明天就能应用的判断标准如下。
- 第二次输入相同值的瞬间就是信号:如果正在复制常量、校验规则或配置值,先检查能否移到共享模块,或改成生成方式
- 声明原始来源:缓存、副本和摘要本身都不是问题。确认代码和文档是否明确写着“原始来源在这里”,以及更新是否单向
- 让工具而不是人发现不一致:将Schema校验、契约测试和生成代码的diff检查加入CI,让同步失败在合并前暴露
- 警惕完美主义:为了消除所有重复而创建过度抽象,本身也会产生成本。应优先为经常变化,或不一致时代价高的信息建立SSOT
这与重构中的DRY(Don’t Repeat Yourself)原则同源。DRY针对代码逻辑重复,SSOT则将其扩展到数据和知识重复,可视为更高层次的原则。下次出现“这个值那边也有,要一起改吗?”时,那里就是建立SSOT的地方。

