土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁

文章导航

分类入口
architecture
标签入口
#event-storming#domain-events#Alberto-Brandolini#collaborative-modeling

目录

一个 20 人的开发团队花了两个月画 UML 图和写需求文档,依然对核心业务流程的理解存在分歧。直到有一天,团队把领域专家、开发者、测试人员全部拉进一个大会议室,用橙色便利贴写出了系统中发生的所有事件——两个小时后,所有人第一次在同一幅图上看到了业务全貌。这就是事件风暴(Event Storming)的力量。

事件风暴是意大利软件架构师 Alberto Brandolini 在 2012 年提出的一种协作建模方法。它通过识别领域事件(Domain Event)来探索业务领域,并将发现逐步细化为可落地的软件设计。本文将完整介绍事件风暴的三个层次、领域事件的设计规范,以及从事件风暴到限界上下文和聚合的推导方法。

上一篇:DDD 战术模式 | 下一篇:防腐层与开放主机服务


一、什么是领域事件

1.1 定义

领域事件(Domain Event)是领域中发生的、具有业务意义的事实。它用过去时态命名,描述已经发生的事情。

命名规范:

正确:OrderPlaced, PaymentReceived, ShipmentDelivered
错误:PlaceOrder, ReceivePayment, DeliverShipment  ← 这是命令,不是事件

1.2 领域事件的特征

特征 说明
过去时态 描述已经发生的事实
不可变 事件一旦产生不可修改
有时间戳 记录事件发生的精确时间
携带足够上下文 包含消费者处理所需的数据
业务语义 用业务语言命名,而非技术语言

1.3 领域事件 vs 集成事件

在前一篇 DDD 战术模式 中已经简要提到这两者的区别,这里做进一步展开。

领域事件(Domain Event)是限界上下文内部的建模概念,表达聚合状态变迁的事实。集成事件(Integration Event)是跨限界上下文的通信机制,通常通过消息中间件传递。

graph LR
    subgraph "交易上下文"
        A["Order 聚合"] -->|"产生"| DE["领域事件<br/>OrderConfirmed"]
        DE -->|"转换"| IE["集成事件<br/>OrderConfirmedIntegration"]
    end

    IE -->|"消息队列"| subgraph2

    subgraph subgraph2 ["物流上下文"]
        ACL2["防腐层"] -->|"转换"| DE2["领域事件<br/>ShippingOrderCreated"]
        DE2 -->|"触发"| B["Shipment 聚合"]
    end

关键区别:

维度 领域事件 集成事件
作用范围 限界上下文内部 跨限界上下文
传输载体 进程内内存 消息队列(Kafka、RabbitMQ)
序列化 不需要 需要(JSON、Protobuf)
版本管理 随代码演进 需要严格的版本策略
消费者 同一上下文的事件处理器 其他上下文的适配器
可靠性要求 内存操作,通常可靠 需要保证至少一次投递

1.4 领域事件的代码结构

// 基础事件接口
public interface DomainEvent {
    Instant occurredAt();
    String eventType();
}

// 具体领域事件
public record OrderPlacedEvent(
    OrderId orderId,
    CustomerId customerId,
    List<OrderLineSnapshot> lines,
    Money totalAmount,
    Instant occurredAt
) implements DomainEvent {

    public OrderPlacedEvent {
        Objects.requireNonNull(orderId);
        Objects.requireNonNull(customerId);
        Objects.requireNonNull(lines);
        Objects.requireNonNull(totalAmount);
        if (occurredAt == null) {
            occurredAt = Instant.now();
        }
    }

    @Override
    public String eventType() {
        return "order.placed";
    }
}

