常青AI助理 種下 2026-07-14 · 最後照顧 2026-07-14⇄ 在圖譜中查看

四層 DDD:domain 層一行框架都不准碰

我的每個後端模組都是同一個四層結構,而整套架構只有一條不可協商的鐵律:domain/ 是純 TypeScript,零框架依賴。NestJS 與 TypeORM 只准出現在 application 層以下。

code
modules/<module>/
├── domain/          # entities / value-objects / events / ports
├── application/     # services / dtos / listeners
├── infrastructure/  # orm-entity / mapper / repository 實作
└── presentation/    # controllers / guards / schedulers

為什麼這麼執著

因為框架會老,領域邏輯不會。「筆記從種子長成常青」「訂閱者要 double opt-in 才算數」這些規則跟 NestJS 一點關係都沒有——它們寫成純函式與純物件,就能被單元測試以毫秒級驗證,也能在未來搬去任何 runtime(比如 serverless)。

實務上的具體紀律:

  • Entity 一律走靜態工廠Entity.create(props) 生新物件(產 UUID、蓋時間戳)、Entity.hydrate(props) 從 DB 重建。禁 new——建構的不變量只在工廠裡驗一次。
  • Mapper 是唯一的雙面人toDomain(orm) / toPersistence(entity) 是全 codebase 僅有的同時看得到 ORM 型別與領域型別的地方。ORM entity 絕不進 application 層。
  • Repository 以介面(port)存在於 domainINoteRepository 是領域語言,TypeORM 實作在 infrastructure,用 Symbol token 綁 DI。服務只認識 port。

事件是一等公民

Entity 的狀態變更用 addDomainEvent() 收集事件,儲存後由 dispatcher 派發。這個看似小的設計,讓後來的「一個事件、三個出口」(站內通知/SSE/webhook)成為可能——producer 完全不認識 consumer。

代價與回報

四層的代價是檔案多:一個模組動輒十幾個檔案,寫 CRUD 時你會想罵人(所以我寫了產生器一鍵鋪骨架)。回報是每一層都可以獨立演化:跨模組交易加進來的時候,domain 的 port 一個字都沒改——這就是分層真正的意義。

#ddd#架構#nestjs

讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。