在幾乎所有經歷過三到五年快速迭代的後端專案中,你都能在資料庫或領域層找到一個令人聞風喪膽的「神級物件(God Object)」:

它通常叫 User、Order 或是 Account。

以一個電商系統的 User 類別為例:它最初只有簡單的 id、email 和 password_hash。但隨著業務膨脹,行銷團隊塞入了 referral_code 與 vip_points;金流團隊補上了 tax_id、credit_card_token 與 invoice_title;物流團隊掛上了 shipping_address、receiver_phone 與 door_code;風控團隊又加了 kyc_status、risk_score 與 last_login_ip。

五年後,這個 users 資料表膨脹成了擁有 78 個欄位、12 個索引的怪獸實體;對應的 User.ts 或 User.java 檔案超過 2,500 行,並在全系統數百個地方被強行引用。

技術團隊很快會嚐到苦果:任何人都不敢改動 User。

修改一個會員忘記密碼的邏輯,竟會意外導致倉儲系統列印揀貨單時發生 NullPointerException;新增一個海外免稅標記,卻讓註冊頁面的 API 回應超時。

這不是「代碼寫得不夠乾淨」的問題,而是架構師掉進了物件導向與關聯式資料庫最誘人的思維陷阱:全域統一資料模型幻覺(The Illusion of a Single Unified Data Model)。

限界上下文(Bounded Context)切割訊號與模型解構全景 頂部為解構導覽與四大切割訊號維度。中間並列展現三大獨立限界上下文(帳號認證、帳務計費、倉儲履約),各自維護純淨的實體模型。底部為解耦關鍵技術:以無外鍵的強型別 AccountId 取代物理外鍵硬關聯。邊界切割 4 大訊號:① 語義衝突 (Collision)|② 變更步調 (Cadence)|③ 團隊依賴 (Conway)|④ 交易不變量 (Invariant)認證子域Identity BC核心實體:Account【邊界內純淨模型】• id: AccountId (UUID)• email: EmailAddress (VO)• passwordHash: string• mfaSecret / status: Enum• roles: Set<Role>語義核心:身分驗證與授權防護變更特徵:低頻變更、高安全性要求排除噪音:絕無發票統編、收件地址支付財務Billing BC核心實體:Customer【邊界內純淨模型】• id: CustomerId (UUID)• accountId: AccountId (參照)• taxId: TaxIdentifier (統編)• paymentMethods: Card[]• invoiceAddress: Address語義核心:付款憑證、發票與帳目變更特徵:稅務法規驅動、強審計追蹤排除噪音:絕無密碼雜湊、宅配備註倉儲配送Fulfillment BC核心實體:Recipient【邊界內純淨模型】• id: RecipientId (UUID)• accountId: AccountId (參照)• contactPhone: PhoneNumber• deliveryAddress: Address• deliveryNotes / gateCode語義核心:實體交付、司機與包裹動線變更特徵:配送渠道多樣、變動極高頻排除噪音:絕無支付金鑰、登入 Token【實踐架構準則】消除跨庫外鍵約束,以「純 ID 參照(Identity Reference)」維持完全解耦• 嚴禁跨上下文物理 Foreign Key (FK) 關聯: 阻止資料庫層面產生跨模組幽靈鎖定、級聯更新與無休止的跨表 JOIN 泥潭。• 採用強型別 ID 值物件(AccountId / CustomerId): 各上下文透過非同步領域事件(Domain Events)或公開 API 溝通,保證各模組具備獨立演進與部署自由度。
圖解:限界上下文(Bounded Context)切割訊號與「萬能 User 物件」解構為純淨多模型架構

致命幻覺:世上不存在「唯一的真實 User」

人類的大腦傾向於為現實概念建立單一實體。在直覺上,「使用者就是使用者」,理所當然應該對應系統裡唯一的 User 類別與 users 資料表。

