Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Axon Frameworkのイベントストアを独自拡張した話

Axon Frameworkのイベントストアを独自拡張した話

2025/11/15 に JJUG CCC 2025 Fall で発表した登壇資料です。
https://ccc2025fall.java-users.jp/

株式会社ZOZO
ブランドソリューション開発本部
ZOZOMO部 OMOブロック
宮澤 碧

#jjug_ccc

Avatar for ZOZO Developers

ZOZO Developers PRO

November 15, 2025
Tweet

More Decks by ZOZO Developers

Other Decks in Technology

Transcript

  1. © ZOZO, Inc. 株式会社ZOZO ブランドソリューション開発本部 ZOZOMO部 OMOブロック 宮澤 碧 •

    2023年 中途入社(Java歴2年半ほど) • ZOZOMOブランド実店舗の在庫確認・在庫取り置き サービスやショップ直送サービスの開発、運用に従事 • CQRS+ES構成のアプリケーションの設計、実装を担当 2
  2. © ZOZO, Inc. https://zozo.jp/ 3 • ファッションEC • 1,600以上のショップ、9,000以上のブランドの取り扱い •

    常時107万点以上の商品アイテム数と毎日平均2,700点以上の新着 商品を掲載(2025年6月末時点) • ブランド古着のファッションゾーン「ZOZOUSED」や コスメ専門モール「ZOZOCOSME」、シューズ専門ゾーン 「ZOZOSHOES」、ラグジュアリー&デザイナーズゾーン 「ZOZOVILLA」を展開 • 即日配送サービス • ギフトラッピングサービス • ツケ払い など
  3. © ZOZO, Inc. 4 このセッションで扱うこと / 扱わないこと   扱うこと ❏

    CQRS+ES/Axon Frameworkの簡単な紹介 ❏ Axon Frameworkの標準イベントストアの採用を見送った背景 ❏ Axon FrameworkのイベントストアをAmazon DynamoDBで実装した方法 ❏ 独自拡張で得られた成果と、そのトレードオフ   扱わないこと ❏ CQRS+ESの概念に関する詳細な解説 ❏ Axon Frameworkの使い方やAPIの詳細 ❏ Amazon DynamoDBやAmazon Kinesisのサービス仕様の詳細
  4. © ZOZO, Inc. 5 用語の説明 Command Query Responsibility Segregation(CQRS) ❑

    コマンド(書き込み)とクエリ(読み取り)を別々のデータモデルに分離 する設計パターン ❑ 複雑なビジネスロジックが必要とされることの多い書き込み操作を、読み 取り操作の関心事から分離することで、それぞれのモデルを比較的シンプ ルに保つことができる ❑ 実装方式によって、DBをコマンドとクエリで分離してパフォーマンスや 可用性向上を図る。ただしDB間の同期が必要になるのでシステム構成は より複雑になる(本セッションはこの構成)
  5. © ZOZO, Inc. 6 用語の説明 Event Sourcing(ES) ❏ データの現在状態を直接保存せず、変更をイベントとして追記し、その 再生で状態を復元する

    ❏ イベントに対して直接クエリをかけることは難しいため読み取り処理が 複雑・非効率になりがち ❏ 読み取り要件に応じて読み取り処理を分離するCQRSと併用される
  6. © ZOZO, Inc. 8 Axon Framework ❑ CQRS+ESの実装を支援するOSSのJavaフレームワーク ◦ GitHub

    Stars: 3.5k+ / Contributors: 180 / Since 2011 *1 ◦ 公開事例は北米・欧州が中心(金融や政府機関など)*2 ❑ Event Store, Event Bus等のCQRS+ESコンポーネントの標準実装を提供 ❑ 開発者はこれらを利用して複雑なアーキテクチャの構築を効率化できる ❑ Spring Bootにも対応しており、Beanやアノテーションによる設定が可能 ❑ 開発者がインターフェイスを実装することで独自拡張も可能 *1 : https://github.com/AxonFramework/AxonFramework *2 : https://www.axoniq.io/use-cases
  7. © ZOZO, Inc. 14 Axon Server / RDBMS + Kafkaが標準実装として提供されている

    ❑ Axon Server ▪ AxonIQが提供・推奨する専用サーバーで、Jarファイルで提供される ▪ クラスター構成を利用するにはライセンス購入が必要 ▪ イベントストアとイベントバスを内包 ❑ RDBMS + Kafka ▪ JPA/JDBC 向けの実装をフレームワークが提供 ▪ Kafka Extensionを使用することでKafkaを接続できる イベントストアの実装方式
  8. © ZOZO, Inc. 16 Axon Serverの懸念 既存の運用体制との親和性が低い ❑ 内部実装が公開されておらず、障害時の原因特定が難しい ❑

    クラスタ管理に固有知識が必要になる ❑ サポート契約が海外法人となり、英語でのコミュニケーション、窓口時間が異 なる*1 *1 : https://support.axoniq.io/support/solutions/articles/80000966496-severity-levels-availability-hours-and-response-times
  9. © ZOZO, Inc. 17 書き込み性能や運用コストに懸念 ❑ 書き込みスループットの懸念 ▪ 一般的に書き込みが単一ライターに集中する構造で、水平スケールが難 しい

    ▪ DBとKafkaの間で整合性を担保するためのトランザクションが必要にな りスループットに影響を及ぼす(もしくはDBポーリングが必要になる) ❑ Kafkaの運用コスト ▪ クラスタ管理が必要で運用負荷が高い ▪ チームで深い運用知見がない RDBMS + Kafkaの懸念
  10. © ZOZO, Inc. 19 Amazon DynamoDBとAmazon Kinesisについて ❑ Amazon DynamoDB

    ▪ フルマネージドのNoSQLデータベース ▪ 自動パーティションで書き込みの水平スケールが容易 ▪ 後述する変更データキャプチャ機能を利用してKinesisと連携可能 ❑ Amazon Kinesis ▪ フルマネージドのリアルタイムストリーミングサービス ▪ 複雑なクラスタ管理が不要
  11. © ZOZO, Inc. イベントストアの抽象クラス AbstractEventStorageEngine*1を継承 AWS SDKを使い、Amazon DynamoDBへのイベント保存・読み取り処理を行う 22 Amazon

    DynamoDBによるイベントストアの実装 *1:https://github.com/AxonFramework/AxonFramework/blob/axon-4.12.x/eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/AbstractEventSto rageEngine.java
  12. © ZOZO, Inc. Amazon DynamoDBのテーブルはDomainEventData*1インターフェースに準拠 23 Amazon DynamoDB テーブル設計 *1:https://github.com/AxonFramework/AxonFramework/blob/axon-4.12.x/messaging/src/main/java/org/axonframework/eventhandling/DomainEventData.java

    Attribute 説明 aggregateIdentifier(パーティションキー) 集約のID sequenceNumber(ソートキー) シーケンス番号 eventIdentifier イベントID(UUID) aggregateType 集約のクラス timestamp イベントの発生時刻 serializedPayload イベントのペイロード payloadType イベントペイロードのクラス payloadRevision イベントペイロードのリビジョン番号 serializedMetaData メタデータ
  13. © ZOZO, Inc. 24 AbstractEventStorageEngineの継承 // イベントをイベントストアに保存 void appendEvents (List<Event>

    events, Serializer serializer) // IDとシーケンス番号からイベントを取得 Stream<DomainEventData> readEventData(String identifier, long fromSequence) // スナップショットを保存 void storeSnapshot(DomainEventMessage<?> snapshot, Serializer serializer) // スナップショットを取得 Stream<DomainEventData> readSnapshotData(String aggregateIdentifier) イベントの永続化に関する振る舞いを定義する抽象クラス 実装が必要となる中核メソッドを抜粋
  14. © ZOZO, Inc. 25 AbstractEventStorageEngineの継承 // イベントをイベントストアに保存 void appendEvents (List<Event>

    events, Serializer serializer) // IDとシーケンス番号からイベントを取得 Stream<DomainEventData> readEventData(String identifier, long fromSequence) // スナップショットを保存 void storeSnapshot(DomainEventMessage<?> snapshot, Serializer serializer) // スナップショットを取得 Stream<DomainEventData> readSnapshotData(String aggregateIdentifier) 最も基本的な責務であるイベントの保存/取得を考えてみる
  15. © ZOZO, Inc. appendEvents 実装イメージ 27 • AWS SDKを使用してDynamoDBに書き込みを行う •

    Amazon DynamoDBの条件付き書き込みを使用して同一シーケンス番号の重複を防止 void appendEvents(List<Event> events, Serializer serializer) { // トランザクションを使用して書き込み dynamoDbClient.transactWriteItems( events.stream().map(event -> TransactWriteItem.builder().put(p -> p .tableName("event_store_table") .item(...) // イベントをDynamoDBのItemに変換 // 楽観的ロック .conditionExpression( "attribute_not_exists(aggregateIdentifier) AND " + "attribute_not_exists(sequenceNumber)") ).build() ).toList()); }
  16. © ZOZO, Inc. readEventData 実装イメージ 28 • Query APIで、特定の集約の指定したシーケンス番号以降のイベントを取得 •

    パーティションキーとソートキーによる高速検索 Stream<DomainEventData> readEventData(String identifier, long fromSequence) { QueryRequest request = QueryRequest.builder().tableName("event_store_table") // 集約のイベント履歴を検索 .keyConditionExpression( "aggregateIdentifier = :id AND sequenceNumber >= :seq") .expressionAttributeValues(...) .consistentRead(true) //強い整合性の読み取り .build(); return dynamoDbClient.queryPaginator(request) .items().stream().map(DynamoDBEventEntry::new); }
  17. © ZOZO, Inc. 33 Amazon DynamoDBの変更データキャプチャ*1を利用して二重書き込みを回避 • アプリケーションは Amazon DynamoDBへの書き込みだけ

    • Amazon DynamoDBへ書き込みが成功するとAWSの機能でAmazon Kinesis へ変更履歴が配信される • アプリケーション側で二重書き込みやポーリング処理が不要になりシンプル な構成になる *1: https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/streamsmain.html 変更データキャプチャによるAmazon Kinesisへの連携
  18. © ZOZO, Inc. 35 AWS公式のコンシューマライブラリ(Kinesis Client Library)*1を使用 イベントハンドラーの拡張は開発コストが高くAxon Frameworkを使用しない •

    KCLは負荷分散/チェックポイント管理など堅牢な実装を低コストで実装可能 • 別プロダクトで導入実績があり開発資産がある • Axon Frameworkで上記を拡張機能として実装するのは開発コストが高い • イベントハンドラーではイベントから状態を復元する必要がなく、リード側 のDB更新やメール送信といったシンプルな実装なのでAxon Frameworkを使 用する恩恵は少ないと判断 *1: https://github.com/awslabs/amazon-kinesis-client イベントハンドラーの実装
  19. © ZOZO, Inc. Amazon Kinesis コンシューマの実装イメージ 36 • シャード割当/負荷分散/ポーリング等はライブラリ側が自動で処理 •

    開発者が実装すべき処理はShardRecordProcessor*1インターフェース(一部抜粋) public class EventProcessor implements ShardRecordProcessor { @Override public void processRecords(ProcessRecordsInput input) { for (KinesisClientRecord record : input.records()) { // レコードをDTOに変換 eventDTO dto = recordToDto(record, EventDTO.class); // リードDBを更新 readModelUpdater.process(dto) } // 処理完了をチェックポイント input.checkpointer().checkpoint(); } } *1: https://github.com/awslabs/amazon-kinesis-client/blob/master/amazon-kinesis-client/src/main/java/software/amazon/kinesis/processor/ShardRecordProcessor.java
  20. © ZOZO, Inc. 39 AxonFrameworkの恩恵を受けてドメインロジックの実装に集中しつつ、 イベントストアの独自拡張で運用負荷/書き込み性能を向上した構成を実現 • コマンド側は AxonFrameworkのコンポーネント(Aggregate, Repository

    など) を活用し、ドメインロジックの実装に集中 • イベントストアにAmazon DynamoDBを用いることで、水平スケールによる 高い書き込み性能を実現 • AWSのフルマネージドサービスを使用することでAxon Server や Kafka で 懸念された運用負荷を低減 得られた成果
  21. © ZOZO, Inc. 40 初期開発コストと将来的な保守コストが発生する • イベントストアをAmazon DynamoDBで独自拡張するための初期開発コスト が発生。さらに、今後Axon Framework

    本体がバージョンアップした際、拡 張部分の追従・検証コストが発生 • Amazon Kinesisを対象にするイベントハンドラーはAxon Frameworkの標準 実装として提供されておらず、独自拡張するかAxon Framework以外の方法 (Kinesis Client Library)で実装する必要があり、初期開発コストが発生 トレードオフと今後の課題