Software Architecture

What Is Event-Driven Architecture? Building Reactive Systems

What Is Event-Driven Architecture? Building Reactive Systems

Event-driven architecture (EDA) routes state changes between software components as events. A producer publishes a fact and has no idea which consumers will act on it.

Consumers subscribe through an event channel such as a broker or bus and react on their own schedule. Microsoft’s Azure Architecture Center lists real-time processing and high-volume workloads such as IoT as a fit.

The label covers more than one design. Martin Fowler reported in 2017 that a Thoughtworks workshop sorted event-driven systems into event notification, event-carried state transfer, event sourcing and CQRS.

A system called event-driven usually runs at least one of those, so two teams can share the label and still build very different architectures.

What is event-driven architecture?

YouTube player

Components talk to each other through events here, not direct calls. A producer announces that something happened and moves on without knowing which consumers will react.

Consumers pick the event up later, whenever they get to it.

What counts as an event

An event is an immutable record of a state change. An order was placed, a payment failed, a sensor crossed a threshold.

The Apache Kafka documentation describes the structure as a key, a value, a timestamp and optional metadata headers. The payload says what happened, and what to do about it is up to the consumer.

Why one label covers several designs

Martin Fowler reported in February 2017 that a Thoughtworks workshop on the topic found people use the word events for quite different things. The group sorted the usage into these patterns:

  • Event notification
  • Event-carried state transfer
  • Event sourcing
  • CQRS (command query responsibility segregation)

Most systems called event-driven have at least one of these in play, and often several. That’s why two teams can use the same label and describe architectures with little in common.

What drives the global software industry?

Uncover software development statistics: industry growth, methodology trends, developer demographics, and the data behind modern software creation.

Discover Software Insights →

What event-driven architecture is not

People mix it up with event sourcing, which stores every state change as the record of truth. A system can pass events around without keeping any history.

It also isn’t a product. Apache Kafka, RabbitMQ and Amazon EventBridge implement the pattern, but none of them is the pattern. Direct calls aren’t banned either, since queries that need an immediate answer still fit request-response.

Teams often pair EDA with microservices architecture, but the two are separate decisions. A monolith can publish events, and microservices can talk over direct calls.

TIBCO describes event-driven architecture as complementary to service-oriented architecture.

How does an event move through an event-driven system?

YouTube player

A producer detects a state change and publishes an event to an event channel. The channel routes or stores it, and each subscribed consumer processes it independently. The producer carries on without waiting for a reply.

Producers, event channels and consumers

The producer detects a state change and publishes an event describing it, like a checkout service announcing an order. The event channel carries events from producers to consumers. The Azure Architecture Center says it’s often an event broker or an ingestion service, and some products call it an event bus.

Consumers subscribe to event types and react. Particular Software’s documentation points out that a consumer can publish events of its own, so one service often sits on both sides.

One event, step by step

Say the checkout service completes an order and publishes an order-placed event. The broker accepts it and routes it to every subscription on that event type. Inventory, billing and notification services each get their own copy and react when they’re ready.

If one consumer fails, the others don’t notice. The checkout service never hears about it either.

That last part is the real trade. Fault isolation costs you visibility, and Fowler flags the same cost in event notification, where nicely decoupled systems make it easy to lose sight of the larger flow.

How consumers process what arrives

On the consumer side, the Azure Architecture Center describes simple event processing, where an event triggers an action right away, and basic event correlation, which matches a few discrete events by a shared identifier. Further along, complex event processing detects patterns across a series of events, and event stream processing feeds events from a streaming platform into stream processors.

Azure Functions with an Event Grid trigger is Microsoft’s example for the simple end. Azure Stream Analytics covers the complex end.

When a broker is optional

One producer talking directly to one consumer can skip the broker.

Add a second consumer and the broker becomes what keeps the producer from knowing about either one.

Event-driven architecture vs request-response

YouTube player

In request-response, the caller sends a request to a service it knows and waits for the answer. With EDA the producer publishes an event and keeps working, and consumers it never addresses react later.

A RESTful API call over HTTP is the familiar request-response case. The client holds until the server replies.

Coupling, waiting and failure

In request-response the caller has to know the callee and its address, and then it waits. A slow callee slows the caller, and a failed callee fails the request. Want a second receiver? You change the caller.

Event-driven systems work differently. The producer knows only the event type and the channel. Consumers fail and slow down on their own, away from the producer, and a new consumer subscribes without touching the producer.

Request-response is easier to read, though. Follow the call chain and you have the whole flow.

Synchronous answers inside an event-driven system

