Slide 1

Slide 1 text

Axon Frameworkの イベントストアを独自拡張した話 JJUG CCC 2025 Fall 株式会社ZOZO
 ブランドソリューション開発本部 ZOZOMO部 OMOブロック
 宮澤 碧 Copyright © ZOZO, Inc. 1

Slide 2

Slide 2 text

© ZOZO, Inc. 株式会社ZOZO ブランドソリューション開発本部 ZOZOMO部 OMOブロック 宮澤 碧 ● 2023年 中途入社(Java歴2年半ほど) ● ZOZOMOブランド実店舗の在庫確認・在庫取り置き サービスやショップ直送サービスの開発、運用に従事 ● CQRS+ES構成のアプリケーションの設計、実装を担当 2

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

© ZOZO, Inc. 5 用語の説明 Command Query Responsibility Segregation(CQRS) ❑ コマンド(書き込み)とクエリ(読み取り)を別々のデータモデルに分離 する設計パターン ❑ 複雑なビジネスロジックが必要とされることの多い書き込み操作を、読み 取り操作の関心事から分離することで、それぞれのモデルを比較的シンプ ルに保つことができる ❑ 実装方式によって、DBをコマンドとクエリで分離してパフォーマンスや 可用性向上を図る。ただしDB間の同期が必要になるのでシステム構成は より複雑になる(本セッションはこの構成)

Slide 6

Slide 6 text

© ZOZO, Inc. 6 用語の説明 Event Sourcing(ES) ❏ データの現在状態を直接保存せず、変更をイベントとして追記し、その 再生で状態を復元する ❏ イベントに対して直接クエリをかけることは難しいため読み取り処理が 複雑・非効率になりがち ❏ 読み取り要件に応じて読み取り処理を分離するCQRSと併用される

Slide 7

Slide 7 text

© ZOZO, Inc. 7 理論はわかったけどゼロから実装するのは難しい →フレームワークを使用する

Slide 8

Slide 8 text

© 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

Slide 9

Slide 9 text

© ZOZO, Inc. 9 簡単に構成図を紐解いてみる

Slide 10

Slide 10 text

© ZOZO, Inc. 10 Axon Frameworkのアーキテクチャ概要 引用元:https://legacy-docs.axoniq.io/reference-guide/v/2.4/architecture-overview.html events reply command events events events Analysis Database sql email Database Messageing sql DTOs query Event Store

Slide 11

Slide 11 text

© ZOZO, Inc. 11 Axon Frameworkのアーキテクチャ概要(コマンド側) 1. イベントストアからモデルを再構築 2. ビジネスロジックを適用してイベント生成 3. イベントストアに追記 4. イベントバスに配信してクエリ側に伝搬

Slide 12

Slide 12 text

© ZOZO, Inc. 12 Axon Frameworkのアーキテクチャ概要(クエリ側) 1. 配信されたイベントでリードモデル更新 2. 別サービスへの伝搬など 3. クエリAPIはリードモデルを参照する 12

Slide 13

Slide 13 text

© ZOZO, Inc. 13 中心部であるイベントストアとイベントバスを どのように実装するか技術選定が重要になる

Slide 14

Slide 14 text

© ZOZO, Inc. 14 Axon Server / RDBMS + Kafkaが標準実装として提供されている ❑ Axon Server ▪ AxonIQが提供・推奨する専用サーバーで、Jarファイルで提供される ▪ クラスター構成を利用するにはライセンス購入が必要 ▪ イベントストアとイベントバスを内包 ❑ RDBMS + Kafka ▪ JPA/JDBC 向けの実装をフレームワークが提供 ▪ Kafka Extensionを使用することでKafkaを接続できる イベントストアの実装方式

Slide 15

Slide 15 text

© ZOZO, Inc. 15 それぞれ懸念が。。

Slide 16

Slide 16 text

© ZOZO, Inc. 16 Axon Serverの懸念 既存の運用体制との親和性が低い ❑ 内部実装が公開されておらず、障害時の原因特定が難しい ❑ クラスタ管理に固有知識が必要になる ❑ サポート契約が海外法人となり、英語でのコミュニケーション、窓口時間が異 なる*1 *1 : https://support.axoniq.io/support/solutions/articles/80000966496-severity-levels-availability-hours-and-response-times

Slide 17

Slide 17 text

