一個真實系統的「考古現場」
我掃自己的電商後端時,挖到三處「新舊兩套並存」的現場:
- 下單流程兩條路:舊路
place_order(先建訂單再付款、當場扣庫存)跟新路CheckoutSession(先付款成功才建單、用預佔)同時活著,兩條都有 code 在走。 - 物流模型兩套:舊的
Shipment模型註解寫著「已停用」,但後台的出貨按鈕、物流查詢實際上還在操作它;新的DeliveryGroup設計更好(支援分倉分批),卻只有建立時被寫入一次,之後沒有任何 code 更新它的狀態——新軌鋪了鐵,火車還在舊軌上跑。 - 通知觸發兩軌:一半的通知走新的事件驅動(訂單事件發出去、監聽者發通知),一半還是舊的直接呼叫
NotificationService,code 裡散著好幾個TODO(Phase1-Event)。
沒有要罵這個系統的意思——這是所有活著的系統的常態。重構不可能一夜切換,新舊並存是必經的隧道。真正的問題是:這三處並存,沒有一處有標記「現在走到哪、誰是真的、何時收斂」。隧道沒有出口指標,走著走著就住下來了。
雙軌為什麼危險:三種具體的痛
- 不知道信誰。兩套模型都有資料,哪邊是 source of truth(唯一可信的那份)?我那個物流案例的答案違反直覺:被標「停用」的舊模型才是真的,新模型是空殼。判斷方法只有一個——追寫入點(誰真的被 code 更新),跟 code 註解、跟設計文件都無關。文件會說謊,寫入點不會。
- 行為出現多路徑。我的系統裡「訂單轉出貨中」有兩個入口、「扣庫存」有三條路徑。多路徑代表:改一條忘了另一條,就出現「有時候對有時候錯」的幽靈 bug——最難查的那種。
- 新人以為新的是真的。接手的人讀 code,自然假設設計好的新模型在運作,於是在空軌上繼續蓋——dead code 疊 dead code。
這個問題有個有名字的解法:Strangler Fig
絞殺榕(strangler fig)是一種樹:沿著老樹往上長,根系逐漸包住老樹,最後老樹枯掉、新樹自立。Martin Fowler 拿它命名漸進式遷移:不重寫、不一刀切,新系統一小塊一小塊地包住舊系統,每包一塊就讓那一塊的流量真的走新路,直到舊的自然死亡。
關鍵字是「真的走新路」。對照我的物流案例,你會發現它卡在哪:新模型建了(榕樹種了),但流量沒切過去(根沒接到土)——這不是 strangler fig,這是在老樹旁邊擺了一棵盆栽。
雙軌期的三條紀律
雙軌本身不是病,沒有管理的雙軌才是。三條紀律:
- 寫下收斂計畫,帶日期。開新軌的那天就寫:舊軌何時停止寫入、何時停止讀取、何時刪 code。沒有日期的「之後再收」=永遠不收。我系統裡那些
TODO(Phase1-Event)就是沒有日期的願望。 - 標記 source of truth,禁止雙寫失衡。並存期間明確宣告誰是真的;如果過渡期需要兩邊都寫(雙寫),要嘛包在同一個交易、要嘛加對帳兜底(見〈對帳 pattern〉)——不然兩邊遲早分歧。
- 用「寫入點掃描」驗收收斂進度。收斂不是「新 code 上線了」,是「舊軌的寫入點歸零了」。一條 grep 就能驗:舊模型還有幾處被寫入?歸零才能刪。這跟〈dead state〉那篇是同一招——看接線,不看宣言。
反思
系統考古學第一定律:判斷什麼是真的,追寫入點,不要信註解。
而給自己系統的提醒是反過來的:你今天開的每一條新軌,六個月後都會變成別人(包括未來的你)的考古現場——差別只在你有沒有留下「出口指標」:誰是真的、何時收斂、怎麼驗收。三行註解的事,省下的是半年後三天的考古。
相關
- 「看接線不看宣言」的姊妹篇 → dead-state 治理
- 雙寫分歧的兜底 → 對帳 pattern
- DB 層的漸進遷移(expand-contract)→ database 章規劃中