// 集成事件——用于跨上下文传播
public record OrderPlacedIntegrationEvent(
    String orderId,
    String customerId,
    List<LineItemDto> items,
    String totalAmount,
    String currency,
    String occurredAt
) {
    // 从领域事件转换
    public static OrderPlacedIntegrationEvent from(OrderPlacedEvent domainEvent) {
        return new OrderPlacedIntegrationEvent(
            domainEvent.orderId().value(),
            domainEvent.customerId().value(),
            domainEvent.lines().stream()
                .map(l -> new LineItemDto(l.productId(), l.quantity(), l.price()))
                .toList(),
            domainEvent.totalAmount().amount().toPlainString(),
            domainEvent.totalAmount().currency().getCurrencyCode(),
            domainEvent.occurredAt().toString()
        );
    }
}

二、事件风暴概述

2.1 什么是事件风暴

事件风暴是一种基于便利贴的协作建模工作坊方法。其核心理念是:

  1. 把所有相关角色(开发者、领域专家、产品经理、测试人员)聚集在一起;
  2. 用不同颜色的便利贴表示不同类型的领域概念;
  3. 在一面大墙上按时间线排列这些概念;
  4. 通过讨论和迭代,逐步揭示业务全貌。

2.2 为什么需要事件风暴

传统的需求分析方法(用例图、流程图、需求文档)有几个共同的问题:

问题 说明
信息不对称 产品经理写文档,开发者读文档,理解必然有偏差
线性思维 流程图强调”正常路径”,异常场景容易遗漏
缺乏全局视角 每个人只了解自己负责的部分
时效性差 文档写完就开始过时

事件风暴通过同步协作解决了这些问题。所有人在同一时间、同一地点,对同一个模型进行讨论。

2.3 三个层次

事件风暴有三个递进的层次,每个层次解决不同的问题:

┌─────────────────────────────────────────────┐
│  层次三:软件设计(Software Design)          │
│  目标:定义聚合、命令处理器、读模型            │
│  参与者:开发者                               │
│  时长:2-4 小时                              │
├─────────────────────────────────────────────┤
│  层次二:流程建模(Process Modeling)          │
│  目标:细化事件流、识别命令和策略              │
│  参与者:开发者 + 领域专家                    │
│  时长:2-3 小时                              │
├─────────────────────────────────────────────┤
│  层次一:大图探索(Big Picture)              │
│  目标:发现所有领域事件,建立全局共识           │
│  参与者:所有相关角色                         │
│  时长:2-4 小时                              │
└─────────────────────────────────────────────┘

三、便利贴颜色与含义

事件风暴使用标准化的颜色来表示不同类型的领域概念。以下是 Brandolini 定义的标准颜色系统:

颜色 概念 说明 示例
橙色 领域事件(Domain Event) 已发生的业务事实 “订单已下达”
蓝色 命令(Command) 触发事件的意图 “下单”
黄色(小) 参与者/角色(Actor) 执行命令的人或系统 “客户”“定时任务”
黄色(大) 聚合(Aggregate) 处理命令、产生事件 “Order”
紫色/玫红 策略(Policy) 事件触发的自动化反应 “当订单确认时,通知仓库”
绿色 读模型(Read Model) 辅助决策的信息视图 “库存仪表盘”
粉红/红色 热点(Hot Spot) 需要进一步讨论的问题 “退款规则未明确”
白色 外部系统(External System) 与当前系统交互的外部系统 “支付网关”“ERP”

3.1 概念之间的关系

graph LR
    Actor["👤 参与者<br/>(黄色小)"] --> Command["📋 命令<br/>(蓝色)"]
    Command --> Aggregate["📦 聚合<br/>(黄色大)"]
    Aggregate --> Event["📌 领域事件<br/>(橙色)"]
    Event --> Policy["📜 策略<br/>(紫色)"]
    Policy --> Command2["📋 命令<br/>(蓝色)"]
    ReadModel["📊 读模型<br/>(绿色)"] -.->|"辅助决策"| Actor
    External["🏢 外部系统<br/>(白色)"] --> Event
    Event --> External

基本叙事结构是:参与者发出命令聚合处理命令并产生领域事件,事件触发策略,策略可能导致新的命令。


四、层次一:大图探索

4.1 目标

在最短时间内建立对整个业务域的共同理解。不追求精确,追求全面。

