觸發場景:POS App 前端有多個服務各自持有「以單據 id 為 key」的狀態——輪詢的差異比對基準、事件追蹤記錄、當前品項快照。後端合併兩張單據後,舊 id 全部作廢,這些狀態何去何從? 疑問來源:最初的直覺是「全部搬家」——把每一份狀態的 key 都改成新 id。實作後發現有些搬了是白搬、有些不搬會出大事,差異在哪? 整理目的:把「上游身份轉移時的前端狀態遷移」整理成一個二分判準:自持 vs 可導出。 本文邊界:讀側設計的完整階梯見 讀模型的升級判準


1. 三份狀態、兩種命運

合併發生後,前端各服務持有的狀態逐一檢視:

狀態性質遷移策略不遷移的後果
輪詢差異比對基準自持——「上一輪看到什麼」只存在於前端記憶,上游沒有這份資料收到合併通知時把基準搬到新 id 下新 id 查無基準 → 整張單據被誤判為新增 → 重複列印
事件追蹤記錄自持——事件當下的內容與處理進度,上游不保存這個視角通知搬家(改掛新 id)以 id 查詢的畫面從此找不到記錄
當前品項快照可導出——每一輪輪詢整批重建自上游不搬,下一輪同步自動對齊至多一個輪詢週期的短暫過時

判準一句話:這份狀態消失後,能不能單靠向上游重新查詢完整重建? 能 → 可導出,讓同步機制自然收斂;不能 → 自持,身份轉移時必須主動遷移。

2. 兩個方向的分類錯誤

把自持誤當可導出(沒搬家):差異比對基準等不到任何同步來拯救——上游根本沒有「你上次看到什麼」的資料。結果是功能級事故(重複列印、記錄失聯)。

把可導出誤當自持(多搬家):對快照做手工搬移,短期看似加速對齊,實際引入了第二個資料來源——搬移邏輯與同步邏輯對同一份狀態各寫一次,兩者的分岔遲早出現。本案初版就搬了快照,後來發現搬進去的是舊內容(明細 id 還是死的),真正的新內容要等下一輪同步——手工搬移只製造了「看起來已對齊」的假象。

正確形態裡兩類狀態的程式碼形狀差異很清楚:自持狀態有一個「身份轉移通知」的入口方法;可導出狀態沒有任何遷移程式碼,只有既有的同步迴圈。

3. 加速對齊的正確位置:催一次同步,而不是代替同步

可導出狀態「等下一輪」的延遲若不可接受(本案:合併後使用者可能立刻取消品項,需要新明細 id),正確做法不是手工搬——是立即觸發一次同步。同一條同步路徑、只是提早跑。

這裡還埋著一個順序陷阱:同步路徑內部有依賴(輪詢用本地單據列表過濾資料),催同步之前要先讓依賴就緒(先同步列表、再刷新快照),否則催出來的是空結果、反而污染比對基準。「催同步」不是一個呼叫,是一小段有順序約束的編排——這個順序錯誤實際發生過,由流程測試首跑抓到(T.C6)。

4. 與讀模型階梯的關係

「可導出狀態」就是讀模型精神在前端的形態:它是上游真相的投影,正確性靠「可隨時重建」保證,而不是靠維護它的每一次增量修改都正確。判準也同構——讀模型的升級判準問「讀的形狀還是 aggregate(聚合根)的形狀」,這裡問「這份狀態的真相在誰手上」。真相在上游 → 投影化、別自持;真相只在本端 → 承認自持、為它設計完整的生命週期(包含身份轉移)。

長期方向:自持狀態越少越好。本案的追蹤記錄之所以必須自持,是因為上游不保存事件的處理進度——若上游未來補上這份資料,追蹤記錄就能降級為投影,「合併搬家」的程式碼隨之刪除。每一份自持狀態都是一筆負債,債主是所有會改變上游身份的操作。

5. 可複用的判準

  1. 上游身份轉移時,對每份以上游 id 為 key 的狀態問:「消失後能否單靠重新查詢完整重建?」——能,不搬;不能,通知搬家。
  2. 可導出狀態的加速對齊 = 催一次既有同步(注意內部順序依賴),不是另寫搬移邏輯。
  3. 自持狀態要有明確的「身份轉移」入口,且它是所有身份轉移操作(合併、拆分)的必經站。
  4. 盤點自持狀態清單、逐項問「上游為什麼不保存這個」——每消除一項,就少一類遷移 bug。

下一步