Architecture Patterns

Event-Driven Architecture: Messaging Patterns, Event Sourcing, and CQRS

Learn event-driven architecture patterns including publish/subscribe, event sourcing, CQRS, and how to build loosely coupled systems that scale.

22 min readevent-drivenmessagingevent sourcingCQRSpub/subarchitecture

Why Event-Driven?

In traditional request-response architectures, services call each other directly. This creates tight coupling and makes it hard to add new features without modifying existing services. Event-driven architecture solves this by having services communicate through events rather than direct calls.

Key insight: Events are facts that something happened. Commands are requests to do something. Understanding this distinction is crucial for event-driven design.


Core Concepts

Events vs Commands vs Queries

TypeDirectionResponseIdempotent
EventPublisher → SubscribersNone (fire-and-forget)Yes, typically
CommandClient → ServiceAck/NackUsually
QueryClient → ServiceDataYes

Event Structure

json
{
  "eventId": "uuid",
  "eventType": "OrderPlaced",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "orderId": "12345",
    "customerId": "67890",
    "total": 99.99
  }
}

Publish/Subscribe Pattern

How It Works

Use Cases

Benefits

  1. Decoupling: Publishers don't know who consumes their events
  2. Scalability: Add new consumers without changing producers
  3. Resilience: If a consumer is down, events are queued
  4. Audit trail: Events are a natural log of what happened

Event Streaming with Apache Kafka

Kafka is the backbone of many event-driven systems. It durably stores events and allows multiple consumers to process them independently.

Kafka Architecture

Kafka Concepts

ConceptDescription
TopicA named stream of events
PartitionOrdered, immutable sequence within a topic
OffsetPosition of a message in a partition
Consumer GroupSet of consumers that share partitions
RetentionHow long messages are kept (configurable)
💡

Why Kafka over queues? Kafka maintains message order within partitions, supports message replay (you can re-read from the beginning), and handles high throughput (millions of messages/second).


Event Sourcing

Instead of storing current state, store the sequence of events that led to that state. The current state is derived by replaying events.

Traditional vs Event Sourcing

Event Store

sql
-- Instead of updating a row, append events
CREATE TABLE events (
    id UUID PRIMARY KEY,
    aggregate_id UUID NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    event_data JSONB NOT NULL,
    created_at TIMESTAMP NOT NULL
);

-- Query events to reconstruct state
SELECT * FROM events 
WHERE aggregate_id = 'order-123' 
ORDER BY created_at;

Benefits of Event Sourcing

  1. Complete audit trail: You know exactly how state evolved
  2. Time travel: Query state at any point in time
  3. Replay and debug: Replay events to reproduce bugs
  4. Multiple projections: Create different read models from the same events
  5. Integration: Other systems can consume the same event stream

Challenges

ChallengeSolution
Event schema changesUpcasting, versioning
Large event storesSnapshots, archiving old events
eventual consistencyShow loading states, be clear about latency
DebuggingCorrelation IDs, structured logging

CQRS (Command Query Responsibility Segregation)

Separate read and write operations into different models. Write side handles commands (changes state), read side handles queries (reads state).

Traditional vs CQRS

Example: E-Commerce

Benefits

  1. Optimize reads and writes independently: Different storage for different purposes
  2. Scalability: Scale read and write sides separately
  3. Flexibility: Read models can be tailored to specific UI needs
  4. Separation of concerns: Simpler command and query logic
⚠️

CQRS is complex: Only use it when you have a genuine need (different read/write patterns, high scalability requirements). For most applications, a single model is simpler and sufficient.


Combining Event Sourcing + CQRS

This is a powerful combination used by systems like Event Store and Axon Framework.


Practical Implementation

Choosing an Event Broker

BrokerBest ForConsiderations
KafkaHigh throughput, event streamingOperational complexity
RabbitMQTraditional messaging, task queuesSingle-node can be bottleneck
AWS EventBridgeAWS-native, serverlessVendor lock-in
Google Pub/SubGCP-native, real-timeVendor lock-in
NATSLightweight, high performanceLess ecosystem

Event Design Best Practices

  1. Use past tense: OrderPlaced, PaymentProcessed, not PlaceOrder
  2. Include context: Enough data in the event to reconstruct state
  3. Version your events: Schema evolution is inevitable
  4. Be idempotent: Consumers should handle duplicate events
  5. Correlate events: Include correlation IDs for tracing
json
{
  "eventType": "OrderPlaced",
  "version": "2.0",
  "correlationId": "req-12345",
  "causationId": "cmd-67890",
  "data": {
    "orderId": "order-123",
    "items": [...],
    "customerId": "cust-456",
    "totalAmount": 99.99,
    "currency": "USD"
  }
}

What to Remember for Interviews

  1. Events are facts: Past tense, immutable records of what happened
  2. Event sourcing: Store events, not state; replay to reconstruct
  3. CQRS: Separate read and write models for flexibility
  4. Decoupling: Event-driven enables independent service evolution
  5. Complexity: These patterns solve real problems but add operational complexity

Practice: Design an event-driven architecture for a ride-sharing app (like Uber). What events are published? What consumers need them? How would you handle a driver accepting a ride?