Spring Boot ドメインイベントの実装例
はじめに
本記事では、Spring Bootを使用したドメインイベントの実装方法についてコード例を交えながら紹介します。
ドメインイベントとは
ドメインイベントはドメインモデル内で重要な出来事(イベント)を表現し、他のコンポーネントに通知するための仕組みです。
イベントをトリガーとして、関連するビジネスロジックを実行することが可能です。
例えば、ユーザーが新規登録した際にイベントを発行しそれを受け取った別のコンポーネント(イベントハンドラー)がウェルカムメールを送信するというケースがあります。
なぜドメインイベントを使用するのか
今回ドメインイベントを実装する目的は同じような処理が複数のユースケースに散在することを防ぎ、関連する処理を適切なイベントハンドラー側に集約したかったからです。
ドメインイベントを使用することでユースケースの実装がシンプルになり、責務の分離を実現することも目的としています。
さらに、リポジトリでのイベント発行のチェックを追加することで必要なイベント登録の漏れを防止し、イベントの発行漏れによるバグを検知する仕組みをつくることもできます。
以上の目的を達成するためにドメインイベントを使用することには、いくつかのメリットがあり同時に注意すべき課題も存在します。
ドメインイベントのメリット
- 各ユースケースで処理の重複を避けることができ、コードの冗長性が排除できる
- リポジトリにドメインイベントの存在チェックを組み込むことで、開発者がイベント発行が必要かどうか意識しやすくなる
- 新しい機能を追加する際には既存のコードに手を加えず、必要なドメインイベントを発行し対応するイベントハンドラーを追加するだけで機能拡張が可能
ドメインイベントの課題
- ドメインイベントの仕組みを理解していないと関連処理を見逃す可能性がある
- ドメインイベントは万能な防御策ではなく実装者がその仕組みやドメインに関する知識を持つことが重要
実装例
以下に、Spring Bootでのドメインイベントの具体的な実装例を紹介します。
使用技術
- Java 17
- Spring Boot 2.7.18
- Hibernate 5.6.1
1. 集約ルートの基本クラス
ドメインイベントを登録するクラスは、次の集約ルートの基本クラスを継承します。
/**
* 集約ルートの基本クラスです。
*/
public class AbstractAggregateRoot<A extends AbstractAggregateRoot<A>> extends AbstractEntity {
@Transient private final List<Object> domainEvents = new ArrayList<>();
/*
* ドメインイベントを登録します。
*/
protected <T> void registerEvent(T event) {
this.domainEvents.add(event);
}
/**
* ドメインイベントのコレクションを取得します。
*/
@DomainEvents
protected Collection<Object> domainEvents() {
return Collections.unmodifiableList(this.domainEvents);
}
}
今回の実装では、すべてのエンティティは独自の基本クラスAbstractEntity
を継承する仕様となっています。
また、Javaは多重継承をサポートしていないため、Spring Bootが提供する AbstractAggregateRoot
をそのまま利用できませんでした。
そのため、Spring Bootの AbstractAggregateRoot
を参考に AbstractEntity
を継承した独自の AbstractAggregateRoot
クラスを実装しました。
2. ドメインイベントを登録するクラス
public class Contract extends AbstractAggregateRoot<Contract> {
public contract() {
registerEvent(new ContractRegistered(this));
}
}
集約ルートとして AbstractAggregateRoot
を継承し、ドメイン上の重要な出来事が発生した際にドメインイベントを登録します。
3. ドメインイベント
public record ContractRegistered(Contract contract) {}
レコードクラスを使用することで、不変性に近い状態を保ちながら、データの整合性を確保しています。
また、イベント名に過去形を用いることでイベントの発生が完了していることを明示的に表しています。
4. イベントハンドラー
public class ContractEventHandler {
/** メールを送信するアプリケーションサービス */
private final MailSenderService mailSenderService;
/**
* 契約登録時のイベントハンドラー
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleRegisteredEvent(ContractRegistered registered) {
// 契約が登録されたらメールを送信する
this.mailSenderService.sendMailService(registered);
}
}
このハンドラーはドメインイベントを検知し、適切なタイミングでメール送信などの後続処理を実行します。
@TransactionalEventListener
アノテーションにより、トランザクションの特定のフェーズでイベントを購読します
5. リポジトリ
/** 契約リポジトリ */
@Repository
@RequiredArgsConstructor
public class ContractRepositoryImpl implements ContractRepository {
/** JPA契約リポジトリ */
private final JpaContractRepository jpaContractRepository;
/**
* 契約を保存します。
*
* @param contract 契約
* @return 契約
*/
@NotNull
public Contract save(@NonNull Contract contract) {
if (contract.getDomainEvents().isEmpty()) {
throw new AssertionError("ドメインイベントの発行が必要");
}
return this.jpaContractRepository.save(contract);
}
}
イベントの発行が必須となるかどうかは設計方針によるところが大きいですが、今回の実装では開発者がイベントの登録を忘れるのを防ぎ、ビジネスルールの一貫性を保証するためドメインイベントの存在をチェックする仕組みを実装しています。
まとめ
Spring Bootを用いたドメインイベントの実装により、以下のメリットがあります。
- ビジネスロジックの適切な分離と集約
- システムの拡張性と保守性の向上
- ドメインルールの一貫性保証
ただし、効果的に活用するには適切な設計とチーム全体での理解が重要となります。
参考文献
私たち BABY JOB は、子育てを取り巻く社会のあり方を変え、「すべての人が子育てを楽しいと思える社会」の実現を目指すスタートアップ企業です。圧倒的なぬくもりと当事者意識をもって、こどもと向き合う時間、そして心のゆとりが生まれるサービスを創出します。baby-job.co.jp/
Discussion