一個真實系統的「考古現場」

我掃自己的電商後端時,挖到三處「新舊兩套並存」的現場:

  1. 下單流程兩條路:舊路 place_order(先建訂單再付款、當場扣庫存)跟新路 CheckoutSession(先付款成功才建單、用預佔)同時活著,兩條都有 code 在走。
  2. 物流模型兩套:舊的 Shipment 模型註解寫著「已停用」,但後台的出貨按鈕、物流查詢實際上還在操作它;新的 DeliveryGroup 設計更好(支援分倉分批),卻只有建立時被寫入一次,之後沒有任何 code 更新它的狀態——新軌鋪了鐵,火車還在舊軌上跑。
  3. 通知觸發兩軌:一半的通知走新的事件驅動(訂單事件發出去、監聽者發通知),一半還是舊的直接呼叫 NotificationService,code 裡散著好幾個 TODO(Phase1-Event)

沒有要罵這個系統的意思——這是所有活著的系統的常態。重構不可能一夜切換,新舊並存是必經的隧道。真正的問題是:這三處並存,沒有一處有標記「現在走到哪、誰是真的、何時收斂」。隧道沒有出口指標,走著走著就住下來了。

雙軌為什麼危險:三種具體的痛

  1. 不知道信誰。兩套模型都有資料,哪邊是 source of truth(唯一可信的那份)?我那個物流案例的答案違反直覺:被標「停用」的舊模型才是真的,新模型是空殼。判斷方法只有一個——追寫入點(誰真的被 code 更新),跟 code 註解、跟設計文件都無關。文件會說謊,寫入點不會。
  2. 行為出現多路徑。我的系統裡「訂單轉出貨中」有兩個入口、「扣庫存」有三條路徑。多路徑代表:改一條忘了另一條,就出現「有時候對有時候錯」的幽靈 bug——最難查的那種。
  3. 新人以為新的是真的。接手的人讀 code,自然假設設計好的新模型在運作,於是在空軌上繼續蓋——dead code 疊 dead code。

這個問題有個有名字的解法:Strangler Fig

絞殺榕(strangler fig)是一種樹:沿著老樹往上長,根系逐漸包住老樹,最後老樹枯掉、新樹自立。Martin Fowler 拿它命名漸進式遷移:不重寫、不一刀切,新系統一小塊一小塊地包住舊系統,每包一塊就讓那一塊的流量真的走新路,直到舊的自然死亡。

關鍵字是「真的走新路」。對照我的物流案例,你會發現它卡在哪:新模型建了(榕樹種了),但流量沒切過去(根沒接到土)——這不是 strangler fig,這是在老樹旁邊擺了一棵盆栽。

雙軌期的三條紀律

雙軌本身不是病,沒有管理的雙軌才是。三條紀律:

  1. 寫下收斂計畫,帶日期。開新軌的那天就寫:舊軌何時停止寫入、何時停止讀取、何時刪 code。沒有日期的「之後再收」=永遠不收。我系統裡那些 TODO(Phase1-Event) 就是沒有日期的願望。
  2. 標記 source of truth,禁止雙寫失衡。並存期間明確宣告誰是真的;如果過渡期需要兩邊都寫(雙寫),要嘛包在同一個交易、要嘛加對帳兜底(見〈對帳 pattern〉)——不然兩邊遲早分歧。
  3. 用「寫入點掃描」驗收收斂進度。收斂不是「新 code 上線了」,是「舊軌的寫入點歸零了」。一條 grep 就能驗:舊模型還有幾處被寫入?歸零才能刪。這跟〈dead state〉那篇是同一招——看接線,不看宣言

反思

系統考古學第一定律:判斷什麼是真的,追寫入點,不要信註解。

而給自己系統的提醒是反過來的:你今天開的每一條新軌,六個月後都會變成別人(包括未來的你)的考古現場——差別只在你有沒有留下「出口指標」:誰是真的、何時收斂、怎麼驗收。三行註解的事,省下的是半年後三天的考古。

相關

  • 「看接線不看宣言」的姊妹篇 → dead-state 治理
  • 雙寫分歧的兜底 → 對帳 pattern
  • DB 層的漸進遷移(expand-contract)→ database 章規劃中