Claude Codeでイベント駆動アーキテクチャを設計する実践ガイド
Claude Codeでイベント駆動設計を進める手順。契約、冪等性、再試行、監視、失敗例まで解説。
イベント駆動アーキテクチャは、Claude Codeに「いい感じに疎結合で」と頼むだけでは危険です。イベント名、データ契約、再送、重複、監視、個人情報の扱いを決めないまま実装すると、最初は動いても障害時に原因を追えません。
この記事では、Claude Codeを設計者として丸投げするのではなく、レビュー役と実装補助として使いながら、イベント駆動アーキテクチャを小さく本番品質へ近づける手順をまとめます。SaaS登録、決済Webhook、監査ログ、通知パイプラインを題材に、コピペできる契約テンプレート、Node.jsハンドラー、Mermaid図、レビューchecklist、runbookまで用意します。
イベント駆動アーキテクチャの基本
イベント駆動アーキテクチャ、つまりEvent-Driven Architectureは、あるサービスで起きた事実を「イベント」として発行し、別のサービスがそれを購読して処理する設計です。イベントは命令ではなく、過去に起きた事実です。たとえば com.claudecodelab.user.created.v1 は「ユーザーを作れ」ではなく「ユーザーが作成された」という記録です。
初心者が最初に覚える用語は四つで十分です。producerはイベントを出す側、consumerはイベントを受け取る側、event busはイベントを配送する通路、schemaはpayloadの約束事です。payloadはイベントに入るデータ本体で、schemaは「userIdは文字列、emailはメール形式」のように機械でも人間でも確認できる契約です。
公式の考え方を確認するなら、共通イベント形式は CloudEvents と CloudEvents spec が入口になります。AWSを使うなら、イベントを受け取り、絞り込み、配送するサービスとして Amazon EventBridge を参照できます。監視は traces、metrics、logs を扱う OpenTelemetry docs を基準にすると、後からツールを変えても観点がぶれにくくなります。
Claude Codeには、最初から「最適なイベント駆動設計を考えて」と頼まないほうが安全です。代わりに、既存API、DB、Webhook、障害時の復旧条件を読ませて、「このイベント名は曖昧ではないか」「payload変更で既存consumerを壊さないか」「重複配送に耐えるか」をレビューさせます。アーキテクチャの責任者は人間で、Claude Codeは見落としを減らすためのレビュアーです。
まずイベント契約を固定する
イベント駆動で一番大事なのは、コードより先に契約を決めることです。契約がないままconsumerを増やすと、producer側の小さな変更が通知、請求、監査ログまで連鎖して壊します。CloudEvents風の包みを使うと、どのイベントにも id、type、source、time、specversion のような共通メタデータを持たせられます。
次のYAMLは、SaaSのユーザー作成イベントのテンプレートです。typeにはドメイン、事実、バージョンを入れます。idempotencykeyは冪等性、つまり同じイベントが2回届いても安全に1回分として扱うためのキーです。correlationidは、一つのリクエストから派生したログやtraceを追うためのIDです。
specversion: "1.0"
id: "evt_01JZ0YV8Y9N3A7Z7K6Y1G9X2Q4"
type: "com.claudecodelab.user.created.v1"
source: "/services/identity"
subject: "users/usr_123"
time: "2026-06-02T09:30:00Z"
datacontenttype: "application/json"
dataschema: "https://example.com/schemas/user-created.v1.json"
idempotencykey: "user.created:usr_123:2026-06-02"
correlationid: "req_7fc42b"
data:
userId: "usr_123"
email: "masa@example.com"
plan: "starter"
locale: "ja-JP"
payloadのschemaはJSON Schemaで別に管理します。Claude Codeに実装させるときは、このschemaを先に渡し、「schemaにないフィールドへ依存しない」「必須項目を増やすときはv2にする」と明記します。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user-created.v1.json",
"title": "UserCreatedV1",
"type": "object",
"additionalProperties": false,
"required": ["userId", "email", "plan", "locale"],
"properties": {
"userId": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" },
"plan": { "type": "string", "enum": ["free", "starter", "pro"] },
"locale": { "type": "string", "pattern": "^[a-z]{2}-[A-Z]{2}$" }
}
}
命名は過去形を基本にします。user.createやsendEmailは命令に見えます。user.created、payment.authorized、invoice.finalizedのように、発生済みの事実にします。複数の意味を詰め込んだ user.updated は便利に見えますが、後からconsumerが条件分岐だらけになります。重要な状態変化は user.email_changed.v1 や subscription.plan_changed.v1 のように分けたほうが、レビューも運用も簡単です。
配送フローを図にする
文章だけでイベント駆動を説明すると、非同期なのか同期なのか、どこでretryするのかが曖昧になります。Claude Codeには、実装前にMermaid図を出させるとレビューしやすくなります。
flowchart LR
A["Identity API<br/>producer"] --> B["Event bus<br/>filter and route"]
B --> C["Onboarding consumer<br/>workspace setup"]
B --> D["Email consumer<br/>welcome message"]
B --> E["Audit consumer<br/>append-only log"]
C --> F["Idempotency store"]
D --> F
C --> G["Dead-letter queue"]
D --> G
B --> H["OpenTelemetry<br/>traces metrics logs"]
この図で見るべき点は、producerがconsumerの完了を待っていないことです。ユーザー登録APIが「メール送信が終わるまでレスポンスしない」なら、それはイベント駆動ではなく、隠れた同期依存です。同期処理が必要な場合は、最初からAPI契約として表に出します。非同期でよい場合は、イベント発行後に登録完了を返し、後続処理の失敗はrunbookで復旧できるようにします。
Node.jsハンドラーの最小実装
次のコードは、ユーザー作成イベントを受け取り、オンボーディング作成と歓迎メール投入を行う最小サンプルです。DBやRedisの代わりに Map を使っていますが、本番ではidempotency storeをRedis、DynamoDB、PostgreSQLなどの共有ストアに置き換えます。
const crypto = require("node:crypto");
const processedEvents = new Map();
const deadLetterQueue = [];
function payloadHash(payload) {
return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
}
function eventKey(event) {
return event.idempotencykey || `${event.type}:${event.id}`;
}
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function withRetry(operation, options = {}) {
const attempts = options.attempts ?? 3;
const delayMs = options.delayMs ?? 250;
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === attempts) break;
await wait(delayMs * attempt);
}
}
throw lastError;
}
async function handleUserCreated(event, services) {
if (event.specversion !== "1.0") {
throw new Error(`Unsupported CloudEvents version: ${event.specversion}`);
}
if (event.type !== "com.claudecodelab.user.created.v1") {
throw new Error(`Unexpected event type: ${event.type}`);
}
const key = eventKey(event);
const currentHash = payloadHash(event.data);
const existing = processedEvents.get(key);
if (existing?.status === "succeeded" && existing.payloadHash === currentHash) {
return { status: "duplicate_ignored", key };
}
if (existing && existing.payloadHash !== currentHash) {
throw new Error("Idempotency key reused with a different payload");
}
processedEvents.set(key, {
status: "processing",
payloadHash: currentHash,
updatedAt: new Date().toISOString(),
});
try {
await withRetry(() => services.createOnboardingWorkspace(event.data.userId), {
attempts: 3,
delayMs: 200,
});
await withRetry(
() =>
services.enqueueWelcomeEmail({
userId: event.data.userId,
email: event.data.email,
correlationId: event.correlationid,
}),
{ attempts: 3, delayMs: 200 },
);
processedEvents.set(key, {
status: "succeeded",
payloadHash: currentHash,
updatedAt: new Date().toISOString(),
});
return { status: "processed", key };
} catch (error) {
processedEvents.set(key, {
status: "failed",
payloadHash: currentHash,
updatedAt: new Date().toISOString(),
errorMessage: error.message,
});
deadLetterQueue.push({
key,
event,
failedAt: new Date().toISOString(),
errorMessage: error.message,
});
throw error;
}
}
const services = {
async createOnboardingWorkspace(userId) {
console.log("workspace ready", { userId });
},
async enqueueWelcomeEmail(message) {
console.log("email queued", {
userId: message.userId,
correlationId: message.correlationId,
});
},
};
const exampleEvent = {
specversion: "1.0",
id: "evt_01JZ0YV8Y9N3A7Z7K6Y1G9X2Q4",
type: "com.claudecodelab.user.created.v1",
source: "/services/identity",
time: "2026-06-02T09:30:00Z",
idempotencykey: "user.created:usr_123:2026-06-02",
correlationid: "req_7fc42b",
data: {
userId: "usr_123",
email: "masa@example.com",
plan: "starter",
locale: "ja-JP",
},
};
handleUserCreated(exampleEvent, services)
.then((result) => console.log(result))
.catch((error) => console.error(error));
module.exports = { handleUserCreated, withRetry, deadLetterQueue };
Claude Codeにこのコードを作らせる場合は、「成功済みイベントを再実行しない」「同じidempotency keyでpayloadが違う場合は止める」「失敗イベントはdead-letter queueに残す」と条件を明示します。曖昧に「retryも入れて」とだけ頼むと、何度もメールを送る実装になりがちです。
使いどころを四つに分ける
| ユースケース | 発行するイベント | consumerの例 | 注意点 |
|---|---|---|---|
| SaaS登録とオンボーディング | user.created.v1、workspace.created.v1 | 初期設定、歓迎メール、CRM同期 | 登録APIが全consumerを待たない |
| 決済Webhookから提供開始 | payment.succeeded.v1、subscription.activated.v1 | 権限付与、請求書、Slack通知 | Webhook署名検証と冪等性を必須にする |
| 監査ログとイベントストリーム | role.changed.v1、api_key.revoked.v1 | 追記専用ログ、監査検索、SIEM連携 | PIIをpayloadやログへ入れすぎない |
| 通知パイプライン | comment.mentioned.v1、report.ready.v1 | メール、アプリ内通知、Push通知 | 通知設定と配信停止をconsumerで確認する |
決済Webhookは、特にイベント駆動と相性が良い一方で事故も起きやすい領域です。Webhook受信の入口は Claude CodeでWebhook実装 と合わせて確認してください。API契約から始める流れは Claude Codeで本番API開発 が近く、イベントのv1/v2運用は APIバージョニング実践ガイド と同じ考え方で扱えます。
監査ログでは、イベントを「後から消しにくい記録」として扱います。ただし、メールアドレス、住所、アクセストークン、決済詳細をそのままログへ入れると、監査基盤自体が情報漏えいリスクになります。セキュリティ観点は Claude Codeセキュリティ監査 と セキュリティベストプラクティス にもつなげて確認します。失敗時のレスポンスや例外設計は エラーハンドリングパターン が参考になります。
落とし穴と失敗例
一つ目の失敗は、イベント名が曖昧なことです。user.updatedだけでは、メール変更、プラン変更、退会、最終ログイン更新が全部混ざります。consumerはpayloadの中身を見て推測するしかなくなり、条件分岐が増えます。Claude Codeには「このイベント名でconsumerが条件分岐なしに目的を理解できるか」とレビューさせます。
二つ目は、payloadの破壊的変更です。emailを消してcontact.emailへ移すような変更は、既存consumerを壊します。追加は原則安全ですが、削除、必須化、型変更、意味変更はv2にします。新旧イベントを一定期間並行発行し、consumerの移行状況を見てからv1を止めます。
三つ目は、重複配送を想定していないことです。多くのqueueやevent busは「少なくとも1回」配送を採用します。これは「0回になりにくい」代わりに「2回以上届くことがある」という意味です。メール送信、ポイント付与、請求確定ではidempotency keyがないと二重処理になります。
四つ目は、producerがconsumerの成功を暗黙に待つことです。登録APIがイベントを出した後にconsumerのDBを読みに行くと、見た目は非同期でも実態は同期結合です。必要なら同期APIに戻す、非同期なら最終的整合性として画面文言と復旧手順を用意する、どちらかに決めます。
五つ目は、replay計画がないことです。consumerのバグで3時間分のイベント処理に失敗したとき、どこから再処理するのか決まっていないと、手作業でDBを直すことになります。イベントの保持期間、replay対象、重複時の挙動、replay中の通知抑制をrunbookに書きます。
六つ目は、観測できないことです。event id、type、correlation id、consumer名、retry回数、DLQ投入理由がログやmetricsにないと、障害調査が「たぶんメールconsumer」になります。OpenTelemetryのtraceやmetricを使うなら、producerからconsumerまで同じcorrelation idを運ぶ設計にします。
七つ目は、PIIを雑に記録することです。PIIは個人を特定できる情報です。ログにメールアドレスを出すより、userIdとevent idで追えるようにします。どうしても必要な属性はmaskし、保持期間を決め、Claude Codeにも「ログへpayload全体を出さない」と明示します。
Claude Codeへの依頼テンプレート
Claude Codeは、白紙のアーキテクトではなく、契約と差分を読むレビュー担当として使うと効果が出ます。次のテンプレートをPR前の確認に使えます。
# Claude Code EDA review checklist
対象:
- event contract: schemas/user-created.v1.json
- producer: services/identity
- consumers: onboarding, email, audit-log
確認してほしいこと:
- event nameが過去形の事実になっているか
- payload変更が既存consumerにとって破壊的ではないか
- idempotency keyがあり、重複配送で二重処理しないか
- consumerがproducerへ同期的に依存していないか
- retry回数、backoff、dead-letter queueの条件が明示されているか
- replay手順で同じ副作用が二重発生しないか
- logsにPIIやsecretを出していないか
- OpenTelemetryでevent id、correlation id、consumer名を追えるか
出力形式:
- P0/P1/P2のリスク一覧
- 変更すべきファイル
- 追加すべきテスト
- 人間が意思決定すべき未確定事項
このテンプレートの最後に「実装もして」と足す前に、まずレビューだけを返させます。Claude Codeが危険な前提を見つけたら、人間がイベント境界を決め直します。レビューで問題が絞れた後に、schema、handler、test、runbookの順で実装させると、差分が読みやすくなります。
運用runbookを最初から置く
イベント駆動は、障害時に運用できて初めて価値があります。runbookは、障害が起きた後に書くものではなく、最初のconsumerを作る時点で一緒に置きます。
# Runbook: event backlog or DLQ growth
## Symptoms
- Queue age is over 5 minutes
- Dead-letter queue has more than 10 messages
- Consumer error rate is over 2 percent for 10 minutes
## First checks
1. Identify event type, consumer name, and correlation id.
2. Check whether the failure is validation, downstream timeout, or permission.
3. Confirm whether the producer is still publishing new events.
4. Stop replay if the event triggers email, payment, or irreversible side effects.
## Recovery
1. Fix the consumer or downstream dependency.
2. Replay a small batch with idempotency enabled.
3. Compare processed count, duplicate count, and DLQ count.
4. Resume normal processing.
5. Write the incident note with event ids, time range, and customer impact.
## Never do
- Do not edit payloads manually without recording the reason.
- Do not replay payment or email events without suppression rules.
- Do not paste full payloads with PII into chat or issue trackers.
このrunbookは短く見えますが、障害時には十分効きます。特に「replayしてよいイベント」と「replayしてはいけないイベント」を分けておくと、深夜の判断が楽になります。Claude Codeには、実装PRの最後で「このrunbookで復旧できない失敗は何か」と聞くと、権限不足、schema不一致、外部API停止などの抜けを見つけやすくなります。
まとめとCTA
イベント駆動アーキテクチャは、サービスを疎結合にする便利な設計ですが、イベント名、schema、versioning、idempotency、ordering、retry、dead-letter handling、observabilityを決めないまま導入すると、障害時の調査が難しくなります。Claude Codeは、設計を丸投げする相手ではありません。契約、差分、テスト、runbookを読ませて、人間の判断を補強するレビュー役として使うのが堅実です。
ClaudeCodeLabでは、Claude Codeを使ったイベント駆動設計レビュー、Webhook/API設計、監査ログ、障害runbook整備、チーム向け研修をまとめて支援できます。自社SaaSで「Webhookから後続処理を安全に動かしたい」「通知や監査ログを非同期化したい」「Claude Codeのレビュー観点をチーム標準にしたい」段階なら、Claude Code研修・導入相談 から相談してください。まず自分で整える場合は、無料チートシート と 商品一覧 のテンプレートも使えます。
この記事で紹介した内容を実際に試した結果、Masaの検証用SaaSでは、最初にevent contractとidempotency keyを決めたケースほど、Claude Codeの実装差分が小さくなりました。逆に、イベント名を user.updated のまま進めた試作では、通知consumerと監査consumerがpayloadの中身を見て分岐し始め、replay手順も曖昧になりました。最終的にはイベント名を分割し、DLQとrunbookを先に置くことで、障害時に「どのイベントを、どこから、何件再処理するか」を説明できる状態にできました。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code Permission Receipt Pattern: 許可、証拠、ロールバックを残す運用
Claude Codeの権限運用を安全にする permission receipt。許可範囲、承認待ち、検証コマンド、CTA導線を記録します。
Claude CodeとCodex、結局どっち?事故らない“併用”の現実解
OpenAIのCodexとClaude Code、どっちが得意でどっちに任せる?両方を安全に併用する作業分担と権限・検証のワークフローを、僕の失敗談つきで解説します。
Claude Codeサブエージェント実装ガイド: 記事・コード作業を安全に並列委譲する方法
Claude Codeサブエージェントで記事・コード作業を安全に並列化する実装ガイド。委譲基準、プロンプト、失敗例を解説。