© ZOZO, Inc. 17 書き込み性能や運用コストに懸念 ❑ 書き込みスループットの懸念 ▪ 一般的に書き込みが単一ライターに集中する構造で、水平スケールが難 しい ▪ DBとKafkaの間で整合性を担保するためのトランザクションが必要にな りスループットに影響を及ぼす(もしくはDBポーリングが必要になる) ❑ Kafkaの運用コスト ▪ クラスタ管理が必要で運用負荷が高い ▪ チームで深い運用知見がない RDBMS + Kafkaの懸念

Slide 18

Slide 18 text

© ZOZO, Inc. 18 運用負荷が低く、書き込み性能が高いサービスで実現したい →開発実績や運用実績があるサービスだとベスト Amazon DynamoDB Amazon Kinesis

Slide 19

Slide 19 text

© ZOZO, Inc. 19 Amazon DynamoDBとAmazon Kinesisについて ❑ Amazon DynamoDB ▪ フルマネージドのNoSQLデータベース ▪ 自動パーティションで書き込みの水平スケールが容易 ▪ 後述する変更データキャプチャ機能を利用してKinesisと連携可能 ❑ Amazon Kinesis ▪ フルマネージドのリアルタイムストリーミングサービス ▪ 複雑なクラスタ管理が不要

Slide 20

Slide 20 text

© ZOZO, Inc. 20 この部分を

Slide 21

Slide 21 text

© ZOZO, Inc. 21 こうできると良さそう Amazon Kinesis Amazon DynamoDB

Slide 22

Slide 22 text

© 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

Slide 23

Slide 23 text

© 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 メタデータ

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

© ZOZO, Inc. 25 AbstractEventStorageEngineの継承 // イベントをイベントストアに保存 void appendEvents (List events, Serializer serializer) // IDとシーケンス番号からイベントを取得 Stream readEventData(String identifier, long fromSequence) // スナップショットを保存 void storeSnapshot(DomainEventMessage snapshot, Serializer serializer) // スナップショットを取得 Stream readSnapshotData(String aggregateIdentifier) 最も基本的な責務であるイベントの保存/取得を考えてみる

Slide 26

Slide 26 text

© ZOZO, Inc. 26 実装イメージを記載 (あくまで参考程度に捉えてください)

Slide 27

Slide 27 text

© ZOZO, Inc. appendEvents 実装イメージ 27 ● AWS SDKを使用してDynamoDBに書き込みを行う ● Amazon DynamoDBの条件付き書き込みを使用して同一シーケンス番号の重複を防止 void appendEvents(List 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()); }

Slide 28

Slide 28 text

© ZOZO, Inc. readEventData 実装イメージ 28 ● Query APIで、特定の集約の指定したシーケンス番号以降のイベントを取得 ● パーティションキーとソートキーによる高速検索 Stream 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); }

Slide 29

Slide 29 text

© ZOZO, Inc. 29 DynamoDBへのイベント保存/取得が可能に Amazon DynamoDB

Slide 30

Slide 30 text

© ZOZO, Inc. 30 イベント追加をイベントバスに配信したい Amazon DynamoDB

Slide 31

Slide 31 text

© ZOZO, Inc. 31 二重書き込みをしたくない Amazon DynamoDB

Slide 32

Slide 32 text

© ZOZO, Inc. 32 Amazon DynamoDBの 変更データキャプチャを利用して連携する Amazon DynamoDB Amazon Kinesis

Slide 33

Slide 33 text

© 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への連携

Slide 34

Slide 34 text

© ZOZO, Inc. 34 あとはAmazon Kinesisのイベントを処理する イベントハンドラーがあれば完成 Amazon DynamoDB Amazon Kinesis

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

© 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

Slide 37

Slide 37 text

© ZOZO, Inc. 37 イベント追記をクエリ側に配信することが可能になり CQRS+ES構成が実現 Amazon DynamoDB Amazon Kinesis

Slide 38

Slide 38 text

© ZOZO, Inc. 38 まとめ

Slide 39

Slide 39 text

© ZOZO, Inc. 39 AxonFrameworkの恩恵を受けてドメインロジックの実装に集中しつつ、 イベントストアの独自拡張で運用負荷/書き込み性能を向上した構成を実現 ● コマンド側は AxonFrameworkのコンポーネント(Aggregate, Repository など) を活用し、ドメインロジックの実装に集中 ● イベントストアにAmazon DynamoDBを用いることで、水平スケールによる 高い書き込み性能を実現 ● AWSのフルマネージドサービスを使用することでAxon Server や Kafka で 懸念された運用負荷を低減 得られた成果

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

No content