イベントの扱い方
現代のシステムにおいて、DDD(Domain-Driven Design)やクリーンアーキテクチャなどのアーキテクチャのベストプラクティスに従う場合、イベントは状態の変化をモデル化し、コンポーネントやシステム間の非同期通信を行うための基本的なメカニズムです。 イベントは、アプリケーションのドメイン内で既に発生した出来事を表し、システムの他の部分や外部サービスに関心がある場合があります。 補足: This section also covers shadow entities and the Transactional Outbox flow with DDD, MediatR, MassTransit, RabbitMQ, Hangfire, and Unit of Work for reliable messaging.
ドメインイベント
ドメインイベントは、アプリケーションのコンテキスト内で発生した重要な事実を表します。 これらはシステム内部で生成および消費され、状態の変更を疎結合に伝播させることができます。つまり、オブジェクト同士が直接知る必要はありません。
ドメインイベントはいつ使用するのか?
- 操作によってドメイン内に重要な変更が生じ、システムの他の部分に通知またはトリガーを行う必要がある場合。
- ドメインモデルの一貫性を保ち、異なるプロセスを直接的な依存関係なしに起動できるようにする場合。
Lino では、新しいドメインイベントの作成は簡単です。次のコマンドを実行できます:
lino event new lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event edit --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>
CLIウィザードは以下を要求します:
- サービス – イベントが作成されるサービス。
- モジュール – イベントが作成されるモジュール(モジュール型サービスのみ)。
- エンティティ – イベントが作成/関連付けされるエンティティ。
- イベントタイプ – ドメインイベントまたは統合イベント。
- イベント名 – ドメインで使用され、エンティティに関連付けられる名前。
例
User エンティティに関連付けられたドメインイベント UserCreated を作成すると、
システムは自動的に UserCreatedDomainEvent という名前のイベントを生成します。
この名前により、イベントを消費するシステムのどの部分でも、ユーザー作成アクションがすでに完了していることが明確になります。
Lino によって生成された構造:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── Users/
├── User.cs
├── Errors/
├── Events/
│ └── UserCreatedDomainEvent.cs
├── Repositories/
└── Resources/
Technical flow in Lino
- The aggregate registers an
IDomainEventwithRegisterDomainEvent. - The command handler saves through
IUnitOfWork, preferablySaveChangesInTransactionAsyncwhen events are involved. - The Unit of Work persists changes and publishes events through MediatR.
- A domain handler can register an integration event in
IOutbox.
Events are not commands; use past-tense names such as UserCreatedDomainEvent.
ドメインイベントハンドラー
Domain Event Handler は、ドメインイベント に反応し、アプリケーションの内部状態に関連するアクションを実行するクラスであり、常に同じトランザクションコンテキスト内で動作します。
これらのハンドラーの主な目的は、システムを一貫性のある状態で疎結合に保ち、エンティティやアグリゲートのコアロジックを過負荷にせずに追加ルールを適用できるようにすることです。
例えば、User を作成した後に、統計を更新したり、内部ログを生成したり、他のアグリゲートに通知したりする必要がある場合があります。
これらのアクションはドメイン内で意味があり、同期的に実行でき、即時の整合性を保証します。
しかし、外部リソースに依存する操作(メール送信やサードパーティAPIの呼び出しなど)は、ドメインイベントから直接実行すべきではありません。これによりトランザクションが遅延や不安定なタスクに縛られてしまうためです。 この場合、ドメインは統合イベント(Outboxに記録)を生成し、後で非同期かつ耐障害性のある方法で処理されます。
主な特徴:
- イベントに反応するが、決して変更しない。
- ドメインの整合性に関連するインプロセス操作のみを実行する。
- すべてが同じトランザクション内で実行されることを保証する。
新しいドメインイベントハンドラーを作成するには、次のコマンドを実行します:
lino event-handler new
CLIウィザードは以下を要求します:
- サービス – イベントハンドラーを作成するサービス。
- モジュール – イベントハンドラーを作成するモジュール(モジュラーサービスのみ)。
- エンティティ – イベントハンドラーを作成するエンティティ。
- イベントタイプ – ドメインイベントまたは統合イベント。
- イベント – 消費されるイベント。
- イベントハンドラー名 – エンティティとドメインイベントに関連付けられる名前。
例
User エンティティと UserCreatedDomainEvent イベントに関連付けられたドメインイベントハンドラー UserCreated を作成すると、システムは自動的に UserCreatedDomainEventHandler という名前のイベントハンドラーを生成します。
Linoによって生成された構造:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── Users/
├── Commands/
├── EventHandlers/
│ └── Domain/
│ └── UserCreatedDomainEventHandler.cs
├── Logging/
├── Queries/
└── Resources/
Technical responsibilities
- Implements
IDomainEventHandlerand reacts inside the application boundary. - Runs internal consistency, logging, tracing, or integration registration work.
- Uses
IOutbox.RegisterIntegrationEventfor reliable messages. - Keeps the business change and
OutboxMessagein the same transaction.
統合イベント
統合イベントは、重要な出来事が発生したことを通知するメッセージであり、外部システムや他のマイクロサービスと共有する必要があります。
ドメインイベントとは異なり、ここでの目的はシステム間の通信と状態の同期です。
統合イベントを作成するタイミング:
- あなたのシステムの変更を他のシステムに反映させる必要がある場合。
- あなたのマイクロサービスが変更を公開し、他のマイクロサービスがそれに反応する必要がある場合。
ドメインイベントと統合イベントの主な違い:
| 項目 | ドメインイベント | 統合イベント |
|---|---|---|
| 対象 | 内部 | 外部 |
| 結合度 | 低(内部) | 必要(システム間) |
| 処理時間 | 即時 | 非同期の場合あり、配信保証付き |
| 永続化の必要性 | 必須ではない | あり(信頼性と耐障害性のため) |
Lino では、新しい統合イベントの作成は簡単です。 次のコマンドを実行できます:
lino event new lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>
CLIウィザードで以下が求められます:
- サービス – イベントを作成するサービス。
- モジュール – イベントを作成するモジュール(モジュラーサービスの場合のみ)。
- エンティティ – イベントを作成 / 関連付けるエンティティ。
- イベントタイプ – ドメインイベントまたは統合イベント。
- イベント名 – エンティティに関連付けられる統合イベントで使用する名前。
例
User エンティティに関連付けられた統合イベント UserCreated を作成すると、システムは自動的に UserCreatedIntegrationEvent という名前のイベントを作成します。
この名前により、イベントを消費するシステムのどの部分にとっても、ユーザー作成のアクションが既に完了していることが明確になります。
Lino によって生成された構造:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Integration.Events/
├── <ProjectName>.<ServiceName>.Integration.Events.csproj
└── Users/
└── UserCreatedIntegrationEvent.cs
Reliable publication
An integration event implements IIntegrationEvent and crosses boundaries between modules, services, or systems. The application registers the event in IOutbox, the Unit of Work persists an OutboxMessage, and a Hangfire worker later publishes it through MassTransit/RabbitMQ.
統合イベントハンドラー
Integration Event Handler は、通常は他のサービスやコンテキストによって公開された Integration Event を消費し、 自身のドメイン内で特定のアクションを実行するクラスです。
これらのハンドラーは、RabbitMQ、Kafka、Azure Service Bus などのメッセージングメカニズムを通じてイベントを受け取り、 通常は Outbox パターンと組み合わせて使用され、信頼性の高い配信と非同期処理を保証します。
例えば、Identity サービスが UserCreated イベントを公開した場合、別のコンテキストの対応するハンドラーは、
ウェルカムメールを送信したり、外部 API を呼び出したりして反応することができます。
これらの操作は遅延したり失敗する可能性がありますが、メイントランザクションの外で処理されるため、アプリケーションの内部整合性は損なわれません。
主な特徴:
- 他のコンテキストに関連するビジネス上の事実を表すイベントに反応します。
- 遅い可能性のある操作や外部操作(例:メール送信、API 呼び出し)を実行します。
- 非同期かつレジリエントに処理され、リトライやモニタリングが行われることが多いです。
- 外部の失敗が元のドメイントランザクションに影響を与えないことを保証します。
- バウンデッドコンテキストや分散システム間の統合を容易にします。
新しい統合イベントハンドラーを作成するには、次のコマンドを実行します:
lino event-handler new lino event-handler new --name <EventHandlerName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event-handler list --service <ServiceName> --module <ModuleName> --entity <EntityName>
CLI ウィザードは以下を要求します:
- サービス – イベントハンドラーを作成するサービス。
- モジュール – イベントハンドラーを作成するモジュール(モジュール型サービスのみ)。
- エンティティ – イベントハンドラーを作成するエンティティ。
- イベントタイプ – ドメインイベントまたは統合イベント。
- サービス – 消費するイベントが存在するサービス。
- モジュール – 消費するイベントが存在するモジュール(モジュール型サービスのみ)。
- エンティティ – 消費するイベントが存在するエンティティ。
- イベント – 消費するイベント。
- イベントハンドラー名 – エンティティおよび統合イベントに関連付けられる名前。
例
User エンティティおよび UserCreatedIntegrationEvent イベントに関連付けられた統合イベントハンドラー
SendEmailOnUserCreated を作成すると、システムは自動的に SendEmailOnUserCreatedIntegrationEventHandler という名前のハンドラーを作成します。
Lino によって生成された構造:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── Users/
├── Commands/
├── EventHandlers/
│ └── Integration/
│ └── SendEmailOnUserCreatedIntegrationEventHandler.cs
├── Logging/
├── Queries/
└── Resources/
Operational guidance
- Implements
IIntegrationEventHandlerand consumes messages with MassTransit. - Does not read the producer database directly.
- Updates local state, projections, or shadow entities.
- Should be idempotent because retries and duplicate messages can happen.
Shadow Entities
shadow entity は、所有者が別のモジュールまたはサービスにあるデータを、ローカルに最小限かつ制御された形で保持するコピーです。consumer が producer のデータベースへ直接アクセスせずに参照データを照会、検証、表示する必要がある場合に、結合度を下げるために存在します。
Shadow Entities は eventual consistency を許容できる場合に有効です。producer が integration event を公開し、メッセージが Outbox に永続化され、worker が bus へ公開し、consumer が integration event handler を通じてローカルコピーを更新します。
lino shadow-entity new lino event new lino event-handler new
このコマンドは lino shadow new という alias でも利用できます。対話フローでは、Lino が対象のサービスまたはモジュール、元のエンティティ、コピーするプロパティを尋ねます。これにより identifier type、必須指定、string 長、基本的な参照を手作業で再作成する必要がなくなり、source と consumer の不整合を減らせます。
shadow entity は完全なレプリカではありません。Catalog モジュールが Tenancy と Security のデータを消費する例では、catalog は Tenant.Id、Tenant.Slug、User.Id、User.Email、User.TenantId だけを保持できます。元の user entity には、hash、confirmation、tokens、dates など、catalog コンテキストに属さない内部フィールドが引き続き含まれます。
使用する場面
- consumer が別のコンテキストに属する entity の一部フィールドだけを必要とする場合。
- producer の変更から consumer の更新までの小さな遅延をルールが許容する場合。
- producer データベースを直接読むと、モジュールまたはサービス間に不適切な結合が生じる場合。
- grids、filters、ローカル検証、projections が外部参照データを必要とする場合。
避ける場面
- ルールが即時に更新されたデータを要求し、eventual consistency を許容しない場合。
- ローカルコピーが元の aggregate の完全なレプリカになってしまう場合。
- consumer が producer に属する invariants に依存し始める場合。
推奨フロー
- consumer が必要とするフィールドだけで shadow entity をモデル化します。
- producer が公開する integration event を作成または選択します。
- consumer 側に integration event handler を作成します。
- producer key または natural identifier を使って idempotent な upsert を実装します。
- retries、重複メッセージ、恒久的な失敗を診断できるだけの logs を記録します。
イベント駆動同期
全体のフローは、domain、application、Unit of Work、Outbox、worker、bus、consumer を組み合わせます。目的は、モジュールまたはサービス間の壊れやすい直接呼び出しを避け、公開または消費が失敗したときに retry、audit、reprocessing を可能にすることです。
- aggregate が変更され、domain event を登録します。
- Unit of Work がその変更を transaction 内で保存します。
- domain handler がその事実に反応し、integration event を
IOutboxに登録します。 - Unit of Work が business data と
OutboxMessageを同じ transaction で永続化します。 - Hangfire worker が Outbox を読み取り、MassTransit/RabbitMQ 経由で bus へ公開します。
- consumer が
IIntegrationEventHandlerを通じてメッセージを受信します。 - consumer は local model、shadow entity、projection、email、external integration、または別の asynchronous flow を更新します。
Domain change -> Domain Event -> Domain Handler -> Integration Event -> Outbox -> Worker -> Event Bus -> Consumer -> Local model
設計ルール
- invariants は domain に置きます。events は必須ルールを隠すためではなく、reactions のために使います。
- integration events は business change と同じ transaction 内で登録します。
- integration contracts は小さく、versionable で安定したものに保ちます。
- retries や重複メッセージが起こり得るため、idempotent な consumers を実装します。
- worker failures と Outbox に滞留した messages を監視します。
実務上の結果: producer は consumers を知る必要がなく、consumers は producer データベースへアクセスせず、broker または consumer が一時的に利用できない場合でも、コンテキスト間の通信は resilient に保たれます。
