這篇把整條流程在一個具體場景裡走完:一個電商平台的訂單管理子系統。目的不是建完整的系統,而是讓每個步驟的輸入/輸出 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(黃色)
PlaceOrderCustomer
CapturePaymentPayment Gateway(外部系統)
ConfirmOrderSystem(自動,觸發於 PaymentSucceeded)
ShipOrderWarehouse Staff / Admin
RequestCancellationCustomer
ApproveRefundAdmin

識別 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 Map
  • hotspots.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 推導

OrderListItemOrderDetail 直接定義前端組件的資料需求。

組件 ↔ 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 Manager

Pattern 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 Event

Step 5 輸出 artifact

  • docs/patterns/saga.md
  • docs/patterns/cqrs-read-model-per-view.md
  • CLAUDE.md 設計決策段落更新(引用 pattern 名稱)

每步 Artifact 匯總

步驟主要輸入主要輸出接進 cc-sdd 的位置
Event Storming需求、領域專家知識domain-events.md、aggregates.md、bounded-contexts.md、hotspots.mdbrief.md 的 scope + domain model
Problem Frameshotspots.mdframe.md、w-register.mdbrief.md 的 assumptions 段落
Event Sourcingaggregates.md、w-register.mdaggregate-design.md、event-schema.md、projections.mdspec-design 的 data model + API shape
前端推導projections.mdcomponent-map.md、command-map.md、domain-types/spec-requirements 的 frontend behaviors
Pattern Language以上所有docs/patterns/*.mdCLAUDE.md 的 design decisions

延伸