這篇把整條流程在一個具體場景裡走完:一個電商平台的訂單管理子系統。目的不是建完整的系統,而是讓每個步驟的輸入/輸出 artifact 具體可見——這些 artifact 是你交給 AI agent 執行下一步的原料。
場景:一個 B2C 電商平台,需要設計「用戶下單 → 付款 → 出貨 → 到貨/取消」這條主流程的後端與前端。
Step 1:Big Picture Event Storming
參與者:PM、後端工程師、前端工程師、客服代表(知道用戶最常問什麼)。
時間:2–3 小時(電商訂單是比較清楚的領域,不需要太長的 Big Picture 階段)。
橘色 Domain Events(時間序)
CustomerRegistered → ProductViewed → ProductAddedToCart →
CartCheckedOut → OrderPlaced → PaymentInitiated →
PaymentSucceeded / PaymentFailed →
OrderConfirmed → StockReserved →
OrderShipped → OrderDelivered →
── 或 ──
CancellationRequested → OrderCancelled → RefundInitiated → RefundCompleted
識別 Commands 與 Actors
| Command(藍色) | Actor(黃色) |
|---|---|
PlaceOrder | Customer |
CapturePayment | Payment Gateway(外部系統) |
ConfirmOrder | System(自動,觸發於 PaymentSucceeded) |
ShipOrder | Warehouse Staff / Admin |
RequestCancellation | Customer |
ApproveRefund | Admin |
識別 Aggregates(淡黃色)
- Order:核心 aggregate,擁有訂單生命週期的所有狀態
- Cart:暫時性的,checkout 後轉為 Order
- Inventory(另一個 Bounded Context):庫存扣減邏輯獨立
Hotspots(紫色,待釐清)
🔴 付款失敗後,庫存要不要先扣?(庫存扣減時機)
🔴 用戶在 Confirmed 狀態下還能取消嗎?(取消政策)
🔴 退款多久到帳?(依支付方式不同)
🔴 同一個用戶同時下多筆單,庫存怎麼處理 race condition?
Bounded Contexts 邊界
[Order Management] [Payment] [Inventory] [Shipping]
Order Aggregate ←→ Payment Gateway → Stock Reserve → Logistics
Cart Aggregate (External System) Aggregate (External)
Step 1 輸出 artifact:
domain-events.md:所有 Domain Events 列表(命名 + 時間序 + 所屬 BC)aggregates.md:Aggregate 清單 + 口頭描述的不變量bounded-contexts.md:BC 邊界圖 + Context Maphotspots.md:未解決問題清單(直接成為 W Register 的to-confirm項目)
Step 2:Problem Frames(輔助)
把 Hotspots 和 Aggregate 不變量轉成 W Register,用 S∧W⊨R 框架顯性化假設。
這步的完整說明見:kiro-frame:Problem Frames 形式化工具
本場景的關鍵 W Register 條目:
W1: 庫存扣減在 PaymentSucceeded 事件後觸發(不在 PlaceOrder 時)
status: assumed-risk
risk: 付款期間可能超賣(接受此風險,用付款成功後再檢查庫存的補償邏輯)
W2: Payment Gateway 在 30 秒內回應
status: to-confirm(需和 Payment 團隊確認 SLA)
W3: 用戶在 OrderConfirmed 之前可取消,之後不可
status: verified(已與 PM 確認取消政策)
W4: 退款時間由 Payment Gateway 決定,平台不控制
status: assumed-risk(UI 上顯示「3-5 個工作日」)Step 2 輸出 artifact:
frame.md:各子問題的 Problem Frame 分類(Information Display / Commanded Behaviour 等)w-register.md:Domain Assumptions 清單,每條有 verified / assumed-risk / to-confirm 狀態
Step 3:Event Sourcing + CQRS 後端設計
以 Order Aggregate 為主,設計事件儲存與 Projection。
Order Aggregate 狀態機
OrderCreated
│
▼
PaymentPending ──── PaymentFailed ───► OrderCancelled
│
PaymentSucceeded
│
▼
StockReserved (或 StockInsufficient → OrderCancelled)
│
▼
OrderConfirmed
│
├──── CancellationRequested → OrderCancelled → RefundInitiated → RefundCompleted
│
▼
OrderShipped
│
▼
OrderDelivered
Event Store Schema(Order Aggregate)
stream_id : "order-{orderId}"
events:
OrderCreated { orderId, customerId, items: [{productId, qty, unitPrice}], totalAmount, currency }
PaymentInitiated { paymentId, method, amount }
PaymentSucceeded { paymentId, gatewayTransactionId }
PaymentFailed { paymentId, reason }
StockReserved { reservationId, items: [{productId, qty}] }
OrderConfirmed { confirmedAt }
OrderShipped { carrierId, trackingNumber, shippedAt }
OrderDelivered { deliveredAt }
OrderCancelled { reason, cancelledAt }
RefundInitiated { refundId, amount }
RefundCompleted { refundId, completedAt }
Projections(Read Models)
// Projection 1:OrderListItem(訂單清單頁)
// 消費事件:OrderCreated, OrderConfirmed, OrderShipped, OrderDelivered, OrderCancelled
interface OrderListItem {
orderId: string
placedAt: Date
customerName: string // 從 CustomerProjection denormalize
totalAmount: number
currency: string
itemCount: number
status: OrderStatus
}
// Projection 2:OrderDetail(訂單詳情頁)
// 消費所有 Order 事件 + payment 事件
interface OrderDetail {
orderId: string
customer: { id: string; name: string; email: string; shippingAddress: Address }
items: OrderLineItem[]
totalAmount: number
payment: { method: string; status: string; paidAt?: Date }
shipping?: { carrier: string; trackingNumber: string; estimatedDelivery?: Date }
statusHistory: { status: OrderStatus; occurredAt: Date; note?: string }[]
}
// Projection 3:AdminOrderDashboard(後台統計)
// 消費所有訂單事件,建立彙總數字
interface OrderDashboardStats {
date: string // YYYY-MM-DD
totalOrders: number
totalRevenue: number
cancelledCount: number
pendingPaymentCount: number
}Step 3 輸出 artifact:
aggregate-design.md:Order Aggregate 狀態機圖 + 不變量清單 + Command 處理邏輯event-schema.md:所有 Domain Events 的 schema(欄位 + 型別 + 語義說明)projections.md:每個 Projection 的輸入事件、輸出 schema、更新邏輯
Step 4:前端從 Read Models 推導
以 OrderListItem 和 OrderDetail 直接定義前端組件的資料需求。
組件 ↔ Read Model 對應
頁面:訂單清單(/orders)
組件:<OrderList>
props: OrderListItem[]
子組件:<OrderStatusBadge status={order.status} />
<OrderAmountDisplay amount={order.totalAmount} currency={order.currency} />
頁面:訂單詳情(/orders/:id)
組件:<OrderDetailPage>
props: OrderDetail
子組件:<OrderTimeline statusHistory={order.statusHistory} />
<OrderItemTable items={order.items} />
<PaymentInfo payment={order.payment} />
<ShippingInfo shipping={order.shipping} />
<CancelOrderButton orderId={...} disabled={order.status !== 'confirmed'} />
Command ↔ 用戶行為對應
// 結帳頁:PlaceOrder command
const handleCheckout = async () => {
await execute('PlaceOrder', {
cartId: cart.id,
shippingAddressId: selectedAddress.id,
paymentMethodId: selectedPayment.id,
})
// Optimistic UI:先跳轉到「訂單處理中」頁
navigate(`/orders/processing`)
}
// 訂單詳情頁:CancelOrder command
const handleCancel = async () => {
await execute('RequestCancellation', { orderId, reason: cancellationReason })
// 最終一致性:不立刻重整,等 WebSocket 推送 OrderCancelled 事件
}型別共享(TypeScript + monorepo)
packages/
domain-types/ ← shared package
src/
events.ts ← Domain Event types (OrderCreated, etc.)
read-models.ts ← OrderListItem, OrderDetail, etc.
commands.ts ← PlaceOrder, CancelOrder command shapes
backend/ ← 後端使用 domain-types
frontend/ ← 前端使用 domain-types(直接 import,零翻譯)
Step 4 輸出 artifact:
component-map.md:頁面 → 組件 → Read Model 對應圖command-map.md:UI 互動 → Command → API endpoint 對應domain-types/:共享型別定義(Events、Read Models、Commands)
Step 5:Pattern Language——固化重複解法
從這次設計中萃取出兩個 pattern,寫進 docs/patterns/。
Pattern 1:Saga Pattern(跨 Aggregate 流程)
## Saga Pattern
**情境**:一個業務流程需要跨越多個 Aggregate(Order + Payment + Inventory),
且需要在某步驟失敗時能補償(rollback-like)。
**Forces**:
- 業務要求:付款成功 → 庫存扣減 → 訂單確認 必須一起完成
- 技術約束:三個 Aggregate 不能在同一個 transaction 裡更新
- 分散式系統沒有「分散式 transaction」這個免費選項
**解法**:
用 OrderSaga(Process Manager)監聽 Domain Events,依序發出 Commands;
失敗時發補償 Commands(CancelPayment、ReleaseStock、CancelOrder)。
**後果**:
✓ 各 Aggregate 保持自己的邊界和一致性
✓ 失敗可補償
✗ 有短暫的中間狀態(PaymentSucceeded 但 StockReserved 還沒完成)
✗ Saga 本身需要持久化(崩潰後要能從上次狀態繼續)
**關聯**:Aggregate, Domain Event, Process ManagerPattern 2:CQRS Read Model per View
## CQRS Read Model per View
**情境**:同一份業務資料需要在多個不同頁面以不同形式顯示
(訂單清單 vs 訂單詳情 vs 後台統計)。
**Forces**:
- 每個頁面的查詢需求不同(欄位、排序、過濾)
- 用同一個 API endpoint 服務所有頁面:要麼 under-fetch(欄位不夠),要麼 over-fetch(欄位太多)
- 把非正規化欄位放進 Write Model 違反單一職責
**解法**:
每個主要 UI View 有自己的 Projection,各自優化。
讀的是 Projection 輸出,不是 Aggregate 狀態。
**後果**:
✓ 每個頁面只拿自己需要的欄位,無多餘傳輸
✓ Projection 可獨立優化(index 策略、儲存引擎)
✗ Projection 數量增加,維護成本上升
✗ 每次 Domain Event 新增欄位,所有相關 Projection 都要更新
**關聯**:Event Sourcing, Aggregate, Domain EventStep 5 輸出 artifact:
docs/patterns/saga.mddocs/patterns/cqrs-read-model-per-view.mdCLAUDE.md設計決策段落更新(引用 pattern 名稱)
每步 Artifact 匯總
| 步驟 | 主要輸入 | 主要輸出 | 接進 cc-sdd 的位置 |
|---|---|---|---|
| Event Storming | 需求、領域專家知識 | domain-events.md、aggregates.md、bounded-contexts.md、hotspots.md | brief.md 的 scope + domain model |
| Problem Frames | hotspots.md | frame.md、w-register.md | brief.md 的 assumptions 段落 |
| Event Sourcing | aggregates.md、w-register.md | aggregate-design.md、event-schema.md、projections.md | spec-design 的 data model + API shape |
| 前端推導 | projections.md | component-map.md、command-map.md、domain-types/ | spec-requirements 的 frontend behaviors |
| Pattern Language | 以上所有 | docs/patterns/*.md | CLAUDE.md 的 design decisions |
延伸
- Event Storming 詳細說明 → Event Storming:協作式領域探索
- Event Sourcing + CQRS 詳細說明 → Event Sourcing + CQRS:後端落地
- 前端推導詳細說明 → 前端從領域模型推導
- Pattern Language 詳細說明 → Pattern Language:沉澱可重用設計詞彙
- Problem Frames 詳細說明 → kiro-frame:Problem Frames 形式化工具
- 回到全景 → Event Storming + Event Sourcing + Pattern Language 專欄首頁