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:

AspectRequest-ResponseEvent-Driven
CommunicationSynchronousAsynchronous
CouplingTight (knows about services)Loose (doesn't know consumers)
TimingImmediate responseEventual processing
ScalabilityLimitedHigh (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:

  1. Event ordering: Sequence numbers, buffering
  2. Deduplication: In-memory + persistent cache
  3. Idempotent processing: Safe to process multiple times
  4. Event store: Persistent storage for replay
  5. 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

About the author

InterviewCrafted helps you master system design with patience. We believe in curiosity-led engineering, reflective writing, and designing systems that make future changes feel calm.