Event-driven systems handle a need for an immediate answer with request-response messaging. The Azure Architecture Center describes it as two queues, one for requests and one for responses.

Its example is checking a customer’s eligibility before accepting an order.

My rule is simple. If the caller can’t move on without the answer, use request-response. Events are for announcing a fact.

Publish-subscribe and event streaming compared

YouTube player

Publish-subscribe pushes each event to the subscribers that exist at publish time, and after delivery the event is gone. Streaming works differently. Events are appended to a durable log, and consumers read it at their own pace and can read it again.

How publish-subscribe delivers events

The messaging infrastructure tracks subscriptions and sends each published event to every subscriber. The Azure Architecture Center states that a received event isn’t stored in a durable log, so new subscribers don’t see past events.

Delivery becomes a retry problem instead of a storage problem. Azure Event Grid retries a failed delivery with exponential backoff, up to 30 attempts or 1,440 minutes by default, whichever limit is reached first, per current Microsoft Learn documentation.

Event Grid also makes no ordering promise, so subscribers can receive events out of order.

How event streaming stores and serves events

A topic is a named, durable log of events with any number of producers and consumers (Apache Kafka documentation). It’s split into partitions spread across brokers, and events with the same key land in the same partition and get read in the order written.

Consumers in a consumer group divide a topic’s partitions between them, so a streaming consumer scales out by adding members. Events aren’t deleted after consumption, either. They stay until the retention window ends, and that’s what makes replay possible.

Order holds inside one partition. Across partitions Kafka promises no global order, so the choice of event key decides what stays in sequence (Confluent Developer).

Retention and replay in practice

Figures below come from vendor documentation as of October 2026.

PlatformModelReplay and retention
Azure Event Grid (push delivery)Publish-subscribeNo replay. Failed deliveries retry up to 30 times or 24 hours by default
Azure Event HubsEvent streaming1 hour minimum. Maximum 7 days on Standard, 90 days on Premium and Dedicated
Apache Kafka 4.4Event streaming168 hours (7 days) by default, set with log.retention.hours

Replay is a retention window, not a permanent archive. Event Hubs Standard stops at 7 days, so longer history needs Event Hubs Capture, which writes events to Azure Storage or Azure Data Lake.

Which model to pick

Streaming makes sense when new consumers need history or a consumer recovers by replaying, and also when ingestion volume is high (Azure IoT workloads are Microsoft’s example). Publish-subscribe is enough when consumers only react to new events and nobody needs the past.

Explainers disagree on how many patterns exist. SAP’s training material names two main patterns for transmitting events (publish/subscribe and event streaming), while Fowler’s workshop named four.

The two-pattern count describes transport. Wider counts mix in design patterns such as CQRS, which describe how data is modeled, not how events travel.

Which topology controls the event flow: broker or mediator?

Broker topology has no central controller. Each component publishes the next event when it finishes, so the flow emerges from the chain.

Mediator topology puts one component in charge, and that component holds workflow state and handles errors and restarts.

AttributeBroker topologyMediator topology
Control flowNone central, events chain from processor to processorMediator directs each step
Error handlingLeft to each processorMediator manages errors and restarts
CouplingLowHigher
Weak pointNo single view of workflow stateMediator can become a bottleneck or a reliability concern
Best fitRelatively simple flowsMulti-step events that need orchestration

Broker topology

Components broadcast what happened to the whole system, and the others either act on the event or ignore it. The Azure Architecture Center recommends this when the processing flow is relatively simple.

There are fewer moving parts to deploy, and new processors join without changes elsewhere. The catch is that following one business transaction means tracing events across components.

Mediator topology

Mark Richards, in Software Architecture Patterns (O’Reilly, 2015), uses a stock trade to show where a mediator earns its place. A single trade event needs validating, a compliance check, a broker assignment and a commission calculation, in that order.

A central mediator keeps that sequence and decides what happens when a step fails. The price is extra coupling, since a change to one processor often means a change to the mediator.

Choreography and orchestration

The same split appears between services. Choreography works like the broker topology, with no central coordinator, and orchestration works like the mediator topology, with one component directing the steps.

I’d start with the broker topology and add a mediator only when a flow has dependent steps that need central error handling or restart.

Event types: notification, state transfer and domain events

These event types differ in how much data the event carries and who reads it. Notifications carry a signal and little else. State-transfer events carry what consumers need, and domain events name a business fact in the language of the domain.

Event notification

The source tells other systems that something changed and doesn’t care much about the response. The receiver learns the fact, gets minimal detail and calls back to the sender when it needs more (Martin Fowler, 2017).

Coupling stays low and setup is simple. But every callback adds traffic back to the source, and a consumer that fetches the full record right away has swapped one call for a slower one.

Event-carried state transfer

The Azure Architecture Center describes two ways to fill the payload, and you can mix them per consumer.

A full payload means consumers need no lookup against the source. The costs are larger payloads, which raise transport cost and bandwidth use, copies of the data in several systems that drift apart after updates, and contracts and versioning that get harder to manage.

Sending keys only flips most of that. There’s one system of record, so consistency is better, and events are smaller with simpler contracts. Consumers end up querying the source often, which slows them down.

Particular Software’s documentation adds a warning. Bloated event contracts point to service boundaries drawn along technical lines instead of business ones.

Domain events

Microsoft’s .NET microservices guidance defines a domain event as something that happened in the domain that other parts of the same domain, in-process, need to know about. It can be handled synchronously or asynchronously, while an integration event that crosses to other microservices is always asynchronous.

Under domain-driven design, domain experts name these events in their own vocabulary. Order started reads the same to a developer and to the business.

How fine-grained should an event be

Too many fine-grained events saturate the system and hide the overall flow. Too few consolidated events push work onto consumers that didn’t need it.

The Azure Architecture Center’s example is a compliance check that publishes only two event types, compliant and noncompliant, so only the relevant consumers react.

What delivery guarantees do events get?

At-most-once delivery can lose events, at-least-once can duplicate them, and exactly-once avoids both only inside a defined scope.

GuaranteeWhat happens on failureWhat the consumer handles
At-most-onceNo retry, the event is lostGaps in the data
At-least-onceRedelivery until acknowledged, duplicates appearRepeated events
Exactly-onceNo loss, no duplicates inside one systemEverything outside that system

Kafka defaults to at-least-once, according to Confluent’s delivery documentation. RabbitMQ reaches at-least-once only when consumers use acknowledgements, and its reliability guide says that without them loss is possible and only at-most-once holds.

Where exactly-once stops

Kafka added idempotent producers and transactions in version 0.11.0. Exactly-once then holds when a consumer reads one Kafka topic and writes to another, as in Kafka Streams, while other systems depend on Kafka Connect’s offset management (Confluent documentation).

Other vendors draw the line elsewhere.

  • Amazon SQS FIFO queues deliver each message once, and the default quota is 3,000 messages per second per API action with batching or 300 per second without. High throughput mode raises that ceiling (AWS documentation).
  • Google Cloud Pub/Sub makes exactly-once a per-subscription setting. Subscriptions without it run at-least-once.

Each of these covers different ground, and none covers a database write or an outside API call made inside a handler. Confluent’s own documentation warns that systems claiming exactly-once often ignore failures outside the system.

Duplicates: make the consumer idempotent

An idempotent consumer produces the same result when it handles the same event twice. Chris Richardson’s pattern catalog says consumers usually need this, because a message broker can deliver a message more than once.

Record processed event IDs and skip repeats, or lean on upserts instead of inserts. Checking current state before acting works too.

Failed events and dead letters

The Azure Architecture Center’s pattern for failures is an error-handler processor. The consumer sends the problem event aside and keeps working, and the handler retries it or forwards it to a dead-letter queue (DLQ) for an administrator. Events it resubmits are processed out of sequence.

Azure Service Bus dead-letters a message after 10 delivery attempts by default. That limit can’t be disabled, but you can set it to a large number.

Google Cloud Pub/Sub forwards to a dead-letter topic after 5 delivery attempts by default, configurable from 5 to 100. Amazon SQS moves a message to the DLQ when its maximum receive count is exceeded, and the DLQ must match the source queue type.

Ordering across platforms

Ordering is a setting on most platforms, not a default.

Pub/Sub does not guarantee published order unless message ordering is on for the subscription and the publisher sends an ordering key. SQS standard queues offer best-effort ordering, while FIFO queues order within each message group.

Running several instances of one consumer type adds a second ordering problem, as the Azure Architecture Center notes. Particular Software’s documentation gives the practical answer, which is to design business processes so they don’t depend on strict ordering.

Event sourcing, CQRS, sagas and the outbox: what each adds

Event-driven architecture leaves some gaps open, and each of these companion patterns closes one. Event sourcing keeps the history and CQRS separates reads from writes. A saga coordinates a transaction across services, and the transactional outbox makes publishing reliable.

Eventual consistency

