Bounded Context(限界上下文)
是什麼?
Bounded Context 是 DDD 戰略設計的核心概念。它定義了一個語意邊界,在這個邊界內,所有的術語(Ubiquitous Language)都有明確且一致的定義。不同的 Bounded Context 可以對同一個現實世界概念有不同的模型。
ℹ️DDD 戰略設計
Bounded Context 是 DDD 戰略設計(Strategic Design)的基礎。如果說 Entity、Value Object 是「怎麼寫程式」,Bounded Context 就是「怎麼切系統」。
核心觀念
- 語意邊界:同一個詞在不同 BC 裡有不同的模型。「Customer」在銷售 BC 有聯絡方式和購買紀錄,在物流 BC 只有收件地址
- 一個 BC 一個 Ubiquitous Language:每個 BC 內部維護自己的統一語言,跨 BC 的同名詞彙互不干擾
- 獨立的模型:每個 BC 有自己的 Entity、Value Object、Aggregate,不與其他 BC 共享 Domain Model
- BC 和微服務的關係(BC 不等於微服務):BC 是邏輯邊界(程式碼和模型的劃分),微服務是部署邊界(如何打包和運行)。三種常見對應方式:(1) 一個 BC 對應一個微服務(最常見);(2) 一個 BC 拆成多個微服務(BC 太大時,如「訂單 BC」拆成「訂單管理服務」+「訂單查詢服務」);(3) 多個 BC 部署在同一個服務中(初期單體,用模組隔離,未來可拆)
- 明確的邊界介面:BC 之間透過定義好的介面溝通。常見方式:同步用 REST API 或 gRPC,非同步用 Domain Event + Message Queue。選擇依據:如果下游可以容忍幾秒的延遲就用非同步事件(更鬆耦合),如果需要即時回應就用同步 API
常見誤區
⚠️常見誤區
誤區一:試圖建立一個「統一的 Domain Model」跨越整個系統。這會導致模型越來越臃腫,充滿 if-else 來處理不同場景。每個 BC 有自己的模型才對。
誤區二:把 Bounded Context 等同於微服務。BC 是邏輯邊界,微服務是部署邊界。一個 BC 可以拆成多個微服務,也可以多個 BC 部署在同一個服務中。
誤區三:BC 之間共享資料庫表。每個 BC 應該擁有自己的資料儲存,避免透過資料庫造成隱性耦合。
劃分流程
識別子領域
分析業務,找出核心域、支撐域、通用域
發現語意分歧
找出同名但不同義的概念(如 Product 在銷售和倉儲的差異)
劃定邊界
把語意一致的概念劃入同一個 Bounded Context
定義上下文地圖
畫出各 BC 之間的關係(Context Map)
設計整合方式
決定 BC 之間用 API、Events、Shared Kernel 等方式溝通
程式碼範例
C# 版本
// === 銷售 BC (Sales Context) ===
namespace Sales.Domain
{
// 銷售 BC 的 Customer:有購買紀錄和聯絡方式
public class Customer : Entity<Guid>
{
public string Name { get; private set; }
public Email Email { get; private set; }
public CustomerTier Tier { get; private set; } // VIP、一般
private readonly List<OrderId> _orderHistory = new();
public Customer(Guid id, string name, Email email) : base(id)
{
Name = name;
Email = email;
Tier = CustomerTier.Regular;
}
public void UpgradeToVip()
{
if (_orderHistory.Count >= 10)
Tier = CustomerTier.VIP;
}
}
}
// === 物流 BC (Shipping Context) ===
namespace Shipping.Domain
{
// 物流 BC 的 Customer:只關心收件資訊
public class Customer : Entity<Guid>
{
public string RecipientName { get; private set; }
public Address ShippingAddress { get; private set; }
public PhoneNumber Phone { get; private set; }
public Customer(Guid id, string recipientName, Address address, PhoneNumber phone)
: base(id)
{
RecipientName = recipientName;
ShippingAddress = address;
Phone = phone;
}
}
}
// === Anti-Corruption Layer:轉換不同 BC 的模型 ===
namespace Shipping.Infrastructure
{
public class SalesCustomerAdapter
{
public Shipping.Domain.Customer ToShippingCustomer(
Sales.Contracts.CustomerDto salesCustomer,
Address shippingAddress)
{
return new Shipping.Domain.Customer(
id: salesCustomer.Id,
recipientName: salesCustomer.Name,
address: shippingAddress,
phone: new PhoneNumber(salesCustomer.Phone)
);
}
}
}TypeScript 版本
// === 銷售 BC ===
// sales/domain/customer.ts
class SalesCustomer {
constructor(
public readonly id: string,
public name: string,
public email: Email,
public tier: CustomerTier = CustomerTier.Regular,
private orderHistory: string[] = [],
) {}
upgradeToVip(): void {
if (this.orderHistory.length >= 10) {
this.tier = CustomerTier.VIP;
}
}
}
// === 物流 BC ===
// shipping/domain/customer.ts
class ShippingCustomer {
constructor(
public readonly id: string,
public recipientName: string,
public shippingAddress: Address,
public phone: PhoneNumber,
) {}
}
// === ACL:模型轉換 ===
// shipping/infrastructure/sales-customer-adapter.ts
function toShippingCustomer(
salesCustomer: SalesCustomerDto,
address: Address,
): ShippingCustomer {
return new ShippingCustomer(
salesCustomer.id,
salesCustomer.name,
address,
new PhoneNumber(salesCustomer.phone),
);
}Python 版本
# === 銷售 BC ===
# sales/domain/customer.py
class SalesCustomer:
def __init__(self, id: str, name: str, email: Email):
self.id = id
self.name = name
self.email = email
self.tier = CustomerTier.REGULAR
self._order_history: list[str] = []
def upgrade_to_vip(self) -> None:
if len(self._order_history) >= 10:
self.tier = CustomerTier.VIP
# === 物流 BC ===
# shipping/domain/customer.py
class ShippingCustomer:
def __init__(self, id: str, recipient_name: str,
shipping_address: Address, phone: PhoneNumber):
self.id = id
self.recipient_name = recipient_name
self.shipping_address = shipping_address
self.phone = phone
# === ACL ===
# shipping/infrastructure/sales_customer_adapter.py
def to_shipping_customer(
sales_customer: SalesCustomerDto, address: Address
) -> ShippingCustomer:
return ShippingCustomer(
id=sales_customer.id,
recipient_name=sales_customer.name,
shipping_address=address,
phone=PhoneNumber(sales_customer.phone),
)概念圖
此圖展示三個 BC 如何各自定義「Customer」的不同模型,以及 BC 之間的溝通方式。Sales 和 Shipping 之間透過 ACL 做模型轉換,Sales 和 Billing 之間透過 Domain Event 非同步通訊。這就是 Bounded Context 的核心價值——同名概念各自建模,互不污染。
BC 和微服務的對應策略
| 階段 | 策略 | 說明 | |------|------|------| | 早期/新專案 | 單體 + 模組隔離 | 每個 BC 是一個 namespace/project,共用一個部署單位。先把邊界劃對,以後再拆 | | 中期/需求穩定 | 逐步拆分 | 把最獨立、最常變動的 BC 先拆成獨立服務,其餘留在單體 | | 成熟期/大團隊 | 一個 BC 一個服務 | 每個團隊負責一個 BC,獨立部署、獨立資料庫 |
核心原則:先劃對 BC 邊界,再決定部署方式。先拆微服務但 BC 邊界劃錯,比沒拆更糟——你會得到「分散式單體」(Distributed Monolith)。
實戰補充
💡資深開發者筆記
在 C# 單體應用中,用 namespace 或 Project 來隔離 Bounded Context 是最實用的做法。例如 Sales.Domain、Shipping.Domain、Billing.Domain 各自是獨立的專案,共用同一個 Solution。
劃分 BC 的一個實用技巧:當你發現一個 Entity 的屬性和行為在不同場景中差異很大時(如 Customer 在銷售和物流),就是需要拆分成不同 BC 的訊號。
BC 之間的資料同步用 Domain Events 或 Integration Events。千萬不要讓兩個 BC 共用同一張資料表 — 這會讓兩邊的模型演化被綁死在一起。
理解測驗
🤔 為什麼「Customer」在銷售 BC 和物流 BC 可以是不同的模型?
🤔 Bounded Context 和微服務的關係是?
🤔 以下哪個做法違反了 Bounded Context 的原則?
重點整理
💡一句話記住
口訣:「BC 是語意的牆、不是伺服器的牆」—— 先劃對邊界再談部署,順序不能反。
| 概念 | 說明 | |------|------| | 語意邊界 | 同名概念在不同 BC 有不同模型 | | 獨立模型 | 每個 BC 有自己的 Entity/VO/Aggregate | | 獨立儲存 | 每個 BC 有自己的資料庫或 Schema | | BC vs 微服務 | 邏輯邊界 vs 部署邊界,可以不對齊 | | ACL | 跨 BC 溝通時用 Anti-Corruption Layer 轉換模型 |