🌏 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["✅ 驗證規則<br/>• 庫存檢查<br/>• 折扣合法性<br/>• 價格有效性<br/>• 客戶額度"]
end
User -->|只能調用<br/>applyDiscount 方法| Agg
Root --> Child1
Root --> Child2
Root --> Val
Root --> Rules
BadWay["❌ 不允許<br/>直接修改<br/>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<br/>購物車上下文"]
C["Customer"]
C1["- CustomerID<br/>- 瀏覽歷史<br/>- 購物偏好"]
C --> C1
end
subgraph Order["📦 Order BC<br/>訂單上下文"]
O["Customer"]
O1["- CustomerID<br/>- 收貨地址<br/>- 支付方式<br/>- 信用額度"]
O --> O1
end
subgraph Payment["💳 Payment BC<br/>支付上下文"]
P["Customer"]
P1["- CustomerID<br/>- 銀行卡<br/>- 風險評分"]
P --> P1
end
EventBus["📡 Event Bus<br/>鬆耦合通信"]
Cart -->|公佈事件<br/>CartCreated| EventBus
Order -->|公佈事件<br/>OrderPlaced| EventBus
Payment -->|公佈事件<br/>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["• 改一處壞一處<br/>• 難以測試所有組合<br/>• 每次修改都害怕<br/>• 新需求來臨再改..."]
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 聚合根自身驗證<br/>VIP 折扣規則"] C --> D["✅ 問題解決:"] D --> E["• 只改一個地方<br/>• Order 內部完整測試<br/>• 其他服務完全不受影響<br/>• 修改安全可控"] 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["😕 王經理:為什麼<br/>這麼複雜?"] N6 --> Result1 end subgraph DD ["✅ 有 DDD"] D1["評估影響:"] D2["Order 聚合根內<br/>applyDiscount() 方法"] D3["檢查客戶類型<br/>newCustomer?"] D4["應用 15% 折扣規則"] D1 --> D2 --> D3 --> D4 Result2["😊 王經理:明白了!<br/>就改 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["問題:如何分離成<br/>多個微服務?"]
subgraph BadWay ["❌ 錯誤做法:共享 Customer 模型"]
B1["所有服務共用<br/>一個 Customer 表"]
B2["購物車加欄位<br/>↓ 所有服務更新"]
B3["訂單加欄位<br/>↓ 所有服務更新"]
B4["...每個服務都<br/>只用 20% 的欄位<br/>卻維護 100% 的複雜度"]
B1 --> B2 --> B3 --> B4
end
subgraph GoodWay ["✅ DDD 做法:分離 Bounded Context"]
G1["購物車 BC 擁有<br/>自己的 Customer"]
G2["訂單 BC 擁有<br/>自己的 Customer"]
G3["支付 BC 擁有<br/>自己的 Customer"]
G1 -.->|Event| EventBus["📡<br/>Event<br/>Bus"]
G2 -.->|Event| EventBus
G3 -.->|Event| EventBus
EventBus -.->|Subscribe| G1
EventBus -.->|Subscribe| G2
EventBus -.->|Subscribe| G3
end
A --> BadWay
A --> GoodWay
BadWay --> Result1["😞 單體地獄<br/>改不了"]
GoodWay --> Result2["😊 微服務天堂<br/>獨立演進"]
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<br/>聚合根
participant InventoryBC as Inventory BC
participant BillingBC as Billing BC
participant RecommendBC as Recommend BC
User->>OrderBC: 下單<br/>Order.create()
activate OrderBC
OrderBC->>OrderBC: ✅ 檢驗庫存<br/>✅ 檢驗折扣<br/>✅ 檢驗額度
OrderBC->>OrderBC: 發佈事件<br/>OrderPlaced
deactivate OrderBC
Note over OrderBC: Order 作為完整的<br/>業務規則守護者<br/>所有驗證在此完成
OrderBC->>InventoryBC: OrderPlaced 事件
OrderBC->>BillingBC: OrderPlaced 事件
OrderBC->>RecommendBC: OrderPlaced 事件
par 並行異步處理
activate InventoryBC
InventoryBC->>InventoryBC: 監聽 OrderPlaced<br/>異步扣減庫存
InventoryBC-->>User: 庫存已扣
deactivate InventoryBC
and
activate BillingBC
BillingBC->>BillingBC: 監聽 OrderPlaced<br/>異步生成發票
BillingBC-->>User: 發票已生成
deactivate BillingBC
and
activate RecommendBC
RecommendBC->>RecommendBC: 監聽 OrderPlaced<br/>異步更新推薦模型
RecommendBC-->>User: 推薦已更新
deactivate RecommendBC
end
DDD 的核心價值
graph LR
subgraph Before ["無 DDD<br/>散落的邏輯"]
B1["Service A"]
B2["Service B"]
B3["Service C"]
B4["Service D"]
B5["Service E"]
B_logic["❌ 業務規則<br/>散落在各處"]
B1 --- B2
B2 --- B3
B3 --- B4
B4 --- B5
end
subgraph After ["有 DDD<br/>聚集的規則"]
A1["Aggregate Root<br/>🏛️"]
A_logic["✅ 業務規則<br/>集中管理"]
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 重新設計它。你會發現,原本糾纏的邏輯變得清晰,修改變得安全。