イベントソーシングの概念を理解した後、次に直面するのは「実際にどのように実装するか」という実践的な課題です。
イベントストアの設計・集約ルートの実装・スナップショットパターン・リプレイ機能の構築など、イベントソーシングの実装には習得すべき技術要素が多数あります。
ドメイン駆動設計(DDD)の概念と組み合わせることで、よりクリーンで保守性の高い実装が実現します。
本記事では、イベントソーシングの実装方法、手順とパターン、イベントストア・スナップショット・リプレイ機能・集約ルート・ドメイン駆動設計などについて詳しく解説していきます。
イベントソーシングの実装はイベントストアと集約ルートの設計から始まる
それではまず、イベントソーシング実装の全体像と、核心となるイベントストアと集約ルートの設計方針について解説していきます。
イベントソーシングの実装において最も重要な設計決定は、①どのようなドメインイベントを定義するか、②集約(Aggregate)の境界をどこに引くか、③イベントストアをどのように実装するかという3点です。
ドメインイベントの設計
ドメインイベントは不変のオブジェクトとして設計します。
必須フィールドとして、イベントID(UUID)・イベントタイプ(文字列)・発生日時(タイムスタンプ)・集約ID(どの集約に関するイベントか)・イベントバージョン(スキーマバージョン管理用)を含めます。
Javaでのドメインイベント実装例:
public record OrderPlaced(
String eventId,
String orderId,
String customerId,
List<OrderItem> items,
BigDecimal totalAmount,
Instant occurredAt,
int eventVersion
) implements DomainEvent {}
イベント名は「何が起きたか」を表す過去形の動詞句で命名します。
OrderPlaced・PaymentConfirmed・OrderCancelled・ProductShippedのような形が理想的です。
集約ルート(Aggregate Root)の実装
集約ルートはビジネスロジックを持ち、コマンドを受け取ってドメインイベントを生成する責任を持ちます。
集約ルートの実装パターン:
public class Order {
private String orderId;
private OrderStatus status;
private List<DomainEvent> uncommittedEvents = new ArrayList<>();
// コマンドの処理:イベントを生成してapplyする
public void place(String customerId, List<OrderItem> items) {
if (status != null) throw new IllegalStateException(“Already placed”);
apply(new OrderPlaced(UUID.randomUUID().toString(), orderId, customerId, items, …));
}
// イベントの適用:状態を変化させる
private void apply(OrderPlaced event) {
this.status = OrderStatus.PLACED;
this.uncommittedEvents.add(event);
}
}
イベントストアの実装
イベントストアの最低限必要な操作は「イベントの追記(Append)」と「集約IDによるイベントの取得(Load)」の2つです。
イベントストアのインターフェース例:
public interface EventStore {
void append(String aggregateId, List<DomainEvent> events, int expectedVersion);
List<DomainEvent> loadEvents(String aggregateId);
List<DomainEvent> loadEventsFromVersion(String aggregateId, int fromVersion);
}
expectedVersionは楽観的ロック(Optimistic Locking)のために使用し、同時更新による競合を検知します。
スナップショットパターンとリプレイ機能の実装
続いては、大量イベント時のパフォーマンス問題を解決するスナップショットパターンと、状態を再構築するリプレイ機能の実装方法を確認していきます。
スナップショットパターンの実装
スナップショットは一定間隔で集約の現在の状態をキャプチャして保存する最適化パターンです。
通常、100イベントごと、または特定のイベントタイプ発生時にスナップショットを作成します。
スナップショット取得のロジック:
public Aggregate loadAggregate(String aggregateId) {
Snapshot snapshot = snapshotStore.findLatest(aggregateId);
List<DomainEvent> events;
if (snapshot != null) {
// スナップショット以降のイベントのみロード
events = eventStore.loadEventsFromVersion(aggregateId, snapshot.getVersion());
return aggregate.restoreFromSnapshot(snapshot).applyEvents(events);
} else {
// すべてのイベントをロード
events = eventStore.loadEvents(aggregateId);
return new Aggregate().applyEvents(events);
}
}
スナップショットはイベントストアと別に管理し、スナップショットが存在する場合は最新スナップショット以降のイベントのみをリプレイすることで、状態再構築のコストをO(全イベント数)からO(スナップショット間隔)に削減できます。
プロジェクションとリードモデルの更新
イベントハンドラー(プロジェクション)はイベントストアから発行されるイベントを受け取り、リードモデルを更新します。
プロジェクションハンドラーの実装例:
@Component
public class OrderProjection {
@EventHandler
public void on(OrderPlaced event) {
OrderView view = new OrderView();
view.setOrderId(event.orderId());
view.setStatus(“PLACED”);
view.setTotalAmount(event.totalAmount());
orderViewRepository.save(view);
}
@EventHandler
public void on(OrderCancelled event) {
OrderView view = orderViewRepository.findById(event.orderId());
view.setStatus(“CANCELLED”);
orderViewRepository.save(view);
}
}
EventStoreDBやAxon Frameworkの活用
イベントソーシングを一から実装する代わりに、専用フレームワーク・ミドルウェアを活用する方法もあります。
EventStoreDBはイベントソーシング専用のデータベースで、パーシスタント サブスクリプション・スナップショット・プロジェクションなどの機能をネイティブに提供します。
Axon Framework(Java)はイベントソーシング・CQRSの実装を強力にサポートするフレームワークで、集約・イベントハンドラー・コマンドハンドラーの実装を大幅に簡素化できます。
まとめ
本記事では、イベントソーシングの実装方法、手順とパターン、イベントストア・スナップショット・リプレイ機能・集約ルート・ドメイン駆動設計などについて解説しました。
イベントソーシングの実装はドメインイベントの設計・集約ルートの実装・イベントストアの構築という3つの核心要素から始まります。
スナップショットパターンでパフォーマンスを最適化し、プロジェクションでリードモデルを構築することで、実用的なイベントソーシングシステムが完成します。
EventStoreDBやAxon Frameworkなどの専用ツールを活用することで実装コストを大幅に削減できますので、プロジェクトの規模と要件に応じて適切なアプローチを選択することをおすすめします。