但領域驅動設計(DDD)給出了一個截然相反的殘酷真理:在不同的業務領域中,同一個名詞關注的本質完全不同,甚至根本就是不同的生命週期。

  • 認證與授權領域(Identity Context): 它根本不在乎這個人叫什麼名字、住在台北還是台南。它只在乎身分憑證(Credentials):帳號、密碼雜湊、MFA Token、被賦予的權限角色。
  • 帳務與計費領域(Billing Context): 它完全不關心使用者的密碼是什麼。它唯一在乎的是付款實體(Customer):統一編號、發票寄送抬頭、綁定的信用卡 Token、稅籍合規狀態。
  • 物流與履約領域(Fulfillment Context): 它連使用者的 Email 都不想知道。它只在乎收件受體(Recipient):精確收件地址、聯絡市話/手機、社區門禁管制備註與司機送貨時段。

當硬把這三種完全不同的生命週期捏進同一個物件時,系統必然爆發三大反模式:

1. 欄位 Nullable 爆炸(Nullable Explosion)

因為註冊階段不可能要求填寫統一編號與送貨門禁密碼,為了讓新用戶成功建檔,資料庫中超過 70% 的欄位被迫設定為 NULLABLE。這意味著領域模型徹底失去了「資料完整性保證」,任何業務邏輯在使用欄位前,都必須加上層層防禦性 if (user.taxId !== null)。

2. 驗證規則互相踩踏(Conflicting Invariants)

在認證領域,Email 是不可更改且唯一的強制主鍵;但在物流領域,收件通知 Email 可能隨每筆包裹臨時變更,甚至允許留空(只留手機 SMS)。當兩者綁在同一實體時,驗證邏輯必然在不同情境中打架。

3. 併發狀態機死鎖(State Machine Entanglement)

使用者的狀態到底是「Active(已驗證)」、「Delinquent(欠費催繳中)」還是「Banned(被風控凍結)」?把三個維度的狀態塞進一個 status: string,會直接導致狀態轉移圖爆炸,出現 ACTIVE_BUT_DELINQUENT_SUSPENDED 這類無法維護的怪胎枚舉。


邊界切割的 4 大工程訊號

要解開 God Object 的詛咒,架構師必須拿起 DDD 的戰略手術刀——限界上下文(Bounded Context)。

限界上下文是領域模型的語義邊界。在邊界內部,任何領域名詞(Ubiquitous Language)都有且僅有唯一的確切含義;而走出邊界,這個概念就不復存在或由另一個獨立模型接管。

那麼,何時該切開邊界?你可以透過以下 4 大客觀的工程訊號來做決策:

訊號一:語義衝突(Semantic Collision)

當同一個名詞在不同團隊口中出現分歧時,就是邊界切割的絕對訊號。 例如「訂單(Order)」:

  • 在**銷售上下文(Sales)**中:訂單是客戶挑選的商品清單與預估金額,狀態只有「待付款」、「已建立」。
  • 在**倉儲上下文(Warehouse)**中:訂單變成了「揀貨履約單(Pick List)」,核心是貨架位置(Aisle/Bin)、包裹尺寸與實體重量,根本不需要商品原價。
  • 在**財務記帳上下文(General Ledger)**中:訂單只是一連串具有會計科目的「分錄憑證(Journal Entries)」,完全不需要知道具體買了哪款顏色的 T-shirt。

停止規則: 只要一個名詞在與不同業務關係人溝通時,需要補上一句「我指的訂單不是那個揀貨單,而是⋯⋯」,這兩個概念就絕對不能共用同一個類別與資料表。

訊號二:變更步調(Change Cadence)

檢查代碼庫的變更歷史(Git Churn)。 認證授權(Identity)是基礎設施,半年可能都不會修改一次欄位結構;但行銷促銷(Marketing)每週都要根據營運活動新增會員標籤、積點規則與抽獎資格。

