🌏 Read this article in English
你是否曾經歷過這樣的場景:產品經理說「支援新的折扣規則」,工程師開始修改 OrderService、DiscountService、PromotionService、PricingService、CustomerService…改著改著才意識到,這個簡單的需求竟然觸及了 5 個不同的模組。
這不是個別案例。這是系統設計缺陷的症狀。
領域驅動設計(Domain-Driven Design,簡稱 DDD)不是新概念,但許多工程師對它的理解停留在「複雜」和「過度設計」的刻板印象。實際上,DDD 的目的恰恰相反:透過清晰的邊界與角色定義,讓複雜系統反而變得簡單。
本文不談 DDD 的歷史或學術定義。我們從痛點出發,透過三個不同角色的視角,理解 DDD 如何一步步解決系統設計中的根本問題。
核心概念:DDD 的四大支柱
在深入故事前,先建立共同語言。DDD 的核心由四個緊密相連的概念構成:
1. 聚合根(Aggregate)- 業務規則的守門人
什麼是聚合根?
聚合根是一個領域對象,它負責確保其範圍內的所有業務規則都得以滿足。簡單說:聚合根就是一個自我保護的實體,它不允許違反業務規則的操作發生。
以訂單系統為例:Order(訂單)是聚合根。它管理著 OrderLineItem(訂單項目)、OrderDiscount(訂單折扣)等子對象。重要的是,Order 不會允許你直接修改 LineItem 的價格。所有的業務檢驗(折扣是否有效、庫存是否充足、客戶額度是否足夠)都由 Order 自己負責。
為什麼重要?
無聚合根的設計會導致業務邏輯分散: – Discount 服務驗證折扣有效性 – Customer 服務驗證客戶額度 – Inventory 服務驗證庫存 – Order 服務協調上述所有驗證
結果是:修改任何一個業務規則,都需要改多個地方。
有聚合根的設計集中化邏輯:
Order.applyDiscount(code) { // Order 內部檢驗所有規則 // 如果規則改變,只改 Order 類 }
聚合根的邊界與保護
graph LR
User["🧑 外部調用者"]
subgraph Agg ["📦 Order Aggregate"]
Root["Order Aggregate Root"]
Child1["LineItem Entity 1"]
Child2["LineItem Entity 2"]
Val["OrderDiscount Value Object"]
Rules["✅ 驗證規則
• 庫存檢查
• 折扣合法性
• 價格有效性
• 客戶額度"]
end
User -->|只能調用
applyDiscount 方法| Agg
Root --> Child1
Root --> Child2
Root --> Val
Root --> Rules
BadWay["❌ 不允許
直接修改
LineItem.price"]
User -.->|禁止| BadWay
style Agg fill:#f3e5f5
style Root fill:#9c27b0,color:#fff
style Rules fill:#ce93d8
style BadWay fill:#ffcdd2
2. 通用語言(Ubiquitous Language)- 跨角色溝通
什麼是通用語言?
開發者、產品經理、業務分析師用同一套詞彙討論系統。不是「我們的 Order Service」,而是「訂單聚合根」。不是「user table」,而是「客戶」。
這聽起來簡單,但威力巨大。當所有人都用「訂單無法在已支付後修改」而不是「post_status = 2 時不允許更新」時,理解變得即時且準確。
3. 限界上下文(Bounded Context)- 明確的邊界
什麼是限界上下文?
複雜系統往往跨越多個業務領域。購物車系統與訂單系統看似接近,但它們對「客戶」有不同的理解:
- 購物車 BC:客戶 = ID + 購物偏好 + 瀏覽歷史
- 訂單 BC:客戶 = ID + 收貨地址 + 支付方式
如果你試圖用同一個 Customer 對象服務兩個系統,結果就是:修改收貨地址時,購物車系統不需要但也被迫存儲了它。
DDD 的解決方案:每個限界上下文有自己的 Customer 定義。它們透過事件通信,而不是共享資料庫。
多個 Bounded Context 的獨立設計
graph LR
subgraph Cart["🛒 Shopping Cart BC
購物車上下文"]
C["Customer"]
C1["- CustomerID
- 瀏覽歷史
- 購物偏好"]
C --> C1
end
subgraph Order["📦 Order BC
訂單上下文"]
O["Customer"]
O1["- CustomerID
- 收貨地址
- 支付方式
- 信用額度"]
O --> O1
end
subgraph Payment["💳 Payment BC
支付上下文"]
P["Customer"]
P1["- CustomerID
- 銀行卡
- 風險評分"]
P --> P1
end
EventBus["📡 Event Bus
鬆耦合通信"]
Cart -->|公佈事件
CartCreated| EventBus
Order -->|公佈事件
OrderPlaced| EventBus
Payment -->|公佈事件
PaymentSucceeded| EventBus
EventBus -->|訂閱| Cart
EventBus -->|訂閱| Order
EventBus -->|訂閱| Payment
style C fill:#c8e6c9
style O fill:#f3e5f5
style P fill:#fff3e0
4. 領域事件(Domain Event)- 異步解耦
什麼是領域事件?
當訂單被支付時,系統不應該直接調用「扣減庫存」、「發送發票」、「更新推薦引擎」。而是發佈一個事件:「訂單已支付」。
各個系統各自訂閱這個事件: – 庫存系統:聽到「訂單已支付」→ 扣減庫存 – 計費系統:聽到「訂單已支付」→ 生成發票 – 推薦系統:聽到「訂單已支付」→ 更新推薦模型
好處: 新增功能時無需修改訂單核心邏輯。只需訂閱事件即可。
三個角色的故事
故事 1:後端工程師的困境與救贖
人物: 李明,在線電商平台的後端工程師,2 年經驗
第一幕:陷阱
2024 年 Q1,公司決定支援「VIP 折扣」功能。李明接到這個任務,開始思考:
訂單處理流程: 1. 使用者下單 → 調用 OrderService.createOrder() 2. OrderService 檢驗庫存 → 調用 InventoryService.checkStock() 3. OrderService 計算總價 → 調用 PricingService.calculatePrice() 4. OrderService 應用折扣 → 調用 DiscountService.applyDiscount() 5. OrderService 檢驗客戶額度 → 調用 CustomerService.validateCredit()
新需求:VIP 客戶的折扣規則不同。李明的做法:
// 無 DDD 的做法
public class OrderService {
public Order createOrder(String customerId, List<Item> items) {
// 驗證庫存
inventoryService.checkStock(items);
// 計算價格
Money basePrice = pricingService.calculatePrice(items);
// 應用折扣
Money discountedPrice = discountService.applyDiscount(
basePrice, customerId, getCurrentPromoCode()
);
// VIP 特殊折扣(新增)
if (customerService.isVIP(customerId)) {
discountedPrice = discountedPrice.multiply(0.9);
// 額外 10% 折扣
}
// 驗證客戶額度
customerService.validateCredit(customerId, discountedPrice);
// 儲存訂單
return orderRepository.save(new Order(...));
}
}
看起來合理。但 3 個月後,折扣規則變複雜了:
- VIP 客戶在促銷期間折扣加倍
- 新客戶首次購買有額外折扣
- 組合商品有特殊折扣
- 某些商品分類有上限折扣
OrderService 變成 500 行的怪物,每次改折扣規則都要小心翼翼地修改多個地方。而且,修改時容易破壞其他邏輯。
視覺化對比:無 DDD vs 有 DDD
graph TD
A["需求:支持 VIP 折扣翻倍"]
A --> B["修改 OrderService"]
A --> C["修改 DiscountService"]
A --> D["修改 PricingService"]
A --> E["修改 CustomerService"]
A --> F["修改 InventoryService"]
B --> B1["調整訂單計算邏輯"]
C --> C1["添加 VIP 折扣規則"]
D --> D1["修改價格計算"]
E --> E1["修改客戶驗證"]
F --> F1["庫存檢驗邏輯"]
B1 --> G["❌ 問題:"]
C1 --> G
D1 --> G
E1 --> G
F1 --> G
G --> H["• 改一處壞一處
• 難以測試所有組合
• 每次修改都害怕
• 新需求來臨再改..."]
style A fill:#ffebee
style G fill:#ffcdd2
style H fill:#ff9800,color:#fff
第二幕:覺醒
李明參加了一個 DDD 工作坊。他開始重新思考:為什麼 Order 不能自己管理折扣邏輯?
引入 DDD 後的設計:
// DDD 的做法
public class Order {
private OrderId orderId;
private CustomerId customerId;
private List<LineItem> lineItems;
private OrderDiscount discount;
private OrderStatus status;
private List<DomainEvent> events = new ArrayList<>();
// 聚合根負責業務規則
public static Order create(CustomerId customerId, List<Item> items) {
Order order = new Order(OrderId.generate(), customerId);
// Order 自己驗證庫存(委託給 Domain Service)
order.validateItems(items);
// Order 添加訂單項目
for (Item item : items) {
order.addLineItem(item);
}
return order;
}
// 應用折扣 - 由 Order 自己決定
public void applyDiscount(DiscountCode code, Customer customer) {
// 驗證折扣有效性
if (!code.isValid()) {
throw new InvalidDiscountException();
}
// 驗證客戶是否有權使用此折扣
if (customer.isVIP()) {
// VIP 折扣規則
this.discount = OrderDiscount.createVIPDiscount(code);
} else if (customer.isNewCustomer()) {
// 新客戶折扣規則
this.discount = OrderDiscount.createNewCustomerDiscount(code);
} else {
// 普通折扣規則
this.discount = OrderDiscount.create(code);
}
// 檢驗折扣後的價格是否在允許範圍
if (this.getTotalPrice().isNegative()) {
throw new InvalidDiscountAmountException();
}
}
// 下單 - 由 Order 確保一致性
public void place() {
// 最終檢驗:所有業務規則都滿足
if (lineItems.isEmpty()) {
throw new EmptyOrderException();
}
if (!this.status.equals(OrderStatus.DRAFT)) {
throw new InvalidOrderStatusException();
}
this.status = OrderStatus.PLACED;
// 發佈事件
this.events.add(new OrderPlacedEvent(
this.orderId, this.customerId, this.getTotalPrice()
));
}
// 獲取訂單總價(包含折扣)
public Money getTotalPrice() {
Money basePrice = lineItems.stream()
.map(LineItem::getPrice)
.reduce(Money.ZERO, Money::add);
if (discount != null) {
return basePrice.minus(discount.getAmount());
}
return basePrice;
}
}
有 DDD:Order 聚合根自主管理
graph TD
A["需求:支持 VIP 折扣翻倍"]
A --> B["修改 Order.applyDiscount()"]
B --> C["Order 聚合根自身驗證
VIP 折扣規則"]
C --> D["✅ 問題解決:"]
D --> E["• 只改一個地方
• Order 內部完整測試
• 其他服務完全不受影響
• 修改安全可控"]
style A fill:#c8e6c9
style B fill:#81c784,color:#fff
style D fill:#4caf50,color:#fff
style E fill:#a5d6a7
現在,修改折扣規則只需改 Order 類。新增 VIP 折扣?改 Order。新增組合商品折扣?改 Order。其他服務完全不受影響。
而且,Order 對象本身就是文件。新人看著 Order 類就能理解「訂單是如何工作的」。
故事 2:產品經理的協作變化
人物: 王經理,電商產品負責人
無 DDD 時的對話
王經理:「我們需要支援 VIP 客戶的特殊折扣。」
李明(工程師):「好的。這會影響 OrderService、DiscountService、PricingService、CustomerService。需要 2-3 週。」
王經理:「但這只是個折扣啊,為什麼要改 4 個 Service?」
李明:「因為折扣邏輯分散在多個地方…」
王經理:最後什麼都不懂,只能相信李明的估時。而且,修改變得很危險——改 OrderService 時可能無意中破壞了 DiscountService 的邏輯。
有 DDD 時的對話
王經理:「我們需要支援 VIP 客戶的特殊折扣。」
李明:「好的。這個邏輯完全在 Order 聚合根內,只需改 Order 類。5 個工作天。」
王經理:「為什麼只要改一個地方?」
李明:「因為我們用 DDD 設計了系統。Order 聚合根責任就是管理訂單的所有業務規則,包括折扣。修改只會影響 Order,不會波及其他服務。」
王經理:立即相信,而且能理解邊界。
另一個例子:新客戶首次購買有額外折扣
王經理:「新客戶首次購買要額外優惠 15%。」
李明:「需要改 Order 的 applyDiscount() 方法,加一個 Customer.isNewCustomer() 的判斷。3 天。」
王經理:「萬一改壞了怎麼辦?」
李明:「Order 類有完整的單元測試。所有折扣組合都有測試用例。改壞了測試會失敗,發佈前會被攔住。」
王經理:看到 Order 的測試:
@Test void 新客戶首次購買應該獲得額外折扣() { Order order = Order.create(customerId, items); Customer newCustomer = new Customer(customerId, CustomerType.NEW);
order.applyDiscount(promoCode, newCustomer);
// 驗證折扣是否正確應用 assertEquals(expectedDiscountAmount, order.getDiscount().getAmount()); }
@Test void VIP客戶應該獲得最高折扣() { Order order = Order.create(customerId, items); Customer vipCustomer = new Customer(customerId, CustomerType.VIP);
order.applyDiscount(promoCode, vipCustomer);
assertEquals(expectedVIPDiscountAmount, order.getDiscount().getAmount()); }
王經理的信心瞬間提升。他看到了業務邏輯的完整定義和完整的測試覆蓋。
需求評估的複雜度對比
graph TD
Req["「支持新客戶首購 15% 折扣」"]
subgraph NoDD ["❌ 無 DDD"]
N1["評估影響:"]
N2["需改 OrderService?"]
N3["需改 CustomerService?"]
N4["需改 DiscountService?"]
N5["需改 PricingService?"]
N6["...還要改別的嗎?"]
N1 --> N2 --> N3 --> N4 --> N5 --> N6
Result1["😕 王經理:為什麼
這麼複雜?"]
N6 --> Result1
end
subgraph DD ["✅ 有 DDD"]
D1["評估影響:"]
D2["Order 聚合根內
applyDiscount() 方法"]
D3["檢查客戶類型
newCustomer?"]
D4["應用 15% 折扣規則"]
D1 --> D2 --> D3 --> D4
Result2["😊 王經理:明白了!
就改 Order 一個地方"]
D4 --> Result2
end
Req --> NoDD
Req --> DD
style Result1 fill:#ffcdd2,color:#c62828
style Result2 fill:#c8e6c9,color:#2e7d32
故事 3:系統架構師的拆分之痛
人物: 張架構師,負責系統從單體向微服務的遷移
無 DDD 時的微服務化困境
公司決定拆分微服務。張架構師的計畫:
- 購物車微服務(購物車邏輯)
- 訂單微服務(訂單邏輯)
- 支付微服務(支付邏輯)
- 推薦微服務(推薦邏輯)
問題:四個微服務都需要用 Customer 對象。但 Customer 的定義是什麼?
購物車需要:使用者 ID、購物車項目、購物偏好 訂單需要:使用者 ID、收貨地址、支付方式 支付需要:使用者 ID、信用卡資訊、風控額度 推薦需要:使用者 ID、瀏覽歷史、購買歷史
如果四個服務都共享一個 Customer 對象,任何改動都會引發連鎖反應:
- 購物車加了「購物偏好」欄位
- 訂單服務也被迫更新 Customer schema
- 支付服務也被迫更新 Customer schema
改著改著,Customer 變成了包含所有欄位的怪物,每個服務都用不到 80% 的欄位,卻要為它們付出維護成本。
有 DDD 時的微服務化
張架構師改變了思路:定義限界上下文。
購物車 BC (Bounded Context):
- Cart(購物車聚合根)
- CartItem
- CustomerReference (只含 ID)
- CartPreference(購物偏好)
訂單 BC:
- Order(訂單聚合根)
- OrderLineItem
- ShippingAddress
- BillingInfo
- CustomerReference (只含 ID)
支付 BC:
- Payment(支付聚合根)
- PaymentMethod
- RiskScore
- CustomerReference (只含 ID)
推薦 BC:
- RecommendationProfile(推薦配置聚合根)
- BrowsingHistory
- PurchaseHistory
- CustomerReference (只含 ID)
每個 BC 有自己的 Customer 概念。它們不共享資料庫,而是透過事件通信:
購物車 BC 發佈事件: – “CartCreated” → 推薦 BC 訂閱,更新瀏覽歷史
訂單 BC 發佈事件: – “OrderPlaced” → 庫存 BC 訂閱,扣減庫存 – “OrderPlaced” → 計費 BC 訂閱,生成發票 – “OrderPlaced” → 推薦 BC 訂閱,更新推薦模型
支付 BC 發佈事件: – “PaymentSucceeded” → 訂單 BC 訂閱,更新訂單狀態 – “PaymentFailed” → 訂單 BC 訂閱,標記支付失敗
結果:
-
每個微服務獨立演變:購物車想加新欄位?只改購物車 BC,其他服務不受影響。
-
通信清晰:透過事件而不是 API 呼叫,系統更鬆散耦合。
-
故障隔離:推薦服務掛了,不會影響訂單流程。
-
擴展簡單:想加新功能(如積分系統)?加一個新 BC,訂閱相關事件即可,無需改現有代碼。
從單體到微服務的演進
graph TD
A["問題:如何分離成
多個微服務?"]
subgraph BadWay ["❌ 錯誤做法:共享 Customer 模型"]
B1["所有服務共用
一個 Customer 表"]
B2["購物車加欄位
↓ 所有服務更新"]
B3["訂單加欄位
↓ 所有服務更新"]
B4["...每個服務都
只用 20% 的欄位
卻維護 100% 的複雜度"]
B1 --> B2 --> B3 --> B4
end
subgraph GoodWay ["✅ DDD 做法:分離 Bounded Context"]
G1["購物車 BC 擁有
自己的 Customer"]
G2["訂單 BC 擁有
自己的 Customer"]
G3["支付 BC 擁有
自己的 Customer"]
G1 -.->|Event| EventBus["📡
Event
Bus"]
G2 -.->|Event| EventBus
G3 -.->|Event| EventBus
EventBus -.->|Subscribe| G1
EventBus -.->|Subscribe| G2
EventBus -.->|Subscribe| G3
end
A --> BadWay
A --> GoodWay
BadWay --> Result1["😞 單體地獄
改不了"]
GoodWay --> Result2["😊 微服務天堂
獨立演進"]
style BadWay fill:#ffebee
style GoodWay fill:#e8f5e9
style Result1 fill:#ff5252,color:#fff
style Result2 fill:#4caf50,color:#fff
常見誤區
誤區 1:DDD = 微服務
錯誤: DDD 需要微服務,或者用了微服務就是在用 DDD。
真相: DDD 是設計思想,可用於單體應用或微服務。一個良好設計的單體應用完全可以用 DDD。反之,拆成微服務但沒用 DDD 反而會增加複雜度。
實例: 只有 Order 和 Customer 兩個主要業務的小公司,用單體 + DDD 比微服務更簡單。
誤區 2:DDD = 複雜框架
錯誤: DDD 需要特殊框架和工具。
真相: DDD 是純粹的設計思想。Order、LineItem、DiscountCode 都是普通的 Java 類。框架是輔助,不是 DDD 本身。
誤區 3:所有項目都要用 DDD
錯誤: DDD 是銀彈,所有項目都該用。
真相: DDD 的收益來自於複雜的業務邏輯。如果你的系統業務規則簡單(如 CRUD API),DDD 反而是過度設計。
評估清單:你的項目適合 DDD 嗎?
- 業務邏輯複雜(超過 50 個業務規則或用例)→ 適合
- 多個團隊並行開發 → 適合
- 業務規則頻繁變化 → 適合
- 只是簡單的 CRUD API → 不適合
- 無人長期維護 → 不適合
誤區 4:DDD = 過度設計
錯誤: DDD 會讓系統變複雜。
真相: DDD 的目標是簡化複雜系統。是的,引入 DDD 需要更多初始思考,但它是為了減少未來的複雜度和維護成本。
對比:
無 DDD:100 行簡單 Service → 6 個月後變成 2000 行怪物 有 DDD:200 行設計 + 結構 → 6 個月後仍然清晰,修改容易
實踐指導:如何開始
第一步:識別限界上下文(不要寫代碼)
用 30 分鐘,與業務人員一起列出:
- 所有的業務角色:客戶、客服、倉管、財務… 2. 所有的主要流程:下單、支付、退貨、推薦… 3. 概念在不同流程中的變化:「客戶」在下單流程中是什麼?在推薦流程中是什麼?
例如: – 訂單 BC:客戶 = 收貨地址 + 支付方式 – 推薦 BC:客戶 = 瀏覽歷史 + 購買偏好
第二步:定義通用語言
寫一個簡短的詞彙表,確保開發者、PM、業務都用同一套術語。例如:
訂單 (Order): 客戶購買商品的記錄,包含商品、數量、折扣、支付方式
折扣碼 (DiscountCode): 特定條件下可應用於訂單的優惠
聚合根 (Aggregate Root): 邊界內所有業務規則的守護者
第三步:設計核心聚合根
從最複雜的業務流程開始。以訂單系統為例,Order 是聚合根,它包含:
- OrderLineItem(子對象)
- OrderDiscount(子對象)
- 所有業務規則檢驗
第四步:寫測試,然後寫代碼
@Test void 訂單應該驗證折扣碼有效性() { Order order = Order.create(customerId, items); InvalidDiscountCode code = new InvalidDiscountCode();
assertThrows(InvalidDiscountException.class, () -> order.applyDiscount(code, customer) ); }
@Test void VIP客戶應該獲得額外折扣() { Order order = Order.create(customerId, items); DiscountCode code = new DiscountCode("VIP2024"); Customer vipCustomer = new Customer(customerId, CustomerType.VIP);
order.applyDiscount(code, vipCustomer);
Money expectedDiscount = basePrice.multiply(0.2); // 20% 折扣 assertEquals(expectedDiscount, order.getDiscount().getAmount()); }
測試寫好了,業務邏輯就明確了。然後實現 Order 類來通過測試。
完整工作流程示範
訂單處理的 DDD 流程
sequenceDiagram
actor User as 用戶
participant OrderBC as Order BC
聚合根
participant InventoryBC as Inventory BC
participant BillingBC as Billing BC
participant RecommendBC as Recommend BC
User->>OrderBC: 下單
Order.create()
activate OrderBC
OrderBC->>OrderBC: ✅ 檢驗庫存
✅ 檢驗折扣
✅ 檢驗額度
OrderBC->>OrderBC: 發佈事件
OrderPlaced
deactivate OrderBC
Note over OrderBC: Order 作為完整的
業務規則守護者
所有驗證在此完成
OrderBC->>InventoryBC: OrderPlaced 事件
OrderBC->>BillingBC: OrderPlaced 事件
OrderBC->>RecommendBC: OrderPlaced 事件
par 並行異步處理
activate InventoryBC
InventoryBC->>InventoryBC: 監聽 OrderPlaced
異步扣減庫存
InventoryBC-->>User: 庫存已扣
deactivate InventoryBC
and
activate BillingBC
BillingBC->>BillingBC: 監聽 OrderPlaced
異步生成發票
BillingBC-->>User: 發票已生成
deactivate BillingBC
and
activate RecommendBC
RecommendBC->>RecommendBC: 監聽 OrderPlaced
異步更新推薦模型
RecommendBC-->>User: 推薦已更新
deactivate RecommendBC
end
DDD 的核心價值
graph LR
subgraph Before ["無 DDD
散落的邏輯"]
B1["Service A"]
B2["Service B"]
B3["Service C"]
B4["Service D"]
B5["Service E"]
B_logic["❌ 業務規則
散落在各處"]
B1 --- B2
B2 --- B3
B3 --- B4
B4 --- B5
end
subgraph After ["有 DDD
聚集的規則"]
A1["Aggregate Root
🏛️"]
A_logic["✅ 業務規則
集中管理"]
A2["其他服務"]
end
Before -->|引入 DDD| After
style B_logic fill:#ff5252,color:#fff
style A_logic fill:#4caf50,color:#fff
總結
DDD 不是銀彈,但對於複雜業務系統,它是正確的藥。它的核心承諾是:透過清晰的邊界與角色,讓複雜系統變得簡單。
關鍵收穫:
- 聚合根集中化業務邏輯:修改時不會波及系統各處 2. 限界上下文明確邊界:不同業務領域有獨立的概念定義 3. 領域事件實現解耦:新功能無需修改核心邏輯 4. 通用語言統一溝通:開發者、PM、業務用同一套詞彙
下一步,選擇你最複雜的業務流程,嘗試用 DDD 重新設計它。你會發現,原本糾纏的邏輯變得清晰,修改變得安全。