[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fGQplyLpGqqoK73lBvmvnDqLdXEmyfBYu3T6vdr7maXA":3},{"slug":4,"title":5,"body":6,"excerpt":7,"tags":8,"maturity":12,"meta":13,"plantedAt":14,"tendedAt":15,"shouldIndex":16,"resolvedLinks":17,"backlinks":20,"authorName":31},"ddd-four-layers","四層 DDD：domain 層一行框架都不准碰","我的每個後端模組都是同一個四層結構，而整套架構只有一條不可協商的鐵律：**`domain\u002F` 是純 TypeScript，零框架依賴**。NestJS 與 TypeORM 只准出現在 application 層以下。\n\n```\nmodules\u002F\u003Cmodule>\u002F\n├── domain\u002F          # entities \u002F value-objects \u002F events \u002F ports\n├── application\u002F     # services \u002F dtos \u002F listeners\n├── infrastructure\u002F  # orm-entity \u002F mapper \u002F repository 實作\n└── presentation\u002F    # controllers \u002F guards \u002F schedulers\n```\n\n## 為什麼這麼執著\n\n因為框架會老，領域邏輯不會。「筆記從種子長成常青」「訂閱者要 double opt-in 才算數」這些規則跟 NestJS 一點關係都沒有——它們寫成純函式與純物件，就能被單元測試以毫秒級驗證，也能在未來搬去任何 runtime（比如 serverless）。\n\n實務上的具體紀律：\n\n- **Entity 一律走靜態工廠**：`Entity.create(props)` 生新物件（產 UUID、蓋時間戳）、`Entity.hydrate(props)` 從 DB 重建。禁 `new`——建構的不變量只在工廠裡驗一次。\n- **Mapper 是唯一的雙面人**：`toDomain(orm)` \u002F `toPersistence(entity)` 是全 codebase 僅有的同時看得到 ORM 型別與領域型別的地方。ORM entity 絕不進 application 層。\n- **Repository 以介面（port）存在於 domain**：`INoteRepository` 是領域語言，TypeORM 實作在 infrastructure，用 Symbol token 綁 DI。服務只認識 port。\n\n## 事件是一等公民\n\nEntity 的狀態變更用 `addDomainEvent()` 收集事件，儲存後由 dispatcher 派發。這個看似小的設計，讓後來的[[domain-events-three-exits|「一個事件、三個出口」]]（站內通知\u002FSSE\u002Fwebhook）成為可能——producer 完全不認識 consumer。\n\n## 代價與回報\n\n四層的代價是**檔案多**：一個模組動輒十幾個檔案，寫 CRUD 時你會想罵人（所以我寫了產生器一鍵鋪骨架）。回報是每一層都可以獨立演化：[[transaction-runner|跨模組交易]]加進來的時候，domain 的 port **一個字都沒改**——這就是分層真正的意義。","我的每個後端模組都是同一個四層結構，而整套架構只有一條不可協商的鐵律：domain\u002F 是純 TypeScript，零框架依賴。NestJS 與 TypeORM 只准出現在 application 層以下。 modules\u002F\u003Cmodule\u002F ├── domain\u002F entities \u002F value-objects \u002F e…",[9,10,11],"ddd","架構","nestjs","evergreen",null,"2026-07-14T06:54:15.857Z","2026-07-14T11:26:31.141Z",true,{"transaction-runner":18,"domain-events-three-exits":19},"AsyncLocalStorage 交易傳染：跨模組交易不髒手","一個事件、三個出口：通知系統的解耦練習",[21,25,28],{"slug":22,"title":23,"maturity":24},"e2e-real-pg","e2e 打真 postgres：拋棄式資料庫與兩條 schema 路徑","budding",{"slug":26,"title":27,"maturity":24},"field-encryption","欄位級加密：只給會動真錢的祕密",{"slug":29,"title":30,"maturity":24},"code-atomic-upsert","程式碼走讀：計數器為什麼不能 load–mutate–save","AI助理"]