😇

RooCodeを使ってCQRS + ESのコード(API)の出力ができました!

に公開

とりあえずデモ。動画が貼れなかったのでXのリンクで

https://x.com/i/status/1914587888240746834

ずっとモヤモヤしてきたこと

  • DDD(CQRS + ES)はいいのだけれど、ファイル数が多いのよな、腱鞘炎になる。
  • LLMに任せたいけど、LLMって結構品質の低いコードも出すので、すぐに開発速度が落ちるのよなー

っていう悩みがあって色々試行錯誤してました。そしたら、Claude sonnet 3.7とRoo Codeという組み合わせてできちゃいました!Cursorは多分system promptが微妙な感じで一般的にはこうじゃない?みたいな感じが強くてダメでした。RooCodeはちゃんとレールを弾いてあげたら上手くいきました。たぶんよく設計されたコードの方が参照元と参照先の行間を読み取ってくれる+TypeScripで方針が違うと方エラーが起きるというのが強いです。

プロンプトは再現性どこまであるのかを検証中なので公開はしばしお待ちをできたよという事実だけ先駆けで共有です!

HERP Careersというチームにお世話になっていてそちらでもDDDを使って開発をしているのですが、
再現性は高そうです!API連携やコンポーネントも結構設計方針を守ってくれてそう!
また、HERP CareersではDevinを導入したりAI駆動開発をガンガン進めているので詳しくは聞きにきてください!

ちなみに複雑なpromptはなしRulesも未記載です。むしろ未記載の方がいいかも?

以下にはどのようなシステムでAPIの一発出しができたかを載せておきます!

確認したこと

  • ReadのDBのスキーマ更新もやってくれる。
  • 3つくらいAPIを一緒に作らせると参考実装を忘れて暴走するので、キューに入れてまっさらなとこから3回実行させるのが良さそう。
  • ReadModelUpdaterも更新してくれる!
  • 同期的なReadModel更新はCQRSの本来の設計からズレるけどそういったイレギュラーも既存実装から読み取ってくれます!

システムアーキテクチャ

このダイアグラムは家計簿アプリのシステムアーキテクチャを示しています。

コアアーキテクチャコンセプト (1/3)

1. CQRS

書き込み操作(コマンド)と読み取り操作(クエリ)を分離

コマンドサイド:

  • コマンドハンドラ: リクエストを受け取り、検証し、コマンドオブジェクトを作成
  • コマンド: 必要なデータを含む単一の書き込み操作をカプセル化
  • リポジトリ: 集約の永続化のための抽象化レイヤー
  • イベントストア: 追記専用ログにすべてのドメインイベントを保存

クエリサイド:

  • クエリハンドラ: リクエストを受け取り、検証し、クエリオブジェクトを作成
  • クエリ: フィルタリング条件を含む単一の読み取り操作をカプセル化
  • リードモデル: 効率的なクエリのために最適化されたデータ構造

2. イベントソーシング

システムはイベントを真実の源として使用します:

ドメインイベント:

  • イベントタイプ: ReceiptCreated、ReceiptItemAddedなどの特定イベント
  • イベントメタデータ: イベントがいつ、誰によって作成されたかの追加情報
  • イベントペイロード: 変更された実際のデータ

イベント処理:

  • イベントハンドラ: ドメインイベントを処理し、副作用をトリガー
  • リードモデルアップデータ: ドメインイベントに基づいてリードモデルを更新

状態再構築:
イベントを再生することでシステム状態を再構築可能

コアアーキテクチャコンセプト (2/3)

3. ドメイン駆動設計 (DDD)

システムはリッチなドメインモデルを持つビジネスドメインを中心に構成されています:

集約:
ドメインオブジェクトのクラスターへのアクセスをカプセル化し制御する一貫性の境界

  • レシート集約: レシートデータとそのアイテムを管理
  • ファミリー集約: ファミリーデータとそのメンバーを管理
  • ユーザー集約: ユーザーデータと設定を管理

エンティティ:
IDとライフサイクルを持つオブジェクト

  • レシートエンティティ: コアレシート情報
  • レシートアイテムエンティティ: レシート内の個々のアイテム
  • ファミリーメンバーエンティティ: ファミリー内の個々のメンバー

値オブジェクト:
IDを持たない不変オブジェクト

  • 商店住所: 商店の位置情報
  • レシートカテゴリ: レシートの分類情報
  • マネー: 通貨付きの金銭的価値を表現

