Designing and Implementing Event-Driven Architecture with Claude Code
Learn about designing and implementing event-driven architecture using Claude Code. Practical tips and code examples included.
What Is Event-Driven Architecture?
Event-driven architecture (EDA) is a pattern that decouples systems by publishing and subscribing to events. With Claude Code, you can efficiently move from EDA design to implementation.
A Type-Safe Event Bus
type EventMap = {
"user.created": { userId: string; email: string };
"user.updated": { userId: string; changes: Record<string, unknown> };
"order.created": { orderId: string; userId: string; total: number };
"order.completed": { orderId: string; completedAt: Date };
"order.cancelled": { orderId: string; reason: string };
"payment.processed": { paymentId: string; amount: number };
};
type EventName = keyof EventMap;
type EventHandler<T extends EventName> = (payload: EventMap[T]) => Promise<void>;
class EventBus {
private handlers = new Map<string, Set<Function>>();
on<T extends EventName>(event: T, handler: EventHandler<T>) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
// Return an unsubscribe function
return () => {
this.handlers.get(event)?.delete(handler);
};
}
async emit<T extends EventName>(event: T, payload: EventMap[T]) {
const handlers = this.handlers.get(event);
if (!handlers) return;
const results = await Promise.allSettled(
Array.from(handlers).map((handler) => handler(payload))
);
const failures = results.filter((r) => r.status === "rejected");
if (failures.length > 0) {
console.error(
`${failures.length} handlers failed for ${event}:`,
failures
);
}
}
}
const eventBus = new EventBus();
Registering Event Handlers
// User creation handlers
eventBus.on("user.created", async ({ userId, email }) => {
// Send welcome email
await emailQueue.add("send", {
to: email,
subject: "Welcome!",
template: "welcome",
data: { userId },
});
});
eventBus.on("user.created", async ({ userId }) => {
// Create default settings
await prisma.userSettings.create({
data: {
userId,
theme: "light",
language: "en",
notifications: true,
},
});
});
// Order completion handler
eventBus.on("order.completed", async ({ orderId }) => {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { user: true, items: true },
});
if (!order) return;
// Update stock
for (const item of order.items) {
await prisma.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } },
});
}
});
Domain Event Pattern
abstract class DomainEvent {
readonly occurredAt: Date;
readonly eventId: string;
constructor() {
this.occurredAt = new Date();
this.eventId = crypto.randomUUID();
}
}
class OrderCreatedEvent extends DomainEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly items: Array<{ productId: string; quantity: number }>,
public readonly total: number
) {
super();
}
}
// Aggregate root
class Order {
private domainEvents: DomainEvent[] = [];
static create(params: {
userId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
}): Order {
const order = new Order();
const orderId = crypto.randomUUID();
const total = params.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
order.domainEvents.push(
new OrderCreatedEvent(orderId, params.userId, params.items, total)
);
return order;
}
pullDomainEvents(): DomainEvent[] {
const events = [...this.domainEvents];
this.domainEvents = [];
return events;
}
}
CQRS Pattern
// Command side (writes)
interface Command<T = void> {
execute(): Promise<T>;
}
class CreateOrderCommand implements Command<string> {
constructor(
private userId: string,
private items: Array<{ productId: string; quantity: number }>
) {}
async execute(): Promise<string> {
// Validation
for (const item of this.items) {
const product = await prisma.product.findUnique({
where: { id: item.productId },
});
if (!product || product.stock < item.quantity) {
throw new Error(`Insufficient stock: ${item.productId}`);
}
}
// Create the order
const order = await prisma.order.create({
data: {
userId: this.userId,
status: "pending",
items: {
create: this.items.map((item) => ({
productId: item.productId,
quantity: item.quantity,
})),
},
},
});
// Emit event
await eventBus.emit("order.created", {
orderId: order.id,
userId: this.userId,
total: 0,
});
return order.id;
}
}
// Query side (reads)
interface Query<T> {
execute(): Promise<T>;
}
class GetOrdersQuery implements Query<OrderSummary[]> {
constructor(
private userId: string,
private page: number = 1
) {}
async execute(): Promise<OrderSummary[]> {
// Read from a read-only view
return prisma.orderView.findMany({
where: { userId: this.userId },
orderBy: { createdAt: "desc" },
take: 20,
skip: (this.page - 1) * 20,
});
}
}
Event Store
interface StoredEvent {
id: string;
aggregateId: string;
aggregateType: string;
eventType: string;
payload: Record<string, unknown>;
version: number;
occurredAt: Date;
}
class EventStore {
async append(
aggregateId: string,
aggregateType: string,
events: DomainEvent[],
expectedVersion: number
) {
// Optimistic locking
const currentVersion = await this.getVersion(aggregateId);
if (currentVersion !== expectedVersion) {
throw new Error("Concurrency conflict");
}
const storedEvents = events.map((event, i) => ({
id: event.eventId,
aggregateId,
aggregateType,
eventType: event.constructor.name,
payload: event as any,
version: expectedVersion + i + 1,
occurredAt: event.occurredAt,
}));
await prisma.event.createMany({ data: storedEvents });
// Publish the events
for (const event of events) {
await eventBus.emit(
event.constructor.name as any,
event as any
);
}
}
async getEvents(aggregateId: string): Promise<StoredEvent[]> {
return prisma.event.findMany({
where: { aggregateId },
orderBy: { version: "asc" },
});
}
private async getVersion(aggregateId: string): Promise<number> {
const last = await prisma.event.findFirst({
where: { aggregateId },
orderBy: { version: "desc" },
});
return last?.version ?? 0;
}
}
Using It With Claude Code
An example prompt for asking Claude Code to implement event-driven architecture. For async processing, see job queues and async processing, and for external integration, see webhook implementation patterns.
Introduce event-driven architecture.
- Implement a type-safe event bus
- Domain event pattern
- CQRS: separate commands and queries
- Implement an event store
- Refactor and integrate the existing service layer
For more on event-driven design, see Martin Fowler - Event-Driven Architecture. For Claude Code usage, check the official documentation.
Summary
Event-driven architecture improves loose coupling and scalability of systems. With Claude Code, you can incrementally adopt EDA, from a type-safe event bus to CQRS and event sourcing.
Level up your Claude Code workflow
50 battle-tested prompt templates you can copy-paste into Claude Code right now.
Free PDF: Claude Code Cheatsheet in 5 Minutes
Key commands, shortcuts, and prompt examples on a single printable page.
About the Author
Masa
Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.
Related Posts
Getting Started with Claude Code Agent SDK — Build Autonomous Agents Fast
Learn how to build autonomous AI agents with Claude Code Agent SDK. Covers setup, tool definitions, and multi-step execution with practical code examples.
The Complete Guide to Context Management in Claude Code
Learn practical techniques to maximize Claude Code's context window. Covers token optimization, conversation splitting, and CLAUDE.md usage.
Mastering Claude Code Hooks: Auto-Format, Auto-Test, and More
Mastering Claude Code Hooks: Auto-Format, Auto-Test, and More. A practical guide with code examples.