🗺️

【Cursor rules付き】EventStormingは"使いづらい"のか?実装への繋げ方のidea

2025/02/27に公開

動機・記事の目的

みなさんご存知の通り、coding agentが代替可能な作業の領域は日に日に拡大していっていると感じます。

一方で、

現実世界とAIのインターフェースとなり、ソリューションとしてのビジネスプロセス(toCだったらカスタマージャーニー的な)を整理し、捨象し、デザインし、砕き、実装タスクとして渡す役割は今のところ人間に残されていそうです。


そのような現実世界のビジネスプロセス<->実装を繋ぐときの媒体として、イベントストーミングはシステム開発の要としての役割が期待できるかもしれないと感じます。

イベントストーミングとは、ドメイン駆動設計(DDD)におけるワークショップ手法で、ビジネスプロセスをイベント(出来事)を中心に可視化するものです。カラフルな付箋を使い、イベント(オレンジ)、コマンド(青)、制約(黄)などを時系列に並べることで、複雑なビジネスドメインの全体像を共同で素早く把握し、ソフトウェア設計へと繋げることができます。

イベントストーミングの強みと思っていること:

  • 🗺️全体観を示し、議論のベースとなることでそれを活性化させるコラボレーションツール
    • ソリューションの絵姿を書く
    • アイディアの創発、暗黙知の可視化、ボトルネックや課題の発見
  • 🔍現実の物事・プロセス(ドメイン)を、イベントあるいは流れで自然に整理するツール
    • 物事を現実ベースで整理し、砕き、実装へと繋げる
      • テーブル駆動やクラス駆動ではなく、現実のビジネスプロセス・出来事駆動で考えられる
      • 業務フロー図 的なものよりシステム設計への連続性があると考える
        • ある程度は形式化されているので、比較的AIリーダブル

しかし一方で以下の課題から、私自身業務でフル活用できているとは言い難いです

  • 🤔イベントソーシングありきでないと実装に繋がらない?
  • 🤔サービス間がイベント駆動で連携しているようなアーキテクチャでないと実装に繋がらない?
    • あるいはアクターモデルのような格好
  • 🤔静的モデリングへの接続が出来ない?
    • ER図やクラス図的なタイプのモデリング

本来的には、イベントストーミングは上記の設計や実装の具体からは独立した概念であるものの、「実際そういうところあるよね...」とはやはり思ってしまいます。

それでも上記のような実装都合から、EventCentricな現代的DDDの要とも言えるかもしれないイベントストーミングの恩恵を受けられないのは勿体無いとも感じます。
そこで、この記事では:

  • モノリス
  • ステートソーシング

の前提でどのように イベントストーミング->コードレベルの実装へ接続できるか、アイディアを実践してみようと思います。

サンプルコードにはKotlinを使用します。
kotlin-resultライブラリは使用

また、Experimentalなオレオレプロンプトではありますが、イベスト結果->コード へ落とし込む際のプロンプトをおまけで付けてみました。

結論

  • イベントストーミングは、モデルの境界(の仮説)の抽出に使える
    • コンテキスト レベル
    • 集約 レベル
      • 集約の「輪郭」と「骨組み」は定義できる
        • Constrain(黄色の付箋)が、最小公倍数的に、集約を最も小さくなる形で規定する
        • コマンド(青の付箋)とその結果のイベント(オレンジの付箋)の束として集約が実装される
      • 集約がのりしろとなってイベントストーミング -> コード(モデル)が接続される
  • イベントストーミングだけではコードは書けない
    • イベントストーミングは鳥瞰レベルのBigPictureであり、細かい情報構造やビジネスルールの1つ1つは書かないので、それはそう

輪郭と骨組みだの、のりしろとなってだの、どういうことでしょうか?以降、順を追って説明させてください。

お題


事業について:

  • ライブコマース・Eコマース市場において、補完商品や代替商品の価格連動を活用した「相互連動型ダイナミックプライシング」を提供する事業です。
  • プライシング担当者の経験と判断を活かしつつ、リアルタイムな市場データを提供することで、収益最大化と在庫最適化を実現します。

ビジネスプロセスについて:

  • プライシング担当者が「商品一覧」画面で市場データ(受注状況、在庫状況など)をリアルタイムにモニタリングし、補完商品や代替商品の価格を手動で調整します。
  • 調整された価格は5秒以内に顧客画面に反映され、その結果データが収集・分析され、次の価格調整の判断材料として活用されます。

実践

