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 |