Advanced

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.

イベント駆動アーキテクチャとは

イベント駆動アーキテクチャ(EDA)は、イベントの発行と購読によってシステムを疎結合に設計するパターンです。Claude Codeを使えば、EDAの設計から実装まで効率的に進められます。

型安全なイベントバス

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);

    // unsubscribe関数を返す
    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();

イベントハンドラの登録

// ユーザー作成時の処理
eventBus.on("user.created", async ({ userId, email }) => {
  // ウェルカムメール送信
  await emailQueue.add("send", {
    to: email,
    subject: "ようこそ!",
    template: "welcome",
    data: { userId },
  });
});

eventBus.on("user.created", async ({ userId }) => {
  // デフォルト設定の作成
  await prisma.userSettings.create({
    data: {
      userId,
      theme: "light",
      language: "ja",
      notifications: true,
    },
  });
});

// 注文完了時の処理
eventBus.on("order.completed", async ({ orderId }) => {
  const order = await prisma.order.findUnique({
    where: { id: orderId },
    include: { user: true, items: true },
  });

  if (!order) return;

  // 在庫の更新
  for (const item of order.items) {
    await prisma.product.update({
      where: { id: item.productId },
      data: { stock: { decrement: item.quantity } },
    });
  }
});

ドメインイベントパターン

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 パターン

// Command側(書き込み)
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> {
    // バリデーション
    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}`);
      }
    }

    // 注文作成
    const order = await prisma.order.create({
      data: {
        userId: this.userId,
        status: "pending",
        items: {
          create: this.items.map((item) => ({
            productId: item.productId,
            quantity: item.quantity,
          })),
        },
      },
    });

    // イベント発行
    await eventBus.emit("order.created", {
      orderId: order.id,
      userId: this.userId,
      total: 0,
    });

    return order.id;
  }
}

// Query側(読み取り)
interface Query<T> {
  execute(): Promise<T>;
}

class GetOrdersQuery implements Query<OrderSummary[]> {
  constructor(
    private userId: string,
    private page: number = 1
  ) {}

  async execute(): Promise<OrderSummary[]> {
    // 読み取り専用のビューから取得
    return prisma.orderView.findMany({
      where: { userId: this.userId },
      orderBy: { createdAt: "desc" },
      take: 20,
      skip: (this.page - 1) * 20,
    });
  }
}

イベントストア

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
  ) {
    // 楽観的ロック
    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 });

    // イベントを発行
    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;
  }
}

Claude Codeでの活用

イベント駆動アーキテクチャの実装をClaude Codeに依頼する例です。非同期処理についてはジョブキュー・非同期処理、外部連携はWebhook実装パターンも参照してください。

イベント駆動アーキテクチャを導入して。
- 型安全なイベントバスの実装
- ドメインイベントパターン
- CQRS: コマンドとクエリの分離
- イベントストアの実装
- 既存のサービス層をリファクタリングして統合

イベント駆動設計の詳細はMartin Fowler - Event-Driven Architectureを参照してください。Claude Codeの使い方は公式ドキュメントで確認できます。

Summary

イベント駆動アーキテクチャはシステムの疎結合性とスケーラビリティを高めます。Claude Codeを使えば、型安全なイベントバスからCQRS、イベントソーシングまで、段階的にEDAを導入できます。

#Claude Code #event-driven #architecture #CQRS #design patterns