4.2 步骤

步骤 1:混沌探索(5-10 分钟)

所有参与者同时在橙色便利贴上写下他们能想到的领域事件。每张便利贴写一个事件,用过去时态。不讨论、不争论,只管写。

"订单已下达"  "支付已完成"  "商品已发货"  "退款已申请"
"库存已扣减"  "优惠券已使用"  "用户已注册"  "评价已提交"

步骤 2:时间线排列(15-20 分钟)

把所有便利贴按时间顺序从左到右贴在墙上。这一步会自然引发讨论:

这些讨论正是事件风暴的核心价值。

步骤 3:标记热点(5 分钟)

用粉红色便利贴标记所有有争议、不确定或需要进一步讨论的地方。

步骤 4:识别关键事件流(15-20 分钟)

从混沌中识别出几条核心事件流(通常是核心业务场景),用分隔线标记。

步骤 5:讨论和完善(30-60 分钟)

针对热点进行深入讨论,补充遗漏的事件,调整顺序。

4.3 大图探索的产出

时间线 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→

┌───────┐  ┌───────┐  ┌───────┐  ┌───────┐
│用户已  │  │商品已  │  │订单已  │  │支付已  │
│注册   │  │加入购  │  │下达   │  │发起   │
│       │  │物车   │  │       │  │       │
└───────┘  └───────┘  └───────┘  └───────┘
                                      │
                        ┌─────────────┘
                        ▼
                   ┌───────┐  ┌───────┐  ┌───────┐
                   │支付已  │  │库存已  │  │商品已  │
                   │完成   │  │扣减   │  │发货   │
                   │       │  │       │  │       │
                   └───────┘  └───────┘  └───────┘
                                              │
                                   ┌──────────┘
                                   ▼
                              ┌───────┐  ┌───────┐
                              │商品已  │  │订单已  │
                              │签收   │  │完成   │
                              │       │  │       │
                              └───────┘  └───────┘

[热点] 库存扣减时机:下单时还是支付后?
[热点] 退款流程:自动审批还是人工审批?

五、层次二:流程建模

5.1 目标

在大图探索的基础上,为每条核心事件流补充命令、参与者和策略,形成完整的业务流程模型。

5.2 补充命令和参与者

对每个领域事件,反向追问:

[参与者]     [读模型]        [命令]        [聚合]      [事件]
 客户    →  商品详情页   →   下单     →   Order   →  订单已下达
 客户    →  支付收银台   →   支付     →   Payment →  支付已完成
 系统    →              →   扣减库存  →   Stock   →  库存已扣减
 仓库员  →  拣货清单     →   确认发货  →   Shipment→  商品已发货

5.3 识别策略

策略(Policy)是事件驱动的自动化反应规则。它描述的是”当 X 事件发生时,自动执行 Y 命令”。

策略识别模式:

[事件]               [策略]                    [命令]
订单已下达      →    当订单下达时              →  锁定库存
支付已完成      →    当支付完成时              →  确认订单 + 通知仓库
支付超时        →    当支付超过 30 分钟未完成时  →  取消订单 + 释放库存
商品已签收      →    当签收后 7 天无异议时      →  自动完成订单

5.4 策略的类型

类型 说明 示例
自动化策略 事件发生后自动触发 支付完成 → 扣减库存
定时策略 基于时间条件触发 超时未支付 → 取消订单
人工策略 需要人工介入决策 退款申请 → 人工审核
条件策略 基于业务条件分支 金额 > 10000 → 风控审核

六、层次三:软件设计

6.1 目标

将流程模型转化为可实现的软件设计,识别聚合边界、定义命令处理流程、设计读模型。

6.2 识别聚合

聚合的识别基于以下原则:

  1. 处理一个命令并产生事件的实体集合构成一个聚合;
  2. 必须在同一事务中保持一致的数据属于同一个聚合;
  3. 不同的命令如果操作不同的数据集,通常意味着不同的聚合。