讓每週高頻變更的業務代碼,與半年前通過資安滲透測試的認證實體綁在同一個資料表,結果就是行銷團隊上線一次,全站使用者的登入功能就得跟著冒險重啟一次。變更頻率脫鉤,就是物理邊界切分的明確指標。

訊號三:組織與團隊依賴(Conway’s Law)

康威定律(Conway’s Law)指出:系統的架構往往映射出組織的溝通結構。 如果「會員表」的修改,需要同時發 PR 給身分安全小組、金流結算小組與物流運營小組審查,並且經常在 GitHub 上引發跨團隊的 Git Merge Conflict,這個類別就已經違反了團隊自治原則。邊界應當由單一自治團隊全權擁有。

訊號四:交易不變量範圍(Transaction Invariant Boundary)

這是在資料庫層面最硬核的技術判準。 問自己一個問題:「當使用者修改通訊地址時,這個動作需要跟他的信用卡扣款、或是重設密碼放在同一個資料庫交易(ACID Transaction)裡原子提交嗎?」

答案顯然是否定的。如果兩者不需要強一致性,它們就不屬於同一個交易不變量邊界(Transactional Boundary)。將它們強制放在同一個 DB 交易或關聯表中,只會增加資料庫行鎖(Row Lock)的鎖定時間與死鎖(Deadlock)機率。


TypeScript 重構實踐:從 God Object 到三位一體

接下來,我們以具體的代碼重構,將充斥 70 多個欄位的龐大 User 上帝物件,用手術刀精準拆解為三個獨立的限界上下文。

重構前:災難級的 God Object

// ❌ 反模式:上帝實體(揉雜認證、金流、收件、風控)
export class User {
  // 認證關注
  public id: string;
  public email: string;
  public passwordHash: string;
  public roles: string[];

  // 帳務關注 (Nullable 泛濫)
  public taxId?: string;
  public creditCardToken?: string;
  public invoiceTitle?: string;
  public billingAddress?: string;

  // 物流關注 (業務雜訊)
  public shippingAddress?: string;
  public recipientName?: string;
  public recipientPhone?: string;
  public gateCode?: string;

  // 風控關注
  public riskScore: number;
  public isKycVerified: boolean;

  constructor(data: any) {
    // 充滿防禦性檢查與潛在的驗證衝突
    this.id = data.id;
    this.email = data.email;
    this.passwordHash = data.passwordHash;
    this.roles = data.roles || [];
    // ...
  }
}

重構後:三個獨立自治的限界上下文模型

我們將其解構為三個邊界內純淨的 Aggregate Root,每個上下文只管自己的事情:

1. 身分與認證上下文(Identity Context)

// ✅ 認證領域:只關心帳號憑證與防護
export type AccountId = string & { readonly __brand: unique symbol };

export class Account {
  private constructor(
    public readonly id: AccountId,
    private email: string,
    private passwordHash: string,
    private roles: ReadonlySet<string>,
    private isSuspended: boolean,
  ) {}

  public static create(id: AccountId, email: string, hash: string): Account {
    if (!email.includes("@")) throw new Error("Invalid email address");
    return new Account(id, email, hash, new Set(["USER"]), false);
  }

  public verifyPassword(inputHash: string): boolean {
    if (this.isSuspended) return false;
    return this.passwordHash === inputHash;
  }
}

2. 帳務與計費上下文(Billing Context)

// ✅ 帳務領域:只關心付款實體與稅務發票合規
export type CustomerId = string & { readonly __brand: unique symbol };

export class Customer {
  private constructor(
    public readonly id: CustomerId,
    public readonly accountId: AccountId, // 僅透過強型別 ID 參照,解除物理強綁定
    private taxId: string | null,
    private defaultCardToken: string,
    private invoiceAddress: string,
  ) {}

  public static enroll(
    id: CustomerId,
    accountId: AccountId,
    cardToken: string,
    address: string,
  ): Customer {
    if (!cardToken)
      throw new Error("Payment token is strictly required for Billing");
    return new Customer(id, accountId, null, cardToken, address);
  }