実装対象 -> 商品群 集約(Merchandise.kt):

  • 商品のライフサイクル(追加、更新、販売停止)と表示順序を管理し、商品名の一意性や最大10件までの制限などの不変条件を保証する集約です。
  • プライシング担当者による価格戦略の実行基盤と、品揃え担当者によるマーチャンダイジング施策(商品の表示順序管理など)を支える中核的な集約として機能します。

こちらについて実装していきます。

👀動くコード全体を先に貼っておきます

全体像

集約「商品群」を

  • イベントストーミング: 図 による表現
  • Aggregate Design Canvas: 自然言語(markdown+mermaid)による表現
  • Kotin: コードに よる表現

で表します。開発プロセスとしても

  • イベントストーミング -> Aggregate Design Canvas -> Kotin

という成果物ブローになります。

イベントストーミングによる表現:

図:商品登録機能のイベントストーミング。青い付箋がコマンド(操作)、オレンジの付箋がイベント(結果)、黄色の付箋が不変条件を示しています。

コードによる表現

// イベントのユニオンとそれに生えたコマンド の集合としての集約
sealed interface Merchandise {
    // それぞれの状態(=イベント: オレンジ付箋に対応)
    data object Empty : Merchandise {
        // この状態から許可された遷移アクションたち(=コマンド: 青付箋に対応)
        fun addProduct( // 商品追加
            // input
            metaData: Product.ProductMetaData,
            // read model: 緑付箋に対応
            displayOrder: DisplayOrder,
            allProductNames: ProductNames
        ): Result<Open.Added, Open.MerchandiseError>
            = registerNewProduct(metaData, displayOrder, allProductNames)
        // 入荷停止 
        fun suspendStocking(reason: NonEmptyString): Suspended = Suspended(reason)
    }

    sealed interface Open : Merchandise {
        // Openのサブ状態たち/あるいはOpen状態で起こり得るイベント
        data class Opened(
            val reason: String
        ): Open
        data class Added(
            val product: Product.OnSale,
            val displayOrder: DisplayOrder
        ) : Open

        data class Updated(
            val product: Product.OnSale
        ) : Open

        // error path
        sealed interface MerchandiseError {
            data class ProductAlreadyExists(
                val productId: ID<Product>,
                val message: String
            ) : MerchandiseError

            data class MaxProductCountExceeded(
                val productId: ID<Product>,
                val message: String
            ) : MerchandiseError
        }
        // Open状態から可能な遷移アクションたち
        fun addProduct(
            metaData: Product.ProductMetaData,
            displayOrder: DisplayOrder,
            allProductNames: ProductNames
        ): Result<Added, MerchandiseError>
            = registerNewProduct(metaData, displayOrder, allProductNames)

        fun update( // 商品情報更新
            metaData: Product.ProductMetaData,
            allProductNames: ProductNames
        ): Result<Updated, MerchandiseError> = updateProductMetadata(metaData, allProductNames)

        fun suspendStocking(reason: NonEmptyString): Suspended = Suspended(reason)
    }

    data class Suspended( // 入荷停止
        val reason: NonEmptyString
    ) : Merchandise {
        fun resumeStocking(): Merchandise = Open.Opened("resume")
    }
}

上記コードでは、Merchandiseという集約をsealed interfaceとして定義し、各状態(Empty、Open、Suspended)とそれぞれの状態で実行可能なコマンドを表現しています。Result型を使用してエラーケースも明示的に扱っています。

Aggregate Design Canvas(markdown+mermaid)による表現
(現時点のAIリーダビリティから、markdown+mermaidで書いてます)

