cqrs/esをAWSサービスを使って実装してみる

参考
- CQRS & Event Sourcing モダンアーキテクチャにおける役割と実装
- Amazon DynamoDB を使った CQRS イベントストアの構築
- アクターシステムに頼らずEvent Sourcingする方法について
- Amazon DynamoDB の変更データキャプチャ
構成イメージ
- DynamoDB Streamsを使って変更データキャプチャ
- LambdaとRDSの間にSNSを噛ませて複数サービスにファンアウトは今のところ考えない。余裕あったら試す。

現時点で見えている懸念点
- キャプチャしているLambdaの実行が何らかの理由で失敗してしまった場合、変更の再送はできるのか?(データは24時間しか保存されない)
関数がエラーを返した場合、処理が正常に完了するか、データの有効期限が切れるまで Lambda はバッチを再試行します。Lambda では、より小さなバッチで再試行したり、再試行回数を制限したり、古すぎるレコードを破棄したりするよう設定できます。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Streams.Lambda.html-
代替案(いずれも重複・順序の入れ替えが発生しうるので冪等・順序を保証すべき)
- Kinesis Data Streamsを使って変更データキャプチャする。この場合データは最大1年保存可。
- キャプチャするLambdaはシンプルにSNS/SQSに流すだけにして、デッドレターキューを活用
-
代替案(いずれも重複・順序の入れ替えが発生しうるので冪等・順序を保証すべき)
- イベントA、イベントBの順でStreamsに流れてきた時、イベントAの処理が長く、イベントBの保存が先に終了・イベントAの処理が失敗するケースにどう対応するか?
- 同期的に処理されるので問題なさそう
Lambda 関数が同期的に呼び出されます。同じ DynamoDB ストリームに最大 2 つの Lambda 関数をサブスクライブできます。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Streams.Lambda.html - そもそも長時間実行されるLambdaはよろしくない
パフォーマンスのベストプラクティスとして、Lambda 関数は存続期間を短くする必要があります。また、不必要な処理の遅延が発生するのを防ぐため、複雑なロジックは実行しないでください。特に高速ストリームの場合は、同期的に長時間実行する Lambda よりも、非同期的な後処理ステップ関数ワークフローをトリガーすることをお勧めします。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Streams.Lambda.html
- 同期的に処理されるので問題なさそう

サブ目的〜Hono*ts*関数型プログラミング〜
Command側Appの実装にHonoを使ってみる。

Honoの環境構築
create hono
npm create hono@latest
> npx
> create-hono
create-hono version 0.14.3
? Target directory command-app
? Which template do you want to use? nodejs
? Do you want to install project dependencies? yes
? Which package manager do you want to use? npm
✔ Cloning the template
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd command-app
初期状態
初期状態(http://localhost:3000)

関数型*TSで実装
biome入れた
問題点1:構造的部分型による不正値の混入問題
type name = {
readonly value: string
}
type id = {
readonly value: string
}
const id = {value: "id"}
const name = id // これさせたくない
- classであればprivateフィールドを定義
- ブランド型*
as typeName
によるアサーション
で回避できるがしっくり来ない
解決策
newtype-tsを使ってnominalな型を定義。
newtypeを自作する。
参考
Result型の利用
候補
neverthrowが軽量サクッと試すにはよさそう。
つまりポイント: Resultの合成
やりたいこと
できたもの
const validateRegisterItem = (
command: RegisterItemCommand,
): Result<ValidatedRegisterItem, RegisterItemValidationError> => {
const itemName = createItemName(command.name);
const itemPrice = createItemPrice(command.price);
const values = Result.combineWithAllErrors([itemName, itemPrice]);
return values.map(([name, price]) => ({ name, price }));
};
参考combine
だとエラーが最初の一つのみしか返されないので注意
イベントペイロードのデータ型
理想:protobuf
一旦JSONで次に進む

Railway Oriented Programing
Result is a tool for domain modeling, so if the domain model doesn’t need it, don’t use it.
A similar example can be found when implementing event sourcing, in the command processing function which has the standard signature'state -> 'command -> 'event list
If something goes wrong in executing the command, how does that affect the return value (the list of events created by the command) in practice? Of course you need to handle errors and log them, but do you actually need to return a Result from the function itself? It will make the code more complicated for not much benefit.
https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#5----dont-use-result-if-no-one-cares-about-the-error-cases