用 Claude Code 设计事件驱动架构:从契约到运维
用 Claude Code 审查事件驱动架构:契约、幂等、重试、DLQ、可观测性与常见失败。
事件驱动架构很容易被说成“天然解耦”,但真正上线时,问题往往出在细节:事件名太模糊、payload 没有契约、重复投递导致重复扣费、失败消息没有 dead-letter queue,日志里还混进了个人信息。只让 Claude Code “帮我做成事件驱动”并不安全。
这篇文章把 Claude Code 放在更合适的位置:审查者和实现助手,而不是不加质疑的架构师。你先决定业务边界和风险,Claude Code 负责检查事件命名、schema 兼容性、幂等性、顺序假设、重试、死信、replay 和可观测性。示例会覆盖 SaaS 注册、支付 Webhook 到履约、审计日志事件流、通知管道四个场景。
先理解事件驱动的基本词汇
事件驱动架构是指:一个服务把已经发生的事实发布成事件,其他服务订阅后各自处理。事件不是命令。com.claudecodelab.user.created.v1 表示“用户已经创建”,不是“请创建用户”。这个差异很重要,因为 producer 不应该知道每个 consumer 要做什么。
初学者先记四个词就够了。producer 是发布事件的一方,consumer 是消费事件的一方,event bus 或 queue 是传递通道,schema 是 payload 的契约。payload 是事件里的业务数据,schema 则规定 userId、email、plan 这些字段的类型和含义。
官方资料可以从 CloudEvents 和 CloudEvents spec 开始,它们提供通用事件信封的参考。AWS 环境可以参考 Amazon EventBridge 的事件总线和路由概念。可观测性建议按 OpenTelemetry docs 的 traces、metrics、logs 来组织。
不要让 Claude Code 从零“决定架构”。更稳妥的做法是,把现有 API、数据库表、Webhook 入口和事故恢复要求交给它,让它回答:事件名是否精确,payload 是否向后兼容,重复投递是否安全,是否能 replay,是否能从 producer 追踪到 consumer。架构决策仍然由人负责。
先固定事件契约
事件驱动的第一步不是写 handler,而是写契约。没有契约时,consumer 会悄悄依赖 producer 当前恰好输出的字段。producer 删除一个字段,可能同时破坏 onboarding、billing、audit log 和 notification。
下面的 YAML 是 SaaS 用户创建事件的模板。type 里包含领域、事实和版本。idempotencykey 用于幂等,也就是同一事件重复到达时只产生一次副作用。correlationid 用于把同一次请求派生出的日志和 trace 串起来。
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: "zh-CN"
payload 的细节用 JSON Schema 管理。让 Claude Code 实现时,要明确告诉它:不能依赖 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 去 payload 里猜“到底更新了什么”。重要变化应该拆成 user.email_changed.v1、subscription.plan_changed.v1 这类更清楚的事件。
用图暴露同步依赖
实现前让 Claude Code 生成 Mermaid 图,可以很快看出哪里在 retry、哪里进 DLQ、producer 是否偷偷等待 consumer。
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 契约;可以异步就接受最终一致性,并准备好状态展示和恢复流程。
可复制的 Node.js consumer
下面的代码处理用户创建事件,创建 onboarding 工作区,加入欢迎邮件队列,忽略完全相同的重复事件,并把失败事件放进 dead-letter queue。示例为了可读性使用 Map,生产环境应替换成 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: "zh-CN",
},
};
handleUserCreated(exampleEvent, services)
.then((result) => console.log(result))
.catch((error) => console.error(error));
module.exports = { handleUserCreated, withRetry, deadLetterQueue };
给 Claude Code 的指令要具体:成功事件不要重复执行;同一个 idempotency key 搭配不同 payload 要报错;临时失败要 retry;最终失败要保留到 DLQ。只写“加上重试”很容易得到重复发邮件或重复开通权限的实现。
四个实用场景
| 场景 | 事件 | consumer | 主要风险 |
|---|---|---|---|
| SaaS 注册和 onboarding | user.created.v1、workspace.created.v1 | 初始设置、欢迎邮件、CRM 同步 | 注册 API 等待所有 consumer |
| 支付 Webhook 到履约 | payment.succeeded.v1、subscription.activated.v1 | 权限开通、发票、Slack 通知 | 缺少签名校验和幂等 |
| 审计日志和事件流 | role.changed.v1、api_key.revoked.v1 | 追加日志、审计搜索、SIEM | PII 被写入长期日志 |
| 通知管道 | comment.mentioned.v1、report.ready.v1 | 邮件、站内通知、Push | 忽略用户通知设置和退订 |
支付 Webhook 很适合事件驱动,但必须谨慎。入口实现可参考 Claude Code Webhook 实现。API 契约思路可结合 Claude Code API 开发。事件 v1/v2 迁移和 API versioning 的原则相同。
审计日志要特别注意安全。不要默认记录完整 payload。可以结合 Claude Code security audit 和 security best practices 判断哪些字段能长期保存。失败响应和异常形状则参考 error handling patterns。
常见失败和避坑
第一,事件名太模糊。user.updated 会把判断成本推给每个 consumer,最后大家都要在 payload 里猜自己是否关心这个事件。
第二,payload 破坏性变更。删除 email、改变字段类型、把可选字段变成必填,都会破坏独立部署的 consumer。新增字段通常安全,删除、改类型、改语义应新建版本。
第三,没有处理重复投递。很多事件系统是 at-least-once delivery,意思是事件至少会到达一次,也可能到达多次。邮件、支付、权益、积分都需要幂等键和持久化处理记录。
第四,隐藏同步依赖。producer 发事件后又读取 consumer 拥有的数据,再返回给用户,这不是解耦。要么承认这是同步 API,要么把产品体验设计成最终一致。
第五,没有 replay 计划。consumer bug 造成三小时事件失败时,团队必须知道保留窗口、replay 条件、重复处理行为和副作用抑制规则。
第六,可观测性不足。日志应包含 event id、type、correlation id、consumer 名称、retry 次数和 DLQ 原因。指标应包含 backlog age、失败率、重复数和 replay 数。
第七,日志记录 PII。PII 是能识别个人的信息,例如邮箱、姓名、地址、支付信息和 token。尽量用 event id 与 userId 定位,敏感字段要 mask,并设定保留时间。
Claude Code 审查模板
先让 Claude Code 审查,再让它改代码。下面的模板适合放进 PR 或任务说明。
# Claude Code EDA review checklist
Scope:
- event contract: schemas/user-created.v1.json
- producer: services/identity
- consumers: onboarding, email, audit-log
Please review:
- Is the event name a past-tense fact?
- Is the payload change backward compatible for existing consumers?
- Is there an idempotency key, and does duplicate delivery avoid double side effects?
- Does any consumer call back into the producer synchronously?
- Are retry count, backoff, and dead-letter rules explicit?
- Can replay run without duplicate email, payment, or irreversible effects?
- Do logs avoid PII and secrets?
- Can OpenTelemetry show event id, correlation id, and consumer name?
Output:
- P0/P1/P2 risks
- Files that should change
- Tests that should be added
- Open decisions a human must make
如果 Claude Code 找到危险前提,先调整边界,不要马上写实现。边界收窄后,再按 schema、handler、test、runbook 的顺序推进,差异会更容易审查。
运维 runbook
事件驱动系统的价值,要在故障时才能看出来。第一个 consumer 上线时,就应该附带 runbook。
# 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.
合并前可以再问 Claude Code:“这个 runbook 不能恢复哪类失败?” 这个问题经常能暴露权限不足、schema 漂移、外部 API 停止等缺口。
总结与咨询入口
事件驱动架构能让系统更容易扩展,但前提是事件契约被当作公开接口来维护。事件名、schema、版本、幂等、顺序、重试、DLQ、replay 和可观测性都要明确。Claude Code 最适合做的是审查这些决策,并在契约确定后实现小而清晰的改动。
ClaudeCodeLab 可以提供 Claude Code 培训、事件驱动设计审查、Webhook/API 契约、审计日志、事故 runbook 和团队工作流整理。如果你的团队想安全处理 Webhook、把通知迁移到异步 worker,或把 Claude Code 审查模板标准化,可以从 Claude Code training and consulting 开始。自助材料可看 free cheat sheet 和 product templates。
Masa 在一个小型 SaaS 原型里实际验证了这套流程。先写 event contract 和 idempotency key 时,Claude Code 生成的改动更小,也更容易 review。早期只用 user.updated 的原型里,通知和审计 consumer 都开始读取 payload 细节并各自分支,replay 规则也说不清。拆分事件名并加入 DLQ runbook 后,团队终于能说明:从哪个时间点 replay 哪些事件、预计处理多少条、哪些副作用需要抑制。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code Permission Receipt Pattern:记录权限、证据和回滚方式
Claude Code 权限 receipt:记录允许动作、需要批准的边界、验证命令、回滚说明,以及 Gumroad 和咨询 CTA 检查。
Claude Code/Codex 安全 Agent Harness 实战:权限、验证与回滚
用权限策略、执行计划、验证脚本和回滚日志,为 Claude Code 与 Codex 搭建更安全的 AI Agent 工作流。
Claude Code 子代理实战指南:安全委派并行文章与代码工作
用 Claude Code 子代理安全拆分文章和代码工作:委派规则、提示词模板、失败模式与检查清单。