Event-Driven Architecture

Communicating through immutable, persisted events — replayability, idempotency, event sourcing, CQRS, and the trade-offs of eventual consistency.

Event-driven systems communicate through events — records of state changes (“order placed”, “payment failed”) rather than direct commands.

Flow: Producer → Broker (queue/log) → Consumer(s)

Two properties make events powerful:

  • Immutable — never updated, only appended.
  • Persisted — stored durably, enabling replay and recovery.

Replayability

Because the event log is a complete history, system state can be rebuilt by reprocessing events from the beginning. This enables:

  • Self-healing — after a bug corrupts state, fix the handler and replay the log.
  • Debugging and auditing — every change has a traceable cause.
  • New consumers — subscribe and replay history to build a current view.

Brokers like Kafka and RabbitMQ provide the persistence, retry, and delivery machinery.

Reliability Requirements

Idempotency

At-least-once delivery means the same event can arrive multiple times. Handlers must be idempotent — processing an event twice must be safe. Common technique: track processed event IDs and dedupe.

Retries + ACKs

Consumers acknowledge messages only after successful processing. Unacked messages are redelivered — this is what makes delivery reliable, and also why idempotency is non-negotiable.

Event Sourcing & CQRS

  • Event Sourcing: store all changes as events, not final state. The database is the event log; current state is derived.
  • CQRS (Command Query Responsibility Segregation): separate the write model (commands producing events) from the read model (projections built from those events). Each side scales and evolves independently.

Strengths

  • Loose coupling — services evolve independently; add new consumers without touching producers.
  • Scalability — asynchronous processing absorbs spikes naturally.
  • Availability & fault tolerance — consumers can be down while events accumulate safely in the broker.

Weaknesses

  • No strict ordering guarantee by default — depends on partitioning/design.
  • Eventual consistency — reads may lag writes; wrong fit for strong transactional requirements.
  • Distributed debugging — tracing one request across many async consumers is hard without correlation IDs.
  • Careful handling required for retries, duplicates, ordering, and failure paths.

Real-World Uses

  • Payments, notifications, analytics pipelines
  • Git (the commit history is an event log)
  • Gaming (rebuilding game state from event history)

Not ideal when you need strong transactional consistency across a write — use a relational design there instead.


Part of the Messaging series.

Related Notes