Event-Driven Architecture Pattern

A comprehensive guide to implementing event-driven architectures for scalable, loosely-coupled systems.

Overview

Event-driven architecture (EDA) is a design paradigm where system components communicate through events, enabling loose coupling and high scalability.

Core Components

graph TB
    subgraph "Event Producers"
        P1[User Service]
        P2[Order Service]
        P3[Inventory Service]
    end

    subgraph "Event Bus"
        EB[Message Broker<br/>Kafka/RabbitMQ]
    end

    subgraph "Event Consumers"
        C1[Email Service]
        C2[Analytics Service]
        C3[Audit Service]
    end

    P1 -->|User Events| EB
    P2 -->|Order Events| EB
    P3 -->|Inventory Events| EB

    EB -->|Subscribe| C1
    EB -->|Subscribe| C2
    EB -->|Subscribe| C3

    style EB fill:#ffd,stroke:#333,stroke-width:3px

Implementation Example

Event Definition

// events/order.events.ts
export interface OrderEvent {
  eventId: string;
  eventType: 'ORDER_CREATED' | 'ORDER_UPDATED' | 'ORDER_CANCELLED';
  timestamp: Date;
  aggregateId: string;
  payload: any;
  metadata: {
    userId: string;
    correlationId: string;
    version: number;
  };
}

export class OrderCreatedEvent implements OrderEvent {
  eventType: 'ORDER_CREATED' = 'ORDER_CREATED';

  constructor(
    public eventId: string,
    public timestamp: Date,
    public aggregateId: string,
    public payload: {
      orderId: string;
      customerId: string;
      items: Array<{ productId: string; quantity: number; price: number }>;
      totalAmount: number;
    },
    public metadata: {
      userId: string;
      correlationId: string;
      version: number;
    }
  ) {}
}

Event Publisher

// services/event-publisher.ts
import { EventEmitter } from 'events';

export class EventPublisher {
  private eventBus: EventEmitter;
  private deadLetterQueue: any[] = [];

  constructor(eventBus: EventEmitter) {
    this.eventBus = eventBus;
  }

  async publish(event: OrderEvent): Promise<void> {
    try {
      // Add to event store
      await this.saveToEventStore(event);

      // Publish to event bus
      this.eventBus.emit(event.eventType, event);

      // Log for audit
      console.log(`Event published: ${event.eventType}`, {
        eventId: event.eventId,
        aggregateId: event.aggregateId
      });
    } catch (error) {
      // Handle failed events
      this.deadLetterQueue.push({ event, error, timestamp: new Date() });
      throw error;
    }
  }

  private async saveToEventStore(event: OrderEvent): Promise<void> {
    // Persist event to event store database
    // Implementation depends on your database choice
  }
}

Event Consumer

// services/event-consumer.ts
export class EventConsumer {
  private handlers: Map<string, Function[]> = new Map();

  subscribe(eventType: string, handler: Function): void {
    if (!this.handlers.has(eventType)) {
      this.handlers.set(eventType, []);
    }
    this.handlers.get(eventType)!.push(handler);
  }

  async handleEvent(event: OrderEvent): Promise<void> {
    const handlers = this.handlers.get(event.eventType) || [];

    // Execute handlers in parallel
    await Promise.all(
      handlers.map(async (handler) => {
        try {
          await handler(event);
        } catch (error) {
          console.error(`Handler failed for ${event.eventType}:`, error);
          // Implement retry logic or dead letter queue
        }
      })
    );
  }
}

Patterns & Best Practices

1. Event Sourcing

Store all events as the source of truth:

sequenceDiagram
    participant Client
    participant API
    participant EventStore
    participant Projection

    Client->>API: Create Order
    API->>EventStore: Store OrderCreated Event
    EventStore->>Projection: Update Read Model
    API-->>Client: Order Created

2. CQRS Integration

Separate read and write models:

// Write side - Command Handler
class CreateOrderCommandHandler {
  async handle(command: CreateOrderCommand): Promise<void> {
    // Business logic
    const order = Order.create(command);

    // Publish event
    await this.eventPublisher.publish(
      new OrderCreatedEvent(/* ... */)
    );
  }
}

// Read side - Projection
class OrderProjection {
  async handle(event: OrderCreatedEvent): Promise<void> {
    // Update read model
    await this.orderReadModel.create({
      id: event.payload.orderId,
      // ... denormalized data for queries
    });
  }
}

3. Saga Pattern

Manage distributed transactions:

class OrderSaga {
  private state: 'STARTED' | 'PAYMENT_PENDING' | 'COMPLETED' | 'FAILED';

  async handle(event: OrderEvent): Promise<void> {
    switch (event.eventType) {
      case 'ORDER_CREATED':
        await this.processPayment(event);
        break;
      case 'PAYMENT_PROCESSED':
        await this.updateInventory(event);
        break;
      case 'INVENTORY_UPDATED':
        await this.completeOrder(event);
        break;
      case 'PAYMENT_FAILED':
      case 'INVENTORY_INSUFFICIENT':
        await this.compensate(event);
        break;
    }
  }
}

Benefits

  • Loose Coupling: Services don't need to know about each other
  • Scalability: Easy to add new consumers without affecting producers
  • Resilience: System continues functioning even if some components fail
  • Flexibility: Easy to add new features by adding event consumers
  • Audit Trail: Natural event log for debugging and compliance

Considerations

  • Eventual Consistency: Data may not be immediately consistent
  • Complexity: More moving parts to manage
  • Event Schema Evolution: Need versioning strategy
  • Debugging: Distributed tracing becomes essential
  • Ordering: May need to handle out-of-order events

Technology Choices

Use Case Recommended Technology
High Throughput Apache Kafka
Simple Pub/Sub Redis Pub/Sub
Enterprise RabbitMQ
Cloud Native AWS EventBridge, Azure Event Hub
Real-time Socket.io, WebSockets