Event-Driven Architecture

How to design reactive systems where components communicate through events, achieving decoupling, scalability, and asynchronous processing.

What problem it solves

In a traditional architecture based on synchronous calls (request-response), services are tightly coupled: the sender must know the receiver, wait for its response, and handle its failures. This creates several problems:

  • Temporal coupling: The sender is blocked until the receiver responds
  • Knowledge coupling: The sender must know who processes its request
  • Limited scalability: Adding a new consumer requires modifying the sender
  • Fragility: If the receiver is down, the sender fails too
Synchronous communication (coupled):
  Order Service ──► Inventory Service (waits for response)
                ──► Payment Service (waits for response)
                ──► Notification Service (waits for response)
                ──► Analytics Service (waits for response)
  
  Order Service knows ALL the services
  If one fails, the order fails
  Adding a service = modifying Order Service

How it works

Event-Driven Architecture (EDA) inverts the communication model. Instead of a service calling others directly, it publishes an event that describes something that happened. Interested services subscribe to those events and react independently.

Event-based communication (decoupled):
  Order Service ──event──► Event Bus
                            "OrderCreated"
                                β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β–Ό           β–Ό           β–Ό
              Inventory    Payment    Notification
              Service      Service    Service
  
  Order Service does NOT know the consumers
  Each service reacts independently
  Adding a service = subscribing to the event

Key concepts

ConceptDescriptionExample
EventImmutable record of something that happenedOrderCreated, PaymentProcessed
ProducerService that publishes eventsOrder Service publishes OrderCreated
ConsumerService that reacts to eventsInventory Service listens for OrderCreated
Event Bus/BrokerInfrastructure that transports eventsKafka, RabbitMQ, Amazon SNS/SQS
Topic/QueueChannel through which events floworders.created, payments.processed

Types of events

Domain Events: Represent business facts.

{
  "type": "OrderCreated",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "orderId": "ORD-001",
    "customerId": "USR-123",
    "items": [{"productId": "P001", "quantity": 2}],
    "total": 150.00
  }
}

Integration Events: Communicate changes between bounded contexts.

{
  "type": "InventoryReserved",
  "source": "inventory-service",
  "data": {
    "orderId": "ORD-001",
    "reservationId": "RES-456",
    "status": "confirmed"
  }
}

Delivery patterns

Pub/Sub (Publish-Subscribe): An event is delivered to all subscribers.

Producer ──► Topic ──► Consumer A
                  ──► Consumer B
                  ──► Consumer C

Event Streaming: Events are stored in a persistent log and consumers read at their own pace.

Producer ──► Persistent log (Kafka)
              [event1, event2, event3, ...]
              
  Consumer A: reading event 3 (up to date)
  Consumer B: reading event 1 (behind, processing)
  Consumer C: new, starts from event 1

Delivery guarantees

GuaranteeDescriptionUse
At-most-onceThe event is delivered at most once (may be lost)Metrics, analytics
At-least-onceThe event is delivered at least once (may be duplicated)Most cases (with idempotency)
Exactly-onceThe event is delivered exactly onceFinancial transactions (hard to achieve)

Advantages

  • Decoupling: Producers and consumers do not know each other
  • Scalability: Adding consumers does not require modifying producers
  • Resilience: If a consumer is down, events accumulate and are processed when it comes back
  • Asynchronous processing: The producer does not wait for consumers to finish
  • Natural auditing: Events form a log of everything that happened in the system
  • Extensibility: New features are added by subscribing to existing events

Trade-offs / Disadvantages

  • Eventual consistency: Data is not updated instantly across all services
  • Debugging complexity: Tracing the flow of an event across multiple services is hard
  • Ordering: Guaranteeing the processing order of events requires careful design
  • Duplicates: Consumers must be idempotent to handle duplicate events
  • Additional infrastructure: You need a messaging broker (Kafka, RabbitMQ) along with its operation
  • Complex testing: Testing asynchronous flows is harder than testing synchronous calls

When to use

  • Systems where multiple services need to react to the same event
  • When processing can be asynchronous (you don’t need an immediate response)
  • Systems with high scalability and throughput requirements
  • When you need to decouple services so they evolve independently
  • Complex business flows with multiple steps (orders, payments, shipping)
  • When you need an audit log of everything that happens in the system

When to avoid

  • Operations that require an immediate synchronous response (user queries)
  • Simple systems with few services and linear flows
  • When strong consistency is a non-negotiable requirement
  • Teams without experience in distributed systems and asynchronous debugging
  • When you can’t justify the infrastructure of a messaging broker

Common technologies and implementations

CategoryOptions
Event StreamingApache Kafka, Amazon Kinesis, Azure Event Hubs
Message BrokersRabbitMQ, Amazon SQS/SNS, Google Pub/Sub, NATS
Event StoreEventStoreDB, Kafka (as an event store)
FrameworksSpring Cloud Stream, MassTransit (.NET), Axon Framework
ObservabilityOpenTelemetry, Jaeger, Zipkin (distributed tracing)

Relationship with other patterns

  • Saga Pattern: Uses events to coordinate distributed transactions across services
  • Outbox Pattern: Guarantees reliable publishing of events together with database changes
  • CQRS: Separates reads and writes using events to keep the models in sync
  • Idempotency: Consumers must be idempotent to handle duplicate events
  • Circuit Breaker: Protects consumers from overload when there are event spikes

Next steps

Event-Driven Architecture is the foundation of communication in modern microservices. To protect integration with external systems that don’t speak your event language, explore the Anti-Corruption Layer. To ensure that consumers handle duplicates correctly, review the Idempotency pattern.