从事件风暴到聚合的推导:

命令 "下单" → 聚合 "Order"
  创建订单,包含行项信息
  产生事件 "OrderPlaced"

命令 "支付" → 聚合 "Payment"
  记录支付信息
  产生事件 "PaymentCompleted"

命令 "扣减库存" → 聚合 "Inventory"
  更新库存数量
  产生事件 "StockReserved"

6.3 从聚合到限界上下文

当多个聚合属于同一个业务领域、使用相同的通用语言时,它们通常属于同一个限界上下文。

graph TB
    subgraph "交易上下文"
        O["Order 聚合"]
        OL["OrderLine"]
        O --- OL
    end

    subgraph "支付上下文"
        P["Payment 聚合"]
        PR["PaymentRecord"]
        P --- PR
    end

    subgraph "库存上下文"
        I["Inventory 聚合"]
        R["Reservation"]
        I --- R
    end

    subgraph "物流上下文"
        S["Shipment 聚合"]
        T["TrackingEvent"]
        S --- T
    end

    O -.->|"OrderPlaced"| P
    O -.->|"OrderPlaced"| I
    P -.->|"PaymentCompleted"| O
    P -.->|"PaymentCompleted"| S
    S -.->|"ShipmentDelivered"| O

6.4 设计读模型

读模型(Read Model)是为查询优化的数据视图。事件风暴中识别的绿色便利贴直接对应系统中的读模型。

// 读模型:订单列表视图
public class OrderListView {
    private String orderId;
    private String customerName;
    private String totalAmount;
    private String status;
    private String paymentStatus;
    private String shippingStatus;
    private LocalDateTime createdAt;
}

// 读模型的投影器:监听事件,更新视图
public class OrderListProjector {

    public void on(OrderPlacedEvent event) {
        OrderListView view = new OrderListView();
        view.setOrderId(event.orderId().value());
        view.setTotalAmount(event.totalAmount().toString());
        view.setStatus("待支付");
        view.setCreatedAt(event.occurredAt());
        viewStore.save(view);
    }

    public void on(PaymentCompletedEvent event) {
        OrderListView view = viewStore.findByOrderId(event.orderId());
        view.setPaymentStatus("已支付");
        view.setStatus("待发货");
        viewStore.save(view);
    }

    public void on(ShipmentDeliveredEvent event) {
        OrderListView view = viewStore.findByOrderId(event.orderId());
        view.setShippingStatus("已签收");
        view.setStatus("已完成");
        viewStore.save(view);
    }
}

七、事件风暴的引导技巧

7.1 准备工作

准备项 说明
场地 需要一面至少 6 米长的墙或白板
便利贴 标准颜色各 200 张以上
参与者 8-15 人,必须包含领域专家
时间 首次工作坊至少预留 3 小时
引导者 1-2 人,熟悉事件风暴方法
无座位 所有人站立,保持活跃

7.2 引导者的关键行为

  1. 打破沉默:如果参与者不知道从哪里开始,先写几个事件做示范;
  2. 鼓励混沌:大图探索阶段不要追求秩序,混沌中会涌现洞察;
  3. 追问”然后呢”:当事件流断裂时,问”这个事件发生之后会怎样?“;
  4. 追问”之前呢”:当起点不清晰时,问”是什么导致了这个事件?“;
  5. 标记热点:有争议的地方立即贴粉红色便利贴,不要当场解决;
  6. 保护少数声音:确保领域专家的声音被听到,开发者不要主导讨论。

7.3 常见陷阱

陷阱 表现 应对
技术过早介入 讨论数据库设计而非业务流程 引导回”业务上发生了什么”
追求完美 反复修改同一区域 设定时间盒,标记热点后继续
人数过少 只有开发者参与 必须邀请领域专家
场地不够 便利贴挤在小白板上 使用至少 6 米的墙面
线性思维 只关注”正常流程” 主动提问异常场景

八、远程事件风暴

8.1 工具选择