[Aggregate Design Canvas](https://github.com/ddd-crew/aggregate-design-canvas)

1. Name
ProductAggregate

2. Description

  • このアグリゲートは、相互連動型ダイナミックプライシングシステムにおける商品のライフサイクルを管理します。
  • プライシング担当者による価格戦略の実行と、品揃え担当者によるマーチャンダイジング施策を支援する上で、以下の点に重点を置いています:
    • 商品の一意性確保による正確な価格戦略の実現
    • 表示順序の管理による効果的なマーチャンダイジングの実現
    • 商品情報の整合性維持による信頼性の高い運用の実現
  • 補完商品の連動(クロスプライシング)や代替商品の需要バランス調整といった、高度な価格戦略の基盤となります。

3. State Transitions

5. Enforced Invariants & Corrective Policies

  • Enforced Invariants:
    • 商品名は一意であること
    • 一度に陳列できる商品数は最大10件までであること
    • 商品の表示順序が保持されること
    • 商品コード、名称、説明、カテゴリは必須項目であること
    • 並び替え操作は表示順序の一貫性を維持すること
  • Corrective Policies:
    • 特になし(現状のイベントストーミング図からは是正ポリシーは必要ないと判断)

6. Handled Commands & Created Events

Handled Command Created Event
AddProduct ProductAdded
UpdateProduct ProductUpdated
StopSelling ProductStoppedSelling
ReorderProducts ProductsReordered

個別の解説

集約の「輪郭」と「骨組み」は定義できる について

1. Constrainで集約が決まる?

Constrain(黄色の付箋)が、最小公倍数的に、集約を最も小さくなる形で規定する

今回の例で言うと、

  • 名称はユニーク
  • 最大10件
  • 追加商品は先頭に並ぶ

という制約が見出されています。
単純な例ですが、一貫して守る必要のある不変条件です。(≒Repository経由での書き込みの単位)
この制約の単位は「商品群全て」になり、商品単体だけ見てもこの不変条件が守られているかわかりません。
一方、集約は可能な限り小さく設計したいとしたときに、このConstrainが、最小倍数的に、集約を規定する・集約を貫いた「背骨」になると感じます。

2. 集約はイベントの束?

コマンド(青の付箋)とその結果のイベント(オレンジの付箋)の束として集約が実装される について

まず、Merchandiseは、イベント群のユニオンタイプです(Kotlinなのでsealed interfaceですが)。これはMerchandiseが遷移する状態その断面を切り取ったものとも言えると思います。

そして、それぞれのイベント(≒現在の状態)に対してメソッドが生えています。
これは集約に対するコマンドに一致します。状態遷移アクションと言い換えてもいいかもしれません。
更にこの時、現在の状態から可能な遷移アクションの制限も表現されています。

3. 外側でイベント->リードモデルの投影を行う
また、これを外から見た場合、

  • 集約に対してコマンドすることで、集約の状態が遷移し、その結果であるイベントが返る

という格好になりますね。
返ってきたイベントをステートに投影する処理は、上位のコールサイトで行ってもいいかと思います。
コールサイトはこんな感じです。

/**
 * 商品の追加オーケストレーション 責務:
 * - API層からのリクエストスキーマをドメインモデルに変換
 * - リードモデルの読み込み
 * - 集約に対するコマンドの実行(複数集約をオーケストレーションする場合もある)
 * - 集約が返したイベントのリードモデルへの投影
 */
fun addProductOrchestration(
    // リクエスト
    request: AddProductRequest,
    // リードモデル
    readProductNames: ReadProductNames,
    readDisplayOrder: ReadDisplayOrders,
    // リポジトリ
    saveMerchandise: SaveProduct,
    // 現在のMerchandise状態を取得するためのリポジトリ関数を追加
    restoreMerchandiseState: () -> Merchandise,
): Result<Merchandise.Open.Added, AddProductError> = binding {
    // リクエストスキーマ -> ドメインモデル
    val productMetaData = request.toProductMetaData().mapError { AddProductError.InvalidRequest(it) }.bind()

    // リードモデル解決
    val productNames = readProductNames()
    val displayOrder = readDisplayOrder()
    
    // 現在のMerchandise集約を復元
    val currentMerchandise = restoreMerchandiseState()
    
    // 現在の状態に応じたコマンド実行
    val productAdded = currentMerchandise
        .tryAddProduct(
            metaData = productMetaData,
            displayOrder = displayOrder,
            allProductNames = productNames
        ).mapError { AddProductError.DomainError(it) }.bind()

    val pushedEvent = publishEvent(productAdded) // イベントの発行

    // 集約の状態を保存。 イベント -> ステートの投影
    saveMerchandise(pushedEvent.product, pushedEvent.displayOrder)

    pushedEvent
}

// Merchandiseの拡張関数として実装
private fun Merchandise.tryAddProduct(
    metaData: Product.ProductMetaData,
    displayOrder: DisplayOrder,
    allProductNames: ProductNames
): Result<Merchandise.Open.Added, Merchandise.Open.MerchandiseError> {
    return when (this) {
        is Merchandise.Empty -> addProduct(metaData, displayOrder, allProductNames)
        is Merchandise.Open -> addProduct(metaData, displayOrder, allProductNames)
        is Merchandise.Suspended -> {
            // 入荷停止中は商品の追加ができないためエラーを返す
            Err(Merchandise.Open.MerchandiseError.OperationNotAllowed(
                "商品の追加ができません。システムは現在入荷停止中です: ${this.reason}"
            ))
        }
    }
}

上記コードでは、Result型とbinding関数を使用して安全にエラーハンドリングを行いながら、リクエストからドメインモデルへの変換、集約へのコマンド実行、結果のイベント発行と保存という一連の流れを表現しています。

この上位のオーケストレーション層で、

  • リードモデルの解決->集約に対するコマンド->返ってきたイベントをリードモデルに投影

しています。
これは、モノリス+ステートソーシングを前提として書いています。(集約自体は純粋にイベントを返すだけ)
場合には依りますが、データの価値がいまだ日に日に上がっている昨今、ステートソーシングがベースでもハイブリット的にイベントを累積しておくことは一考に値するかもしれません。

集約がのりしろとなってイベントストーミング -> コード(モデル)が接続される

  • 前提、コードに落とすには、鳥瞰的なイベントストーミングでは細部の情報は足りない
  • 前述の通り、集約とそのフレームを見出した後に肉付けをしていくこととなる
    • 「ユビキタス言語集」「実例マッピング」などに表現されたより細部の情報構造やビジネスルールを注入する
  • また、永続化データのモデルや集約より細かいコードモデル間の関係の表現は必要であれば別途作成(またはコードから逆生成)する必要がある

実装手順

  • イベントストーミング: miro等 -> Aggregate Design Canvas: markdown+mermaid -> コード: Kotlin

というフローを想定します。
miro等のスクショからコード生成は現時点難しいと思うので、Aggregate Design Canvasを挟む格好です。

指示のサンプル

  • miro -> Aggregate Design Canvas
how-to-read-eventstorming.mdc

description: read event storming image
globs:

Event Storming Reading Guidelines

Purpose:
To accurately interpret the event storming diagram by understanding the roles of each component and applying consistent rules for system behavior analysis.

Legend

Color Component Description & Examples
Blue Command Request for an action on the system
Example: Create Order, Add Item
Orange Event Outcome or occurrence resulting from an action
Example: Order Confirmed, Item Discontinued
Purple Policy Trigger activated when conditions are met
Example: Update price every 10 minutes, Stop sale when stock is 0
Dark Pink External System Systems or dependencies outside the primary system
Example: Payment Gateway, Warehouse Management System
Red Hotspot/Debate Critical discussion points or unresolved design issues
Example: Concurrency conflicts, Error handling design
Yellow Constraint/Invariant Business rules or immutable conditions (e.g., aggregation rules)
Example: Maximum 10 items, Single order up to $10,000
Light Green Read Model Internal state or aggregate model used by the system
Example: Order Aggregate, Product Entity
Green View/Query Presentation of information to users or aggregates
Example: Product List View, Order History View
Gray Data/Details Supplementary or attribute information
Example: Order Status, Product Details
Yellow Actor Role of a user or system
Example: Customer, Administrator, Automated Agent

Reading Process

  1. Start with Policies/Triggers:
    Identify what triggers or policies initiate the process (e.g., "Update price every 10 minutes").

  2. Identify Commands/Requests:
    Determine which actions are executed as a result of the triggers (e.g., "Update Price").

  3. Check the Events:
    Recognize what events occur as a result of the commands (e.g., "Sales Price Changed").

  4. Consider Constraints/Immutables:
    Review any business rules or conditions that apply (e.g., "Order must be under $10,000").

  5. Focus on Hotspots/Debates:
    Pinpoint areas requiring further design discussion (e.g., concurrency handling, error processing).

  6. Analyze the Read Models:
    Understand the internal aggregates or entities managed by the system (e.g., Order Entity, Product Entity).

  7. Assess External Systems:
    Identify interactions with external systems (e.g., sending requests to a payment gateway).

  8. Review Views/Queries:
    Determine how information is presented to users (e.g., Order History View, Product List).

  9. Organize Actor Relationships:
    Clarify which actor interacts with which process (e.g., Customer confirms order).

aggregate-design-canvas-creation-guidelines.mdc

description: When reading the event storming diagram and creating the aggregate design map.
globs:

Custom Instruction Prompt: Create the Aggregate Design Canvas

Follow the instructions below to create an "Aggregate Design Canvas" that captures the following sections.

General points to note:

  • only format it according to these instructions using clear markdown.
  • The aggregate name (in the Name section), command names (in the Handled Commands section), and event names (in the Created Events section) must be written in English.
  • All other text must be written in Japanese.
  • here for a sample: sample-aggregate-design-map.mdc

  1. Name

    • Give your aggregate a good name. In some domains it makes sense to include as part of the name the length of a cycle, or some other indication of the life span of the aggregate.
  2. Description

    • Summarise the main responsibilities and purpose of the aggregate. It’s a good idea to include the reasons why such boundaries were chosen and tradeoffs that were made compared to other designs.
  3. State Transitions

    • Express using mermaid notation
    • Usually the aggregate goes through explicit state transitions, that impact the way it can be interacted with. Too many transitions might indicate that process boundaries weren't modelled properly and can be split. Very naive / simple transitions might indicate that the aggregate is anaemic and that logic was pushed out to services. In this section of the canvas list the possible states or draw a small transition diagram.
    • Important: In the state transition diagram, the state names must match the generated event names, and the transition actions must match the handled command names.
  4. Enforced Invariants & Corrective Policies

    • One of the main jobs of the aggregate is to enforce business invariants. These invariants protect business logic. Listing the main ones in this section will make sure that you agree on the responsibilities that the aggregate has. A large number of enforced invariants can indicate high local complexity of the aggregate implementation.
    • If you decide to change the boundaries of the aggregate and relax some of the invariants (for example to reduce the chance of concurrency conflict), then some extra business logic might be required to correct some of the inconsistencies. In the context of this canvas, we call this logic corrective policies. A large number of such policies might indicate that the business logic was pushed outside of the aggregate, which can increase the complexity of implementation.
    • Listing on the canvas both Invariants and Corrective Policies will make design trade-offs explicit and will help you decide whether the boundaries you decided on are useful or not.
  5. Handled Commands & Created Events

    • In this section you list all the commands that the aggregate is capable of handling and all events that will be created as a result. It might be a good idea to create connectors between them in order to validate that you are not missing any of the building blocks.
    • express it in the form of a table as follows.
      | Handled Command | Created Event |
      |------------------|-------------------|
      | Command1 | Event1 |
      | Command2 | Event2 |
  6. Throughput

  • Evaluate the likelihood of concurrency conflicts for a single aggregate instance.
  • Estimate the average and maximum command handling rate, as well as the number of clients issuing commands.
  • Use these metrics to assess the risk of conflicts and potential trade-offs.
  • express it in the form of a table as follows.
    Average Maximum
    Command handling rate <avg_commands_per_day> <max_commands_per_day>
    Total number of clients <avg_clients> <max_clients>
    Concurrency conflict chance <low/medium/high> <low/medium/high>
  1. Size
  • Estimate the size of the aggregate based on the number of events per instance.
  • Consider event granularity and growth rate to evaluate performance impacts.
  • If the number of events is large, consider using snapshots to mitigate performance issues.
  • express it in the form of a table as follows.
    Average Maximum
    Event growth rate <avg_events_per_day> <max_events_per_day>
    Lifetime of a single instance <avg_lifetime> <max_lifetime>
    Number of events persisted <small/medium/large> <small/medium/large>

Ensure that each section is clearly labeled and well-structured in markdown format.

  • Aggregate Design Canvas -> コード
aggregate-design-to-kotlin.mdc

description: Guidelines for generating Kotlin code (aggregate code) from AggregateDesignCanvas
globs: .md,.mdc

Guidelines for Converting AggregateDesignCanvas to Kotlin Code

Overview

AggregateDesignCanvas represents a conceptual design of an aggregate, and this guideline provides a framework for translating it into implementation code.

In this approach, we express an aggregate as a "union type of events" where events themselves represent the aggregate states. This is a key concept: events and states are essentially equivalent in this implementation model.

Important: All sample code is illustrative and should be adjusted according to specific domain requirements and project conventions.

Basic Structure

An aggregate consists of the following elements:

  1. Aggregate Root Type: A sealed interface/class representing a union type of all possible states/events

    sealed interface Merchandise { /* Each state/event is defined here */ }
    
  2. State/Event Classes: Classes implementing the root type, representing both states and events

    data class Created(val id: String, val items: List<Item>) : OrderAggregate
    data object Empty : Merchandise
    
  3. Command Methods: Methods that execute business logic and return new state/event objects

    fun confirm(userId: String): Result<Confirmed, OrderError>
    
  4. Helper Functions: Functions encapsulating state transition logic

Event = State Relationship

In this implementation, events and states are essentially the same:

  • Each event represents a specific state of the aggregate
  • Command methods return new states, which are themselves events
  • The history of an aggregate is the sequence of events, each representing a state at a point in time

Example:

// This class is both a state and an event
data class Confirmed(
    val id: String,
    val items: List<OrderItem>,
    val totalAmount: Money,
    val confirmedAt: Instant,
    val confirmedBy: UserId
) : OrderAggregate

// Command method returns the new state directly
fun confirm(userId: UserId): Result<Confirmed, OrderError> {
    // Validation logic...
    return Result.success(
        Confirmed(
            id = this.id,
            items = this.items,
            totalAmount = this.totalAmount,
            confirmedAt = Instant.now(),
            confirmedBy = userId
        )
    )
}

"Contours" and "Framework" of Aggregates

Key concepts in aggregate design:

  1. Contours (Boundaries): Defined by invariants - the minimum unit where consistency must be maintained.
  2. Framework:
    • Bundle of commands and resulting events
    • Permitted state transitions and state-specific command restrictions

Naming and Conversion Process

Naming Conventions

  • Commands: Use verbs (AddProduct, not ProductCommand)
  • States: Use adjectives/past participles (Added, not AddProduct)
  • Helper Functions: Use domain terminology (registerNewProduct, not updateState)

Conversion Steps

  1. Define the basic structure from "Name" and "Description" sections

    sealed interface OrderAggregate { val id: String }
    
  2. Define state classes from the state transition diagram

    data class Created(override val id: String, val items: List<OrderItem>) : OrderAggregate
    data class Confirmed(override val id: String, val items: List<OrderItem>) : OrderAggregate
    
  3. Implement command methods based on "Handled Commands & Created Events"

    fun confirm(userId: String): Result<Confirmed, OrderError> {
        // Invariant checks...
        return Result.success(Confirmed(id, items, totalAmount, Instant.now(), UserId(userId)))
    }
    

State Constraints and Invariants

State transition constraints are expressed using the type system:

sealed interface Merchandise {
    data object Empty : Merchandise {
        fun addProduct(metaData: ProductMetaData): Result<Open.Added, ProductError>
    }
    sealed interface Open : Merchandise {
        data class Added(val product: Product.OnSale) : Open
        fun update(metaData: ProductMetaData): Result<Updated, ProductError>
    }
}

Invariants define aggregate boundaries and are implemented as validations in command methods:

fun addProduct(metaData: ProductMetaData): Result<Open.Added, ProductError> {
    // Name uniqueness check
    if (productNames.contains(metaData.name)) {
        return Result.failure(ProductError.NameAlreadyExists(metaData.name))
    }
    // Maximum product count check
    if (productNames.size >= 10) {
        return Result.failure(ProductError.MaxProductsExceeded)
    }
    // State transition logic...
}

Value Objects

Use value objects to express domain constraints:

@JvmInline
value class NonEmptyString private constructor(val value: String) {
    companion object {
        fun of(value: String): Result<NonEmptyString, ValidationError> =
            if (value.isNotBlank()) Result.success(NonEmptyString(value))
            else Result.failure(ValidationError.EmptyString)
    }
}

Orchestration and Implementation

Aggregates return events, with result handling in the orchestration layer:

// Orchestration example
fun addProductOrchestration(
    request: AddProductRequest,
    readProductNames: ReadProductNames,
    saveMerchandise: SaveProduct
): Result<ProductAdded, AddProductError> {
    // Convert request...
    val result = Empty.addProduct(productMetaData, displayOrder, productNames)

    return result.map { newState ->
        saveMerchandise(newState)
        ProductAdded(newState)
    }.mapError { AddProductError.DomainError(it) }
}

Best Practices and Notes

  1. Aggregate boundaries are determined by invariants - Keep aggregates as small as possible
  2. Define state-specific properties only in their respective state classes
  3. Validate invariants early in command methods
  4. Use Result types for error handling
  5. Express constraints with value objects instead of primitive types
  6. Keep aggregates focused on pure domain logic

Important notes:

  • This guideline generates the "framework" of an aggregate, not complete code
  • Event storming alone doesn't provide all details needed - additional design work is necessary
  • Prefer minimal states to maintain code understandability
  • Choose the most e

おわりに

私個人は、やはりイベントストーミングが現実世界のビジネスプロセス<->実装を繋ぐのにベターなんじゃないかなあな、と思います。

...のような概念を引き続きキャッチアップ・統合・実務に落とし込んでいくことを考えていきたいと思います。

似たような興味お持ちの方いたら、雑に話しましょう〜
https://pitta.me/matches/bAmssXckcSAb

読んでいただきありがとうございました!!

株式会社ログラス テックブログ

Discussion