Event-Driven Architecture - Designing Loosely Coupled Systems

16 min read | 2025.12.12

What is Event-Driven Architecture?

Event-Driven Architecture (EDA) is a design pattern where communication between systems occurs through events (notifications of occurrences). It reduces dependencies between components and enables building scalable and flexible systems.

What is an event: A message representing a meaningful state change within the system. Examples include “User registered,” “Order completed,” and “Inventory decreased.”

Differences from Traditional Request-Driven Architecture

Request-Driven (Synchronous)

flowchart LR
    Order["Order Service"] --> Inventory["Inventory Service"]
    Inventory --> Payment["Payment Service"]
    Payment --> Notification["Notification Service"]
  • Services are tightly coupled
  • One service failure affects the entire system
  • Processing time is the sum of all services

Event-Driven (Asynchronous)

flowchart TB
    Order["Order Service"] --> Event["Order Completed Event"]
    Event --> Inventory["Inventory Service<br/>(processes independently)"]
    Event --> Payment["Payment Service<br/>(processes independently)"]
    Event --> Notification["Notification Service<br/>(processes independently)"]
  • Services are loosely coupled
  • Failures are localized
  • Parallel processing is possible

Types of Events

Domain Events

Represent occurrences in the business domain.

// Domain event example
{
  "eventType": "OrderPlaced",
  "eventId": "evt_123456",
  "timestamp": "2024-01-15T10:30:00Z",
  "payload": {
    "orderId": "ord_789",
    "customerId": "cust_456",
    "items": [...],
    "totalAmount": 5000
  }
}

Integration Events

Events shared between different services.

Notification Events

Events that only notify of state changes without containing detailed data.

// Notification events (Fat vs Thin)
// Thin Event - details must be fetched separately
{ "eventType": "UserUpdated", "userId": "123" }

// Fat Event - contains all necessary information
{ "eventType": "UserUpdated", "userId": "123", "name": "Alice", "email": "..." }

Event Sourcing

A pattern where state is stored as a history of events.

Traditional CRUD

Store only current state:

Tableidstatus
orders1”shipped”

Event Sourcing

Store all events:

eventstimestamp
1. OrderCreated2024-01-01
2. PaymentReceived2024-01-02
3. OrderShipped2024-01-03

↓ Replay → Current state: status = “shipped”

Benefits

  • Complete audit trail: Can track all change history
  • Time travel: Can reconstruct state at any point in time
  • Event replay: Can rebuild state after bug fixes

Drawbacks

  • Increased complexity
  • Event schema evolution is challenging
  • Read performance requires careful consideration

CQRS (Command Query Responsibility Segregation)

A pattern that separates read (Query) and write (Command) models.

flowchart TB
    Command["Command<br/>(Write Model)"] -->|Publish Events| EventStore["Event Store"]
    EventStore -->|Project| Query["Query<br/>(Read Model)"]

Write Model

// Command handler
async function handlePlaceOrder(command) {
  const order = new Order(command.orderId);
  order.addItems(command.items);
  order.place();

  await eventStore.save(order.getUncommittedEvents());
}

Read Model

// View optimized for reading
const orderSummary = {
  orderId: "123",
  customerName: "Alice",  // Customer info already joined
  itemCount: 3,
  totalAmount: 5000,
  status: "shipped"
};

Implementation Patterns

Saga Pattern

Achieves transactions spanning multiple services through a chain of events.

flowchart LR
    subgraph Success["Success Path"]
        S1["Create Order"] --> S2["Reserve Inventory"]
        S2 --> S3["Process Payment"]
        S3 --> S4["Confirm Order"]
    end
    subgraph Failure["Failure Path (Compensation)"]
        F1["PaymentFailed"] --> F2["Release Inventory"]
        F2 --> F3["Cancel Order"]
    end

Outbox Pattern

A pattern that ensures both database update and event publishing succeed.

StepPhaseActions
1Within a transactionUpdate business data, Insert event into outbox table
2In a separate processPoll outbox table, Publish events to message queue, Mark as published

Considerations

Eventual Consistency

ServiceStateStatus
Order ServiceOrder status = “completed”✓ Updated
Inventory ServiceEvent not yet processedInconsistency period
Inventory ServiceInventory decreasedConsistency restored

Event Ordering

Correct orderIf order is disrupted
1. OrderCreated1. OrderShipped (?)
2. OrderUpdated2. OrderCreated
3. OrderShipped→ State may be corrupted

Idempotency

Design so that the same event delivered multiple times produces the same result.

async function handleInventoryReserved(event) {
  // Check for duplicates using idempotency key
  const processed = await db.processedEvents.findById(event.eventId);
  if (processed) return;

  await db.inventory.reserve(event.payload);
  await db.processedEvents.insert({ eventId: event.eventId });
}

Summary

Event-Driven Architecture is a powerful pattern for designing complex systems with loose coupling and flexibility. Combined with Event Sourcing and CQRS, it can improve auditability and scalability. However, you need to address challenges specific to distributed systems, such as eventual consistency and event ordering.

← Back to list