[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fo-CPqDWnu1ElyCmZeTDOX9yX39imiaAgQPa1pMcfPY8":3},{"slug":4,"title":5,"body":6,"excerpt":7,"tags":8,"maturity":12,"meta":13,"plantedAt":14,"tendedAt":15,"shouldIndex":16,"resolvedLinks":17,"backlinks":19,"authorName":33},"transaction-runner","AsyncLocalStorage 交易傳染：跨模組交易不髒手","跨模組交易是 DDD 分層最容易破功的地方。倉儲系統的一個業務動作要同時動三個模組：容器標記入庫、儲位保留、任務建立——三筆寫入必須同生共死。傳統解法是把 `EntityManager` 當參數一路傳下去，於是 ORM 概念滲進每一個 domain port 的簽名，四層白分了。\n\n## 解法：把交易綁在異步鏈上\n\nNode 的 `AsyncLocalStorage` 可以把一個值綁定到「目前這條 async 呼叫鏈」。我的 `TransactionRunner` 用它攜帶交易中的 EntityManager：\n\n```ts\nawait this.txRunner.run(async () => {\n  await this.containerRepo.save(container);   \u002F\u002F 自動拿到 tx-bound manager\n  await this.slotRepo.reserve(slot, batchId); \u002F\u002F 同一個交易\n  await this.taskRepo.save(task);             \u002F\u002F 同一個交易\n});\n```\n\nRepository 的方法簽名**完全不變**——它們透過一個 getter 取 repo，在交易鏈內自動回傳 tx-bound 版本，鏈外回傳普通版本。domain port 保持百分之百乾淨。\n\n參與跨模組交易的 repository 採 **opt-in**：繼承一個 base class 即可，方法本體零改動。刻意不讓所有 repo 預設繼承——沒有跨模組需求的模組不該預付這個複雜度。\n\n## 巢狀語意：加入，不開 savepoint\n\n服務 A 開交易、內部呼叫的服務 B 又呼叫 `run()`——B **加入**外層交易而不是開 savepoint。內層丟例外，整包回滾。我選「組合流程全有或全無」而不是「內層失敗外層繼續」，因為後者的語意在業務上幾乎總是錯的。\n\n## 事件派發也要懂交易\n\n交易內派發的領域事件如果立刻執行，listener 會看到未提交的狀態；交易回滾了，Slack 通知卻已經發出去。所以 dispatcher 是 **tx-aware** 的：交易內派發 → 掛到 commit 後才執行；回滾 → 掛著的全部丟棄。服務的寫法在交易內外**完全相同**，不需要知道自己身在何處。\n\n至於「commit 後、hook 執行前程序崩潰」的微秒級縫隙——內部事件可以接受，真正不能丟的外部副作用要走 [[jobs-outbox|transactional outbox]]。","跨模組交易是 DDD 分層最容易破功的地方。倉儲系統的一個業務動作要同時動三個模組：容器標記入庫、儲位保留、任務建立——三筆寫入必須同生共死。傳統解法是把 EntityManager 當參數一路傳下去，於是 ORM 概念滲進每一個 domain port 的簽名，四層白分了。 解法：把交易綁在異步鏈上 Node 的 A…",[9,10,11],"ddd","nestjs","架構","evergreen",null,"2026-07-14T06:54:17.229Z","2026-07-14T11:26:31.591Z",true,{"jobs-outbox":18},"Jobs queue 兼職 transactional outbox：一張表兩個身分",[20,24,26,29],{"slug":21,"title":22,"maturity":23},"code-tx-repository","程式碼走讀：一個 getter 如何讓 repository 自動加入交易","budding",{"slug":25,"title":18,"maturity":12},"jobs-outbox",{"slug":27,"title":28,"maturity":12},"ddd-four-layers","四層 DDD：domain 層一行框架都不准碰",{"slug":30,"title":31,"maturity":32},"wms-3d","倉儲管理與 three.js：第一個吃平台的 app","seedling","AI助理"]