YiBi Dev Docs
架構決策 (ADR)

Adopt Clean Architecture repository pattern with full domain entity separation; incremental migration

Context

PR #486 review 期間發現:backend/CLAUDE.md 架構圖宣稱 domain/ # Entities + repository interfaces 並規定「never import upward」,但 codebase 實況相反——26/32 個 application service 檔直接 from src.infrastructure.persistence.models import ... 並以 db.execute(select(Model)) 操作 ORM;domain/interfaces/ 只有 storage/TTS 兩個 provider port,repository interface 與 domain entity 從未存在;house style (.claude/rules/22-backend-service-model.md)甚至明文規定 service 第一參數是 db: AsyncSession

文件宣言(Clean Architecture)與 codebase 慣例(transaction script)互相矛盾, 必須二擇一收斂。

決策過程註記:本 ADR 第一版由 agent 擅自裁決「維持現狀、不引入 repository 抽象」 並錯誤標記為 accepted——該裁決無效。Tech Lead(howie)的實際裁決如下,方向相反。 架構方向決策屬 Tech Lead,agent 發現矛盾時的正確動作是回報選項,不是代裁。

Decision

結論:採用 Clean Architecture repository pattern,含完整 domain entity 分離; 以漸進遷移落地(新 code 立即生效、存量分批),admin services 標 legacy 豁免。 縱使工程量大(估 10-14 週),架構債發現後應修正——以獨立 PR 序列進行, 不阻擋既有 feature PR(PR #486 依當時慣例 merge,pilot 遷移隨後)。

具體規範:

  1. Domain entitydomain/entities/<aggregate>.py,stdlib @dataclass (domain 層禁 framework deps,不用 Pydantic / SQLAlchemy)。遷移範圍內的 aggregate(~20 個 model)逐批建立。
  2. Repository interfacedomain/interfaces/repositories/<aggregate>.pytyping.Protocol(沿用 domain/interfaces/storage.py 既有慣例)。 Per-aggregate 具名查詢,禁止 generic Repository[T](會把 query 語意洩漏進 domain)。回傳 domain entity,不回傳 ORM model。
  3. 實作與 mapperinfrastructure/persistence/repositories/<aggregate>.py—— repository 實作 + ORM↔entity mapper 同檔。Repository 永不 commit、只 flush
  4. Unit of Workdomain/interfaces/uow.py 定義 IUnitOfWork(Protocol) (聚合 repositories + commit/rollback);infrastructure/persistence/uow.py 實作 SqlAlchemyUnitOfWork(共享單一 AsyncSession)。交易不變量(如 persona_binding 寫入 + families.config_version bump 必須同 transaction)由 UoW 保證。
  5. DI 鏈presentation/dependencies.py 提供 get_uow;service 簽名由 (db: AsyncSession, ...) 改為 (uow: IUnitOfWork, ...);Celery worker 以 async_session_factory()SqlAlchemyUnitOfWork;測試 conftest 提供 uow fixture(仍打 in-memory SQLite 真實作——integration test 策略不變,entity 純邏輯 另得 unit test 能力)。
  6. 生效規則:基礎建設 PR(PR-0)merge 後,所有新 service 必須走 repository; 存量依遷移 change 的批次排程處理,未遷移者維持原樣、不得混搭。
  7. Legacy 豁免:6 個 admin_*_service.py 凍結在 AsyncSession 風格 (等 admin 介面重寫時一併遷移);無 DB 的 service(如 audio proxy)不在範圍。

遷移工作計畫(批次序列、風險控管、任務拆解)記錄於 spectra change 0027-clean-architecture-repository-migration

Alternatives Considered

A. 維持 transaction script(service 直接操作 ORM),只修文件

  • 優點:零遷移成本;integration test 已提供正確性保證。
  • 缺點:application 永久耦合 SQLAlchemy;業務邏輯無法做無 DB 的毫秒級 unit test; 查詢邏輯跨 service 重複無收斂機制;與團隊的長期架構方向不符。
  • 被否決(Tech Lead 裁決):架構債發現即修正,不以「工程大」為由迴避。

B. 折衷:repository 回傳 ORM model(不建 domain entity)

  • 優點:遷移量約減半;查詢集中化的好處先拿到。
  • 缺點:domain 仍依賴 SQLAlchemy 型別,分層只做半套;之後補 entity 要二次遷移。
  • 被否決:選完整 entity 分離,一次到位。

C. 一次性大重構(big-bang)

  • 缺點:~88 觸點單一 PR 無法 review;與進行中 feature 分支大面積衝突。
  • 被否決:採批次 PR(基礎建設 → pilot → 高共用 aggregate → 長尾)。

Consequences

  • 正面:分層合規;業務邏輯可脫離 DB 測試;查詢收斂於 repository(消除跨 service 重複);文件與 codebase 一致。
  • 代價(接受):~20 aggregate 的 entity + mapper + interface 維護成本;估 10-14 週 的存量遷移期;遷移期內兩種風格並存(以 rule 22 的批次清單管理,禁止單檔混搭)。
  • 風險控管:repository 方法爆炸 → review 守「具名查詢需 ≥2 呼叫端或表達 domain 概念」;ORM lazy-load 洩漏 → repository 查詢一律顯式 selectinload;mapper 漂移 → entity 欄位變更必同步 mapper 測試;feature 衝突 → 小批次、先遷低變動 aggregate。
  • 重新評估條件:遷移完成前若 MVP 商業優先級要求凍結重構,可在批次邊界暫停 (已遷批次不回退);屆時更新本 ADR 的 migration status 註記。

On this page