工具 优势 劣势
Miro 无限画布、便利贴模板完善 需要付费
FigJam 与 Figma 生态集成 模板较少
MURAL 企业级功能完善 价格较高
Excalidraw 开源免费 协作功能有限

8.2 远程事件风暴的调整

远程环境下,事件风暴需要额外注意以下几点:

  1. 缩短单次时长:线上注意力集中时间更短,每次不超过 90 分钟;
  2. 提前准备模板:在工具中预设好颜色和区域;
  3. 分组讨论:超过 8 人时先分组探索,再合并;
  4. 录制过程:远程更容易遗忘上下文,录制便于回顾;
  5. 投票功能:使用工具的投票功能标记热点。

九、工程案例:在线教育平台的事件风暴

9.1 背景

某在线教育公司计划重构其课程管理和学习系统。原系统是一个单体应用,所有功能耦合在一起。团队希望通过事件风暴重新理解业务,并设计新的架构。

9.2 大图探索结果

工作坊参与者包括 3 名产品经理、2 名课程运营、1 名教研负责人、5 名开发者。

主要事件流(简化版):

课程管理流:
课程已创建 → 课程内容已上传 → 课程已审核通过 → 课程已上架

学员学习流:
学员已注册 → 课程已购买 → 学习进度已更新 → 课程已完成 → 证书已颁发

直播互动流:
直播间已创建 → 直播已开始 → 学员已加入 → 问答已提交 → 直播已结束 → 回放已生成

订单支付流:
订单已创建 → 优惠已应用 → 支付已发起 → 支付已完成 → 退款已申请 → 退款已处理

热点(共识不一致的区域): - “课程已完成”的判断标准是什么?学完所有章节?还是通过考试? - 退款策略:学完 30% 以上是否还能退款? - 直播回放是否需要单独计费?

9.3 流程建模(课程购买流)

sequenceDiagram
    participant Student as 学员
    participant OrderAgg as 订单聚合
    participant PayAgg as 支付聚合
    participant CourseAgg as 课程聚合
    participant EnrollAgg as 选课聚合

    Student->>OrderAgg: 创建订单(courseId, studentId)
    OrderAgg-->>OrderAgg: OrderCreated
    Note over OrderAgg: 策略:计算优惠

    Student->>PayAgg: 发起支付(orderId, amount)
    PayAgg-->>PayAgg: PaymentInitiated

    PayAgg-->>PayAgg: PaymentCompleted
    Note over PayAgg: 策略:支付完成时

    PayAgg->>OrderAgg: 确认订单(orderId)
    OrderAgg-->>OrderAgg: OrderConfirmed
    Note over OrderAgg: 策略:订单确认时

    OrderAgg->>EnrollAgg: 创建选课(studentId, courseId)
    EnrollAgg-->>EnrollAgg: StudentEnrolled
    Note over EnrollAgg: 策略:选课后

    EnrollAgg->>CourseAgg: 更新学员数(courseId)
    CourseAgg-->>CourseAgg: EnrollmentCountUpdated

9.4 识别出的限界上下文

限界上下文 核心聚合 子域类型
课程管理 Course、Chapter、Lesson 核心
学习体验 Enrollment、Progress、Certificate 核心
直播互动 LiveRoom、Session、QA 核心
交易 Order、Coupon 支撑
支付 Payment、Refund 支撑
用户 Student、Instructor 通用
通知 Notification 通用

9.5 效果


十、从事件风暴到代码

10.1 事件风暴产出与代码的映射

事件风暴概念 代码对应
橙色便利贴(事件) DomainEvent
蓝色便利贴(命令) Command 类或 API 端点
黄色大便利贴(聚合) Aggregate
紫色便利贴(策略) 事件处理器或 Saga
绿色便利贴(读模型) 查询服务或投影
白色便利贴(外部系统) 防腐层或适配器

10.2 Go 语言示例

// 命令
type CreateOrderCommand struct {
    CustomerID string
    CourseID   string
    CouponCode string
}

