DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Event-Driven Architecture with Claude Code: Decoupled Service Communication

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// 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();
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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');
  });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This ensures events are never lost even if the process crashes after the DB write.


Summary

Design event-driven architecture with Claude Code:

  1. CLAUDE.md — Event naming, payload structure, reliability requirements
  2. Typed EventBus — TypeScript enforces event name ↔ payload types
  3. Idempotent handlers — Safe to process the same event twice
  4. Outbox pattern — Events survive process crashes

Code Review Pack (¥980) includes /code-review for event design review — missing idempotency, incorrect event naming, reliability gaps.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Claude Code engineer focused on distributed system patterns.

Top comments (0)