許多在部署前才發現的錯誤都有固定模式:前端把折扣率算成10%,後端卻算成15%;文件寫著某欄位必填,但實際API中卻已消失。兩邊的程式碼本身都沒問題。問題只是相同資訊存在於兩處,而只有一邊被修改了。
從結構上避免這類事故的原則,就是Single Source of Truth,簡稱SSOT。名稱聽起來很宏大,但內容只有一句話。每一個資訊片段都必須只有一個權威的原始來源。
重點總結如下。
- SSOT是「資訊只有一個原始來源」的原則,不代表儲存位置只能有一個
- 重複的資訊一定會不一致。真正的問題不是不一致發生的時刻,而是沒有人知道哪一份才正確的時刻
- 需要複製時,請把它做成「衍生物」。不要手動寫兩次,而是從原始來源自動產生
- 快取與複本不違反SSOT。只要清楚標示原始來源在哪裡,以及更新方向即可
為什麼重複一定會造成不一致
一旦把資料放在兩處,維持兩者一致的責任就落到人身上,因為工具和編譯器不知道這個約定。每次修改都必須記得「這個值還在哪裡?」只要忘記一次,兩個值就會走上不同的路。
更棘手的是不一致發生之後。當程式碼兩處的常數出現不同值時,只看程式碼無法知道哪一個正確。接著就得翻git歷史、尋找當時的負責人、查閱規劃文件,展開一場考古。SSOT崩潰的真正成本不是修正錯誤,而是判定「哪一份才是真相」所花的時間。
先來看幾個常見案例。
- 常數重複:最大上傳大小10MB分別硬編碼在前端驗證、後端驗證與錯誤訊息字串中
- 驗證邏輯重複:用不同的正規表示式,在用戶端與伺服器執行電子郵件格式檢查
- 文件與程式碼:API規格文件一份、實際實作一份。幾個月後,文件就變成小說
- 資料庫與快取:更新原始來源後忘了讓快取失效,舊資料持續被提供
- 設計與程式碼:設計稿的色彩值與硬編碼在App中的色彩值有細微差異
形式雖然不同,結構卻一樣:原始來源有兩份,而同步依賴人工處理。
原則不是「禁止複製」,而是「衍生」
誤解SSOT時,很容易把它理解成「資料只能儲存在一處」。如此一來,快取、讀取複本和建置產物看起來全都違反原則。實際原則不同。可以存在於多處,但原始來源只能有一個,其餘都必須從原始來源衍生。
建立衍生物的代表方法就是程式碼產生。
- 從結構描述產生型別:從OpenAPI規格產生用戶端型別與伺服器Stub後,規格就是原始來源,文件與程式碼便沒有不一致的機會
- 設計權杖:把色彩與排版定義在一個權杖檔案中,再分別產生iOS、Android與Web程式碼
- 共用常數模組:讓前端與後端都從同一個套件import常數,從根本阻止硬編碼複製
- 從程式碼擷取文件:從註解與型別產生API文件,文件就會一直跟著程式碼更新
共同點是把「人要寫兩次」的地方,改成「機器產生一次」的地方。同步責任從人的記憶移到建置管線;一旦不一致,CI會先發現。
快取與複本也能用同一框架整理。只要宣告原始來源的位置,更新只沿著原始來源→複本單向流動,並定義複本可能過期的時間(TTL、失效策略),就遵守了SSOT。違反發生的時機不是「存在複本」時,而是「開始直接修改複本」時。
同一原則也適用於組織
SSOT不只是程式碼的議題。在微服務設計中決定「哪個服務擁有這份資料」,以及從散落在公司Wiki、Notion、Slack的政策文件中指定「正式版本」,本質上都是同一個問題。
文件尤其比程式碼更容易不一致,因為沒有編譯器也沒有測試。因此,組織層級的SSOT必須先有共識,再談工具。例如規定「入職流程的原始來源只有Wiki這一頁,其他地方只放連結」。以連結取代複製,就是文件世界的衍生。
實務檢查清單
整理成明天就能套用的判斷基準如下。
- 第二次輸入相同值的瞬間就是訊號:如果正在複製常數、驗證規則或設定值,先檢查能否移到共用模組或改用產生方式
- 宣告原始來源:快取、複製與摘要本身都不是問題。確認程式碼和文件是否明確寫著「原始來源在這裡」,以及更新是否為單向
- 讓工具而不是人找出不一致:把結構描述驗證、契約測試與產生程式碼的diff檢查加入CI,讓同步失敗在合併前浮現
- 留意完美主義:為消除所有重複而建立過度抽象,本身也會產生成本。應先為經常變動,或不一致時代價高昂的資訊建立SSOT
這和重構中的DRY(Don’t Repeat Yourself)原則同源。DRY針對程式邏輯的重複,SSOT則進一步涵蓋資料與知識的重複,可視為更高層次的原則。下次出現「這個值那邊也有,要一起修改嗎?」時,那就是建立SSOT的地方。