ドメインイベント:
ドメインにおける重要な変更を表現

  • レシート作成: 新しいレシートが追加された
  • レシートアイテム追加: レシートに新しいアイテムが追加された
  • レシート削除: レシートがシステムから削除された

コアアーキテクチャコンセプト (3/3)

5. 機能ベースのOrganize

コードベースは技術的レイヤーではなく機能によって整理されています:

ファミリー機能:
ファミリーに関連するすべて

  • ドメインモデル、コマンド、クエリ、リポジトリ、サービス
  • APIエンドポイント、DTO、バリデーション

レシート機能:
レシートに関連するすべて

  • ドメインモデル、コマンド、クエリ、リポジトリ、サービス
  • APIエンドポイント、DTO、バリデーション

ユーザー機能:
ユーザーに関連するすべて

  • ドメインモデル、コマンド、クエリ、リポジトリ、サービス
  • APIエンドポイント、DTO、バリデーション

6. リアルタイム通信

システムはクライアントにリアルタイム更新を提供します:

  • イベント処理: ドメインイベントを処理
  • 通知: イベントに基づいて通知を生成
  • Server-Sent Events: 通知をクライアントにプッシュ

実装テクノロジー

WebフレームワークとAPIレイヤー:

  • Next.js: サーバーサイドレンダリングReactフレームワーク
  • Hono: 軽量で高速なWebフレームワーク
  • OpenAPI/Swagger: APIドキュメントとバリデーション

データベーステクノロジー:

  • DynamoDB: コマンドサイドストレージ用NoSQLデータベース
  • PostgreSQL with DrizzleDB ORM: クエリサイドストレージ用リレーショナルデータベース

実装構造

実装は機能ベースの組織化に従い、各機能内で明確な関心の分離を行っています:

features/receipts/
├── command/           # コマンド(書き込み操作)関連ファイル
│   ├── create-receipt.ts                # コマンド実装
│   ├── create-receipt.route.ts          # HonoによるAPIルート定義
│   └── receipt.dto.ts                   # ZodスキーマによるDTO
├── domain/            # ドメインモデル(集約、エンティティ、値オブジェクト)
│   └── receipt/       # レシート集約
│       ├── receipt.ts                   # 集約実装
│       ├── receipt-item.ts              # エンティティ実装
│       ├── merchant-address.ts          # 値オブジェクト実装
│       └── events/                      # レシートドメインイベント
│           ├── receipt-created.ts       # ドメインイベント実装
│           └── ...
├── query/             # クエリ(読み取り操作)関連ファイル
│   ├── query-find-receipts.ts           # クエリ実装
│   ├── query-find-receipts.route.ts     # HonoによるAPIルート定義
│   └── receipt-sql.ts                   # DrizzleDBによるSQLクエリ
├── repository/        # リポジトリ実装
│   ├── receipt-repository.ts            # リポジトリインターフェースと実装
│   └── receipt-repository.factory.ts    # リポジトリ作成ファクトリ
├── update-read-model/ # リードモデルアップデータとイベントハンドラ
│   ├── receipt-event-handler.ts         # イベントハンドラ実装
│   └── receipt-read-model-updater.ts    # リードモデルアップデータ実装
└── service/           # アプリケーションサービス
    ├── ocr.service.ts                   # レシート処理用OCRサービス
    └── notification.service.ts          # 通知サービス

実装順序

新機能を実装する際の推奨順序:

  1. ドメインモデル設計: 集約、エンティティ、値オブジェクト、ドメインイベントを定義
  2. リポジトリ実装: 永続化戦略とリポジトリインターフェースを作成
  3. サービス実装: 外部サービス統合を実装(オプション)
  4. コマンド/クエリ実装: コマンド、クエリ、DTOを作成
  5. イベントハンドラ実装: イベントハンドラとリードモデルアップデータを実装
  6. APIルート実装: HonoとZodを使用してAPIエンドポイントとリクエスト/レスポンススキーマを定義
  7. API統合: ルートをHonoルーターに登録

まとめ

AIで既存の設計を守ることはできました!とはいえ、フロントに近くなるほどAIの解釈と人間の解釈が変わってくるので(DDDのAPIはほとんどずれない)その辺はAI Firstで業務を見直すのも必要になってくるかなって思います!
HERP Careersでも既存業務を見直すのも含めてAI駆動開発を進めています!

Discussion