CQRSとEventSourcingに挑戦

この記事は アイスタイル [Advent Calendar 2023](https://qiita.com/advent-calendar/2023/istyle) 12日目の記事です。

# はじめに
みなさんこんにちは。
アイスタイルのバックエンドサービスを担当しているkuriyamayです。

アドベントカレンダーへの参加もお馴染みとなりました。
過去の記事はこちらです。

* 2020年
[kubernetes(k8s)を構築してデプロイやスケールで遊んでみた](https://techblog.istyle.co.jp/archives/6296)
* 2021年
[Java SE 11(Java Silver)を受験した話](https://techblog.istyle.co.jp/archives/8225)
* 2022年
[curry化と部分適用をJavaで](https://qiita.com/kuriyamay/items/45f69ceda78d88acd871)
[curry化と部分適用をJavaで ~命名編~](https://qiita.com/kuriyamay/items/8a808d0c8cbbfde10e13)

2023年はCQRSとEventSourcingについて書いていこうと思います。

# CQRS / EventSourcing
## CQRSとは
CQRS（Command Query Responsibility Segregation）は2010年にGreg Youngさんによって提唱されたアーキテクチャで、コマンド・クエリ責務分離の手法です。
このアーキテクチャは、システム内のデータ書き込み操作（コマンド）とデータ読み取り操作（クエリ）を明確に分離し、各操作に対して最適な実装やスケーリング戦略を可能にします。


## EventSourcingとは
システムの状態を変更する全てのアクションをイベントの流れとして記録します。

EventSourcingとは逆の意味合いでStateSourcingという言葉があり、こちらはCreate/Update/Deleteを用い状態を変化させ、最新の状態を永続化します。

EventSourcingはイベント(過去発生した出来事)を全て記録し、そのイベントをもとに状態の再構築や、過去に発生したイベントが完全に残ることでデータ分析などにも役立ちます。
StateSourcingは上書き、EventSourcingは追記でデータを操作するイメージですね。


# 構成図
![2023-12-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889808/9b3fe932-7527-14d1-8298-c8f328a4cb50.png)

* Command
* Query
* Subscriber

こちらの3システムを作っていきます。
Commandで操作されたデータをQueryのデータソースに同期するためSubscriberも必要となり、Pub/Subも必要だったのでRedisを選択しています。
Query側はリレーショナルデータベースにしたかったのでMySQLを選びました。


# 環境情報
* Node.js 18.15.0
* TypeScript 5.1.6
* Redis 7.2.3(Dockerではlatest)
* MySQL 8.1.0


# 作成物
https://github.com/nyankuri/review-api

はい、ということで作ったものがこちらです。
アイスタイルなのでクチコミ投稿や取得を行うサービスにしました。

また、Node.js/TypeScriptは本を読んだくらいだったので勉強のためにもこちらの言語を選びました。

:::note info
上記にある通り勉強用に個人リポジトリで作成しました。
至らない箇所もありますがご容赦ください。
:::


# 全体構成
1リポジトリで3システム内包しています。
rootにあるdocker-composeから起動することで構成図にある`3システム(Command/Query/Subscriber) + Redis + MySQL`が立ち上がります。


# 解説
## Command
https://github.com/nyankuri/review-api/tree/main/command

CQRSのCommandを扱うシステムです。
Cleanを意識しつつ、TypeScriptでDDDを表現するにはどのようなコードになるのか？と疑問があったので色々とチャレンジをしてみました。
クチコミドメインの集約をイベントとして扱い、Redisに永続化後、イベントをpublishしています。
実装中に気にしたポイントは下記になります。

1. 結果整合性のためCommandはステータスコード`202 Accepted`を返す
1. EntityのIDにはアプリケーション内で生成したUUIDを持たせる
1. EventSourcingのため各イベントにバージョンを持たせ、同一IDで更新が走ったらバージョンをインクリメントして永続化する
1. 既存データに対して更新はしないのでロックの心配はいらない
1. StateSourcingの考えで設計すると混乱する

時間の関係で削除処理は実装していません。

## Subscriber
https://github.com/nyankuri/review-api/tree/main/subscriber

RedisにpublishされたイベントをsubscribeしてMySQLに永続化しています。
構成もシンプルでhandlerとデータソースを扱うinfrastructureだけにしています。



## Query
https://github.com/nyankuri/review-api/tree/main/query

取得だけなのでDDDはやっていません。
ビジネスロジックもないのでSubscriber同様シンプルな構成にしています。


# 動作
## POST
```bash
$ curl --location 'localhost:3000/review' \
> --header 'Content-Type: application/json' \
> --data '{
>     "product_id": 222,
>     "recommend": 3,
>     "text": "review post text"
> }'
{"review_id":"f5db294f-25de-4e66-b3f5-d65e2fb1aaaf","version":"1"}
```

## GET
```bash
$ curl --location 'localhost:4000/review/f5db294f-25de-4e66-b3f5-d65e2fb1aaaf'
{"review":{"id":"f5db294f-25de-4e66-b3f5-d65e2fb1aaaf","recommend":"3","text":"review post text","version":"1"},"product":{"product_id":"222"}}
```

## PUT & GET
```bash
# PUT
$ curl --location --request PUT 'localhost:3000/review' \
> --header 'Content-Type: application/json' \
> --data '{
>     "id": "f5db294f-25de-4e66-b3f5-d65e2fb1aaaf",
>     "version": "1",
>     "product_id": 222,
>     "recommend": 5,
>     "text": "review update text"
> }'
{"review_id":"f5db294f-25de-4e66-b3f5-d65e2fb1aaaf","version":"2"}

# GET
$ curl --location 'localhost:4000/review/f5db294f-25de-4e66-b3f5-d65e2fb1aaaf'
{"review":{"id":"f5db294f-25de-4e66-b3f5-d65e2fb1aaaf","recommend":"5","text":"review update text","version":"2"},"product":{"product_id":"222"}}
```
Command/Subsciber/Queryの3システムが連携してデータ登録から取得まで出来ています。

## まとめ
ひとまず練習用に動くものは作れました。
3システムありましたが、役割ごとに分かれているのでそのシステムの関心事だけに集中して実装することが出来ました。

本番運用を想定するとまだまだ考慮しなければいけないことが沢山ありますが、自分で1から考えて実装することでとっかかりは掴めた気がします。
業務でもCQRS + EventSourcingを頭に入れて技術選定や設計を進めていけそうです。

アイスタイルでは年に一度のイベント[@cosme BEAUTY DAY](https://www.cosme.com/campaign/beautyday/)があり、イベント時はトラフィックもかなり増えるので、Query側だけスケールする戦略をしていけばCQRSの旨味も享受できそうですね。

最後まで読んでいただきありがとうございました。
引き続き[Advent Calendar 2023](https://qiita.com/advent-calendar/2023/istyle)をお楽しみください。