Difficulty: Advanced
What is event-driven architecture? Explain event sourcing, CQRS, and the saga pattern.
Event-Driven Architecture (EDA) is a design pattern where components communicate through events rather than direct calls.
Core concepts: 1. Domain Events: Record what happened (OrderPlaced, PaymentReceived, ItemShipped) 2. Event Bus: Distributes events to interested subscribers (Kafka, RabbitMQ, EventBridge) 3. Event Sourcing: Store all changes as a sequence of events instead of current state. Rebuild state by replaying events. 4. CQRS (Command Query Responsibility Segregation): Separate write model (commands) from read model (queries) for independent optimization. 5. Saga Pattern: Coordinate distributed transactions across services using a sequence of events and compensating actions.
Benefits: Loose coupling, auditability, temporal queries (what was the state at time X?), scalability (independent read/write scaling).
// Instead of storing current state:
// { balance: 150 }
// Store the sequence of events:
const events = [
{ type: 'ACCOUNT_CREATED', data: { accountId: 'acc_1' }, timestamp: '2025-01-01' },
{ type: 'MONEY_DEPOSITED', data: { amount: 200 }, timestamp: '2025-01-02' },
{ type: 'MONEY_WITHDRAWN', data: { amount: 50 }, timestamp: '2025-01-03' }
];
// Replay: 0 + 200 - 50 = 150
// Event store
class EventStore {
async append(streamId, event) {
await db.events.create({
streamId,
type: event.type,
data: event.data,
version: await this.getNextVersion(streamId),
timestamp: new Date()
});
// Publish to event bus for subscribers
await eventBus.publish(event.type, { streamId, ...event });
}
async getEvents(streamId, fromVersion = 0) {
return db.events.findMany({
where: { streamId, version: { gte: fromVersion } },
orderBy: { version: 'asc' }
});
}
}
// Rebuild state from events
class BankAccount {
constructor() {
this.balance = 0;
this.status = 'ACTIVE';
}
apply(event) {
switch (event.type) {
case 'ACCOUNT_CREATED':
this.status = 'ACTIVE';
break;
case 'MONEY_DEPOSITED':
this.balance += event.data.amount;
break;
case 'MONEY_WITHDRAWN':
this.balance -= event.data.amount;
break;
case 'ACCOUNT_CLOSED':
this.status = 'CLOSED';
break;
}
}
static fromEvents(events) {
const account = new BankAccount();
events.forEach(e => account.apply(e));
return account;
}
}
Event sourcing stores every change as an immutable event. The current state is derived by replaying events. This provides a complete audit trail and enables time-travel queries (state at any point in history).
// CQRS: Separate write and read models
// WRITE SIDE (Commands)
class OrderCommandHandler {
async createOrder(command) {
// Validate business rules
const inventory = await inventoryService.check(command.items);
if (!inventory.available) throw new Error('Out of stock');
// Append event to event store (write model)
await eventStore.append(`order:${command.orderId}`, {
type: 'ORDER_CREATED',
data: {
orderId: command.orderId,
userId: command.userId,
items: command.items,
total: command.total
}
});
}
}
// READ SIDE (Queries) - optimized denormalized views
// Event handler builds read model from events
class OrderReadModelUpdater {
async handle(event) {
switch (event.type) {
case 'ORDER_CREATED':
// Insert into read-optimized table
await db.orderViews.create({
orderId: event.data.orderId,
userId: event.data.userId,
total: event.data.total,
itemCount: event.data.items.length,
status: 'PENDING',
createdAt: event.timestamp
});
break;
case 'ORDER_SHIPPED':
await db.orderViews.update(event.data.orderId, {
status: 'SHIPPED',
shippedAt: event.timestamp
});
break;
}
}
}
// Query the read model (fast, denormalized)
class OrderQueryHandler {
async getUserOrders(userId, page) {
return db.orderViews.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
skip: page * 20,
take: 20
});
// No JOINs needed - read model is denormalized
}
}
CQRS separates writes (event store, business logic) from reads (denormalized views, fast queries). The read model is rebuilt from events, allowing independent optimization of both sides.
// Saga: coordinate multi-service transactions with compensating actions
// Order placement saga:
// 1. Create Order -> 2. Reserve Inventory -> 3. Charge Payment -> 4. Confirm
// If step 3 fails: compensate step 2 (release inventory) and step 1 (cancel order)
class OrderSaga {
constructor(eventBus) {
this.eventBus = eventBus;
this.setupHandlers();
}
setupHandlers() {
this.eventBus.on('ORDER_CREATED', this.handleOrderCreated.bind(this));
this.eventBus.on('INVENTORY_RESERVED', this.handleInventoryReserved.bind(this));
this.eventBus.on('INVENTORY_RESERVATION_FAILED', this.handleInventoryFailed.bind(this));
this.eventBus.on('PAYMENT_CHARGED', this.handlePaymentCharged.bind(this));
this.eventBus.on('PAYMENT_FAILED', this.handlePaymentFailed.bind(this));
}
async handleOrderCreated(event) {
// Step 2: Reserve inventory
await this.eventBus.publish('RESERVE_INVENTORY', {
orderId: event.orderId,
items: event.items
});
}
async handleInventoryReserved(event) {
// Step 3: Charge payment
await this.eventBus.publish('CHARGE_PAYMENT', {
orderId: event.orderId,
amount: event.total
});
}
async handlePaymentCharged(event) {
// Step 4: All good - confirm order
await this.eventBus.publish('CONFIRM_ORDER', {
orderId: event.orderId
});
}
// COMPENSATING ACTIONS (rollback)
async handlePaymentFailed(event) {
// Payment failed -> release inventory
await this.eventBus.publish('RELEASE_INVENTORY', {
orderId: event.orderId,
items: event.items
});
// Cancel order
await this.eventBus.publish('CANCEL_ORDER', {
orderId: event.orderId,
reason: 'Payment failed'
});
}
async handleInventoryFailed(event) {
// Inventory unavailable -> cancel order
await this.eventBus.publish('CANCEL_ORDER', {
orderId: event.orderId,
reason: 'Out of stock'
});
}
}
The saga pattern replaces distributed ACID transactions with a sequence of local transactions and compensating actions. If any step fails, previous steps are rolled back via compensation events.
Event Sourcing, CQRS, Event Bus, Saga Pattern, Domain Events