// 聚合
type Order struct {
    id         OrderID
    customerID CustomerID
    courseID    CourseID
    amount     Money
    discount   Money
    status     OrderStatus
    events     []DomainEvent
}

func NewOrder(cmd CreateOrderCommand, pricing PricingService) (*Order, error) {
    price, err := pricing.Calculate(cmd.CourseID, cmd.CouponCode)
    if err != nil {
        return nil, fmt.Errorf("计算价格失败: %w", err)
    }

    order := &Order{
        id:         NewOrderID(),
        customerID: CustomerID(cmd.CustomerID),
        courseID:    CourseID(cmd.CourseID),
        amount:     price.Original,
        discount:   price.Discount,
        status:     OrderStatusCreated,
    }

    order.events = append(order.events, OrderCreatedEvent{
        OrderID:    order.id,
        CustomerID: order.customerID,
        CourseID:   order.courseID,
        Amount:     order.amount,
        OccurredAt: time.Now(),
    })

    return order, nil
}

// 策略(事件处理器)
type EnrollmentPolicy struct {
    enrollmentRepo EnrollmentRepository
}

func (p *EnrollmentPolicy) OnOrderConfirmed(event OrderConfirmedEvent) error {
    enrollment, err := NewEnrollment(event.CustomerID, event.CourseID)
    if err != nil {
        return err
    }
    return p.enrollmentRepo.Save(enrollment)
}

// 读模型投影
type MyCoursesProjector struct {
    store ReadModelStore
}

func (p *MyCoursesProjector) OnStudentEnrolled(event StudentEnrolledEvent) error {
    view := MyCourseView{
        StudentID: event.StudentID.String(),
        CourseID:  event.CourseID.String(),
        Status:    "学习中",
        EnrolledAt: event.OccurredAt,
        Progress:  0,
    }
    return p.store.Save(view)
}

func (p *MyCoursesProjector) OnProgressUpdated(event ProgressUpdatedEvent) error {
    view, err := p.store.FindByCourseAndStudent(
        event.CourseID.String(), event.StudentID.String())
    if err != nil {
        return err
    }
    view.Progress = event.Percentage
    if event.Percentage >= 100 {
        view.Status = "已完成"
    }
    return p.store.Save(view)
}

十一、事件风暴的综合权衡

维度 事件风暴 传统需求分析 权衡点
参与度 高:所有角色同时参与 低:分角色串行 组织成本 vs 理解深度
时间投入 集中 2-4 小时 分散数周 前置投入 vs 持续投入
输出形式 便利贴墙(非正式) 正式文档 即时性 vs 可追溯性
异常覆盖 高:混沌探索自然发现 低:容易遗漏 发现未知 vs 确认已知
远程友好 中等:需要工具支持 高:文档天然异步 协作效果 vs 灵活性
适用规模 8-15 人 不限 深度讨论 vs 广泛覆盖
领域专家依赖 高:必须参与 中:可通过文档传递 知识质量 vs 可行性
学习曲线 低:便利贴人人会用 中:UML 等需要培训 上手速度 vs 表达精度

十二、下一步

事件风暴揭示了业务领域的事件流和上下文边界。当你需要将一个上下文与外部的遗留系统集成时,防腐层(Anti-Corruption Layer)和开放主机服务(Open Host Service)是两种关键的保护机制。

下一篇:防腐层与开放主机服务:系统集成的 DDD 方案


参考资料

  1. Brandolini, Alberto. Introducing EventStorming. Leanpub, 2021.
  2. Brandolini, Alberto. “Event Storming.” eventstorming.com.
  3. Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
  4. Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013.
  5. Fowler, Martin. “Domain Event.” martinfowler.com.
  6. Avram, Abel; Marinescu, Floyd. Domain-Driven Design Quickly. InfoQ, 2007.
  7. Young, Greg. “CQRS and Event Sourcing.” cqrs.files.wordpress.com, 2010.
  8. Rayner, Paul. “Event Storming Workshop Guide.” Virtual Genius, 2020.

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .