Guide contents

Lesson 50 of 129

Event-Driven Architecture

Event-driven architecture (EDA) structures a system as components that communicate by producing and reacting to events, usually through a broker. A producer records that something happened and publishes it. Consumers decide independently what to do. The producer does not call the consumers or wait for them.

Messages, events, and commands

A message is a unit of data sent between components through a channel. Messages have different intents.

Event Command
Meaning a fact: something has happened a request: do something
Tense and naming past: OrderPlaced, PaymentCaptured imperative: CapturePayment, SendEmail
Recipients zero to many; the sender does not know them one specific handler
Sender expectation none the action is performed
Can be rejected no, it already occurred yes
Coupling consumers depend on the event; the producer does not depend on consumers the sender knows the handler and its contract
Typical channel pub/sub topic queue

A query requests data and expects a reply; it is typically synchronous.

Commands make the sender responsible for the outcome and are suited to directing a known component. Events let the source announce facts without knowing who cares, which makes it possible to add consumers without changing the producer.

Structure of an event-driven system

graph LR
    O["Order service"] -->|"OrderPlaced"| B["Event broker"]
    B --> I["Inventory service"]
    B --> P["Payment service"]
    B --> N["Notification service"]
    B --> A["Analytics pipeline"]
    I -->|"StockReserved"| B

Event patterns

Event notification

The event says that something happened and carries little data, typically an identifier and type: OrderPlaced { orderId }. A consumer that needs details calls the source service to fetch them.

  • Small events, low coupling on data structure. Example: at 1,000 OrderPlaced events/s with three consumers that each fetch the order, the order service receives 3,000 read requests/s in addition to its own traffic.
  • The producer receives extra load from callbacks, and consumers depend on its availability at processing time.
  • The data the consumer reads is the current state, which may differ from the state at event time.

Event-carried state transfer

The event includes all the data consumers need: OrderPlaced { orderId, customer, items, total }. Consumers keep a local copy of the data they need and never call the source.

  • Consumers are decoupled at runtime from the producer's availability, and read from local data.
  • Events are larger, the data is duplicated across services, and consumers' copies are eventually consistent.
  • Schema changes affect more consumers, because more fields are shared.

Event sourcing

State is stored as the sequence of events, and current state is derived by replaying them. It is a persistence pattern that can also feed other services. See event sourcing and CQRS.

Pattern Payload Consumer fetches from source Coupling to source at runtime
Event notification identifiers only yes higher
Event-carried state transfer full state relevant to consumers no lower

Reliable publication

A service that updates its database and publishes an event faces the dual-write problem: the two operations cannot be made atomic across the database and the broker. If the process fails between them, the database changes and the event is never published, or the reverse. The transactional outbox and CDC pattern solves it by writing the event to an outbox table in the same database transaction and publishing it from there.

Benefits

  • Loose coupling. Producers do not know consumers. New consumers attach without changing the producer.
  • Independent scaling and failure isolation. Consumers scale on their own lag. A consumer outage does not block the producer, and events are retained until it returns.
  • Buffering. Load spikes accumulate in the broker instead of overloading downstream services.
  • Extensibility and auditability. A retained event log supports new features, rebuilding derived data, and audit trails.
  • Natural fit for asynchronous workflows. Long-running business processes proceed step by step. See choreography vs orchestration.

Pitfalls

  • Eventual consistency. After an event is published, consumers' state lags. User interfaces and business rules must accept that.
  • Duplicates and reordering. Delivery is usually at-least-once, so consumers must be idempotent. See idempotent consumers and deduplication.
  • Hidden control flow. A business flow is spread across services reacting to events, so no single place shows the whole process. Tracing, correlation ids, and documented event flows are needed. See distributed tracing and observability.
  • Event schema evolution. Events are public contracts. Breaking changes break unknown consumers. Use additive changes, versioning, and a schema registry.
  • Event storms and cycles. Services that react to each other's events can loop or amplify load.
  • Debugging and testing are harder than with direct calls. Failures may surface far from the cause.
  • Operational overhead of running the broker, monitoring lag, and handling poison messages. See backpressure, retries, and dead letter queues.
  • Overuse. Not every interaction is an event. A user who waits for the answer to a query needs a synchronous call.

When to use

  • Several services react to the same business fact.
  • Producers and consumers should be released and scaled independently.
  • Work can complete after the response returns to the user (emails, indexing, analytics).
  • Bursty inputs need buffering.

Avoid it when the caller needs an immediate result from the operation, when a strong transactional guarantee across the steps is required, or when the team cannot yet operate a broker and trace asynchronous flows.

Interview questions

  • What is the difference between an event and a command? An event is a fact in the past that the producer does not expect anyone to act on, and it can have many consumers. A command asks a specific handler to perform an action and can be rejected.
  • When would you choose event-carried state transfer over event notification? When consumers should not depend on the producer's availability or take extra load on it. The trade-off is larger events, duplicated data, and a wider contract.
  • Why is the outbox pattern needed? Writing to a database and publishing to a broker cannot be one atomic operation. The outbox stores the event in the same transaction as the state change, and a relay publishes it afterwards.
  • What makes event-driven systems hard to debug? The flow is distributed across independent consumers with no central caller. Correlation ids, tracing across message boundaries, and event catalogs restore visibility.