Event-Driven Architecture

0/3 in this phase0/45 across the roadmap

📖 Concept

Event-driven architecture (EDA) is a design pattern where services communicate by producing and consuming events rather than making direct API calls. It's the foundation of scalable, loosely coupled microservices.

Key Patterns

1. Event Notification

Service publishes an event, but the event only contains an identifier. Consumers must fetch details themselves.

  • Event: { type: "order.created", orderId: "123" }
  • Consumer: Calls Order Service API to get full order details

2. Event-Carried State Transfer

Event contains ALL the data consumers need. No callbacks required.

  • Event: { type: "order.created", orderId: "123", items: [...], total: 99.99, userId: "456" }
  • Consumer: Has everything it needs in the event

3. Event Sourcing

Store events as the source of truth. Current state is derived by replaying events.

  • Events: Deposited $100 → Withdrew $30 → Deposited $50 → Balance = $120

4. CQRS + Events

Separate read/write models. Events sync the read model with the write model.

Saga Pattern (Distributed Transactions)

When a business process spans multiple services, use sagas — a sequence of local transactions coordinated by events.

Choreography Saga (Event-based)

Each service publishes events that trigger the next step:

OrderService → OrderCreated
PaymentService (hears OrderCreated) → PaymentCharged
InventoryService (hears PaymentCharged) → ItemsReserved
ShippingService (hears ItemsReserved) → ShipmentCreated

Orchestration Saga (Coordinator-based)

A central orchestrator tells each service what to do:

Orchestrator → Tell PaymentService to charge
Orchestrator → Tell InventoryService to reserve
Orchestrator → Tell ShippingService to ship

Choreography is more decoupled but harder to debug. Orchestration is easier to understand but creates a single point of coordination.

💻 Code Example

codeTap to expand ⛶
1// ============================================
2// Event-Driven Architecture Patterns
3// ============================================
4
5// ---------- Saga Pattern: Choreography ----------
6class ChoreographySaga {
7 constructor(eventBus) {
8 this.eventBus = eventBus;
9 this.setupHandlers();
10 }
11
12 setupHandlers() {
13 // Each service listens and reacts
14 this.eventBus.subscribe('order.created', async (event) => {
15 console.log('[PaymentService] Charging payment...');
16 try {
17 await this.chargePayment(event);
18 this.eventBus.publish('payment.charged', { orderId: event.orderId, amount: event.total });
19 } catch (err) {
20 this.eventBus.publish('payment.failed', { orderId: event.orderId, reason: err.message });
21 }
22 });
23
24 this.eventBus.subscribe('payment.charged', async (event) => {
25 console.log('[InventoryService] Reserving items...');
26 try {
27 await this.reserveInventory(event);
28 this.eventBus.publish('inventory.reserved', { orderId: event.orderId });
29 } catch (err) {
30 this.eventBus.publish('inventory.failed', { orderId: event.orderId });
31 // COMPENSATE: Refund payment
32 this.eventBus.publish('payment.refund', { orderId: event.orderId, amount: event.amount });
33 }
34 });
35
36 // Compensation handlers
37 this.eventBus.subscribe('payment.failed', async (event) => {
38 console.log(`[OrderService] Cancelling order \${event.orderId}`);
39 });
40
41 this.eventBus.subscribe('payment.refund', async (event) => {
42 console.log(`[PaymentService] Refunding \${event.amount} for \${event.orderId}`);
43 });
44 }
45
46 async chargePayment(event) { return { success: true }; }
47 async reserveInventory(event) { return { success: true }; }
48}
49
50// ---------- Saga Pattern: Orchestration ----------
51class OrderOrchestrator {
52 constructor(paymentService, inventoryService, shippingService) {
53 this.payment = paymentService;
54 this.inventory = inventoryService;
55 this.shipping = shippingService;
56 }
57
58 async executeOrder(order) {
59 const steps = [];
60 try {
61 // Step 1: Charge payment
62 const payment = await this.payment.charge(order);
63 steps.push({ service: 'payment', action: 'charge', result: payment });
64
65 // Step 2: Reserve inventory
66 const reservation = await this.inventory.reserve(order);
67 steps.push({ service: 'inventory', action: 'reserve', result: reservation });
68
69 // Step 3: Create shipment
70 const shipment = await this.shipping.create(order);
71 steps.push({ service: 'shipping', action: 'create', result: shipment });
72
73 return { status: 'completed', steps };
74 } catch (error) {
75 console.log('Saga failed, compensating...');
76 await this.compensate(steps);
77 return { status: 'failed', error: error.message };
78 }
79 }
80
81 async compensate(completedSteps) {
82 // Reverse completed steps (compensating transactions)
83 for (const step of completedSteps.reverse()) {
84 switch (step.service) {
85 case 'payment': await this.payment.refund(step.result); break;
86 case 'inventory': await this.inventory.release(step.result); break;
87 case 'shipping': await this.shipping.cancel(step.result); break;
88 }
89 }
90 }
91}
92
93// ---------- Transactional Outbox Pattern ----------
94class OutboxPublisher {
95 constructor(db, kafka) {
96 this.db = db;
97 this.kafka = kafka;
98 }
99
100 async createOrderWithEvent(orderData) {
101 // Single DB transaction ensures both order AND event are saved
102 await this.db.transaction(async (tx) => {
103 const order = await tx.insert('orders', orderData);
104 // Write event to outbox table (same transaction!)
105 await tx.insert('outbox', {
106 event_type: 'order.created',
107 payload: JSON.stringify({ orderId: order.id, ...orderData }),
108 published: false,
109 });
110 });
111 // Separate process polls outbox and publishes to Kafka
112 }
113
114 // Background poller (runs continuously)
115 async pollOutbox() {
116 const events = await this.db.query(
117 'SELECT * FROM outbox WHERE published = false ORDER BY id LIMIT 100'
118 );
119 for (const event of events) {
120 await this.kafka.produce(event.event_type, event.payload);
121 await this.db.query('UPDATE outbox SET published = true WHERE id = $1', [event.id]);
122 }
123 }
124}
125
126const bus = { subscribe: (e, h) => {}, publish: (e, d) => {} };
127new ChoreographySaga(bus);

🏋️ Practice Exercise

  1. Saga Design: Design a saga for a travel booking that reserves a flight + hotel + car rental. Include compensation (rollback) logic for each step.

  2. Choreography vs Orchestration: For a food delivery app (order → restaurant → driver → delivery), design both choreography and orchestration sagas. Compare complexity and debuggability.

  3. Outbox Pattern: Implement the transactional outbox pattern to ensure an event is published if and only if the database transaction succeeds.

  4. Event Schema Evolution: Your "order.created" event needs a new field. How do you add it without breaking existing consumers? Design a schema evolution strategy.

⚠️ Common Mistakes

  • Not handling saga compensation — if step 3 of 5 fails, steps 1 and 2 must be rolled back. Without compensation handlers, you get inconsistent state across services.

  • Publishing events outside the DB transaction — if the DB write succeeds but event publishing fails (or vice versa), your system is inconsistent. Use the transactional outbox pattern.

  • Over-engineering with event-driven architecture for simple CRUD — not every service interaction needs events. Direct API calls are simpler and faster when immediate response is needed.

  • Not versioning events — changing event schemas without versioning breaks consumers. Always include a schema version and support backward compatibility.

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Event-Driven Architecture