Event-Driven Architecture
📖 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
1// ============================================2// Event-Driven Architecture Patterns3// ============================================45// ---------- Saga Pattern: Choreography ----------6class ChoreographySaga {7 constructor(eventBus) {8 this.eventBus = eventBus;9 this.setupHandlers();10 }1112 setupHandlers() {13 // Each service listens and reacts14 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 });2324 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 payment32 this.eventBus.publish('payment.refund', { orderId: event.orderId, amount: event.amount });33 }34 });3536 // Compensation handlers37 this.eventBus.subscribe('payment.failed', async (event) => {38 console.log(`[OrderService] Cancelling order \${event.orderId}`);39 });4041 this.eventBus.subscribe('payment.refund', async (event) => {42 console.log(`[PaymentService] Refunding \${event.amount} for \${event.orderId}`);43 });44 }4546 async chargePayment(event) { return { success: true }; }47 async reserveInventory(event) { return { success: true }; }48}4950// ---------- Saga Pattern: Orchestration ----------51class OrderOrchestrator {52 constructor(paymentService, inventoryService, shippingService) {53 this.payment = paymentService;54 this.inventory = inventoryService;55 this.shipping = shippingService;56 }5758 async executeOrder(order) {59 const steps = [];60 try {61 // Step 1: Charge payment62 const payment = await this.payment.charge(order);63 steps.push({ service: 'payment', action: 'charge', result: payment });6465 // Step 2: Reserve inventory66 const reservation = await this.inventory.reserve(order);67 steps.push({ service: 'inventory', action: 'reserve', result: reservation });6869 // Step 3: Create shipment70 const shipment = await this.shipping.create(order);71 steps.push({ service: 'shipping', action: 'create', result: shipment });7273 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 }8081 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}9293// ---------- Transactional Outbox Pattern ----------94class OutboxPublisher {95 constructor(db, kafka) {96 this.db = db;97 this.kafka = kafka;98 }99100 async createOrderWithEvent(orderData) {101 // Single DB transaction ensures both order AND event are saved102 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 Kafka112 }113114 // 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}125126const bus = { subscribe: (e, h) => {}, publish: (e, d) => {} };127new ChoreographySaga(bus);
🏋️ Practice Exercise
Saga Design: Design a saga for a travel booking that reserves a flight + hotel + car rental. Include compensation (rollback) logic for each step.
Choreography vs Orchestration: For a food delivery app (order → restaurant → driver → delivery), design both choreography and orchestration sagas. Compare complexity and debuggability.
Outbox Pattern: Implement the transactional outbox pattern to ensure an event is published if and only if the database transaction succeeds.
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