After a producer publishes an event, data across services isn’t consistent right away. Consumers process at their own pace, so for a measurable time different parts of the system hold different views of the current state (Azure Architecture Center).

The same guidance calls this a deliberate trade-off. Some workflows favor availability and partition tolerance, others still demand stronger consistency, and downstream reads have to tolerate stale or partly updated data.

Event sourcing

YouTube player

Event sourcing stores the full series of events in an append-only store that acts as the system of record, then rebuilds an entity’s current state by replaying them (Azure Architecture Center).

You get an audit trail, state you can rebuild at any time, and undo through compensating events. What you pay is that the pattern shapes every later design decision and migrating to or from it is expensive. Microsoft’s guidance says the complexity is not justified for most systems.

Don’t adopt it just because the system already passes events. Fowler’s test is whether you can discard the application state at any moment and rebuild it from the log.

CQRS

YouTube player

CQRS splits writes (commands) from reads (queries) into separate models. Separate data stores let each side scale to its own load, but the two stores have to be kept in sync, usually through events the write side publishes.

Reads lag behind writes, so the read side is eventually consistent.

Azure’s guidance notes that CQRS is often paired with event sourcing, though neither requires the other.

Saga

A saga replaces a distributed transaction with a sequence of local transactions. Each step commits in one service and publishes an event that triggers the next, and when a step fails, compensating transactions undo the earlier ones (Chris Richardson, microservices.io).

Sagas reach eventual consistency, not immediate consistency, and every step has to be safe to retry. The compensation path runs rarely, which is why it needs its own tests.

Transactional outbox and change data capture

A service that updates its database and then publishes an event can fail between the two steps. The outbox pattern writes the event to a table in the same database transaction and lets a message relay publish it afterward, so a message is sent if and only if the transaction commits (microservices.io).

The relay can tail the database transaction log for new outbox rows, or a polling publisher can query the outbox table on an interval.

Debezium implements the log-reading route as change data capture. Its outbox event router captures outbox-table changes and routes them to topics, using the aggregate ID as the message key so order holds within a Kafka partition.

The router does not work with the MongoDB connector, which has its own outbox transformation.

When event-driven architecture fits and when it does not

YouTube player

Event-driven architecture fits when several subsystems must react to the same events, when work runs in near real time or at high volume, and when producers and consumers need to scale on their own. It fits poorly when a transaction needs strong consistency, when synchronous calls already meet the requirements, or when the team has never run an asynchronous system.

That is the Azure Architecture Center’s guidance as of its March 2026 update.

On the plus side, new consumers join without changes to producers or to other consumers, and there are no point-to-point integrations to maintain. Consumers react as events occur, and each subsystem keeps its own view of the event stream.

The costs are real, and a few of them only show up in production.

  • Eventual consistency windows between a write and the read that shows it
  • Duplicate and out-of-order events to handle
  • No shared call stack, so tracing a failure takes a correlation ID in every event
  • Separate deployments, so a schema change breaks consumers that do not know the new shape
  • Events visible to many components, so payload contents need an assume-breach review

Where event-driven architecture pays off

YouTube player

It pays off when two or more subsystems process the same events, or when you need real-time reaction with minimal lag. Pattern matching or aggregation over time windows is another good case, and so is high-volume, high-velocity data such as IoT telemetry.

That last case is where independent consumers earn their keep for software scalability. A busy consumer adds instances without touching the producer.

Where it fails

YouTube player

Azure’s guidance says the style doesn’t repay its cost in a few situations. Synchronous calls may already meet latency and throughput needs, or a business transaction may need strong consistency across services. The team might also have no experience operating distributed asynchronous systems.

Teams skip the first one most often. For straightforward interactions, a monolithic architecture with direct calls is cheaper to run than a broker plus asynchronous error handling.

For each interaction, ask these questions.

  1. Do two or more subsystems need the same fact when it happens?
  2. Can the user-facing result tolerate a delay between a write and the read that shows it?
  3. Does someone on the team own broker operations, dead-letter handling and tracing?

All yes answers favor events. A single no is a reason to keep that interaction synchronous.

How to build an event-driven system, step by step

The work starts with business events and ends with a monitored system.

  1. Name the events by listing the state changes the business cares about, each as a past-tense fact such as order placed or payment failed.
  2. Write the contract before the first consumer ships by choosing an envelope and a schema format and setting a compatibility rule. It goes wrong when a producer renames a field consumers still read.
  3. Pick the channel, publish-subscribe or streaming, and then a platform. Teams get this wrong when they buy replay they never use and inherit the operations bill.
  4. Build producers so they publish only after the state change commits.
  5. Build consumers that acknowledge after processing, handle duplicates and route failures to a dead-letter destination.
  6. Instrument the flow by putting a correlation ID and trace context in every event, and alert when the dead-letter destination grows.

