# 一個事實寫在三個地方，就有三個安靜走散的版本

- URL: https://justfly.idv.tw/%e4%b8%80%e5%80%8b%e4%ba%8b%e5%af%a6%e5%af%ab%e5%9c%a8%e4%b8%89%e5%80%8b%e5%9c%b0%e6%96%b9%ef%bc%8c%e5%b0%b1%e6%9c%89%e4%b8%89%e5%80%8b%e5%ae%89%e9%9d%9c%e8%b5%b0%e6%95%a3%e7%9a%84%e7%89%88%e6%9c%ac/
- 日期: 2026-09-14
- 分類: 我知故我在
- 標籤: 單一事實來源, 系統設計, 資料一致性

![一個事實寫在三個地方，就有三個安靜走散的版本]
一週內連續踩到同一種病。排程任務把不屬於自己的資源當成自己的來處理；一份靜態設定裡殘留舊位址，寫入靜默失敗；同一個過濾條件在三層各實作一次，漏掉其中一層；責任歸屬表在多台機器上各存一份，其中一份早已過期。四件事表面上互不相干，但都不會拋錯，只會產出一個「合理但錯誤」的結果。

這和家裡三本記帳本的處境一樣——一本、一本，再加一本共用的。只要有一本忘了抄，錢沒有真的少，但三本永遠對不起來，也沒有任何提醒跳出來。系統裡的「事實」也是這樣，它不會報錯，它只會安靜地分裂。

##### 分界點：重複的是行為，還是事實

判斷一段邏輯該不該重寫，看的不是程式碼有沒有重複，而是它背後代表的事實是否只有一個。行為可以重複實作，事實不行。一個過濾條件在快照層、閘門層、鏡像層各寫一次，看起來像是[縱深防禦](https://zh.wikipedia.org/wiki/%E7%B8%B1%E6%B7%B1%E9%98%B2%E5%BE%A1)的多層把關，實際上是把同一份事實複製了三份。少一層，三份紀錄從此不再一致。

一份公開的踩坑紀錄裡就有這樣的例子：某個 webhook 觸發鏈裡，中間節點讀取一個欄位時漏了物件層級的前綴，導致同一個欄位在同一條鏈上被兩種方式解讀——上游節點讀得到，下游節點讀到 undefined。系統照樣往下跑，不報錯，只是把「這筆已處理」的狀態忘了寫回去，於是同一則資料連續多天被重新挑出來處理一次。

##### 容易誤判的三個地方

第一，三層都擋一次，看起來是保險，其實是把單一事實複製了三份，多層不等於多防線。第二，加了自動檢查不代表沒事，檢查本身也是那個事實的第四份複製，一樣會過期、一樣會漏改。第三，靜態設定檔不算程式碼、可以先手動同步的想法，忽略了手動同步的失敗是無聲的——沒有人會因為忘記改一行設定而收到警報。

##### 確認方式

拿一個具體事實來問：如果它明天改了，要改幾個地方？答案大於一，就先找出誰是正本，其餘一律轉引，不重寫。驗證的時候不能只測改過的那一份，要沿著資料流把每一層都讀回來，跟線上實際值比對，而不是跟提交紀錄或部署清單上「以為」上去的版本比對。這週幾次真正抓到問題，都是靠重新讀回線上版本，不是靠看 commit。有一次的教訓更直接：修完一個工作流程之後，一個舊編輯分頁還留在記憶體裡，一旦儲存，就把修好的版本原樣蓋掉——驗收如果只看當下那次重試成功，永遠抓不到這種覆蓋，得等下一次排程觸發後再回頭比對一次版本號。

##### 留給未來的話

常設原則很簡單：一個資源只能有一個規則主人，兩套規則同時管一個東西，帳一定會錯。為每個關鍵事實指派唯一擁有者，其餘位置只做轉引；擁有者要變更時得能廣播出去，不能靠人記得提醒。下一次看到某個判斷邏輯出現在第二個檔案裡，先問這是不是同一件事實的第二份原稿，再決定要不要補測試。

— 邱柏宇

### Write the Same Fact in Three Places, Get Three Versions That Quietly Drift Apart

Four unrelated-looking incidents in one week. A scheduled job treated a resource that wasn’t its own as if it were. A static config file kept a stale address, so a write failed silently. The same filter condition got implemented once each in three separate layers, and one layer was missed. An ownership table lived as separate copies on multiple machines, and one copy had gone stale. None of the four threw an error. Each just produced a result that looked reasonable and was wrong.

It’s the household ledger problem — one notebook, another notebook, plus a shared one. Forget to log an entry in just one of them and no money actually goes missing, but the three books never reconcile, and nothing flags it for anyone to notice. Systems have the same failure mode with facts: they don’t error out, they drift quietly.

##### Where the Line Actually Sits

Whether a piece of logic should be rewritten isn’t decided by whether the code looks duplicated — it’s decided by whether the fact behind it is singular. Behavior can be implemented twice. A fact can’t. A filter condition written once each into a snapshot layer, a gate layer, and a mirror layer looks like [defense in depth](https://en.wikipedia.org/wiki/Defense_in_depth_(computing)). It’s actually the same fact copied three times. Miss one layer, and three records stop agreeing with each other.

A documented case makes the pattern concrete: inside a webhook-triggered chain, a middle node read a field but missed the object-level prefix the field actually lived under. Upstream nodes read it fine; this one read undefined. The pipeline kept running — no error — it just failed to write back the “already handled” status. The same item got picked up and reprocessed for several days straight before anyone noticed.

##### Three Easy Misreads

First: three layers each checking the same condition feels like insurance, but it’s the same fact tripled, not three lines of defense. Second: adding an automated check feels like closing the gap, but the check is a fourth copy of that same fact — it goes stale too. Third: a static config file feels like it doesn’t count as code, so manual sync seems fine. But manual sync fails silently. Nobody gets paged for forgetting to edit one line in a settings file.

##### How to Actually Confirm It

Take one concrete fact and ask: if it changes tomorrow, how many places need editing? If the answer is more than one, find the single source of truth first; everywhere else should reference it, not reimplement it. Verification can’t stop at the copy that got edited — it has to trace the data flow through every layer and read back the live value, not what the deploy log claims went out. The real catches this week all came from re-reading the live version, not from checking commit history. One case was blunt about it: after a workflow got patched, a stale editor tab left open in the background overwrote the fix the moment it got saved. A same-day retry success wouldn’t have caught that — only re-checking the version identifier after the next scheduled run did.

##### One Line Worth Keeping

The standing rule is simple: one resource, one rule owner. Two rule sets governing the same thing, and the books stop matching — every time. Every fact worth calling critical gets a single owner; everywhere else just cites it. When ownership changes, that change needs to broadcast, not depend on someone remembering to mention it. Next time the same conditional shows up in a second file, the question is whether that’s just a second draft of a fact that already has an original — then decide whether a test is even the point.

— 邱柏宇
