alchemy-domain について
`@alchemy.run/domain`を使い、型付きメッセージと状態遷移でバックエンドを構築するためのスキルです。要件分析やモデル設計から、ストーリーによるテスト、Cloudflareでのホスト、foldkitのフロントエンドまで案内します。
主な機能
- 01要件からモデルへの分解
- 02型付きドメイン構成要素
- 03ストーリーによる動作テスト
- 04Cloudflareホスティングの手順
- 05foldkitフロントエンドのワークフロー
活用シーン
- 01コマンドと業務イベントのモデル化
- 02ストーリーを使ったドメイン動作のテスト
- 03Cloudflareでのモデルのホスト
- 04foldkitのWebアプリからモデルを操作
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ alchemy-run/ alchemy --skill alchemy-domainインストール後、Claude Code で/alchemy-domainを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/building-blocks.mdfrontend.mdhosting.mdlayout.mdmodeling.mdtesting.md詳細
@alchemy.run/domain を使った開発
@alchemy.run/domain は、バックエンドを型付きメッセージと純粋な状態遷移としてモデル化します。コマンドは集約が判断し、イベントを記録します。ビューはイベントを畳み込んで読み取りモデルを作成します。ポリシーはイベントに反応します。サガは長時間にわたるプロセスを実行します。プロジェクションはモデルの外部に書き込み、クエリは書き込まれたデータを提供します。同じモデルを、テストではメモリ上で、本番環境では Cloudflare Durable Objects 上で実行できます。
ドキュメント:https://alchemy.run/domain。すべてのページは https://alchemy.run/llms.txt に掲載されています。ここおよびリファレンスファイルの各セクションの末尾には、その内容を詳しく解説するドキュメントページへの「詳細」リンクがあります。完全なリファレンスプロジェクトは、alchemy リポジトリの examples/cloudflare-domain です(foldkit アプリを備えた銀行)。
ワークフロー
次の順序で進めます。各ステップにはテンプレートを含むリファレンスファイルがあります。
- 要件を分解する:構成要素に分けます。references/modeling.md を参照してください。
- モノレポを構成する:ドメインパッケージ、サーバーパッケージ、ウェブパッケージを用意し、ブロックごとに種類を示す接尾辞付きのファイルを1つずつ配置します。references/layout.md を参照してください。
- 各ブロックを作成する:テンプレートに沿って作成します。references/building-blocks.md を参照してください。
- ストーリーで動作を仕様化する:実装の前と実装中に行います。references/testing.md を参照してください。
- モデルをホストする:サーバーパッケージから Cloudflare 上にモデルをホストし、そのトレースをエクスポートして、1つの alchemy スタックでデプロイします。references/hosting.md を参照してください。
- ウェブパッケージから foldkit で操作する:references/frontend.md を参照してください。
詳細:チュートリアル、第1部。
要件から構成要素へ
要件を事実のストーリーとして読み解き、見つかった要素をそれぞれの種類として名付けます。
- ビジネスで記録される事実。過去形で命名します(
MoneyWithdrawn):イベント。 - 何かを変更するリクエスト。
<Verb><Noun>の形式で命名します(WithdrawMoney):コマンド。 - リクエストが拒否される理由(
InsufficientFunds):拒否。 - ルールを1つのトランザクション内で守る必要があるもの(1つの口座の残高):集約。集約は自身の状態に照らしてコマンドを判断します。
- 誰かが尋ねる質問(残高、明細、ダッシュボード):対象をキーとするビュー。
- 「Xが起きたらYを実行する」:ポリシー。
- 手順、待機、取り消しを伴うプロセス(送金:引き落とし、入金、失敗時の返金):ポリシーによって開始されるサガ。
- 呼び出すモデル外部のもの(不正利用のスコアリング、決済):外部システム。
- モデル外部のストアへの書き込み(台帳テーブル):プロジェクション。キーごとに順序付けられ、読み取り可能なログ(口座カード)は、プロジェクションが追記するストリームです。
- そのストアを基にした、キーをまたぐ質問(ページ分割された口座の台帳、全口座を対象とした高額出金の金額順一覧):クエリ。手書きのルートを使ってはいけません。
- 誰が何を実行し、何を閲覧できるか:コマンドと読み取りを列挙するApi。ミドルウェア(セッション、MFA)を備え、各ブロックが自身で認可します。コマンドは
decideで、ビューまたはストリームはそれぞれの Layer のvalidateで、クエリはハンドラーで認可します。
複数の集約にまたがるルールが、集約間のトランザクションになることはありません。不変条件に、それが保護するものをキーとする独自の集約を割り当てます(ユーザー名をキーとする UsernameClaim)。または、ポリシーやサガで先に予約し、後から確定します。待機が発生するものには期限を設けます。
詳細:ドメイン駆動設計とは?、ドメインモデル。
規約
- ファイルごとにブロックを1つ作成し、
<Noun>.<kind>.tsという名前を付けます。 種類は.aggregate、.command、.view、.policy、.saga、.projection、.stream、.query、.external、.api、.modelです。さらに.middleware、.context、.rejectionがあります。集約の ID は、隣接する<Aggregate>.id.tsに置きます。通常の値(Money.ts)やヘルパーには通常の名前を付けます。 - コマンドファイルには、コマンド、そのコマンドが記録するイベント、そのコマンドだけが返す拒否をまとめます。 複数のコマンドで使われる拒否やイベントには、独自のファイルを用意します。
- ブロックの実装は、そのブロックのファイルに置きます(
TransferExecution.policy.ts内のTransferExecutionLive)。インフラストラクチャに触れる場合は、サーバーパッケージに置きます。 - フォルダーはサブドメインです(
Account/、Transfer/)。モデル、Apis、ミドルウェア、リクエストコンテキストはパッケージのルートに置きます。 - ドメインパッケージがインポートするのは
@alchemy.run/domainとeffectだけです。 Cloudflare、Stripe、データベース、HTTP セッションの処理はサーバーパッケージに置きます。 - モデル(境界づけられたコンテキスト)ごとにドメインパッケージを1つ用意します。 2つ目のモデルには、2つ目のドメインパッケージが必要です。1つのサーバーで複数のモデルをホストできます。
詳細:プロジェクトの構成。
モデルの正しさを保つルール
decideは純粋関数にするか、そのコマンドのミドルウェアが提供するもの(yield* CurrentActor)だけを読み取るEffect.fnにします。イベントまたは拒否を返し、I/O は決して実行しません。evolveはイベントを状態に適用するだけです。何も判断しません。また、evolveとreplyは全域関数にします。すべての状態を網羅し、失敗しないようにします。実行できない処理はすでにdecideが拒否しているためです。イベントを適用できない状態はそのまま保持し(s._tag === "Open" ? { ...s, balance: e.balanceAfter } : s)、例外を投げるガードは使いません。- イベントには
decideが算出した結果(balanceAfter)を含めます。evolveはルールを繰り返すのではなく、その値をコピーします。 - コマンドが返す可能性のある拒否は、すべて
rejectsに列挙します。呼び出し側では、それぞれをEffect.catchTagで処理します。 - プロジェクションが書き込むストアを提供するには、手書きのルートではなく、Api 上の
Domain.Queryを使います。ハンドラーが呼び出し元を検証し、読み取る行の範囲を限定します。 - クライアントは集約を直接読みません。ビュー(単一キー、ライブ)、ストリーム、またはクエリを通じて読み取ります。クエリのハンドラーは、
yield* Accountの後にaccounts.get(ref)を実行して、集約の最新状態を読み取ることができます。 - ポリシーとプロジェクションは少なくとも1回実行されます。処理を冪等にするため、ハンドラーから送信するコマンドには決定的な ID を付け、ストアの行は
stream:seqをキーにします。 - ポリシー内でサーガの完了を待ちません。対象となる集約をキーにして
startし、そのステップが記録するイベントに反応します。 - ハンドラーは
toLayerで依存関係を一度だけ解決し、呼び出しごとに依存関係を要求しません。 - Layer を1つ構築し、一度だけ提供します(
Layer.mergeAll、Layer.provide)。Effect.provideを連鎖させてはいけません。 throwは決して使いません。タグ付きエラー(Domain.Rejection、Schema.TaggedError)で失敗を表します。- トレースは自動で行われます。ペイロード値を記録する場合は、ハンドラー内でのみ
Effect.annotateCurrentSpanを使います。値はそのハンドラーに対応するエンジンのスパンに記録されます。シークレットはSchema.Redactedにして、トレースやログに出ないようにします。 - モデルの動作はストーリーを通してテストします。イベント、応答、拒否、ビュー、サーガの結果を検証します。サーガの内部ステップはテストしません。
詳細:ドメインモデル:集約が判断し、すべてを一度にコミットする、Cloudflare:これらの保証を前提にする。
変更のチェックリスト
- 新しい事実、コマンド、拒否を列挙します。各コマンドは、イベントと拒否を含めて、専用の
<Command>.command.tsに記述します。 - コマンドをそのアグリゲートの
commandsに追加し、対応するdecideの分岐と、各イベントに対応するevolveの分岐を追加します。 - 新しいイベントに関係するビュー、ポリシー、プロジェクションを更新します。
- クライアントが送信または読み取る場合は、必要なミドルウェアとともに Api 経由で公開し、最後の引数に
{ description }を指定します。 - ハッピーパス、すべての拒否、すべてのポリシーの反応、各クエリの認可について、ストーリーを作成します。
Domain.verify(Model)が空になることを確認するテストを含め、ドメインパッケージのテストを実行します。その後、モノレポの型チェック(tsc -b)を行い、サーバーと Web のテストを実行します。