Choosing an event broker

YouTube player

Kafka is the default name in these conversations, but a managed queue or bus is the simpler operation until replay or throughput forces a change. The platforms differ on model and replay behavior.

RabbitMQ works as a queue-based broker. Quorum queues and streams replaced mirrored classic queues, which RabbitMQ 4.0 removed, and its documentation points to streams for repeatable reads and large fanouts.

Amazon SNS with SQS fans one message out as identical copies to several SQS queues. Amazon EventBridge uses rules to route events on a bus to targets, and archived events can be replayed, but only onto the bus they came from.

EventBridge also counts as the serverless architecture option, so there’s no broker cluster to size or patch.

Defining and versioning event contracts

CloudEvents, a graduated CNCF project, standardizes the envelope. Its specification requires four attributes on every event (id, source, specversion and type), and the producer keeps the source plus id pair unique for each distinct event.

That pair works as a deduplication key for an idempotent consumer.

Confluent Schema Registry checks each new schema version against the previous one. Its default compatibility type is BACKWARD, not BACKWARD_TRANSITIVE, and Confluent says the main reason is that BACKWARD lets you rewind consumers to the start of a topic (Confluent documentation).

In Avro, a new field needs a default value to stay backward compatible.

AsyncAPI 3.0.0, released in December 2023, documents message-driven APIs with OpenAPI-like syntax. Its operation reply object describes the response destination for request-reply over queues.

Tracing events across services

Include a correlation ID in every event so downstream consumers and logging can connect related operations into one trace. The Azure Architecture Center advises planning for it from the start, because retrofitting observability into a decoupled system is harder than building it in.

OpenTelemetry’s messaging conventions specify the mechanism. The producer attaches a message creation context to each message, and consumer traces cannot be linked to producer traces unless every component propagates that context.

As of October 2026, the messaging conventions carry Development status, and OpenTelemetry tells existing instrumentations not to change the version they emit by default until the conventions stabilize.

Event-Driven Architecture FAQ

How is an event different from a command?

A command tells one receiver to act and expects an outcome. An event states that something already happened, and zero, one or many consumers can react while the producer expects nothing back.

A message is only the envelope. It can carry either one.

Is event-driven architecture an architecture or a pattern?

Sources use both terms. Mark Richards calls it an architecture pattern, while the Azure Architecture Center files it as an architecture style, a family of designs that constrain which elements appear and how they connect.

The label changes little in practice, since the style combines with others.

Is event-driven architecture the same as microservices?

YouTube player

No. Microservices split a system into independently deployable services, while event-driven architecture defines how components communicate.

The Azure Architecture Center lists microservices, pipes and filters, and event sourcing as typical combinations.

Do you need Kafka for event-driven architecture?

No. Any broker or bus that routes events works, including Azure Event Grid, Amazon EventBridge, RabbitMQ and Google Cloud Pub/Sub.

Kafka earns its place when consumers need to replay history from a durable log.

How do you test an asynchronous event-driven system?

Test in layers, because end-to-end checks across decoupled components take more deliberate strategies than a synchronous call chain, per the Azure Architecture Center.

Feed each consumer recorded events, deliver one event twice to prove idempotency, and keep integration testing for a few flows that wait for the outcome.

Can event-driven architecture run serverless on AWS, Azure or Google Cloud?

Yes, all three clouds support it. Amazon EventBridge is a serverless event bus, and Azure Functions run code from Event Grid or Service Bus triggers.

Google Cloud’s Eventarc routes events in the CloudEvents format.

Moving One Workflow to Events Without a Rewrite

Event-driven architecture enters an existing system one workflow at a time, starting with an interaction where several subsystems need the same fact and a delay between write and read is acceptable.

Publish events beside the current synchronous call first. Then add one idempotent consumer with a dead-letter destination, and put the correlation ID in before the second consumer arrives.

Producers come first because consumers need a reliable source, and the correlation ID goes in early because a second consumer multiplies the paths to trace.

The trade-off is running both paths in parallel until the consumer matches the synchronous result, plus a broker to operate in the meantime.

This sequence holds as of October 2026. It changes if the first workflow needs strong consistency across services, in which case that workflow stays synchronous.

Bogdan Sandu

Stay sharp. Ship better code.

Every week: one curated article, one tool worth knowing, one tip you can use tomorrow. No noise, no padding.