Topic Overview
Event-Driven Architecture
Master event-driven architecture: event sourcing, CQRS, event streaming, event choreography vs orchestration, and building scalable event-driven systems.
Event-Driven Architecture (EDA) is an architectural pattern where services communicate through events. Services produce events when something happens and consume events they're interested in, creating loosely coupled, scalable systems.
What is Event-Driven Architecture?
Event-Driven Architecture features:
- Event producers: Services that publish events
- Event consumers: Services that subscribe to events
- Event bus/broker: Routes events to consumers
- Loose coupling: Services don't know about each other
- Asynchronous: Events processed asynchronously
Benefits:
- Decoupling: Services independent
- Scalability: Easy to add new consumers
- Resilience: Failure in one service doesn't cascade
- Flexibility: Easy to add new features
Event-Driven vs Request-Response
Request-Response (Synchronous)
Service A → Service B: Request
Service A ← Service B: Response
Tight coupling: A knows about B
Synchronous: A waits for B
Event-Driven (Asynchronous)
Service A → Event Bus: Publish Event
Event Bus → Service B: Event (asynchronously)
Event Bus → Service C: Event (asynchronously)
Loose coupling: A doesn't know about B, C
Asynchronous: A doesn't wait
Event Types
1. Domain Events
Business events that occurred:
OrderCreated
PaymentProcessed
InventoryReserved
2. Integration Events
Events for service communication:
UserRegistered
OrderShipped
EmailSent
Event Patterns
1. Event Notification
Notify that something happened:
Order Service → Event: OrderCreated
→ Inventory Service: Reserve inventory
→ Payment Service: Process payment
→ Notification Service: Send email
2. Event Sourcing
Store all events, reconstruct state:
Events: [OrderCreated, ItemAdded, PaymentProcessed, OrderShipped]
State: Reconstruct from events
3. CQRS (Command Query Responsibility Segregation)
Separate read and write models:
Write: Commands → Events → Write Model
Read: Events → Read Model (optimized for queries)
Choreography vs Orchestration
Choreography (Decentralized)
Services react to events independently:
OrderCreated Event:
→ Inventory Service: Reserve inventory
→ Payment Service: Process payment
→ Shipping Service: Prepare shipment
No central coordinator
Benefits:
- Loose coupling
- No single point of failure
- Easy to add services
Drawbacks:
- Hard to track overall flow
- Difficult to handle failures
Orchestration (Centralized)
Orchestrator coordinates workflow:
Orchestrator:
1. Create order
2. Reserve inventory
3. Process payment
4. Ship order
Central coordinator
Benefits:
- Clear workflow
- Easy to handle failures
- Better visibility
Drawbacks:
- Tight coupling to orchestrator
- Single point of failure
Examples
Event-Driven Microservices
// Order Service (Producer)
class OrderService {
async createOrder(orderData: OrderData): Promise<Order> {
const order = await this.database.createOrder(orderData);
// Publish event
await this.eventBus.publish('order.created', {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total,
timestamp: Date.now()
});
return order;
}
}
// Inventory Service (Consumer)
class InventoryService {
constructor() {
// Subscribe to events
this.eventBus.subscribe('order.created', async (event) => {
await this.reserveInventory(event.items);
});
}
async reserveInventory(items: Item[]): Promise<void> {
for (const item of items) {
await this.database.decrementStock(item.id, item.quantity);
}
// Publish event
await this.eventBus.publish('inventory.reserved', {
orderId: event.orderId,
items: items
});
}
}
// Payment Service (Consumer)
class PaymentService {
constructor() {
this.eventBus.subscribe('inventory.reserved', async (event) => {
await this.processPayment(event.orderId);
});
}
async processPayment(orderId: string): Promise<void> {
const order = await this.getOrder(orderId);
await this.charge(order.total);
// Publish event
await this.eventBus.publish('payment.processed', {
orderId: orderId,
amount: order.total
});
}
}
Event Sourcing
class EventSourcedAggregate {
private events: Event[] = [];
private state: State;
async applyEvent(event: Event): Promise<void> {
// Apply event to state
this.state = this.apply(this.state, event);
// Store event
await this.eventStore.append(this.id, event);
this.events.push(event);
}
async reconstructState(): Promise<State> {
// Load all events
const events = await this.eventStore.getEvents(this.id);
// Reconstruct state from events
let state = this.initialState();
for (const event of events) {
state = this.apply(state, event);
}
return state;
}
}
Common Pitfalls
- Event ordering: Events processed out of order. Fix: Use event versioning, sequence numbers
- Duplicate events: Processing same event twice. Fix: Idempotent handlers, event deduplication
- Event loss: Events lost if consumer fails. Fix: Persistent event bus, acknowledgments
- Too many events: Event explosion. Fix: Aggregate related events, use event versioning
- No event schema: Events change, break consumers. Fix: Version events, backward compatibility
Interview Questions
Beginner
Q: What is event-driven architecture and how does it differ from request-response?
A:
Event-Driven Architecture is where services communicate through events asynchronously.
Differences:
| Aspect | Request-Response | Event-Driven |
|---|---|---|
| Communication | Synchronous | Asynchronous |
| Coupling | Tight (knows about services) | Loose (doesn't know consumers) |
| Timing | Immediate response | Eventual processing |
| Scalability | Limited | High (easy to add consumers) |
Example:
Request-Response:
Order Service → Inventory Service: Reserve inventory (wait)
Order Service ← Inventory Service: Response
Event-Driven:
Order Service → Event: OrderCreated
Event Bus → Inventory Service: OrderCreated (async)
Event Bus → Payment Service: OrderCreated (async)
Benefits:
- Decoupling: Services independent
- Scalability: Easy to add consumers
- Resilience: Failure doesn't cascade
- Flexibility: Easy to add features
Intermediate
Q: Explain event choreography vs orchestration. When would you use each?
A:
Choreography (Decentralized):
Services react to events independently:
OrderCreated Event:
→ Inventory Service: Reserves inventory
→ Payment Service: Processes payment
→ Shipping Service: Prepares shipment
No central coordinator
Benefits:
- Loose coupling
- No single point of failure
- Easy to add services
Drawbacks:
- Hard to track overall flow
- Difficult to handle failures
Orchestration (Centralized):
Orchestrator coordinates workflow:
Orchestrator:
1. Create order
2. Reserve inventory (Inventory Service)
3. Process payment (Payment Service)
4. Ship order (Shipping Service)
Central coordinator
Benefits:
- Clear workflow
- Easy to handle failures
- Better visibility
Drawbacks:
- Tight coupling to orchestrator
- Single point of failure
When to use:
- Choreography: Simple workflows, loose coupling needed
- Orchestration: Complex workflows, need coordination
Senior
Q: Design an event-driven system for an e-commerce platform handling millions of events per day. How do you handle event ordering, deduplication, and ensure reliability?
A:
class ScalableEventDrivenSystem {
private eventBus: EventBus;
private eventStore: EventStore;
private eventRouter: EventRouter;
private deduplicator: Deduplicator;
constructor() {
// Kafka for event streaming
this.eventBus = new KafkaEventBus({
brokers: ['kafka1', 'kafka2', 'kafka3'],
replicationFactor: 3
});
this.eventStore = new EventStore();
this.eventRouter = new EventRouter();
this.deduplicator = new Deduplicator();
}
// 1. Event Publishing
async publishEvent(event: Event): Promise<void> {
// Deduplicate
if (await this.deduplicator.isDuplicate(event.id)) {
return; // Already processed
}
// Add metadata
const enrichedEvent = {
...event,
id: this.generateEventId(),
timestamp: Date.now(),
version: 1,
source: this.getServiceName()
};
// Publish to event bus
await this.eventBus.publish(event.topic, enrichedEvent);
// Store in event store
await this.eventStore.append(enrichedEvent);
// Mark as processed
await this.deduplicator.markProcessed(event.id);
}
// 2. Event Ordering
class EventOrdering {
async ensureOrdering(events: Event[]): Promise<Event[]> {
// Sort by sequence number
return events.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
}
async handleOutOfOrder(event: Event): Promise<void> {
// Buffer out-of-order events
await this.buffer.add(event);
// Process when in order
if (await this.canProcess(event)) {
await this.process(event);
}
}
}
// 3. Event Deduplication
class Deduplicator {
private processed: Set<string>; // In-memory cache
private persistentStore: Redis; // Persistent store
async isDuplicate(eventId: string): Promise<boolean> {
// Check cache
if (this.processed.has(eventId)) {
return true;
}
// Check persistent store
const exists = await this.persistentStore.exists(`event:${eventId}`);
if (exists) {
this.processed.add(eventId);
return true;
}
return false;
}
async markProcessed(eventId: string): Promise<void> {
this.processed.add(eventId);
await this.persistentStore.setex(`event:${eventId}`, 86400, '1'); // 24 hours
}
}
// 4. Event Consumption (Idempotent)
class EventConsumer {
async consume(event: Event): Promise<void> {
// Check if already processed
const processed = await this.isProcessed(event.id);
if (processed) {
return; // Idempotent: Skip if already processed
}
// Process event
await this.processEvent(event);
// Mark as processed
await this.markProcessed(event.id);
}
async processEvent(event: Event): Promise<void> {
// Idempotent processing
// Can be called multiple times safely
switch (event.type) {
case 'order.created':
await this.handleOrderCreated(event);
break;
case 'payment.processed':
await this.handlePaymentProcessed(event);
break;
}
}
}
// 5. Event Store
class EventStore {
async append(event: Event): Promise<void> {
// Store event with sequence number
await this.database.insert({
id: event.id,
type: event.type,
data: event.data,
sequenceNumber: await this.getNextSequence(),
timestamp: event.timestamp
});
}
async getEvents(aggregateId: string): Promise<Event[]> {
// Load all events for aggregate
return await this.database.query(
'SELECT * FROM events WHERE aggregate_id = ? ORDER BY sequence_number',
[aggregateId]
);
}
}
// 6. Event Routing
class EventRouter {
async route(event: Event): Promise<void> {
// Route to appropriate consumers
const consumers = this.getConsumers(event.type);
for (const consumer of consumers) {
await this.deliver(event, consumer);
}
}
}
}
Features:
- Event ordering: Sequence numbers, buffering
- Deduplication: In-memory + persistent cache
- Idempotent processing: Safe to process multiple times
- Event store: Persistent storage for replay
- Reliability: Replication, acknowledgments
Key Takeaways
- Event-driven architecture: Services communicate via events asynchronously
- Benefits: Decoupling, scalability, resilience, flexibility
- Event types: Domain events, integration events
- Patterns: Event notification, event sourcing, CQRS
- Choreography: Decentralized, services react independently
- Orchestration: Centralized, orchestrator coordinates
- Event ordering: Use sequence numbers, handle out-of-order
- Deduplication: Idempotent handlers, event deduplication
- Best practices: Version events, ensure ordering, handle duplicates, persistent event store