Event-Driven Architecture with Claude Code: A Practical Review Guide
Design event-driven systems with Claude Code: contracts, idempotency, retries, DLQs, observability, and pitfalls.
Event-driven architecture is easy to oversell and easy to damage. If you ask Claude Code to “make it loosely coupled” without event names, payload contracts, retry rules, replay rules, and logging boundaries, the first demo may work while the first incident becomes hard to debug.
This guide treats Claude Code as a reviewer and implementation assistant, not as an unquestioned architect. You will design the boundaries, then ask Claude Code to challenge naming, schema compatibility, duplicate delivery, ordering assumptions, dead-letter behavior, and observability. The examples cover SaaS signup events, payment webhooks to fulfillment, audit-log streams, and notification pipelines.
The Small Vocabulary That Matters
Event-driven architecture means that one service publishes a fact that already happened, and other services react to that fact. The event is not a command. com.claudecodelab.user.created.v1 means “a user was created”, not “please create a user”. That distinction keeps producers from knowing too much about consumers.
Use four beginner-friendly terms. A producer is the service that emits the event. A consumer is the service that receives and handles it. An event bus or queue is the delivery path. A schema is the contract for the event payload, such as “userId is a non-empty string and email is an email address”. When a team agrees on those words, Claude Code can review the design with much less guessing.
For official references, CloudEvents and the CloudEvents spec are useful for a common event envelope. If you are on AWS, Amazon EventBridge is a practical event bus and routing reference. For observability, OpenTelemetry docs give a shared language for traces, metrics, and logs.
Do not start by asking Claude Code to invent the whole architecture. Start by giving it the existing API, database tables, webhook entry points, and incident constraints. Then ask: is the event name precise, is the payload backward compatible, is delivery idempotent, can the event be replayed, and can we trace it from producer to consumer? The architecture decision remains human-owned.
Fix the Event Contract First
The contract comes before the handler code. Without a contract, every consumer quietly depends on whatever fields happen to exist today. A tiny producer change can break onboarding, billing, audit logging, and notifications at the same time.
This CloudEvents-style YAML is a copy-paste starting point for a SaaS user-created event. The type contains the domain, fact, and version. The idempotencykey lets a consumer safely ignore duplicate delivery. The correlationid connects logs and traces that came from the same original request.
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: "en-US"
Keep the payload schema in a separate JSON Schema file. When Claude Code implements producers or consumers, instruct it not to depend on fields outside the schema and not to make optional fields required without a version bump.
{
"$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}$" }
}
}
Name events as past-tense facts. user.create and sendEmail sound like commands. user.created, payment.authorized, and invoice.finalized describe facts. Be careful with vague names like user.updated; they often become a bucket for email changes, plan changes, profile edits, and login timestamps. If consumers must inspect payload details to know what happened, split the event into names such as user.email_changed.v1 or subscription.plan_changed.v1.
Draw the Delivery Flow
Before implementation, ask Claude Code to draw the delivery flow. The diagram exposes hidden synchronous dependencies, retry locations, and dead-letter behavior much faster than prose.
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"]
The important review point is that the producer does not wait for every consumer to finish. If the signup API blocks until welcome email delivery succeeds, the system is not truly asynchronous. It has a hidden synchronous dependency. Sometimes that is the right product choice, but it should be visible in the API contract rather than hidden behind an event.
A Minimal Node.js Consumer
The following consumer handles the user-created event, creates onboarding state, queues a welcome email, ignores exact duplicate delivery, and moves failed events to a dead-letter queue. It uses an in-memory Map for readability; production systems should use Redis, DynamoDB, PostgreSQL, or another shared idempotency store.
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: "en-US",
},
};
handleUserCreated(exampleEvent, services)
.then((result) => console.log(result))
.catch((error) => console.error(error));
module.exports = { handleUserCreated, withRetry, deadLetterQueue };
The prompt to Claude Code should be specific: do not rerun successful events, reject the same idempotency key with a different payload, retry transient failures, and keep failed events in a dead-letter queue. A vague prompt like “add retry” can easily create duplicate emails or repeated fulfillment.
Four Practical Use Cases
| Use case | Events | Consumers | Main risk |
|---|---|---|---|
| SaaS signup and onboarding | user.created.v1, workspace.created.v1 | Settings, welcome email, CRM sync | The signup API waits for every consumer |
| Payment webhook to fulfillment | payment.succeeded.v1, subscription.activated.v1 | Entitlements, invoice, Slack alert | Missing signature validation or idempotency |
| Audit log and event stream | role.changed.v1, api_key.revoked.v1 | Append-only log, audit search, SIEM | PII is copied into long-lived logs |
| Notification pipeline | comment.mentioned.v1, report.ready.v1 | Email, in-app, push | Preferences and unsubscribe rules are skipped |
Payment webhooks are a natural fit for event-driven design, but they are unforgiving. Pair this guide with Webhook implementation with Claude Code. For API contract thinking, use Production API development with Claude Code. For v1/v2 event migration, API versioning with Claude Code applies directly.
Audit streams need security discipline. Do not log full payloads by default. Read Claude Code security audit and Claude Code security best practices when deciding what may live in long-retention logs. For failure responses and exception shapes, connect the design to error handling patterns.
Failure Cases to Catch Early
The first failure case is vague event names. user.updated pushes work into every consumer, because each one must inspect the payload and guess whether it cares. Ask Claude Code to identify event names that force downstream branching.
The second is breaking payload changes. Removing email, changing a string ID to an object, or making an optional field required can break consumers that are deployed independently. Additive changes are usually safer; removals, type changes, and semantic changes need a new version and a migration window.
The third is duplicate delivery. Many event systems provide at-least-once delivery, which means the same event may arrive more than once. Email, payment, entitlement, and point-award consumers need an idempotency key and a durable processed-event record.
The fourth is a hidden synchronous dependency. If a producer emits an event and then reads a consumer-owned table before responding, the event did not decouple the services. Either make the dependency explicit as a synchronous API call or design the user experience around eventual consistency.
The fifth is no replay plan. When a consumer bug drops three hours of events, the team needs to know the retention window, replay filter, duplicate behavior, and side-effect suppression rules. Without that, recovery becomes manual database surgery.
The sixth is weak observability. Logs should include event id, type, correlation id, consumer name, retry count, and DLQ reason. Metrics should expose backlog age, failure rate, duplicate count, and replay count. Traces should connect the original request to the consumer work.
The seventh is logging PII. PII means information that can identify a person. Email addresses, names, addresses, payment details, and access tokens should not be dumped into logs or chat transcripts. Prefer event ids and user ids, mask sensitive fields, and give logs a clear retention policy.
Claude Code Review Template
Use Claude Code as a skeptical reviewer before asking it to edit files. This template works well in PR review or before creating the first consumer.
# 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
Ask for review first, implementation second. If Claude Code finds an unsafe assumption, fix the boundary before writing code. Once the design is narrow, ask for schema changes, handler changes, tests, and runbook updates in that order.
Runbook for Operations
Event-driven systems are only useful if they can be operated during failure. Add a short runbook with the first consumer, not after the first outage.
# 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.
This runbook is intentionally small. It separates diagnosis, replay, and irreversible side effects. Ask Claude Code one final question before merging: “Which failure cannot be recovered with this runbook?” That question often reveals missing permissions, schema drift, or third-party API assumptions.
Conclusion and CTA
Event-driven architecture can make systems more resilient and easier to extend, but only when the event contract is treated as a product surface. Event names, schemas, versioning, idempotency, ordering, retries, dead-letter handling, replay, and observability need explicit decisions. Claude Code is most useful when it reviews those decisions and implements narrow changes against a contract.
ClaudeCodeLab can help with Claude Code training, event-driven design reviews, webhook/API contracts, audit-log strategy, incident runbooks, and team workflows. If your team wants to make webhooks safer, move notifications to async workers, or standardize Claude Code review prompts, start from Claude Code training and consulting. For self-serve material, see the free cheat sheet and product templates.
Masa tested this flow in a small SaaS prototype. When the event contract and idempotency key were written first, Claude Code produced smaller and easier-to-review changes. In an earlier prototype that used only user.updated, notification and audit consumers started branching on payload details, and replay rules were unclear. Splitting the event names and adding the DLQ runbook made the recovery story concrete: which events to replay, from what time range, and how many records to expect.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Receipt Pattern: Record Scope, Proof, and Rollback
A permission receipt pattern for Claude Code: allowed actions, approval boundaries, proof commands, rollback, and revenue CTA checks.
Safe Agent Harness Design for Claude Code and Codex: Permissions, Checks, and Rollback
Build a practical agent harness for Claude Code and Codex with policy, planning, verification, and recovery layers.
Claude Code Subagents: A Practical Guide to Safe Agent Delegation
Claude Code subagent guide for safe parallel article and code work: delegation rules, prompts, pitfalls, and checks.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.