
Ddd Event Storming
- 15 installs
- 1 repo stars
- Updated July 29, 2026
- full-statck-skills/ddd-skills
Facilitates an Event Storming workshop with a 6-step process and sticky-note color conventions to explore a domain and discover aggregates and bounded contexts.
About
Guides facilitation of Brandolini's Event Storming workshop with a six-step collaborative process and standardized note colors. A developer uses it to run a DDD discovery workshop on a new or complex domain.
- 6-step ~2.5 hour workshop structure
- Standardized sticky-note color conventions
Ddd Event Storming by the numbers
- 15 all-time installs (skills.sh)
- Ranked #3,491 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/full-statck-skills/ddd-skills --skill ddd-event-stormingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | full-statck-skills/ddd-skills ↗ |
What it does
Facilitates an Event Storming workshop with a 6-step process and sticky-note color conventions to explore a domain and discover aggregates and bounded contexts.
Files
DDD Event Storming
Event Storming (事件风暴) 是由 Alberto Brandolini 发明的协作式领域探索工作坊方法论。通过跨角色(领域专家 + 技术团队)的即时贴协作,在短时间内完成复杂业务领域的建模,产出聚合、限界上下文和领域事件。
Workflow
6-Step Event Storming Workshop (约 2.5 小时):
1. Step 1: 混沌探索 (30min) — 自由发散,贴出所有领域事件(🟠 橙色便签) 2. Step 2: 时间线排序 (20min) — 按时间顺序排列事件,识别主线与分支 3. Step 3: 关键事件标记 (15min) — 标记业务流程的转折点(★ 标记) 4. Step 4: 命令与角色 (30min) — 为每个事件补全命令 + 角色 5. Step 5: 聚合发现 (30min) — 聚类相关事件为聚合,命名聚合根 6. Step 6: 限界上下文划分 (20min) — 按耦合度划分 BC,确定上下文映射
产出物: 事件时间线 / 聚合候选 / BC 映射 / Hot Spots
参与角色: 领域专家(2-3人) / 开发(2-3人) / 产品(1人) / 架构师(1人)
When to Use
| 适用场景 | 不适用场景 |
|---|---|
| 新项目启动,需要领域建模 | 领域知识已充分文档化,直接 domain-designer |
| 已有系统重构,业务不清晰 | 单人开发,无利益相关者参与 |
| 跨团队协作,需要统一语言 | 纯技术工具开发,无业务领域 |
| 微服务拆分,需确定服务边界 | 简单 CRUD 项目,领域模型清晰 |
Skill Boundary
| 技能 | 定位 | 使用时机 |
|---|---|---|
ddd-event-storming | 工作坊主持 + 领域探索 | 项目前期,多角色协作 |
ddd-domain-designer | 聚合详细设计 + 代码映射 | 工作坊之后,开发之前 |
Audience
This skill is designed for: Backend developers (implementing DDD architectures), Software architects (evaluating and selecting patterns), Tech leads (reviewing team implementations), and DDD beginners (learning domain-driven design fundamentals).
6-Step 流程详解
Step 1: 混沌探索 — 30 min
参与者自由发散,用 🟠 橙色便签 贴出所有已知领域事件(动词过去式,如 OrderPlaced)。禁止讨论——主持人必须严格执行"先贴再说"原则。
活动: 自由贴出领域事件
格式: 动词过去式 (OrderPlaced, PaymentCompleted)
规则: 不讨论、不质疑、不排序
目标: 覆盖所有业务场景,包括异常流Step 2: 时间线排序 — 20 min
将所有 🟠 事件便签按时间顺序排列,识别主线与分支。主线用箭头连接,异常流从主线分叉标注条件。
Step 3: 关键事件标记 — 15 min
标记业务流程的转折点,这些是关键事件(★ 标记),是领域模型的核心锚点。特征:触发后续流程、改变实体状态、跨上下文通信。
Step 4: 命令与角色 — 30 min
为每个事件添加 🔵 蓝色命令(触发动作)和 🟡 黄色角色(执行者)。自动触发的事件也有命令发起者(System、Scheduler、ExternalSystem)。
👤 Customer → 🔵 Create Order → 🟠 OrderPlaced
👤 Admin → 🔵 Approve Refund → 🟠 RefundApproved
🤖 System → 🔵 Deduct Stock → 🟠 InventoryDeductedStep 5: 聚合发现 — 30 min
将相关的事件、命令聚类为聚合,并用业务语言命名聚合根。聚合是事务一致性边界——边界内强一致,边界间最终一致。
┌─ Order Aggregate ────────────────────────┐
│ 🟠 OrderPlaced / 🟠 OrderPaid │
│ 🟠 OrderCancelled / 🟠 OrderCompleted │
│ 🔵 PlaceOrder / 🔵 PayOrder │
│ 👤 Customer │
│ 聚合根: Order | 不变式: 已取消不可支付 │
└──────────────────────────────────────────┘Step 6: 限界上下文划分 — 20 min
根据聚合间的耦合度和业务语言变化划出 BC 边界,标注映射关系。上下文映射类型:Partnership、Shared Kernel、Customer-Supplier、Conformist、Anti-Corruption Layer、Open Host Service。
便签颜色规范
| 颜色 | 含义 | 格式要求 | 示例 |
|---|---|---|---|
| 🟠 橙色 | Domain Event (领域事件) | 动词过去式 | OrderPaid, UserRegistered |
| 🔵 蓝色 | Command (命令) | 动词原形 | PayOrder, RegisterUser |
| 🟡 黄色 | Actor/Role (角色/参与者) | 名词 | Customer, System, Admin |
| 🟢 绿色 | Read Model (读模型/视图) | 名词 | OrderDetailPage, Dashboard |
| 🔴 粉色 | External System (外部系统) | 系统名 | Alipay, WeChatPay |
| 🟣 紫色 | Constraint/Hot Spot (约束/争议) | 业务规则语句 | Max ¥50000 per order |
Workshop Outputs
产出物:Event Timeline (Mermaid 序列图)、Aggregate Candidates、Bounded Context Map (Mermaid C4 图)、Context Mapping、Hot Spot List、Ubiquitous Language 术语表。
Facilitation Tips
- 会前: 准备 5+ 色便签 + 大白板/Miro 模板;必须确保领域专家参与;明确工作坊范围;提前 5 分钟介绍颜色规范和流程
- 会中: Step 1 禁止讨论(最常见的失败原因);用业务语言优先;Hot Spot 用紫色便签标记不现场解决;严格时间盒管理;注意保持能量
- 会后: 立刻拍照存档;24h 内将产出数字化为领域文档;安排 Hot Spot 跟进会;将产出物作为
ddd-domain-designer的输入
Online Tool Recommendations
Miro(分布式团队,推荐)| Mural(视频会议集成)| draw.io(免费轻量)| Physical Whiteboard(同地团队首选)
Relationship with ddd-domain-designer
推荐路径: event-storming (工作坊产出) → domain-designer (聚合详细设计 + 代码落地) → Architecture Skill (架构实现)
event-storming 聚焦协作工作坊主持(领域专家 + 全体团队),产出便签墙、事件列表、BC 划分(概念级);domain-designer 聚焦开发者聚合设计(开发者 + 架构师),产出代码结构、聚合类、仓储接口(代码级)。
Customization
| 场景 | 建议调整 |
|---|---|
| 小型团队(3-5人) | 合并 Step 1+2 为 20min+15min;Skip Step 6 直接讨论 |
| 大型团队(10+人) | Step 1 延长至 45min 分组并行贴便签;Step 6 增加 10min 分组汇报 |
| 时间受限(1小时) | 只做 Step 1-3(事件发散+排序),后续步骤单独安排 |
| 复杂领域(多 BC) | 每个 BC 单独一次工作坊,不要试图一次覆盖 |
| 跨语言团队 | 便签使用双语(中文事件+英文术语),以中文共识为准 |
Quick Start
直接把需求发给 AI,以下开场白可直接使用:
"帮我组织一个电商订单履约的事件风暴工作坊,需要 6 步流程"
"我要做一个保险投保领域的事件风暴,领域专家有 2 位"
"做一次用户中台的事件风暴,重点关注认证和权限"
"之前做完了物流配送的事件风暴,帮我整理产出物"如果你不确定从哪里开始,直接说"帮我做事件风暴",AI 会自动引导你完成。
Security & Safety
This skill is pure documentation. It contains no executable scripts, collects no user data, accesses no external services or networks.
Gotchas — Common Pitfalls
1. 跳过 Brainstorming 直接讨论: 必须先自由发散再排序。过早讨论事件正确性会扼杀创意。 2. 技术人员主导: 必须由领域专家主导,开发做记录和提问。开发主导的模型不是真实业务。 3. CRUD 式命令: 不要写 "CRUD Order",命令应该是业务动作:PlaceOrder、CancelOrder。 4. 遗忘 Hot Spot: 所有分歧和不确定的边界必须记录为紫色便签。不记录的 Hot Spot 会后被遗忘。 5. 跳过 Aggregate Discovery: 做完 Step 4 就结束。没有 Step 5 的聚类,事件风暴只是漂亮的便签墙。 6. 领域专家缺失: 这是致命错误。没有领域专家参与事件风暴会产出错误的领域模型。 7. 试图一次覆盖整个企业: 范围太大会导致浅尝辄止。建议从核心子域开始,一个 BC 一次。 8. 产出物不整理 → 便签墙变装饰品: 工作坊结束后 24h 内必须将产出数字化,否则记忆会丢失。用 workshop-output-template.md 模板快速整理。 9. 没有通用语言记录: 风暴中自然产生的术语定义的共识,必须记录下来成为 Ubiquitous Language 表。不记录等于没达成共识。
FAQ
| 问题 | 回答 |
|---|---|
| 工作坊需要多长时间? | 一次 BC 约 2.5 小时完整 6 步。中型项目总周期约 2 周。 |
| 没有正式领域专家怎么办? | 从业务人员/产品经理/有领域经验的高级开发中选择。 |
| 线上和线下哪个更好? | 线下(实体白板)互动感最强,线上(Miro)可异步协作。 |
| 需要多少参与者? | 最小 3 人(领域专家+开发+产品),最优 6-8 人。 |
| 产出物如何使用? | 作为 ddd-domain-designer 的输入,进入聚合详细设计和代码落地。 |
| 一个工作坊能覆盖多大范围? | 推荐一个 BC 一次。复杂领域可按子域拆分多个工作坊。 |
| 主持人必须是 DDD 专家吗? | 不需要精通 DDD,但需要理解 6 步流程和颜色规范,具备引导能力。 |
| 产出物怎么和代码对应? | 聚合候选 → domain-designer Aggregate,事件 → Domain Event 类,BC → 微服务模块。 |
References
详情见 references/ 和 examples/:
- Templates: sticky-note-template.md, timeline-example.md, workshop-output-template.md
- Guides: facilitation-checklist.md, workshop-deep-guide.md, context-mapping.md, partme-12-event-storming-modeling.md, ddd-strategic.md, clean-ddd-hexagonal-strategic.md
- Examples: 5 个工作坊案例(电商订单履约、保险投保、用户中台、物流配送、金融支付)
案例:电商订单履约事件风暴工作坊
基本信息
- 领域: 电商 — 订单履约
- 日期: 2024-06-15
- 时长: 3 小时
- 参与角色: 业务专家(2)、产品经理(1)、架构师(1)、开发(3)、测试(1)
Step 1:混沌探索 — 领域事件清单
主线事件
🟠 订单已创建 🟠 订单项已添加
🟠 支付已完成 🟠 库存已扣减
🟠 发货已安排 🟠 包裹已揽收
🟠 包裹已签收 🟠 订单已完成
🟠 发票已开具 🟠 评价已提交异常事件
🟠 支付已超时 🟠 订单已取消
🟠 库存已释放 🟠 退款已发起
🟠 退款已到账 🟠 发货已拒绝
🟠 地址已修改 🟠 订单已拆分Step 2:时间线排序
Happy Path:
🟠 订单已创建 → 🟠 支付已完成 → 🟠 库存已扣减 → 🟠 发货已安排 → 🟠 包裹已签收 → 🟠 订单已完成
异常路径:
🟠 订单已创建 → (⏱ 30分钟) → 🟠 支付已超时 → 🟠 订单已取消 → 🟠 库存已释放
🟠 订单已创建 → 🟠 支付已完成 → 🟠 库存不足 → 🟠 退款已发起 → 🟠 退款已到账Step 3:关键事件
| 关键事件 | 理由 |
|---|---|
| ★ 订单已创建 | 触发订单生命周期的起点 |
| ★ 支付已完成 | 触发履约流程(库存+发货) |
| ★ 发货已安排 | 触发物流流程 |
| ★ 包裹已签收 | 触发财务流程(收入确认) |
Step 4:命令与角色
👤 客户 → 🔵 创建订单 → 🟠 订单已创建
👤 客户 → 🔵 提交支付 → 🟠 支付已完成
🤖 系统 → 🔵 扣减库存 → 🟠 库存已扣减
👤 仓库 → 🔵 安排发货 → 🟠 发货已安排
👤 快递员 → 🔵 确认签收 → 🟠 包裹已签收
🔴 支付宝 → → 🟠 支付已完成
🔴 物流系统 → → 🟠 物流已更新
🤖 定时器 → 🔵 超时取消 → 🟠 订单已取消Step 5:聚合发现
┌─ Order Aggregate ─────────────────────┐
│ 🟠 订单已创建 │
│ 🟠 订单项已添加 │
│ 🟠 支付已完成 │
│ 🟠 订单已取消 │
│ 🟠 订单已完成 │
│ 🔵 创建订单 / 提交支付 / 取消订单 │
│ 👤 客户 │
│ 聚合根: Order │
│ 不变式: 已取消的订单不可支付 │
└────────────────────────────────────────┘
┌─ Inventory Aggregate ─────────────────┐
│ 🟠 库存已扣减 │
│ 🟠 库存已释放 │
│ 🟠 库存不足 │
│ 🔵 扣减库存 / 释放库存 │
│ 🤖 系统 │
│ 聚合根: Product │
│ 不变式: 库存不可为负数 │
└────────────────────────────────────────┘
┌─ Shipment Aggregate ──────────────────┐
│ 🟠 发货已安排 │
│ 🟠 包裹已揽收 │
│ 🟠 包裹已签收 │
│ 🟠 发货已拒绝 │
│ 🔵 安排发货 / 确认签收 │
│ 👤 仓库 / 快递员 │
│ 聚合根: Shipment │
└────────────────────────────────────────┘
┌─ Payment Aggregate ───────────────────┐
│ 🟠 支付已完成 │
│ 🟠 退款已发起 │
│ 🟠 退款已到账 │
│ 🔵 发起退款 │
│ 👤 客服 │
│ 聚合根: Payment │
└────────────────────────────────────────┘Step 6:限界上下文划分
┌─────────────────────────────────────────────────────────────┐
│ E-Commerce System │
├──────────────────┬──────────────────┬───────────────────────┤
│ Order Context │ Inventory Context │ Logistics Context │
│ (Core) │ (Supporting) │ (Supporting) │
│ │ │ │
│ 聚合: Order │ 聚合: Product │ 聚合: Shipment │
│ 聚合: Payment │ │ 聚合: Carrier │
├──────────────────┴──────────────────┴───────────────────────┤
│ │
│ 上下文映射: │
│ Order Context → Logistics Context: Customer-Supplier │
│ (通过领域事件 OrderPlaced 触发 Shipment 创建) │
│ Order Context → Inventory Context: Partnership │
│ (稠密协作,每次订单变更同步库存) │
└─────────────────────────────────────────────────────────────┘Hot Spots
| # | 主题 | 描述 | 优先级 |
|---|---|---|---|
| 1 | 库存不足策略 | 部分发货 vs 全部取消? | P0 |
| 2 | 支付超时时间 | 30分钟是否合理? | P1 |
| 3 | 退款流程 | 退款是否需要审批? | P1 |
通用语言
| 术语 | 含义 |
|---|---|
| Order | 客户购买商品集合,包含多个订单项 |
| SKU | 库存单位,唯一标识商品 |
| Shipment | 物流包裹,包含一次发货的所有商品 |
案例:金融支付领域事件风暴工作坊
基本信息
- 领域: 金融科技 — 支付清算
- 日期: 2024-08-20
- 时长: 4 小时
- 参与角色: 业务专家(3)、产品经理(1)、架构师(2)、开发(4)、合规(1)
Step 1:混沌探索 — 领域事件清单
主线事件
🟠 支付指令已接收 🟠 风控检查已完成
🟠 账户余额已锁定 🟠 支付已处理
🟠 清算已发起 🟠 结算已完成
🟠 商户已收到款 🟠 交易对账已通过
🟠 支付通知已发送 🟠 凭证已生成异常事件
🟠 风控规则已触发 🟠 支付已拒绝
🟠 余额已不足 🟠 余额已解锁
🟠 清算已失败 🟠 交易已冲正
🟠 对账已失败 🟠 差错已上报
🟠 退款已发起 🟠 退款已完成Step 2:时间线排序
Happy Path:
🟠 支付指令已接收 → 🟠 风控检查已完成 → 🟠 账户余额已锁定 → 🟠 支付已处理
→ 🟠 清算已发起 → 🟠 结算已完成 → 🟠 商户已收到款 → 🟠 交易对账已通过
异常路径:
🟠 支付指令已接收 → 🟠 风控规则已触发 → 🟠 支付已拒绝
🟠 账户余额已锁定 → 🟠 余额已不足 → 🟠 余额已解锁 → 🟠 交易已冲正Step 3:关键事件
| 关键事件 | 重要性 |
|---|---|
| ★ 风控检查已完成 | 决定交易是否继续的转折点 |
| ★ 支付已处理 | 资金实际发生转移的节点 |
| ★ 交易对账已通过 | 日终结算确认,影响资金安全 |
Step 4:命令与角色
👤 用户 → 🔵 Submit Payment → 🟠 PaymentInstructionReceived
🤖 风控系统 → 🔵 Approve / Reject → 🟠 RiskCheckCompleted / RiskTriggered
👤 系统 → 🔵 Lock Balance → 🟠 BalanceLocked
👤 系统 → 🔵 Process Payment → 🟠 PaymentProcessed
🔴 银联 → → → 🟠 ClearingCompleted
👤 系统 → 🔵 Settle → 🟠 SettlementCompleted
🤖 定时任务 → 🔵 Daily Reconciliation → 🟠 ReconciliationPassed / FailedStep 5:聚合发现
┌─ Payment Aggregate ───────────────────────┐
│ 🟠 PaymentInstructionReceived │
│ 🟠 PaymentProcessed / Rejected │
│ 🟠 PaymentReversed │
│ 🔵 SubmitPayment / Approve / Reverse │
│ 👤 User / RiskSystem │
│ 聚合根: Payment │
│ 不变式: 已拒绝的支付不可再次提交 │
└────────────────────────────────────────────┘
┌─ Settlement Aggregate ─────────────────────┐
│ 🟠 ClearingCompleted / SettlementCompleted │
│ 🟠 ReconciliationPassed / Failed │
│ 🟠 SettlementReversed │
│ 🔵 Settle / Reconcile │
│ 聚合根: Settlement │
└────────────────────────────────────────────┘Step 6:限界上下文划分
┌─ Payment Context ──┐ → ┌─ Settlement Context ──┐ → ┌─ Notification Context ──┐
│ Payment Aggregate │ │ Settlement Aggregate │ │ Notification Aggregate │
│ (Core Domain) │ │ (Core Domain) │ │ (Supporting Domain) │
└────────────────────┘ └───────────────────────┘ └────────────────────────┘Workshop 产出建议
1. 支付聚合 包含指令、处理、冲正 — 事务一致性边界 2. 结算聚合 包含清算、结算、对账 — 日终批量处理 3. 风控 作为外部系统(粉色便签)与 Payment Context 通过 ACL 集成 4. Hot Spots: 日切时间点、跨行清算超时、退款场景的资金归属
案例:保险投保领域事件风暴工作坊
基本信息
- 领域: 保险 — 投保与保单管理
- 日期: 2024-07-20
- 时长: 2.5 小时
- 参与角色: 核保专家(1)、业务运营(1)、产品经理(1)、架构师(1)、开发(2)
Step 1:混沌探索 — 领域事件清单
🟠 投保单已创建 🟠 核保已通过
🟠 核保已拒绝 🟠 缴费通知单已生成
🟠 缴费已完成 🟠 保单已生成
🟠 保单已生效 🟠 保单已续期
🟠 保单已终止 🟠 理赔已申请
🟠 理赔已审批 🟠 理赔款已支付
🟠 受益人已变更 🟠 投保人已变更Step 2:时间线
新投保流程:
🟠 投保单已创建 → 🟠 核保已通过 → 🟠 缴费通知单已生成 → 🟠 缴费已完成 → 🟠 保单已生成 → 🟠 保单已生效
核保失败:
🟠 投保单已创建 → 🟠 核保已拒绝 → 🟠 投保单已关闭
续期流程:
🟠 保单将到期(⏱ 到期前30天) → 🟠 续期通知已发送 → 🟠 缴费已完成 → 🟠 保单已续期
理赔流程:
🟠 理赔已申请 → 🟠 理赔资料已提交 → 🟠 理赔已审批 → 🟠 理赔款已支付Step 3:关键事件
| 关键事件 | 理由 |
|---|---|
| ★ 核保已通过 | 投保流程的关键决策点 |
| ★ 缴费已完成 | 触发保单生成 |
| ★ 保单已生效 | 保险责任开始 |
Step 4:命令与角色
👤 客户 → 🔵 创建投保单 → 🟠 投保单已创建
👤 核保人员 → 🔵 执行核保 → 🟠 核保已通过 / 🟠 核保已拒绝
🤖 系统 → 🔵 生成缴费通知 → 🟠 缴费通知单已生成
👤 客户 → 🔵 完成缴费 → 🟠 缴费已完成
🤖 系统 → 🔵 生成保单 → 🟠 保单已生成
👤 客户 → 🔵 申请理赔 → 🟠 理赔已申请
👤 核保人员 → 🔵 审批理赔 → 🟠 理赔已审批Step 5:聚合发现
┌─ Application Aggregate ──────────────┐
│ 🟠 投保单已创建 / 🟠 核保已通过 │
│ 🟠 核保已拒绝 / 🟠 投保单已关闭 │
│ 🔵 创建投保单 / 执行核保 │
│ 聚合根: Application │
│ 实体: InsuredPerson, SubjectMatter │
└──────────────────────────────────────┘
┌─ Policy Aggregate ───────────────────┐
│ 🟠 保单已生成 / 🟠 保单已生效 │
│ 🟠 保单已续期 / 🟠 保单已终止 │
│ 🟠 受益人已变更 / 🟠 投保人已变更 │
│ 聚合根: Policy │
│ 值对象: Coverage, Premium, Term │
└──────────────────────────────────────┘
┌─ Payment Aggregate ──────────────────┐
│ 🟠 缴费通知单已生成 │
│ 🟠 缴费已完成 │
│ 🔵 生成缴费通知 / 完成缴费 │
│ 聚合根: PaymentNotice │
└──────────────────────────────────────┘
┌─ Claim Aggregate ────────────────────┐
│ 🟠 理赔已申请 / 🟠 理赔资料已提交 │
│ 🟠 理赔已审批 / 🟠 理赔款已支付 │
│ 🔵 申请理赔 / 提交资料 / 审批理赔 │
│ 聚合根: Claim │
└──────────────────────────────────────┘Step 6:限界上下文划分
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 投保上下文 │ │ 保单上下文 │ │ 收款上下文 │ │ 理赔上下文 │
│ (Core) │ │ (Core) │ │ (Supporting) │ │ (Core) │
├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤
│ Application│ │ Policy │ │ Payment │ │ Claim │
│ Aggregate │ │ Aggregate │ │ Aggregate │ │ Aggregate │
└──────┬──────┘ └──────▲──────┘ └──────▲──────┘ └──────┬──────┘
│ │ │ │
└──────┼────────┘ └───────┼────────┘
共享内核: 客户信息 Customer-SupplierHot Spots
| # | 主题 | 描述 |
|---|---|---|
| 1 | 核保规则引擎 | 自动核保 vs 人工核保的分界条件是什么? |
| 2 | 保单生效日 | T+1 还是缴费后即时生效? |
通用语言
| 术语 | 含义 |
|---|---|
| 投保单 | 客户申请保险的请求 |
| 核保 | 保险公司的风险评估过程 |
| 保单 | 保险合同的法律文件 |
案例:物流配送领域事件风暴工作坊
基本信息
- 领域: 物流 — 配送管理与跟踪
- 日期: 2024-09-05
- 时长: 2.5 小时
- 参与角色: 运营专家(1)、配送站长(1)、产品经理(1)、架构师(1)、开发(2)
Step 1:混沌探索 — 领域事件清单
配送流程
🟠 配送单已创建 🟠 拣货已完成
🟠 包裹已出库 🟠 配送单已分配
🟠 配送员已接单 🟠 包裹已揽收
🟠 包裹已到达站点 🟠 配送中
🟠 包裹已签收 🟠 包裹已拒收
🟠 配送已完成 🟠 异常已上报仓储流程
🟠 入库单已创建 🟠 商品已入库
🟠 库存已更新 🟠 库存已预警
🟠 退货已发起 🟠 退货已入库Step 2:时间线
标准配送:
🟠 配送单已创建 → 🟠 拣货已完成 → 🟠 包裹已出库 → 🟠 配送单已分配
→ 🟠 配送员已接单 → 🟠 包裹已揽收 → 🟠 配送中 → 🟠 包裹已签收
异常配送:
🟠 配送中 → (用户拒收) → 🟠 包裹已拒收 → 🟠 退货已发起 → 🟠 退货已入库
🟠 配送中 → (地址错误) → 🟠 异常已上报 → 🟠 配送单已修改 → 🟠 配送中
🟠 配送员已接单 → (⏱ 30分钟未揽收) → 🟠 配送单已重新分配Step 3:关键事件
| 关键事件 | 理由 |
|---|---|
| ★ 包裹已出库 | 货物离开仓库,责任转移 |
| ★ 配送员已接单 | 配送环节开始,时效开始计算 |
| ★ 包裹已签收 | 物流完成,触发财务结算 |
Step 4:命令与角色
👤 运营 → 🔵 创建配送单 → 🟠 配送单已创建
👤 仓库 → 🔵 完成拣货 → 🟠 拣货已完成
🤖 系统 → 🔵 分配配送单 → 🟠 配送单已分配
👤 配送员 → 🔵 接单 → 🟠 配送员已接单
👤 配送员 → 🔵 揽收包裹 → 🟠 包裹已揽收
👤 配送员 → 🔵 确认送达 → 🟠 包裹已签收
👤 客户 → 🔵 拒收包裹 → 🟠 包裹已拒收
🔴 地图API → → 🟠 配送轨迹已更新Step 5:聚合发现
┌─ DeliveryOrder Aggregate ────────────┐
│ 🟠 配送单已创建 / 🟠 配送单已分配 │
│ 🟠 配送单已修改 │
│ 🔵 创建配送单 / 分配配送单 │
│ 聚合根: DeliveryOrder │
│ 值对象: Recipient, DeliveryAddress │
└──────────────────────────────────────┘
┌─ Dispatch Aggregate ─────────────────┐
│ 🟠 拣货已完成 / 🟠 包裹已出库 │
│ 🟠 入库单已创建 / 🟠 商品已入库 │
│ 🔵 完成拣货 / 确认出库 │
│ 聚合根: DispatchTask │
└──────────────────────────────────────┘
┌─ Delivery Aggregate ─────────────────┐
│ 🟠 配送员已接单 / 🟠 包裹已揽收 │
│ 🟠 配送中 / 🟠 包裹已签收 │
│ 🟠 包裹已拒收 / 🟠 配送已完成 │
│ 🟠 异常已上报 │
│ 🔵 接单 / 揽收 / 确认送达 / 拒收 │
│ 👤 配送员 / 客户 │
│ 聚合根: Delivery │
│ 不变式: 已签收不可修改配送状态 │
└──────────────────────────────────────┘
┌─ Inventory Aggregate ────────────────┐
│ 🟠 库存已更新 / 🟠 库存已预警 │
│ 🟠 退货已发起 / 🟠 退货已入库 │
│ 🔵 更新库存 / 发起退货 │
│ 聚合根: InventoryItem │
└──────────────────────────────────────┘Step 6:限界上下文划分
┌──────────────────────────────────────────────────────┐
│ 物流配送系统 │
├──────────────┬──────────────┬───────────┬────────────┤
│ 配送订单 │ 分拣出库 │ 在途配送 │ 库存管理 │
│ (Core) │ (Supporting) │ (Core) │ (Supporting)│
├──────────────┼──────────────┼───────────┼────────────┤
│ DeliveryOrder│ DispatchTask │ Delivery │ Inventory │
│ Aggregate │ Aggregate │ Aggregate │ Aggregate │
├──────────────┴──────────────┴───────────┴────────────┤
│ │
│ 上下文映射: │
│ 配送订单 → 分拣出库: Customer-Supplier │
│ 分拣出库 → 在途配送: Customer-Supplier │
│ 退货入库涉及: 在途配送 → 库存管理: Partnership │
└────────────────────────────────────────────────────────┘Hot Spots
| # | 主题 | 描述 |
|---|---|---|
| 1 | 配送时效计算 | 从哪个节点开始计算配送时效?接单还是揽收? |
| 2 | 异常处理流程 | 地址错误如何修改,是否需要审批? |
通用语言
| 术语 | 含义 |
|---|---|
| 配送单 | 包含配送地址、商品、配送员分配的工单 |
| 揽收 | 配送员从站点取走包裹 |
| 签收 | 客户确认收到包裹 |
案例:用户中台领域事件风暴工作坊
基本信息
- 领域: 用户中台 — 用户认证与权限管理
- 日期: 2024-08-10
- 时长: 3 小时
- 参与角色: 业务专家(1)、产品经理(1)、架构师(1)、开发(2)、测试(1)
Step 1:混沌探索 — 领域事件清单
用户管理
🟠 用户已注册 🟠 用户信息已更新
🟠 密码已重置 🟠 账户已锁定
🟠 账户已解锁 🟠 用户已注销
🟠 手机号已绑定 🟠 邮箱已绑定认证相关
🟠 登录已成功 🟠 登录已失败
🟠 令牌已签发 🟠 令牌已刷新
🟠 令牌已过期 🟠 双因素已验证权限管理
🟠 岗位已创建 🟠 岗位已分配
🟠 权限已分配 🟠 权限已回收
🟠 角色已创建 🟠 角色已变更
🟠 菜单已配置 🟠 系统已创建Step 2:时间线
用户注册流程:
🟠 用户已注册 → 🟠 手机号已绑定 → 🟠 登录已成功 → 🟠 令牌已签发
登录流程:
🟠 登录请求已收到 → 🟠 双因素已验证 → 🟠 令牌已签发 → 🟠 登录已成功
🟠 登录已失败(3次) → 🟠 账户已锁定
权限配置流程:
🟠 系统已创建 → 🟠 岗位已创建 → 🟠 菜单已配置 → 🟠 岗位已分配 → 🟠 权限已分配Step 3:关键事件
| 关键事件 | 理由 |
|---|---|
| ★ 用户已注册 | 用户生命周期的起点 |
| ★ 令牌已签发 | 认证完成,会话开始 |
| ★ 账户已锁定 | 触发安全告警和人工介入 |
Step 4:命令与角色
👤 访客 → 🔵 用户注册 → 🟠 用户已注册
👤 用户 → 🔵 用户登录 → 🟠 登录已成功 / 🟠 登录已失败
🤖 系统 → 🔵 签发令牌 → 🟠 令牌已签发
👤 用户 → 🔵 重置密码 → 🟠 密码已重置
🤖 系统 → 🔵 锁定账户 → 🟠 账户已锁定(连续登录失败3次)
👤 管理员 → 🔵 创建岗位 → 🟠 岗位已创建
👤 管理员 → 🔵 分配权限 → 🟠 权限已分配Step 5:聚合发现
┌─ User Aggregate ──────────────────────┐
│ 🟠 用户已注册 / 🟠 用户信息已更新 │
│ 🟠 密码已重置 / 🟠 用户已注销 │
│ 🟠 手机号已绑定 / 🟠 邮箱已绑定 │
│ 🔵 用户注册 / 更新信息 / 重置密码 │
│ 聚合根: User │
│ 值对象: UserId, PhoneNumber, Email │
└────────────────────────────────────────┘
┌─ Account Aggregate ───────────────────┐
│ 🟠 账户已锁定 / 🟠 账户已解锁 │
│ 🟠 登录已成功 / 🟠 登录已失败 │
│ 🔵 用户登录 / 锁定账户 / 解锁账户 │
│ 聚合根: Account │
│ 不变式: 锁定账户不可登录 │
└────────────────────────────────────────┘
┌─ Role Aggregate ──────────────────────┐
│ 🟠 岗位已创建 / 🟠 岗位已分配 │
│ 🟠 权限已分配 / 🟠 权限已回收 │
│ 🟠 角色已创建 / 🟠 角色已变更 │
│ 🔵 创建岗位 / 分配权限 / 回收权限 │
│ 聚合根: Role │
└────────────────────────────────────────┘
┌─ AuthToken Aggregate ─────────────────┐
│ 🟠 令牌已签发 / 🟠 令牌已刷新 │
│ 🟠 令牌已过期 │
│ 🔵 签发令牌 / 刷新令牌 │
│ 聚合根: AuthToken │
└────────────────────────────────────────┘Step 6:限界上下文划分
┌─────────────────────────────────────────────────────────────┐
│ 用户中台系统 │
├──────────────────┬──────────────────┬───────────────────────┤
│ 用户上下文 │ 认证上下文 │ 权限上下文 │
│ (Core) │ (Supporting) │ (Supporting) │
│ │ │ │
│ 聚合: User │ 聚合: Account │ 聚合: Role │
│ │ 聚合: AuthToken │ │
├──────────────────┴──────────────────┴───────────────────────┤
│ 上下文映射: │
│ 用户上下文 → 认证上下文: Customer-Supplier │
│ 用户上下文 → 权限上下文: Customer-Supplier │
│ 认证上下文 → 权限上下文: Partnership │
└─────────────────────────────────────────────────────────────┘Hot Spots
| # | 主题 | 描述 |
|---|---|---|
| 1 | 用户注销 | 软删除还是硬删除? |
| 2 | 第三方登录 | 微信/钉钉等 OAuth 如何映射到用户模型? |
通用语言
| 术语 | 含义 |
|---|---|
| 用户 | 系统的实际使用者,拥有登录凭证和角色 |
| 账号 | 用户的登录凭证,包含密码和状态 |
| 岗位 | 用户在企业中的职位,对应一组权限 |
DDD Strategic Patterns
Sources:
- Domain-Driven Design: The Blue Book — Eric Evans (2003)
- DDD Resources — Domain Language (Eric Evans)
- Bounded Context — Martin Fowler
- Domain Driven Design — Martin Fowler
- Anti-Corruption Layer — AWS
- Domain Analysis for Microservices — Microsoft
Overview
Strategic DDD patterns help decompose large systems into manageable parts with clear boundaries. They answer: "How do we divide a complex domain?"
DDD is fundamentally collaborative. The patterns below emerge from conversations, whiteboarding, and modeling sessions with domain experts—not from coding alone.
---
Domain Discovery Techniques
Event Storming
A workshop technique for discovering domain events, aggregates, and bounded contexts.
Orange sticky: Domain Event (past tense: "OrderPlaced")
Blue sticky: Command (imperative: "Place Order")
Yellow sticky: Aggregate (noun: "Order")
Pink sticky: External System / Policy
Purple sticky: Problem / QuestionWorkshop flow: 1. Chaotic exploration — Everyone adds events they know about 2. Timeline ordering — Arrange events chronologically 3. Identify aggregates — Group related events 4. Find boundaries — Where language changes = bounded context boundary 5. Surface problems — Mark unclear areas for follow-up
Context Mapping Workshop
For existing systems, map how bounded contexts currently interact: 1. List all systems/services 2. Identify which team owns each 3. Draw relationships (upstream/downstream) 4. Label relationship types (ACL, Conformist, etc.) 5. Identify pain points in current integrations
---
Ubiquitous Language
The foundation of DDD. A shared vocabulary between developers and domain experts that appears in:
- Code (class names, method names)
- Documentation
- Conversations
- UI labels
Principles
1. One language per bounded context - Different contexts may use the same word differently 2. Code reflects the language - Order.confirm() not Order.setStatus("confirmed") 3. Evolve together - When language changes, code changes
Example
❌ Technical language:
"Set the order entity's status field to 2 and insert a record"
✅ Ubiquitous language:
"Confirm the order and record that it was confirmed"// ❌ Technical, not ubiquitous
class Order {
setStatus(status: number): void { this.status = status; }
}
// ✅ Ubiquitous language
class Order {
confirm(): void {
if (this.status !== OrderStatus.Pending) {
throw new OrderCannotBeConfirmedException(this.id);
}
this.status = OrderStatus.Confirmed;
this.confirmedAt = new Date();
this.addDomainEvent(new OrderConfirmed(this.id));
}
}---
Bounded Contexts
A semantic boundary where a particular domain model applies. Within a bounded context, terms have precise, unambiguous meaning.
Key insight: Polysemy (same word, different meanings) across departments is natural, not a problem. The same term meaning different things in different contexts is expected—"the dominant boundary factor is human culture and language variation." — Martin Fowler
Key Concepts
- Each bounded context has its own ubiquitous language
- Each bounded context has its own model
- The same real-world concept may have different representations in different contexts
Example: E-Commerce System
flowchart TB
subgraph ECommerce["E-Commerce System"]
subgraph Sales["Sales Context"]
SC1["Customer: id, email, preferences"]
SC2["Order: items, total, status"]
end
subgraph Shipping["Shipping Context"]
SH1["Recipient: name, address, phone"]
SH2["Shipment: packages, carrier, trackingNo"]
end
subgraph Billing["Billing Context"]
BC1["Payer: name, billingAddress, paymentMethod"]
BC2["Invoice: lineItems, total, dueDate"]
end
subgraph Catalog["Catalog Context"]
CC1["Product: name, description, price"]
CC2["(no customer concept)"]
end
end
style Sales fill:#3b82f6,stroke:#2563eb,color:white
style Shipping fill:#10b981,stroke:#059669,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white
style Catalog fill:#8b5cf6,stroke:#7c3aed,color:white"Customer" means different things:
- Sales: Email, preferences, order history
- Shipping: Delivery address, phone number
- Billing: Payment methods, billing address
Bounded Context = Microservice Boundary
In microservices, each bounded context typically becomes a separate service:
flowchart LR
subgraph Sales["Sales Service"]
S1["Orders DB"]
S2["Order API"]
end
subgraph Shipping["Shipping Service"]
SH1["Shipments DB"]
SH2["Shipping API"]
end
subgraph Billing["Billing Service"]
B1["Invoices DB"]
B2["Billing API"]
end
Sales -->|events| Shipping
Shipping -->|events| Billing
Sales -.->|Integration Events| Events[("Event Bus")]
Shipping -.-> Events
Billing -.-> Events
style Sales fill:#3b82f6,stroke:#2563eb,color:white
style Shipping fill:#10b981,stroke:#059669,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white---
Subdomains
Areas of business expertise. Subdomains are discovered, not designed.
Types
| Type | Description | Investment | Example |
|---|---|---|---|
| Core | Competitive advantage | High | Product recommendation engine |
| Supporting | Necessary but not unique | Medium | Order management |
| Generic | Commodity, buy/outsource | Low | Email sending, payments |
Identification Questions
1. What makes us different from competitors? → Core 2. What do we need but isn't our specialty? → Supporting 3. What does everyone need the same way? → Generic
Example: E-Commerce
flowchart TB
subgraph Subdomains["Subdomains"]
subgraph Core["CORE"]
C1["Product search & recommendations"]
C2["Pricing engine"]
C3["Personalization"]
end
subgraph Supporting["SUPPORTING"]
S1["Order management"]
S2["Inventory"]
S3["Customer support"]
S4["Reporting"]
end
subgraph Generic["GENERIC"]
G1["Authentication (Auth0)"]
G2["Payments (Stripe)"]
G3["Email (SendGrid)"]
G4["File storage (S3)"]
end
end
Core --> CoreStrat["Build in-house\nBest developers"]
Supporting --> SuppStrat["Build or buy\nSolid but simple"]
Generic --> GenStrat["Use third-party\nDon't reinvent"]
style Core fill:#ef4444,stroke:#dc2626,color:white
style Supporting fill:#f59e0b,stroke:#d97706,color:white
style Generic fill:#6b7280,stroke:#4b5563,color:white---
Context Mapping
Describes relationships between bounded contexts.
Relationship Patterns
Partnership
Two contexts succeed or fail together. Teams coordinate closely.
flowchart LR
A["Context A"] <-->|"Partnership\nJoint planning\nShared success"| B["Context B"]
style A fill:#3b82f6,stroke:#2563eb,color:white
style B fill:#3b82f6,stroke:#2563eb,color:whiteShared Kernel
Two contexts share a subset of the domain model.
flowchart LR
subgraph A["Context A"]
SK["Shared Kernel"]
end
subgraph B["Context B"]
B1[" "]
end
SK <-->|shared| B
style A fill:#3b82f6,stroke:#2563eb,color:white
style B fill:#10b981,stroke:#059669,color:white
style SK fill:#f59e0b,stroke:#d97706,color:whiteWarning: Shared kernels create coupling. Use sparingly.
Customer-Supplier
Upstream context provides what downstream needs.
flowchart LR
U["Upstream\n(Supplier)"] -->|"Provides API"| D["Downstream\n(Customer)"]
style U fill:#3b82f6,stroke:#2563eb,color:white
style D fill:#10b981,stroke:#059669,color:whiteConformist
Downstream conforms to upstream's model with no negotiation power.
flowchart LR
U["Upstream\n(Dictator)"] -->|"Take it or leave it"| D["Downstream\n(Conformist)\nUses their model"]
style U fill:#ef4444,stroke:#dc2626,color:white
style D fill:#6b7280,stroke:#4b5563,color:whiteExample: Integrating with a third-party API (Stripe, AWS).
Anti-Corruption Layer (ACL)
Translation layer protecting your model from external models.
flowchart LR
Ext["External\nContext"] --> ACL["ACL\nTranslator + Adapter"]
ACL --> Your["Your\nContext"]
ACL -.->|"Translates external\nmodel to your model"| Note[" "]
style Ext fill:#ef4444,stroke:#dc2626,color:white
style ACL fill:#f59e0b,stroke:#d97706,color:white
style Your fill:#10b981,stroke:#059669,color:white
style Note fill:none,stroke:noneUse when:
- Integrating with legacy systems
- Integrating with third-party APIs
- External model is messy or poorly designed
// Anti-Corruption Layer Example
// infrastructure/external/stripe/stripe_payment_acl.ts
import Stripe from 'stripe';
import { Payment, PaymentStatus } from '@/domain/payment/payment';
import { Money } from '@/domain/shared/money';
export class StripePaymentACL {
constructor(private readonly stripe: Stripe) {}
async createPayment(payment: Payment): Promise<string> {
const paymentIntent = await this.stripe.paymentIntents.create({
amount: payment.amount.cents,
currency: payment.amount.currency.toLowerCase(),
metadata: {
orderId: payment.orderId.value,
customerId: payment.customerId.value,
},
});
return paymentIntent.id;
}
translateStatus(stripeStatus: string): PaymentStatus {
const mapping: Record<string, PaymentStatus> = {
'requires_payment_method': PaymentStatus.Pending,
'requires_confirmation': PaymentStatus.Pending,
'requires_action': PaymentStatus.Pending,
'processing': PaymentStatus.Processing,
'succeeded': PaymentStatus.Completed,
'canceled': PaymentStatus.Cancelled,
'requires_capture': PaymentStatus.Authorized,
};
return mapping[stripeStatus] ?? PaymentStatus.Unknown;
}
translateWebhook(event: Stripe.Event): DomainEvent | null {
switch (event.type) {
case 'payment_intent.succeeded':
const intent = event.data.object as Stripe.PaymentIntent;
return new PaymentCompleted(
PaymentId.from(intent.metadata.orderId),
Money.fromCents(intent.amount, intent.currency.toUpperCase())
);
case 'payment_intent.payment_failed':
return null;
default:
return null;
}
}
}Open Host Service / Published Language
Expose a well-defined protocol for integration.
flowchart TB
subgraph OHS["Open Host Service"]
PL["Published Language\n(REST API, gRPC, Events Schema)"]
BC["Your Bounded Context"]
end
PL --> A["Consumer A"]
PL --> B["Consumer B"]
PL --> C["Consumer C"]
style OHS fill:#3b82f6,stroke:#2563eb,color:white
style PL fill:#10b981,stroke:#059669,color:white
style A fill:#6b7280,stroke:#4b5563,color:white
style B fill:#6b7280,stroke:#4b5563,color:white
style C fill:#6b7280,stroke:#4b5563,color:white---
Context Map Diagram
Visual representation of all bounded contexts and their relationships:
flowchart TB
Identity["Identity Context\n(Generic - Auth0)"]
Legacy["Legacy Catalog\n(Legacy)"]
Sales["Sales Context\n(Core)"]
Shipping["Shipping Context\n(Supporting)"]
Billing["Billing Context\n(Supporting)"]
Stripe["Stripe Gateway\n(Generic)"]
Identity -->|Conformist| Sales
Legacy -->|ACL| Sales
Sales <-->|Customer-Supplier| Shipping
Sales -->|Open Host Service| Billing
Billing -->|Conformist| Stripe
style Identity fill:#6b7280,stroke:#4b5563,color:white
style Legacy fill:#9ca3af,stroke:#6b7280,color:white
style Sales fill:#ef4444,stroke:#dc2626,color:white
style Shipping fill:#f59e0b,stroke:#d97706,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white
style Stripe fill:#6b7280,stroke:#4b5563,color:white---
Integration Patterns
Domain Events for Context Integration
interface OrderPlaced {
eventType: 'sales.order.placed';
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
total: number;
shippingAddress: Address;
occurredAt: string;
}
class ShippingOrderPlacedHandler {
async handle(event: OrderPlaced): Promise<void> {
const shipment = Shipment.create({
orderId: ShipmentOrderId.from(event.orderId),
recipient: Recipient.fromAddress(event.shippingAddress),
packages: this.calculatePackages(event.items),
});
await this.shipmentRepository.save(shipment);
}
}
class BillingOrderPlacedHandler {
async handle(event: OrderPlaced): Promise<void> {
const invoice = Invoice.create({
orderId: InvoiceOrderId.from(event.orderId),
customerId: BillingCustomerId.from(event.customerId),
lineItems: event.items.map(item => ({
description: `Product ${item.productId}`,
quantity: item.quantity,
unitPrice: Money.fromNumber(item.price),
})),
total: Money.fromNumber(event.total),
});
await this.invoiceRepository.save(invoice);
}
}Event Schema Registry
Define and version integration event schemas:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.company.com/events/sales/order-placed/v1.json",
"title": "OrderPlaced",
"description": "Published when an order is successfully placed",
"type": "object",
"required": ["eventType", "eventId", "orderId", "occurredAt"],
"properties": {
"eventType": { "const": "sales.order.placed" },
"eventId": { "type": "string", "format": "uuid" },
"orderId": { "type": "string", "format": "uuid" },
"customerId": { "type": "string", "format": "uuid" },
"total": { "type": "number", "minimum": 0 },
"occurredAt": { "type": "string", "format": "date-time" }
}
}---
Strategic Design Checklist
- [ ] Identify ubiquitous language terms with domain experts
- [ ] Map subdomains (core, supporting, generic)
- [ ] Define bounded context boundaries
- [ ] Document context map with relationships
- [ ] Design anti-corruption layers for external systems
- [ ] Define integration event schemas
- [ ] Ensure each context has its own data store
上下文映射 — 限界上下文之间的关系
上下文映射类型
| 关系类型 | 含义 | 何时使用 | 实现方式 |
|---|---|---|---|
| Partnership(合作) | 两个团队共同演进接口 | 紧密协作的团队 | 同步沟通,共同制定接口 |
| Shared Kernel(共享内核) | 两个上下文共享一部分模型 | 核心概念需要一致 | 共享代码库,严格变更控制 |
| Customer-Supplier(客户-供应商) | 上游定义,下游消费 | 上下游关系明确 | SLA,上游承诺接口稳定性 |
| Conformist(遵奉者) | 下游完全遵从上游模型 | 下游无力影响上游 | 直接使用上游模型,不做翻译 |
| Anti-Corruption Layer(防腐层) | 下游构建翻译层保护自身模型 | 需要隔离外部模型 | 适配器模式,DTO 转换 |
| Open Host Service(开放主机) | 上游提供标准化 API | 多个下游消费者 | REST/gRPC API,版本管理 |
| Published Language(发布语言) | 标准化数据格式 | 跨系统集成 | XML Schema,JSON Schema,Protobuf |
上下文映射落地
// 防腐层(ACL)示例
// domain/gateway/PaymentGateway.java — Domain 层定义接口
public interface PaymentGateway {
PaymentResult process(PaymentRequest request);
}
// infrastructure/gateway/PaymentGatewayImpl.java — Infrastructure 实现防腐
public class PaymentGatewayImpl implements PaymentGateway {
private final ExternalPaymentClient client; // 外部支付系统 SDK
public PaymentResult process(PaymentRequest request) {
ExternalRequest extReq = PaymentMapper.toExternal(request); // DO → 外部模型
ExternalResponse extRes = client.charge(extReq);
return PaymentMapper.toDomain(extRes); // 外部模型 → DO
}
}限界上下文识别方法
Event Storming 工作流
1. Chaotic exploration — 所有人贴出已知事件 2. Timeline ordering — 按时间排列事件 3. Identify aggregates — 将相关事件分组 4. Find boundaries — 语言变化处 = 限界上下文边界 5. Surface problems — 标注模糊区域后续跟进
Context Mapping Workshop(已有系统)
1. 列出所有系统/服务 2. 识别每个系统的所属团队 3. 画出关系(上游/下游) 4. 标注关系类型(ACL, Conformist 等) 5. 找出当前集成中的痛点
示例:电商系统上下文划分
E-Commerce System
├── Sales Context(销售)
│ ├── Customer: id, email, preferences
│ └── Order: items, total, status
├── Shipping Context(物流)
│ ├── Recipient: name, address, phone
│ └── Shipment: packages, carrier, trackingNo
├── Billing Context(财务)
│ ├── Payer: name, billingAddress, paymentMethod
│ └── Invoice: lineItems, total, dueDate
└── Catalog Context(商品)
└── Product: name, description, price"Customer" 在不同上下文含义不同:
- Sales:Email, preferences, order history
- Shipping:Delivery address, phone number
- Billing:Payment methods, billing address
Bounded Context = Microservice Boundary
每个限界上下文通常对应一个微服务,通过事件总线通信:
Sales Service → events → Shipping Service
Shipping Service → events → Billing Service
Sales Service → Integration Events → Event Bus
Shipping Service → Integration Events → Event Bus
Billing Service → Integration Events → Event BusUbiquitous Language(统一语言)
原则: 1. 每个限界上下文一个统一语言 2. 代码反映语言 — Order.confirm() 而不是 Order.setStatus("confirmed") 3. 语言演变时,代码同步演变
// ❌ 技术语言
class Order { void setStatus(int s) { this.status = s; } }
// ✅ 统一语言
class Order { void confirm() { this.status = OrderStatus.CONFIRMED; } }子域类型
| 类型 | 说明 | 投入 | 示例 |
|---|---|---|---|
| Core(核心域) | 竞争优势所在 | 高 | 商品推荐引擎 |
| Supporting(支撑域) | 必要但不独特 | 中 | 订单管理 |
| Generic(通用域) | 可采购/外包 | 低 | 邮件发送、支付 |
识别问题
1. 什么让我们与众不同? → Core 2. 我们需要但不专长的是什么? → Supporting 3. 所有人都一样需要的是什么? → Generic
DDD Strategic Patterns
Sources:
- Domain-Driven Design: The Blue Book — Eric Evans (2003)
- DDD Resources — Domain Language (Eric Evans)
- Bounded Context — Martin Fowler
- Domain Driven Design — Martin Fowler
- Anti-Corruption Layer — AWS
- Domain Analysis for Microservices — Microsoft
Overview
Strategic DDD patterns help decompose large systems into manageable parts with clear boundaries. They answer: "How do we divide a complex domain?"
DDD is fundamentally collaborative. The patterns below emerge from conversations, whiteboarding, and modeling sessions with domain experts—not from coding alone.
---
Domain Discovery Techniques
Event Storming
A workshop technique for discovering domain events, aggregates, and bounded contexts.
Orange sticky: Domain Event (past tense: "OrderPlaced")
Blue sticky: Command (imperative: "Place Order")
Yellow sticky: Aggregate (noun: "Order")
Pink sticky: External System / Policy
Purple sticky: Problem / QuestionWorkshop flow: 1. Chaotic exploration — Everyone adds events they know about 2. Timeline ordering — Arrange events chronologically 3. Identify aggregates — Group related events 4. Find boundaries — Where language changes = bounded context boundary 5. Surface problems — Mark unclear areas for follow-up
Context Mapping Workshop
For existing systems, map how bounded contexts currently interact: 1. List all systems/services 2. Identify which team owns each 3. Draw relationships (upstream/downstream) 4. Label relationship types (ACL, Conformist, etc.) 5. Identify pain points in current integrations
---
Ubiquitous Language
The foundation of DDD. A shared vocabulary between developers and domain experts that appears in:
- Code (class names, method names)
- Documentation
- Conversations
- UI labels
Principles
1. One language per bounded context - Different contexts may use the same word differently 2. Code reflects the language - Order.confirm() not Order.setStatus("confirmed") 3. Evolve together - When language changes, code changes
Example
❌ Technical language:
"Set the order entity's status field to 2 and insert a record"
✅ Ubiquitous language:
"Confirm the order and record that it was confirmed"// ❌ Technical, not ubiquitous
class Order {
setStatus(status: number): void { this.status = status; }
}
// ✅ Ubiquitous language
class Order {
confirm(): void {
if (this.status !== OrderStatus.Pending) {
throw new OrderCannotBeConfirmedException(this.id);
}
this.status = OrderStatus.Confirmed;
this.confirmedAt = new Date();
this.addDomainEvent(new OrderConfirmed(this.id));
}
}---
Bounded Contexts
A semantic boundary where a particular domain model applies. Within a bounded context, terms have precise, unambiguous meaning.
Key insight: Polysemy (same word, different meanings) across departments is natural, not a problem. The same term meaning different things in different contexts is expected—"the dominant boundary factor is human culture and language variation." — Martin Fowler
Key Concepts
- Each bounded context has its own ubiquitous language
- Each bounded context has its own model
- The same real-world concept may have different representations in different contexts
Example: E-Commerce System
flowchart TB
subgraph ECommerce["E-Commerce System"]
subgraph Sales["Sales Context"]
SC1["Customer: id, email, preferences"]
SC2["Order: items, total, status"]
end
subgraph Shipping["Shipping Context"]
SH1["Recipient: name, address, phone"]
SH2["Shipment: packages, carrier, trackingNo"]
end
subgraph Billing["Billing Context"]
BC1["Payer: name, billingAddress, paymentMethod"]
BC2["Invoice: lineItems, total, dueDate"]
end
subgraph Catalog["Catalog Context"]
CC1["Product: name, description, price"]
CC2["(no customer concept)"]
end
end
style Sales fill:#3b82f6,stroke:#2563eb,color:white
style Shipping fill:#10b981,stroke:#059669,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white
style Catalog fill:#8b5cf6,stroke:#7c3aed,color:white"Customer" means different things:
- Sales: Email, preferences, order history
- Shipping: Delivery address, phone number
- Billing: Payment methods, billing address
Bounded Context = Microservice Boundary
In microservices, each bounded context typically becomes a separate service:
flowchart LR
subgraph Sales["Sales Service"]
S1["Orders DB"]
S2["Order API"]
end
subgraph Shipping["Shipping Service"]
SH1["Shipments DB"]
SH2["Shipping API"]
end
subgraph Billing["Billing Service"]
B1["Invoices DB"]
B2["Billing API"]
end
Sales -->|events| Shipping
Shipping -->|events| Billing
Sales -.->|Integration Events| Events[("Event Bus")]
Shipping -.-> Events
Billing -.-> Events
style Sales fill:#3b82f6,stroke:#2563eb,color:white
style Shipping fill:#10b981,stroke:#059669,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white---
Subdomains
Areas of business expertise. Subdomains are discovered, not designed.
Types
| Type | Description | Investment | Example |
|---|---|---|---|
| Core | Competitive advantage | High | Product recommendation engine |
| Supporting | Necessary but not unique | Medium | Order management |
| Generic | Commodity, buy/outsource | Low | Email sending, payments |
Identification Questions
1. What makes us different from competitors? → Core 2. What do we need but isn't our specialty? → Supporting 3. What does everyone need the same way? → Generic
Example: E-Commerce
flowchart TB
subgraph Subdomains["Subdomains"]
subgraph Core["CORE"]
C1["Product search & recommendations"]
C2["Pricing engine"]
C3["Personalization"]
end
subgraph Supporting["SUPPORTING"]
S1["Order management"]
S2["Inventory"]
S3["Customer support"]
S4["Reporting"]
end
subgraph Generic["GENERIC"]
G1["Authentication (Auth0)"]
G2["Payments (Stripe)"]
G3["Email (SendGrid)"]
G4["File storage (S3)"]
end
end
Core --> CoreStrat["Build in-house\nBest developers"]
Supporting --> SuppStrat["Build or buy\nSolid but simple"]
Generic --> GenStrat["Use third-party\nDon't reinvent"]
style Core fill:#ef4444,stroke:#dc2626,color:white
style Supporting fill:#f59e0b,stroke:#d97706,color:white
style Generic fill:#6b7280,stroke:#4b5563,color:white---
Context Mapping
Describes relationships between bounded contexts.
Relationship Patterns
Partnership
Two contexts succeed or fail together. Teams coordinate closely.
flowchart LR
A["Context A"] <-->|"Partnership\nJoint planning\nShared success"| B["Context B"]
style A fill:#3b82f6,stroke:#2563eb,color:white
style B fill:#3b82f6,stroke:#2563eb,color:whiteShared Kernel
Two contexts share a subset of the domain model.
flowchart LR
subgraph A["Context A"]
SK["Shared Kernel"]
end
subgraph B["Context B"]
B1[" "]
end
SK <-->|shared| B
style A fill:#3b82f6,stroke:#2563eb,color:white
style B fill:#10b981,stroke:#059669,color:white
style SK fill:#f59e0b,stroke:#d97706,color:whiteWarning: Shared kernels create coupling. Use sparingly.
Customer-Supplier
Upstream context provides what downstream needs.
flowchart LR
U["Upstream\n(Supplier)"] -->|"Provides API"| D["Downstream\n(Customer)"]
style U fill:#3b82f6,stroke:#2563eb,color:white
style D fill:#10b981,stroke:#059669,color:whiteConformist
Downstream conforms to upstream's model with no negotiation power.
flowchart LR
U["Upstream\n(Dictator)"] -->|"Take it or leave it"| D["Downstream\n(Conformist)\nUses their model"]
style U fill:#ef4444,stroke:#dc2626,color:white
style D fill:#6b7280,stroke:#4b5563,color:whiteExample: Integrating with a third-party API (Stripe, AWS).
Anti-Corruption Layer (ACL)
Translation layer protecting your model from external models.
flowchart LR
Ext["External\nContext"] --> ACL["ACL\nTranslator + Adapter"]
ACL --> Your["Your\nContext"]
ACL -.->|"Translates external\nmodel to your model"| Note[" "]
style Ext fill:#ef4444,stroke:#dc2626,color:white
style ACL fill:#f59e0b,stroke:#d97706,color:white
style Your fill:#10b981,stroke:#059669,color:white
style Note fill:none,stroke:noneUse when:
- Integrating with legacy systems
- Integrating with third-party APIs
- External model is messy or poorly designed
// Anti-Corruption Layer Example
// infrastructure/external/stripe/stripe_payment_acl.ts
import Stripe from 'stripe';
import { Payment, PaymentStatus } from '@/domain/payment/payment';
import { Money } from '@/domain/shared/money';
export class StripePaymentACL {
constructor(private readonly stripe: Stripe) {}
async createPayment(payment: Payment): Promise<string> {
const paymentIntent = await this.stripe.paymentIntents.create({
amount: payment.amount.cents,
currency: payment.amount.currency.toLowerCase(),
metadata: {
orderId: payment.orderId.value,
customerId: payment.customerId.value,
},
});
return paymentIntent.id;
}
translateStatus(stripeStatus: string): PaymentStatus {
const mapping: Record<string, PaymentStatus> = {
'requires_payment_method': PaymentStatus.Pending,
'requires_confirmation': PaymentStatus.Pending,
'requires_action': PaymentStatus.Pending,
'processing': PaymentStatus.Processing,
'succeeded': PaymentStatus.Completed,
'canceled': PaymentStatus.Cancelled,
'requires_capture': PaymentStatus.Authorized,
};
return mapping[stripeStatus] ?? PaymentStatus.Unknown;
}
translateWebhook(event: Stripe.Event): DomainEvent | null {
switch (event.type) {
case 'payment_intent.succeeded':
const intent = event.data.object as Stripe.PaymentIntent;
return new PaymentCompleted(
PaymentId.from(intent.metadata.orderId),
Money.fromCents(intent.amount, intent.currency.toUpperCase())
);
case 'payment_intent.payment_failed':
return null;
default:
return null;
}
}
}Open Host Service / Published Language
Expose a well-defined protocol for integration.
flowchart TB
subgraph OHS["Open Host Service"]
PL["Published Language\n(REST API, gRPC, Events Schema)"]
BC["Your Bounded Context"]
end
PL --> A["Consumer A"]
PL --> B["Consumer B"]
PL --> C["Consumer C"]
style OHS fill:#3b82f6,stroke:#2563eb,color:white
style PL fill:#10b981,stroke:#059669,color:white
style A fill:#6b7280,stroke:#4b5563,color:white
style B fill:#6b7280,stroke:#4b5563,color:white
style C fill:#6b7280,stroke:#4b5563,color:white---
Context Map Diagram
Visual representation of all bounded contexts and their relationships:
flowchart TB
Identity["Identity Context\n(Generic - Auth0)"]
Legacy["Legacy Catalog\n(Legacy)"]
Sales["Sales Context\n(Core)"]
Shipping["Shipping Context\n(Supporting)"]
Billing["Billing Context\n(Supporting)"]
Stripe["Stripe Gateway\n(Generic)"]
Identity -->|Conformist| Sales
Legacy -->|ACL| Sales
Sales <-->|Customer-Supplier| Shipping
Sales -->|Open Host Service| Billing
Billing -->|Conformist| Stripe
style Identity fill:#6b7280,stroke:#4b5563,color:white
style Legacy fill:#9ca3af,stroke:#6b7280,color:white
style Sales fill:#ef4444,stroke:#dc2626,color:white
style Shipping fill:#f59e0b,stroke:#d97706,color:white
style Billing fill:#f59e0b,stroke:#d97706,color:white
style Stripe fill:#6b7280,stroke:#4b5563,color:white---
Integration Patterns
Domain Events for Context Integration
interface OrderPlaced {
eventType: 'sales.order.placed';
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
total: number;
shippingAddress: Address;
occurredAt: string;
}
class ShippingOrderPlacedHandler {
async handle(event: OrderPlaced): Promise<void> {
const shipment = Shipment.create({
orderId: ShipmentOrderId.from(event.orderId),
recipient: Recipient.fromAddress(event.shippingAddress),
packages: this.calculatePackages(event.items),
});
await this.shipmentRepository.save(shipment);
}
}
class BillingOrderPlacedHandler {
async handle(event: OrderPlaced): Promise<void> {
const invoice = Invoice.create({
orderId: InvoiceOrderId.from(event.orderId),
customerId: BillingCustomerId.from(event.customerId),
lineItems: event.items.map(item => ({
description: `Product ${item.productId}`,
quantity: item.quantity,
unitPrice: Money.fromNumber(item.price),
})),
total: Money.fromNumber(event.total),
});
await this.invoiceRepository.save(invoice);
}
}Event Schema Registry
Define and version integration event schemas:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.company.com/events/sales/order-placed/v1.json",
"title": "OrderPlaced",
"description": "Published when an order is successfully placed",
"type": "object",
"required": ["eventType", "eventId", "orderId", "occurredAt"],
"properties": {
"eventType": { "const": "sales.order.placed" },
"eventId": { "type": "string", "format": "uuid" },
"orderId": { "type": "string", "format": "uuid" },
"customerId": { "type": "string", "format": "uuid" },
"total": { "type": "number", "minimum": 0 },
"occurredAt": { "type": "string", "format": "date-time" }
}
}---
Strategic Design Checklist
- [ ] Identify ubiquitous language terms with domain experts
- [ ] Map subdomains (core, supporting, generic)
- [ ] Define bounded context boundaries
- [ ] Document context map with relationships
- [ ] Design anti-corruption layers for external systems
- [ ] Define integration event schemas
- [ ] Ensure each context has its own data store
事件风暴主持检查清单
会前准备
2 周前
- [ ] 确认业务范围(一个 BC vs 整个系统)
- [ ] 确定参与人员名单
- [ ] 协调时间(建议 3 小时不间断)
1 周前
- [ ] 向参与者发送邀请 + 流程简介
- [ ] 准备物理材料(如现场):
- [ ] 5+ 色便签(橙/蓝/黄/绿/粉/紫)
- [ ] 大头笔(多色)
- [ ] 胶带 / 磁扣
- [ ] 大白板或可贴墙的纸
- [ ] 相机(记录产出)
- [ ] 或准备在线工具:
- [ ] Miro/Mural 模板
- [ ] 测试协作权限
- [ ] 准备数字便签模板
1 天前
- [ ] 确认场地/在线空间可用
- [ ] 打印流程备忘单(给主持人和记录员)
- [ ] 准备开场 PPT(5 张:背景/目标/流程/颜色规范/时间安排)
会中主持
开场(5 min)
- [ ] 介绍工作坊目标
- [ ] 讲解释颜色规范
- [ ] 明确"不讨论"原则
- [ ] 指定记录员(拍照/截图)
- [ ] 打开计时器
Step 1:混沌探索(30 min)
- [ ] 强调"只管贴,不讨论"
- [ ] 鼓励沉默者发言
- [ ] 控制发言者不要展开细节
- [ ] 标记紫色便签作为 Hot Spot
Step 2:时间线排序(20 min)
- [ ] 从左到右排列事件
- [ ] 识别主线与分支
- [ ] 标记关键事件 ★
- [ ] 异常流不要遗漏
Step 3:关键事件标记(15 min)
- [ ] 找出业务流程转折点
- [ ] 确认每个关键事件的触发条件
- [ ] 标记时间约束(⏱ 30分钟、⏱ T+1)
Step 4:命令与角色(30 min)
- [ ] 每个事件补全触发命令(蓝色)
- [ ] 每个命令补全发起角色(黄色)
- [ ] 标记外部系统(粉色)
- [ ] 注意:自动触发的事件也有命令(系统/定时器)
Step 5:聚合发现(30 min)
- [ ] 分组相关事件+命令 → 聚合
- [ ] 标记聚合边界(绿色便签边框)
- [ ] 为聚合命名(业务语言)
- [ ] 识别聚合根
Step 6:限界上下文划分(20 min)
- [ ] 根据耦合度划 BC 边界
- [ ] 标注上下文映射关系
- [ ] 记录跨 BC 的集成事件
收尾(10 min)
- [ ] 拍照所有便签墙
- [ ] 确认 Hot Spot 清单
- [ ] 分配跟进负责人
- [ ] 约定总结输出时间
主持人技能要求
| 技能 | 说明 |
|---|---|
| 时间管理 | 严格执行每个步骤的时间盒,宁可移到后面补也不拖延 |
| 引导而非主导 | 主持人负责流程,领域专家负责内容 |
| 识别 Hot Spot | 争论点立刻标记为紫色,不现场解决 |
| 翻译能力 | 把技术语言问成业务语言:"这个 '状态机' 在业务上是什么意思?" |
| 能量维持 | 工作时长高,主持人需要保持气氛活跃 |
会前准备检查完整清单
邀请函模板:
主题:[事件风暴工作坊] {项目名称} — {日期}
大家好,
我们将于 {日期} {时间} 举办 {项目名称} 领域建模事件风暴工作坊。
你的角色:{领域专家 / 开发 / 产品}
预计时长:3 小时
请提前了解:
- 我们正在构建/重构的业务是什么
- 准备 3-5 个你最关心的业务场景
工作坊不需要准备 PPT,只需要带来你对业务的理解。
期待你的参与!
{主持人}中说过,微服务设计为什么要选择 DDD 吗?其中有一个非常重要的原因,就是采用 DDD 方法建立的领域模型,可以清晰地划分微服务的逻辑边界和物理边界。可以说,在 DDD 的实践中,好的领域模型直接关乎微服务的设计水平。因此,我认为 DDD 的战略设计是比战术设计更为重要的,也正是这个原因,我们的内容会更侧重于战略设计。
那么我们该采用什么样的方法,才能从错综复杂的业务领域中分析并构建领域模型呢?
它就是我在前面多次提到的事件风暴。事件风暴是一项团队活动,领域专家与项目团队通过头脑风暴的形式,罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对每一个事件,标注出导致该事件的命令,再为每一个事件标注出命令发起方的角色。命令可以是用户发起,也可以是第三方系统调用或者定时器触发等,最后对事件进行分类,整理出实体、聚合、聚合根以及限界上下文。而
事件风暴正是 DDD 战略设计中经常使用的一种方法,它可以快速分析和分解复杂的业务领域,完成领域建模
那到底怎么做事件风暴呢?事件风暴需要提前准备些什么?又如何用事件风暴来构建领域模型呢?今天我们就来重点解决这些问题,深入了解事件风暴的全过程。
事件风暴需要准备些什么?
1. 事件风暴的参与者
事件风暴采用工作坊的方式,将项目团队和领域专家聚集在一起,通过可视化、高互动的方式一步一步将领域模型设计出来。领域专家是事件风暴中必不可少的核心参与者。很多公司可能并没有这个角色,那我们该寻找什么样的人来担当领域专家呢?
领域专家就是对业务或问题域有深刻见解的主题专家,他们非常了解业务和系统是怎么做的,同时也深刻理解为什么要这样设计。如果你的公司里并没有这个角色,那也没关系,你可以从业务人员、需求分析人员、产品经理或者在这个领域有多年经验的开发人员里,按照这个标准去选择合适的人选。
除了领域专家,事件风暴的其他参与者可以是 DDD 专家、架构师、产品经理、项目经理、开发人员和测试人员等项目团队成员。
领域建模是统一团队语言的过程,因此项目团队应尽早地参与到领域建模中,这样才能高效建立起团队的通用语言。到了微服务建设时,领域模型也更容易和系统架构保持一致。
2. 事件风暴要准备的材料
事件风暴参与者会将自己的想法和意见写在即时贴上,并将贴纸贴在墙上的合适位置,我们戏称这个过程是“刷墙”。所以即时贴和水笔是必备材料,另外,你还可以准备一些胶带或者磁扣,以便贴纸随时能更换位置。
值得提醒一下的是,在这个过程中,我们要用不同颜色的贴纸区分领域行为。如下图,我们可以用蓝色表示命令,用绿色表示实体,橙色表示领域事件,黄色表示补充信息等。补充信息主要用来说明注意事项,比如外部依赖等。颜色并不固定,这只是我的习惯,团队内统一才是重点。
3. 事件风暴的场地
什么样的场地适合做事件风暴呢?是不是需要跟组织会议一样,准备会议室、投影,还有椅子?这些都不需要!你只需要一堵足够长的墙和足够大的空间就可以了。墙是用来贴纸的,大空间可以让人四处走动,方便合作。撤掉会议桌和椅子的事件风暴,你会发现参与者们的效率更高。
事件风暴的发明者曾经建议要准备八米长的墙,这样设计就不会受到空间的限制了。当然,这个不是必要条件,看各自的现实条件吧,不要让思维受限就好。
4. 事件风暴分析的关注点
在领域建模的过程中,我们需要重点关注这类业务的语言和行为。比如某些业务动作或行为(事件)是否会触发下一个业务动作,这个动作(事件)的输入和输出是什么?是谁(实体)发出的什么动作(命令),触发了这个动作(事件)…我们可以从这些暗藏的词汇中,分析出领域模型中的事件、命令和实体等领域对象。
如何用事件风暴构建领域模型?
领域建模的过程主要包括产品愿景、业务场景分析、领域建模和微服务拆分与设计这几个重要阶段。下面我以用户中台为例,介绍一下如何用事件风暴构建领域模型。
1. 产品愿景
产品愿景的主要目的是对产品顶层价值的设计,使产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。
产品愿景的参与角色:领域专家、业务需求方、产品经理、项目经理和开发经理。
在建模之前,项目团队要思考这样两点:
用户中台到底能够做什么?
它的业务范围、目标用户、核心价值和愿景,与其它同类产品的差异和优势在哪里?
这个过程也是明确用户中台建设方向和统一团队思想的过程。参与者要对每一个点(下图最左侧列的内容)发表意见,用水笔写在贴纸上,贴在黄色贴纸的位置。这个过程会让参与者充分发表意见,最后会将发散的意见统一为通用语言,建立如下图的产品愿景墙。如果你的团队的产品愿景和目标已经很清晰了,那这个步骤你可以忽略。
2. 业务场景分析
场景分析是从用户视角出发的,根据业务流程或用户旅程,采用用例和场景分析,探索领域中的典型场景,找出领域事件、实体和命令等领域对象,支撑领域建模。事件风暴参与者要尽可能地遍历所有业务细节,充分发表意见,不要遗漏业务要点。
场景分析的参与角色:领域专家、产品经理、需求分析人员、架构师、项目经理、开发经理和测试经理。
用户中台有这样三个典型的业务场景:
第一个是系统和岗位设置,设置系统中岗位的菜单权限;
第二个是用户权限配置,为用户建立账户和密码,设置用户岗位;
第三个是用户登录系统和权限校验,生成用户登录和操作日志。
我们可以按照业务流程,一步一步搜寻用户业务流程中的关键领域事件,比如岗位已创建,用户已创建等事件。再找出什么行为会引起这些领域事件,这些行为可能是一个或若干个命令组合在一起产生的,比如创建用户时,第一个命令是从公司 HR 系统中获取用户信息,第二个命令是根据 HR 的员工信息在用户中台创建用户,创建完用户后就会产生用户已创建的领域事件。当然这个领域事件可能会触发下一步的操作,比如发布到邮件系统通知用户已创建,但也可能到此就结束了,你需要根据具体情况来分析是否还有下一步的操作。
场景分析时会产生很多的命令和领域事件。
我用蓝色来表示命令,用橙色表示领域事件,用黄色表示补充信息,比如用户信息数据来源于 HR 系统的说明。
3. 领域建模
领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计。
领域建模的参与角色:领域专家、产品经理、需求分析人员、架构师、项目经理、开发经理和测试经理。
具体可以分为这样三步。
第一步:从命令和事件中提取产生这些行为的实体。用绿色贴纸表示实体。通过分析用户中台的命令和事件等行为数据,提取了产生这些行为的用户、账户、认证票据、系统、菜单、岗位和用户日志七个实体。
第二步:根据聚合根的管理性质从七个实体中找出聚合根,比如,用户管理用户相关实体以及值对象,系统可以管理与系统相关的菜单等实体等,可以找出用户和系统等聚合根。然后根据业务依赖和业务内聚原则,将聚合根以及它关联的实体和值对象组合为聚合,比如系统和菜单实体可以组合为“系统功能”聚合。按照上述方法,用户中台就有了系统功能、岗位、用户信息、用户日志、账户和认证票据六个聚合。
第三步:划定限界上下文,根据上下文语义将聚合归类。根据用户域的上下文语境,用户基本信息和用户日志信息这两个聚合共同构成用户信息域,分别管理用户基本信息、用户登录和操作日志。认证票据和账户这两个聚合共同构成认证域,分别实现不同方式的登录和认证。系统功能和岗位这两个聚合共同构成权限域,分别实现系统和菜单管理以及系统的岗位配置。根据业务边界,我们可以将用户中台划分为三个限界上下文:用户信息、认证和权限。
到这里我们就完成了用户中台领域模型的构建了。那由于领域建模的过程中产生的领域对象实在太多了,我们可以借助表格来记录。
4. 微服务拆分与设计
我们在基础篇讲过,原则上一个领域模型就可以设计为一个微服务,但由于领域建模时只考虑了业务因素,没有考虑微服务落地时的技术、团队以及运行环境等非业务因素,因此
在微服务拆分与设计时,我们不能简单地将领域模型作为拆分微服务的唯一标准,它只能作为微服务拆分的一个重要依据。
微服务的设计还需要考虑服务的粒度、分层、边界划分、依赖关系和集成关系。除了考虑业务职责单一外,我们还需要考虑将敏态与稳态业务的分离、非功能性需求(如弹性伸缩要求、安全性等要求)、团队组织和沟通效率、软件包大小以及技术异构等非业务因素。
微服务设计建议参与的角色:领域专家、产品经理、需求分析人员、架构师、项目经理、开发经理和测试经理。
用户中台微服务设计如果不考虑非业务因素,我们完全可以按照领域模型与微服务一对一的关系来设计,将用户中台设计为:用户、认证和权限三个微服务。但如果用户日志数据量巨大,大到需要采用大数据技术来实现,这时用户信息聚合与用户日志聚合就会有技术异构。虽然在领域建模时,我们将他们放在一个了领域模型内,但如果考虑技术异构,这两个聚合就不适合放到同一个微服务里了。我们可以以聚合作为拆分单位,将用户基本信息管理和用户日志管理拆分为两个技术异构的微服务,分别用不同的技术来实现它们。
今天我们讲了事件风暴的设计方法以及如何用事件风暴来构建领域模型。事件风暴是一种不同于传统需求分析和系统设计的方法,最好的学习方法就是找几个业务场景多做几次。
综合我的经验,一般来说一个中型规模的项目,领域建模的时间大概在两周左右,这与我们传统的需求分析和系统设计的时间基本差不多。但是如果在领域建模的过程中,团队成员全员参与,在项目开发之前就建立了共同语言,这对于后续的微服务设计与开发是很有帮助的,时间成本也可以视情况降低。
其实我也了解到了,很多开发人员在初次学习 DDD 时,似乎并不太关心领域建模,而只是想学学 DDD 的战术设计思想,快速上手,开发微服务。我想这是对 DDD 的一个误解,这已经偏离了 DDD 的核心设计思想,即先有边界清晰的领域模型,才能设计出清晰的微服务边界,这两个阶段一前一后是刚需,我们不能忽略。
事件风暴工作坊 — 深度指南
完整流程总览
Step 0: 会前准备 → 产出:领域范围圈定、人员确认
Step 1: 混沌探索 → 产出:领域事件便签墙(橙色)
Step 2: 时间线排序 → 产出:从左到右的时间线
Step 3: 关键事件标记 → 产出:★ 标记的关键事件、异常分支
Step 4: 命令与角色 → 产出:蓝色命令 + 黄色角色 + 粉色外部系统
Step 5: 聚合发现 → 产出:聚合分组(绿色便签边框)
Step 6: 限界上下文 → 产出:BC 边界 + 上下文映射
Step 7: 产出整理 → 产出:事件清单、聚合清单、BC 列表、热点清单便签颜色规范
| 颜色 | 含义 | 格式 | 示例 |
|---|---|---|---|
| 橙色 | 领域事件 | 动词过去式:OrderPaid | 订单已支付 |
| 蓝色 | 命令 | 动词原形:Place Order | 提交订单 |
| 黄色 | 聚合/实体 | 名词:Order | 订单 |
| 绿色 | 读模型/视图 | 名词:Order Detail Page | 订单详情页 |
| 粉色 | 外部系统 | 名词:Payment Gateway | 支付网关 |
| 紫色 | 问题/争议 | 待讨论项 | 退款后库存何时归还? |
---
Step 0:会前准备 — 产出清单
输入材料(主持人需提前准备)
必备:
- [ ] 业务背景简述(1-2 页,发给参会者)
- [ ] 核心用户旅程(如:用户下单 → 支付 → 收货)
- [ ] 已知痛点/改进目标
可选(有助加速):
- [ ] 现有系统架构图/数据流图
- [ ] 上次事件风暴产出(如果是复盘)人员配置
| 角色 | 人数 | 要求 |
|---|---|---|
| 主持人 | 1 | 熟悉流程,控制节奏,翻译技术→业务 |
| 领域专家 | 2-3 | 最懂业务的人(产品/运营/业务方) |
| 开发 | 2-4 | 负责落地实现的开发者 |
| 记录员 | 1 | 实时记录产出,拍照/截图 |
时间分配(总计 3-3.5 小时)
Step 0: 开场 & 说明(5 min)
Step 1: 混沌探索(30 min)
Step 2: 时间线排序(20 min)
Step 3: 关键事件标记(15 min)
—— 休息 5 min ——
Step 4: 命令与角色(30 min)
Step 5: 聚合发现(30 min)
Step 6: 限界上下文划分(20 min)
Step 7: 收尾 & 分配跟进(15 min)---
Step 1:混沌探索 — "只管贴"
主持人话术
"现在请大家每人在便签上写下你认为这个业务中会发生的事件。规则:动词过去式,只管贴,不要讨论对错。"
操作步骤
1. 每人分 5-10 张橙色便签 2. 参与者独立在便签写下领域事件(5-8 min 独立思考) 3. 所有人把便签贴到白板上(2 min) 4. 轮流朗读自己的便签,主持人快速归类合并重复(15 min) 5. 争议项标记紫色便签 → 移入 Hot Spot 区(5 min)
主持人检查点
- [ ] 事件用过去式命名?(OrderPaid ✓ / Pay Order ✗)
- [ ] 超过 30 张橙色便签时停止(太大范围,需收缩)
- [ ] 标记了至少 3 个紫色争议点
- [ ] 没有人说"这个实现上做不到"(Step 1 禁技术讨论)
产出
橙色便签墙:
订单已创建 支付已完成 订单已发货
订单已取消 退款已申请 退款已完成
库存已扣减 积分已累加 通知已发送---
Step 2:时间线排序 — "从左到右"
操作步骤
1. 主持人把便签墙分成主流程区(顶部)和异常流程区(底部) 2. 引导参与者把事件从左(最早)到右(最后)排列(15 min) 3. 识别分支(主流程的 if/else),用线条连接(5 min)
主持人话术
"现在我们把事件按时间排一下。哪个事件先发生?哪个后发生?出现分支的地方画一条线。"
主持人检查点
- [ ] 主线和分支清晰可区分
- [ ] 异常流程(取消/退款/超时)没有遗漏
- [ ] 时间约束事件明确标记(⏱ 30 分钟内未支付自动取消)
产出
时间线:
订单已创建 → 库存已扣减 → 支付已完成 → 积分已累加 → 订单已发货
↳ 支付已超时 → 订单已取消(⏱ 30min)---
Step 3:关键事件标记 — "找转折点"
操作步骤
1. 标记业务流程转折点(状态变更事件)用 ★(5 min) 2. 标注时间约束事件(如"支付超时 30 分钟")(5 min) 3. 确认异常流程完整性(退款、取消、超时)(5 min)
关键事件判断标准
| 事件类型 | 是否关键 | 原因 |
|---|---|---|
| 订单已创建 | ★ | 业务流程起点 |
| 库存已扣减 | ★ | 影响能否下单 |
| 支付已完成 | ★ | 核心状态变更 |
| 通知已发送 | 辅助事件,不影响主干 |
产出
★ 关键事件列表:
1. 订单已创建(触发:用户点击"下单")
2. 库存已扣减(触发:订单创建成功后)
3. 支付已完成(触发:用户支付 / 超时自动取消 ⏱ 30min)
4. 订单已发货(触发:仓库确认发货)
5. 退款已到账(触发:退货审核通过)---
Step 4:命令与角色 — "谁触发的"
操作步骤
1. 每个橙色事件下面贴蓝色命令(10 min) 2. 每个蓝色命令旁边贴黄色角色(10 min) 3. 补充粉色外部系统便签(5 min) 4. 检查遗漏:系统定时器、消息监听器也是"角色"(5 min)
分类模板
| 事件(橙) | 命令(蓝) | 角色(黄) | 外部系统(粉) |
|---|---|---|---|
| 订单已创建 | 提交订单 | 买家 | [无] |
| 支付已完成 | 支付订单 | 买家 | 微信支付 |
| 库存已扣减 | 扣减库存 | 系统(自动) | [无] |
| 订单已发货 | 确认发货 | 仓库管理员 | WMS 仓库系统 |
主持人检查点
- [ ] 每个事件都有对应的命令?
- [ ] 系统自动触发的命令也标了角色(系统/定时器/消息)?
- [ ] 外部系统(支付/短信/物流)都标了粉色便签?
---
Step 5:聚合发现 — "分组+边界"
操作步骤
1. 把相关的事件+命令+实体归组(15 min) 2. 每组选一个聚合根(有独立生命周期的实体)(5 min) 3. 画聚合间引用关系(只通过 ID,不画对象引用)(5 min) 4. 用绿色便签边框圈出每个聚合(5 min)
聚合根选择决策
对于每个候选实体,回答:
1. 是否有独立的生命周期?(创建→修改→删除)
2. 是否有全局唯一 ID?
3. 业务流程中是否作为"主语"出现?
如果 3 个问题都回答"是" → 聚合根
如果 < 3 个"是" → 挂到其他聚合根下聚合设计检查清单
- [ ] 每个聚合 ≤ 5 个实体
- [ ] 聚合间只有 ID 引用,无对象引用
- [ ] 每个聚合有明确的"一事务一聚合"边界
- [ ] 没有共享实体在多个聚合中出现
产出示例
聚合 1:订单聚合
聚合根:Order
实体:OrderItem(内嵌)
事件:订单已创建、订单已支付、订单已取消
命令:提交订单、支付订单、取消订单
聚合 2:库存聚合
聚合根:Inventory
实体:InventoryLog
事件:库存已扣减、库存已释放
命令:扣减库存、释放库存
聚合间关系:
Order → Inventory(通过 inventoryId 引用)---
Step 6:限界上下文划分 — "划清边界"
操作步骤
1. 按聚合间耦合度分组,形成 BC(10 min) 2. 标注 BC 之间的映射关系(Partnership/ACL/Conformist 等)(5 min) 3. 识别跨 BC 的集成事件(5 min)
BC 划分决策树
两个聚合是否需要放在同一 BC?
├── 它们是否有紧密的同步事务需求?
│ ├── 是 → 同一 BC
│ └── 否 → 继续判断
│
├── 它们是否共享相同的领域概念含义?
│ ├── 是(如:Order 的 Product 和 Inventory 的 Product 含义相同)→ 可能同一 BC
│ └── 否(如:Order 的 Customer 是购买者,CRM 的 Customer 是潜在客户)→ 不同 BC
│
└── 它们是否由同一团队维护?
├── 是 → 可能同一 BC
└── 否 → 不同 BC产出示例
限界上下文:
BC-1: 订单上下文(核心域)
聚合:Order、Payment
集成:发布 OrderPlacedIntegrationEvent → BC-2
BC-2: 库存上下文(支撑域)
聚合:Inventory
集成:监听 OrderPlacedIntegrationEvent
上下文映射:
Order BC ──Customer-Supplier──→ Inventory BC
Order BC ──ACL──→ 微信支付(外部系统)---
Step 7:收尾 — "产出与跟进"
立即产出(当天)
1. 拍照/截图全部便签墙(记录原始产出) 2. 汇总 Hot Spot 清单,分配负责人 3. 约定总结文档输出时间(建议 2 个工作日内)
总结文档模板
## {项目名称} 事件风暴工作坊产出
### 基本信息
- 日期:{YYYY-MM-DD}
- 参与人:{名单与角色}
- 业务范围:{限界上下文列表}
### 领域事件清单
| # | 事件 | 触发命令 | BC |
|---|------|---------|-----|
| 1 | 订单已创建 | 提交订单 | 订单上下文 |
| 2 | 支付已完成 | 支付订单 | 订单上下文 |
| 3 | 库存已扣减 | 扣减库存 | 库存上下文 |
### 聚合清单
| 聚合 | 聚合根 | BC | 关键状态 |
|------|--------|-----|---------|
| Order | Order | 订单上下文 | DRAFT → PAID → SHIPPED |
| Inventory | Inventory | 库存上下文 | AVAILABLE → RESERVED → DEDUCTED |
### 限界上下文
| BC | 类型 | 关键聚合 |
|----|------|---------|
| 订单上下文 | 核心域 | Order, Payment |
| 库存上下文 | 支撑域 | Inventory |
### Hot Spot 清单
| # | 问题 | 负责人 | 截止日期 |
|---|------|--------|---------|
| 1 | 退款后库存何时归还? | @张三 | 2024-03-20 |
| 2 | 跨 BC 的幂等保证方案? | @李四 | 2024-03-22 |
### 下一步
- [ ] 领域模型详细设计(聚合内实体/VO/不变式)
- [ ] 代码模型映射(聚合 → 模块/包结构)
- [ ] 领域事件接口契约定义---
常见问题及处理
| 问题 | 处理方式 |
|---|---|
| 事件便签太少(< 15 张) | 主持人用"接下来呢?"等方式追问每个用户旅程步骤 |
| 一个参与者发言过多 | "我们先听听其他人的想法"然后点名沉默者 |
| 技术实现争论 | 立刻标记紫色 → "这个留到 Hot Spot 讨论" |
| 业务范围太大 | 聚焦一个 BC 或一个核心流程,其余下次再开 |
| 聚合划分有分歧 | 先按最小粒度划分,后续设计时合并不需要的 |
| Step 5 耗时过长 | 用"先分再合"策略:每个人先独立分,再对比合并 |
事件风暴便签模板
使用此模板快速开始事件风暴工作坊的便签准备。
颜色速查表
| 颜色 | 含义 | 语法格式 | 示例 |
|---|---|---|---|
| 🟠 橙色 | 领域事件 | 动词过去式 | OrderPlaced, PaymentCompleted |
| 🔵 蓝色 | 命令 | 动词原形 | PlaceOrder, SubmitPayment |
| 🟡 黄色 | 角色/参与者 | 名词 | Customer, Warehouse, System |
| 🟢 绿色 | 读模型/查询 | 名词(视图) | OrderDetailPage, PendingOrders |
| 🔴 粉色 | 外部系统 | 系统名 | Alipay, WeChatPay, LogisticsAPI |
| 🟣 紫色 | 策略/约束 | 业务规则语句 | Max ¥50000 per order |
便签内容模板
领域事件(橙色)
订单已创建
┌──────────────────────┐
│ Order Placed │
│ 触发:PlaceOrder │
│ 产出:orderId │
│ 消耗:Order Aggregate │
└──────────────────────┘命令(蓝色)
创建订单
┌──────────────────────┐
│ Place Order │
│ 角色:Customer │
│ 输入:items, address │
│ 产出:OrderPlaced │
└──────────────────────┘角色/参与者(黄色)
客户
┌──────────────────────┐
│ Customer │
│ 发起的命令: │
│ - PlaceOrder │
│ - SubmitPayment │
│ - CancelOrder │
└──────────────────────┘读模型(绿色)
订单详情页
┌──────────────────────┐
│ Order Detail Page │
│ 数据源:Order Read │
│ 使用场景:查看订单 │
└──────────────────────┘外部系统(粉色)
支付宝
┌──────────────────────┐
│ Alipay │
│ 集成方式:API │
│ 事件:PaymentCompleted│
│ 备选:WeChatPay │
└──────────────────────┘策略/约束(紫色)
单笔限额
┌──────────────────────┐
│ Max ¥50000 per order │
│ 影响:PlaceOrder │
│ 降级:分单支付 │
└──────────────────────┘注意规则
1. 每张便签只写一个事件/命令 — 不要在一张贴纸上写多个 2. 字迹清晰 — 确保他人可以阅读 3. 使用业务语言 — 不要用技术术语 4. 过去式 vs 原形 — 事件用过去式 (Ordered),命令用原形 (Order) 5. 不确定性用紫色 — 不确定的事件、有疑问的边界都用紫色标记,会后跟进
事件风暴时间线示例
电商订单履约流程
主线(Happy Path)
时间 →
──────┬─────────────────────────────────────────────────────────────►
│
🟠 订单已创建 🟠 支付已完成 🟠 库存已扣减 🟠 发货已安排
│ │ │ │ │
│ 订单号: │ │ │
│ ORD-2024-001 │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│Order │ │Payment │ │Inventory│ │Shipment │
│Placed │ │Completed│ │Deducted │ │Arranged │
└─────────┘ └─────────┘ └─────────┘ └─────────┘分支与异常流
🟠 支付超时
(30分钟未支付)
│
▼
🟠 订单已取消
🟠 库存已释放
🟠 支付失败
(余额不足/风控拦截)
│
▼
🟠 支付已重试
(最多3次)
│
┌────┴────┐
▼ ▼
🟠 支付成功 🟠 支付关闭完整泳道图
时间 →
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Customer │ │ Customer │ │ System │ │ System │ │Warehouse │
│ │ │ │ │ │ │ │ │ │
│ Place │ │ Submit │ │ Deduct │ │ Notify │ │ Arrange │
│ Order │ │ Payment │ │ Stock │ │ Seller │ │ Shipment │
│ 🔵 │ │ 🔵 │ │ 🔵 │ │ 🔵 │ │ 🔵 │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 🟠 │ │ 🟠 │ │ 🟠 │ │ 🟠 │ │ 🟠 │
│ Order │ │ Payment │ │Inventory │ │ Seller │ │ Shipment │
│ Placed │──►│Completed │──►│Deducted │──►│Notified │──►│Arranged │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘时间线标记规范
| 标记 | 含义 | 示例 |
|---|---|---|
| 🟠 | 领域事件 | 订单已创建 |
| ★ | 关键事件 | ★ 订单已支付(触发履约) |
| ⚡ | 异常/错误 | ⚡ 支付超时 |
| ❓ | 不确定/待定 | ❓ 是否需要审批? |
| 🛑 | 阻塞点 | 🛑 依赖外部系统确认 |
| 🔄 | 循环/重试 | 🔄 支付重试(最多3次) |
| ⏱ | 时间约束 | ⏱ 30分钟未支付 → 取消 |
时间线排序步骤
1. 自由粘贴 — 先把所有橙色便签贴在墙上 2. 分组排序 — 按事件发生的自然顺序排列(从左到右) 3. 分支识别 — 从主线上分出来并行或可选的路径 4. 异常标记 — 在分支上标记错误处理和补偿流程 5. 关键事件标注 — 用★标记业务流程转折点 6. 枚举所有路径 — 确保覆盖 happy path + 所有异常路径
事件风暴工作坊产出模板
使用方式
工作坊结束后,使用此模板整理产出物,作为 ddd-domain-designer 的输入。
# 事件风暴工作坊总结
## 基本信息
- **领域/项目**: {项目名称}
- **日期**: {YYYY-MM-DD}
- **时长**: {实际耗时}
- **参与角色**:
- 领域专家: {姓名}
- 产品经理: {姓名}
- 架构师: {姓名}
- 开发: {姓名}
- 测试: {姓名}
## 事件时间线
sequenceDiagram participant Customer as 客户 participant System as 系统 participant External as 外部系统
Customer->>System: 创建订单 System->>System: 订单已创建 Customer->>System: 提交支付 System->>External: 请求支付 External-->>System: 支付已完成 System->>System: 库存已扣减 System->>System: 发货已安排
## 聚合候选清单
| 聚合 | 描述 | 聚合根 | 关联实体 | 关键事件 |
|------|------|--------|---------|---------|
| Order | 订单管理 | Order | OrderItem, Payment | OrderPlaced, OrderPaid |
| Inventory | 库存管理 | Product | StockRecord | InventoryDeducted |
| Shipment | 物流管理 | Shipment | Package | ShipmentArranged |
## 限界上下文
| 上下文名称 | 类型 | 包含聚合 | 上下文映射 |
|-----------|------|---------|-----------|
| Order Context | Core | Order | → Logistics: Customer-Supplier |
| Logistics Context | Supporting | Inventory, Shipment | ← Order: ACL |
## 热力图(Hot Spots)
| # | 主题 | 问题描述 | 负责人 | 优先级 |
|---|------|---------|-------|-------|
| 1 | 支付超时处理 | 客户发起支付后30分钟未完成,自动取消还是人工确认? | {负责人} | P0 |
| 2 | 库存不足 | 扣减库存时库存不足,部分发货还是全部取消? | {负责人} | P1 |
## 通用语言表
| 术语 | 含义 | 在哪个上下文使用 |
|------|------|----------------|
| Order | 客户购买商品的记录 | Order Context |
| Customer | 下单的客户 | Order Context, Logistics Context |
| Recipient | 收货人 | Logistics Context |
## 行动清单
| # | 任务 | 负责人 | 截止日期 | 状态 |
|---|------|-------|---------|------|
| 1 | Hot Spot #1 调研 | {负责人} | {日期} | □ |
| 2 | Hot Spot #2 与产品确认 | {负责人} | {日期} | □ |
| 3 | 进入 ddd-domain-designer 阶段 | {负责人} | {日期} | □ |
## 下一步
1. ☐ Hot Spot 专项讨论
2. ☐ 使用 ddd-domain-designer 进行详细聚合设计
3. ☐ 使用 ddd-architecture-selector 选择适合的架构