Direct service calls create tight coupling — every change cascades. Event-driven architecture decouples services, and Claude Code can design the whole system when you give it the right patterns.
CLAUDE.md for Event-Driven Architecture
## Event-Driven Architecture Rules
### Core principles
- No direct HTTP calls between services (unless sync response required)
- All state changes emit events
- Events are immutable (past events never changed)
### Event design
- Naming: {domain}.{entity}.{past-tense verb}
Examples: order.item.added, user.account.created, payment.completed
- Payload must include: eventId, timestamp, version, data
- Backward compatibility: bump version when changing event schema
### Implementation
- Single Node.js service: EventEmitter (lightweight)
- Cross-service: Redis Pub/Sub or BullMQ Events
- Schema validation: Zod for payload validation
### Reliability
- Outbox pattern: save event to DB before publishing
- Consumers must be idempotent (handle duplicate events gracefully)
- Dead Letter Queue: after 3 failures, route to DLQ
Type-Safe EventBus Generation
Generate a type-safe event emitter system.
Requirements:
- TypeScript: enforce event name ↔ payload type mapping
- Auto-attach eventId, timestamp, version to all events
- Error isolation: handler errors don't stop other handlers
- Debug logging: log all events
Event definitions:
- user.created: { userId: string, email: string, name: string }
- order.placed: { orderId: string, userId: string, items: Array<{productId, quantity}>, total: number }
- payment.completed: { paymentId: string, orderId: string, amount: number }
Generate:
- src/events/types.ts
- src/events/eventBus.ts
- src/events/handlers/ (per domain)
Generated Type-Safe EventBus
// src/events/types.ts
export interface EventPayload {
eventId: string;
timestamp: string;
version: number;
}
export interface DomainEvents {
'user.created': EventPayload & {
userId: string;
email: string;
name: string;
};
'order.placed': EventPayload & {
orderId: string;
userId: string;
items: Array<{ productId: string; quantity: number }>;
total: number;
};
'payment.completed': EventPayload & {
paymentId: string;
orderId: string;
amount: number;
};
}
export type DomainEventName = keyof DomainEvents;
// src/events/eventBus.ts
import { EventEmitter } from 'events';
import { v4 as uuidv4 } from 'uuid';
class TypedEventBus extends EventEmitter {
publish<T extends DomainEventName>(
eventName: T,
data: Omit<DomainEvents[T], 'eventId' | 'timestamp' | 'version'>
): void {
const payload = {
eventId: uuidv4(),
timestamp: new Date().toISOString(),
version: 1,
...data,
} as DomainEvents[T];
logger.info({ eventName, eventId: payload.eventId }, 'Event published');
this.emit(eventName, payload);
}
subscribe<T extends DomainEventName>(
eventName: T,
handler: (payload: DomainEvents[T]) => Promise<void>
): void {
this.on(eventName, async (payload: DomainEvents[T]) => {
try {
await handler(payload);
} catch (err) {
// Error isolation — other handlers continue
logger.error({ eventName, eventId: payload.eventId, err }, 'Handler failed');
}
});
}
}
export const eventBus = new TypedEventBus();
Idempotent Event Handlers
Generate order.placed event handlers for chained service actions.
Flow:
1. OrderService: publishes order.placed
2. InventoryService: receives order.placed → decrement stock
3. NotificationService: receives order.placed → queue confirmation email
4. AnalyticsService: receives order.placed → record revenue data
Requirements:
- Each handler is idempotent (check orderId before processing)
- Handler errors are logged only (don't affect order processing)
Generated inventory handler:
// src/events/handlers/inventoryHandler.ts
export function registerInventoryHandlers(): void {
eventBus.subscribe('order.placed', async (payload) => {
// Idempotency check
const processed = await inventoryRepo.isOrderProcessed(payload.orderId);
if (processed) {
logger.info({ orderId: payload.orderId }, 'Already processed, skipping');
return;
}
for (const item of payload.items) {
await inventoryRepo.decrementStock(item.productId, item.quantity);
}
await inventoryRepo.markOrderProcessed(payload.orderId);
logger.info({ orderId: payload.orderId }, 'Inventory updated');
});
}
Outbox Pattern for Reliability
Implement the Outbox pattern for reliable event publishing.
Problem: crash between DB update and event publish → event lost
Solution: save event to outbox table in same DB transaction → poll and publish separately
Requirements:
- DB: Prisma
- outbox table: id, eventName, payload, publishedAt (NULL = unpublished)
- Polling interval: 5 seconds
- After successful publish: set publishedAt
This ensures events are never lost even if the process crashes after the DB write.
Summary
Design event-driven architecture with Claude Code:
- CLAUDE.md — Event naming, payload structure, reliability requirements
- Typed EventBus — TypeScript enforces event name ↔ payload types
- Idempotent handlers — Safe to process the same event twice
- Outbox pattern — Events survive process crashes
Code Review Pack (¥980) includes /code-review for event design review — missing idempotency, incorrect event naming, reliability gaps.
Myouga (@myougatheaxo) — Claude Code engineer focused on distributed system patterns.
Top comments (0)