  public updateTaxIdentifier(newTaxId: string): void {
    // 嚴格依照財務統編邏輯驗證(非 Nullable 亂象)
    if (!/^\d{8}$/.test(newTaxId))
      throw new Error("Invalid Taiwan Tax ID format");
    this.taxId = newTaxId;
  }
}

3. 物流與履約上下文(Fulfillment Context)

// ✅ 物流領域:只關心包裹實體交付動線
export type RecipientId = string & { readonly __brand: unique symbol };

export interface DeliveryInstruction {
  address: string;
  contactPhone: string;
  gateCode?: string;
  preferredTimeSlot: "MORNING" | "AFTERNOON" | "EVENING";
}

export class Recipient {
  private constructor(
    public readonly id: RecipientId,
    public readonly accountId: AccountId,
    private name: string,
    private instruction: DeliveryInstruction,
  ) {}

  public updateDeliveryInstruction(newInstruction: DeliveryInstruction): void {
    if (!newInstruction.contactPhone.startsWith("09")) {
      throw new Error(
        "Mobile phone is strictly required for parcel delivery notifications",
      );
    }
    this.instruction = newInstruction;
  }
}

關聯解耦的靈魂:消除跨庫外鍵,落實「純 ID 參照」

許多工程師在嘗試拆分限界上下文時,常常在資料庫層面功虧一簣:

「雖然我寫成了 Account 與 Customer 兩個類別,但我的 billing_customers 表上,仍然建了 FOREIGN KEY (account_id) REFERENCES identity_accounts(id) ON DELETE CASCADE 啊!」

這正是最危險的半套重構。

物理外鍵(Foreign Key)帶來的隱形毒藥

當你在資料庫層加上實體外鍵時:

  1. 阻斷模組獨立演進: 當身分模組進行資料庫分庫分表(Sharding)、或遷移至專用 Auth 服務(如 Supabase/Clerk)時,資料庫的物理外鍵會讓遷移成本呈指數級暴增。
  2. 死鎖與鎖放大: 當 identity_accounts 進行行鎖更新時,子表的 billing_customers 可能會被資料庫引擎掛上共享鎖(S-Lock),在高併發交易下引發連環死鎖。
  3. 刪除級聯災難: 使用者註銷帳號,意外觸發了金流表的級聯刪除(Cascade Delete),直接導致公司的會計發票紀錄被清空,觸犯稅法。

架構解法:純 ID 參照(Identity Reference)

在 DDD 的上下文邊界之間:

  • 實體只保留其他上下文實體的純 ID(例如 accountId: AccountId)。
  • 資料庫層禁止跨模組的外鍵約束(No cross-boundary FK)。
  • 跨模組一致性依賴領域事件(Domain Events): 當 Account 註銷時,發布 AccountClosedEvent;帳務模組訂閱該事件後,將對應的 Customer 標記為封存或去識別化,但保留合規所需的十年交易憑單。

這樣做,每個限界上下文才能真正達成內部高內聚、邊界零耦合、具備獨立資料庫擴展與演進自由度的架構終極目標。


下一步:在邊界之間築起城牆

把 God Object 切開、定義了各自的限界上下文後,現實世界的另一個殘酷挑戰隨之而來:

上下文之間不可能老死不相往來。

業務流程始終需要跨上下文協同——銷售需要通知倉儲發貨,金流需要通知發票開立,甚至需要對接外部不可控的第三方金流 API(如 Stripe 或綠界科技)。

當這些外部資料湧入時,你的核心領域模型如何不被外部髒資料「餵毒」污染?

在下一篇中,我們將深入剖析戰略設計的第三大支柱:《拒絕被上游餵毒:上下文映射(Context Mapping)與防腐層(ACL)的工程模式》,拆解 7 種上下文拓撲關係,並帶來生產級的 TypeScript 防腐層架構代碼。