# Event-driven architecture: events, brokers, and Kafka vs RabbitMQ

> Event-driven architecture explained: events vs commands, queues vs logs, Kafka vs RabbitMQ and the cloud brokers, contracts, sagas and failure modes.

- URL: https://computese.com/event-driven-architecture/
- Author: Duong Quan Nguyen, CEO, Computese
- Published: 2026-10-16
- Updated: 2026-10-16
- Topics: Integrations, Custom software

## In short
- Event-driven architecture connects components through a broker: producers publish facts (events) and consumers react independently. The producer does not call, or even know about, its consumers.
- The name covers four patterns people mix up: event notification, event-carried state transfer, event sourcing and CQRS. For connecting systems the first two matter most, and the hard parts are delivery semantics and contracts.
- A queue deletes a message once it is acknowledged; a log keeps it for a retention window so consumers can replay it. Decide per flow whether you need replay before you pick a product.
- Exactly-once claims need a boundary: Kafka's covers read-process-write loops between Kafka topics, Pub/Sub's covers pull subscriptions in one region. Beyond it, plan for at-least-once delivery plus an idempotent consumer.
- Skip event-driven design for simple CRUD, for flows where the caller needs an immediate answer, and for teams that cannot operate a broker. Choreographed events also need tracing and correlation IDs to stay debuggable.

Event-driven architecture (EDA) is a way of connecting software in which components publish events, records of something that has already happened, to a message broker, and every interested consumer reacts independently, on its own schedule. A producer [does not know which consumers listen](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven), so consumers can be added or upgraded without changing the producer.

That definition leaves the hard parts open, and the term covers four patterns that people mix up. This guide separates the patterns, explains the queue-or-log choice that shapes the rest, compares Kafka, RabbitMQ and the cloud brokers, then covers contracts, sagas, failure modes and when a plain request is better. It sits alongside [our system integration guide](https://computese.com/system-integration/). Product details are as documented by their vendors in October 2026.

## What does event-driven actually mean?

[Martin Fowler's 2017 note on the term](https://martinfowler.com/articles/201701-event-driven.html) records that "when people talk about 'events', they actually mean some quite different things". He separates four patterns:

- **Event notification.** A system sends a small message saying something changed, "often just some id information and a link back to the sender", and a consumer that needs more calls back for the details. A [webhook](https://computese.com/what-is-a-webhook/) delivers the same kind of notification over plain HTTP, with no broker in between.
- **Event-carried state transfer.** The event carries the changed data, so consumers keep their own copy and "don't need to contact the source system in order to do further work". The cost is more copies and more receiver state; the gain is resilience when the source is down and no remote call to read the data.
- **Event sourcing.** Every state change is recorded as an event, and the system state is purely derived from that log. Fowler's example is version control: the commits are the event store and the working copy is the state. It does not have to be asynchronous.
- **CQRS.** Command Query Responsibility Segregation is "the notion of having separate data structures for reading and writing information", and "Strictly CQRS isn't really about events".

For connecting systems, the first two matter most. Event sourcing and CQRS are choices about how one system stores and reads its own data; adopt either, both or neither without changing how systems talk to each other.

![Two rows. Top: a thin envelope travels from one server to another, with a dashed arrow returning to the first server's database. Bottom: a fat envelope carries a document copy onto the second server's own shelf.](https://computese.com/images/blog/event-driven-architecture/notify-vs-state.d625a810d0-1536.webp)

*A thin notification makes the consumer call back; a state-carrying event brings the data with it.*

The four patterns do not settle events versus commands. An event is a fact in the past tense with no intended recipient ("order 1042 was paid"). A command is an instruction aimed at one recipient ("charge order 1042"). Fowler warns about the "passive-aggressive command": the source system "expects the recipient to carry out an action" but styles the message as an event, which hides a real dependency inside a flow that looks decoupled. A quick test: if the flow breaks when one specific consumer is offline, that message was a command.

## Queue or log: how should the broker hold your events?

The brokers below hold messages in one of two ways.

A **queue** hands each message to one of several competing consumers and removes it once the consumer is done; **pub/sub** instead lets several subscribers each receive a copy. RabbitMQ's streams documentation calls this "destructive consume behaviour": [messages are deleted from the queue when a consumer is finished with them](https://www.rabbitmq.com/docs/streams). In Amazon SQS the consumer [deletes the message after processing](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html); if it does not within the visibility timeout (30 seconds by default), the message becomes visible again for another consumer. SQS standard queues promise [at-least-once delivery and best-effort ordering](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html).

A **log** appends each record to a durable, ordered sequence and keeps it for a retention window. In [Kafka's design documentation](https://kafka.apache.org/43/design/design/), a topic is "a set of totally ordered partitions, each of which is consumed by exactly one consumer within each subscribing consumer group at any given time", so a consumer's position is "just a single integer", its offset. A consumer can "deliberately rewind back to an old offset and re-consume data", which "violates the common contract of a queue, but turns out to be an essential feature for many consumers". Records stay until retention expires: `log.retention.hours` [defaults to 168](https://kafka.apache.org/43/configuration/broker-configs/), seven days. [RabbitMQ streams](https://www.rabbitmq.com/docs/streams) bring the same non-destructive model into RabbitMQ, and [Pub/Sub's seek](https://cloud.google.com/pubsub/docs/replay-overview) replays acknowledged messages when you have configured retention.

![Two lanes side by side. Top: a conveyor where a worker lifts each envelope off after stamping it. Bottom: a fixed row of envelopes stays in place while two readers stand at different positions with markers.](https://computese.com/images/blog/event-driven-architecture/queue-vs-log.35daf7a84a-1536.webp)

*A queue deletes what is acknowledged; a log keeps it, so a second reader can start anywhere.*

The model answers questions you have not asked yet:

| Question                                  | Queue (RabbitMQ queues, SQS)                             | Log (Kafka, RabbitMQ streams)                                                  |
| ----------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
| After the consumer acknowledges           | The message is deleted                                   | The record stays until retention expires                                       |
| A second team wants every event           | Give it its own queue, fed by the same exchange or topic | Add a consumer group with its own offset                                       |
| A bad deploy wrote garbage for 20 minutes | Reprocess only what you saved yourself                   | Rewind the offset and read the window again                                    |
| How far one group scales                  | Add consumers to the queue                               | A regular consumer group gets one consumer per partition, so partitions cap it |

Decide per flow whether you need replay before you pick a product.

## Which broker should carry your events?

Kafka and RabbitMQ both changed recently, and this section follows their 4.3 documentation (October 2026). [Kafka 4.0](https://kafka.apache.org/blog/2025/03/18/apache-kafka-4.0.0-release-announcement/) (March 2025) was "the first major release to operate entirely without Apache ZooKeeper". [Kafka 4.2](https://kafka.apache.org/blog/2026/02/17/apache-kafka-4.2.0-release-announcement/) (February 2026) declared share groups ("Kafka Queues") production-ready: [consumers in a share group can outnumber partitions](https://kafka.apache.org/43/design/design/) and acknowledge records individually, so Kafka now has a queue mode beside its log. [RabbitMQ 4.0](https://blog.rabbitmq.com/docs/4.0/whats-new) removed classic queue mirroring, leaving quorum queues and streams as its replicated types.

| Broker             | Model                                                                                                                                                                               | Ordering                                                                                                                                                          | Replay                                                                                                                                                      | Delivery                                                                                                                                                                                       | You operate or configure                                 |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| Apache Kafka       | Partitioned log; share groups add queue-style consumption                                                                                                                           | Total order within a partition; a record key pins related records to one partition                                                                                | Any offset inside retention (default seven days)                                                                                                            | At-least-once by default; exactly-once for Kafka-to-Kafka loops                                                                                                                                | A cluster you run                                        |
| RabbitMQ           | Exchanges route to queues (classic, quorum); streams add a log                                                                                                                      | FIFO dequeue, but [redelivery can change order](https://www.rabbitmq.com/docs/queues)                                                                             | Queues: none. Streams: any offset or timestamp in retention                                                                                                 | At-least-once with acknowledgements; streams can drop duplicate publishes (producer name plus publishing ID)                                                                                   | A cluster you run                                        |
| Amazon SNS + SQS   | A FIFO topic delivers to subscribed SQS queues                                                                                                                                      | Standard queues: best-effort. FIFO topics and queues: [exact order per message group](https://docs.aws.amazon.com/sns/latest/dg/fifo-topic-message-ordering.html) | FIFO topics [archive up to 365 days and replay](https://docs.aws.amazon.com/sns/latest/dg/fifo-message-archiving-replay.html); SQS deletes after processing | Standard queues: at-least-once. FIFO: once, duplicates dropped within [five minutes](https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html)                                        | Cloud service: queues, subscriptions, visibility timeout |
| Amazon EventBridge | Event bus; on default and Classic buses, each rule has up to five targets                                                                                                           | Not documented                                                                                                                                                    | [Archive, then replay to the same bus](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-replay-archived-event.html)                              | By default [retries for 24 hours, up to 185 times](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-retry-policy.html), then drops the event unless a dead-letter queue is set | Cloud service: rules, targets, retry policy              |
| Azure Service Bus  | [Queues](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-queues-topics-subscriptions) for competing consumers, plus topics with subscriptions and filters | FIFO "typically"; [sessions give ordered processing](https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-sessions) (not Basic tier)             | Not documented                                                                                                                                              | At-least-once in peek-lock mode; [duplicate detection](https://learn.microsoft.com/en-us/azure/service-bus-messaging/duplicate-detection) by MessageId (not Basic tier)                        | Cloud service: tier, sessions, duplicate detection       |
| Azure Event Grid   | Delivers events to the endpoints of matching subscriptions                                                                                                                          | [No ordering guarantee](https://learn.microsoft.com/en-us/azure/event-grid/delivery-and-retry)                                                                    | Not documented                                                                                                                                              | At-least-once per matching subscription; retries, then dead-letter or drop                                                                                                                     | Cloud service: subscriptions, retry policy               |
| Google Pub/Sub     | Topics with pull or push subscriptions                                                                                                                                              | [Ordering keys](https://cloud.google.com/pubsub/docs/ordering), publishing in one region                                                                          | Seek to a timestamp or snapshot; needs retention, snapshots last up to seven days                                                                           | At-least-once; [exactly-once](https://cloud.google.com/pubsub/docs/exactly-once-delivery) on pull subscriptions in one region                                                                  | Cloud service: retention, ordering keys, exactly-once    |

"Not documented" means the cited pages do not describe the feature. Three patterns matter more than any cell.

**Ordering is scoped, not global.** Each ordering guarantee in the table holds within a unit: a Kafka partition, a RabbitMQ queue, an SNS FIFO message group, a Service Bus session or a Pub/Sub ordering key. In Kafka the key you choose (customer, order, device) decides which partition a record lands in, and so both what stays in order and how many consumers in a group can work in parallel.

**Replay separates the families, and the gap is narrowing.** If a consumer bug corrupts derived data, a log lets you fix the code and rewind. SNS FIFO topics and EventBridge offer archive and replay too, and AWS says new applications should start with the Custom Event Bus, which ["retains events for replay"](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html).

**Operations are the price.** Running Kafka or RabbitMQ yourself means owning upgrades, storage and capacity. The cloud services replace that with limits you design around, such as [up to five targets per rule](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html) on EventBridge's default and Classic buses. If your consumers are functions rather than long-running services, [our serverless computing guide](https://computese.com/the-future-of-serverless-computing-streamlining/) explains how events, instances and scale to zero fit together.

## What does exactly-once actually promise?

[Kafka's design documentation](https://kafka.apache.org/43/design/design/) lists three semantics: at most once ("Messages may be lost but are never redelivered"), at least once ("Messages are never lost but may be redelivered") and exactly once ("Each message is processed once and only once"). Then it warns: "Many systems claim to provide 'exactly-once' delivery semantics, but it is important to read the fine print."

Since 0.11.0.0 a producer can send messages atomically to several partitions using transactions, so that "either all messages are successfully written or none of them are". A consumer set to `read_committed` returns only committed transactions, where the default, [`read_uncommitted`](https://kafka.apache.org/43/configuration/consumer-configs/), also returns aborted ones. Together they give exactly-once "when reading, processing and writing data on Kafka topics", and the documentation calls Kafka Streams the simplest way to get it. For any other destination, exactly-once "generally requires cooperation with such systems". The documentation's answer for an external store is to let the consumer "store its offset in the same place as its output", so the data and the position change together.

![A producer sends two envelopes along two parallel lanes toward two log segments. A padlock on the lanes holds both envelopes until both arrive, then opens so they enter together.](https://computese.com/images/blog/event-driven-architecture/tx-two-lanes.3d88cf489a-1536.webp)

*A Kafka transaction makes writes to several partitions appear together, or not at all.*

The producer defaults are already the safe ones: [Kafka 4.3's producer configuration](https://kafka.apache.org/43/configuration/producer-configs/) enables idempotence by default and defaults `acks` to `all`. An exactly-once loop adds a transactional ID and two consumer settings:

```properties
# Kafka 4.3 producer defaults: idempotent, durable writes
enable.idempotence=true
acks=all
max.in.flight.requests.per.connection=5

# Also required for an exactly-once loop inside Kafka
# producer (the id is an example)
transactional.id=orders-enricher-1
# consumer
isolation.level=read_committed
enable.auto.commit=false
```

Everything past that boundary is yours. RabbitMQ's [reliability guide](https://www.rabbitmq.com/docs/reliability) states: "Use of acknowledgements guarantees at least once delivery." It adds that consumers "must be prepared to handle deliveries they have seen in the past". Publishers get duplicates too: a broker "might have sent a confirmation that never reached the producer", so a retried publish can arrive twice. Kafka, SQS standard queues, Event Grid, Pub/Sub and Service Bus in peek-lock mode also document at-least-once delivery.

So the consumer must be idempotent: handling an event twice leaves the same result as handling it once. Record each processed event ID in the same database transaction as the state change, and a replayed event is recognized and skipped. Our [dead letter queue guide](https://computese.com/dead-letter-queue/) covers that pattern, the retries in front of it and the replay after it. The managed services state their own limits. Pub/Sub's exactly-once delivery applies "within a cloud region" and only to pull subscriptions, and combined with ordering it limits throughput to "an order of thousands of messages per second".

> [!TIP]
> Read every "exactly-once" label as "at-least-once transport plus exactly-once effect, inside a stated boundary". Ask what the boundary is, then make the consumer idempotent for everything outside it.

## How do you keep events readable as they change?

A request-driven API has a contract. An event stream needs one even more, because the producer may never meet its consumers. Three pieces do the work.

**A common envelope.** [CloudEvents](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md), which [graduated from the CNCF](https://www.cncf.io/projects/cloudevents/) in January 2024, standardizes the metadata around the payload. Four attributes are required: `id`, `source`, `specversion` and `type`. Producers must keep `source` plus `id` unique for each distinct event, and a re-sent duplicate may reuse its `id`, so consumers "MAY assume that Events with identical source and id are duplicates". That pair is your idempotency key.

```json
{
  "specversion": "1.0",
  "type": "com.example.order.paid",
  "source": "/orders",
  "id": "order-1042-paid-1",
  "time": "2026-10-09T17:58:00Z",
  "datacontenttype": "application/json",
  "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
  "data": {
    "orderId": "1042",
    "total": 129.0,
    "currency": "CAD"
  }
}
```

**A schema registry.** Events outlive the code that wrote them, so the payload shape needs versions of its own. [Confluent Schema Registry](https://docs.confluent.io/platform/current/schema-registry/index.html) provides "a REST service for validating, storing, and retrieving Avro, JSON Schema, and Protobuf schemas", and it [checks compatibility before accepting a new version](https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html). The default rule is BACKWARD: consumers on the new schema can read data written with the old one. Confluent's reason suits logs: it is preferred for Kafka "so that you can rewind consumers to the beginning of the topic". The rule also sets the upgrade order: with BACKWARD, "upgrade all consumers before you start producing new events"; FORWARD reverses it.

![Envelopes from a producer pass a checkpoint gate that compares each envelope's label to a registry card. One envelope with a mismatched label is turned back.](https://computese.com/images/blog/event-driven-architecture/contract-gate.c8b353a26d-1536.webp)

*A schema registry turns a payload shape into a checked contract.*

Three habits keep you out of trouble:

1. Add new fields as optional; Confluent lists "add optional fields" among the changes BACKWARD allows. Treat a changed meaning as a new field.
2. When a change cannot be compatible, Confluent suggests you "create a brand-new topic and start migrating applications" instead of mixing incompatible versions in one topic.
3. Use transitive checks, which compare against all previous versions and not only the last, on any topic a consumer might replay from the start.

**Documentation.** [AsyncAPI](https://www.asyncapi.com/docs/tutorials/getting-started) is a specification for defining the interfaces of an async API, "much like OpenAPI (aka Swagger) does for REST APIs", so a consumer team can see what a stream carries without reading your code. Its current version, [3.1.0](https://github.com/asyncapi/spec/releases/tag/v3.1.0), was released in January 2026. We apply the same contract-first discipline to request APIs in [our enterprise API guide](https://computese.com/the-future-of-enterprise-api-development/); here the contract also has to survive replays of old events.

## Choreography or orchestration: who runs the workflow?

Once several services react to each other's events, somebody owns the overall flow, or nobody does. The Azure Architecture Center describes [choreography](https://learn.microsoft.com/en-us/azure/architecture/patterns/choreography) as letting "each service decide when and how to process a business operation, instead of depending on a central orchestrator". Orchestration is the inverse: one component calls the steps and tracks where each transaction stands.

![Two panels. Left: four service boxes pass envelopes to each other in a ring with no centre. Right: one conductor box in the middle sends arrows out to four service boxes.](https://computese.com/images/blog/event-driven-architecture/choreography-orchestration.9ebfc72ba6-1536.webp)

*Choreography spreads the workflow across services; orchestration gives it one conductor.*

|                       | Choreography                                                                | Orchestration                                                                                                      |
| --------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| The workflow lives in | The wiring between services                                                 | One orchestrator's code                                                                                            |
| Strength              | Workflow logic is decentralized; suits independent operations in parallel   | The orchestrator can consolidate the status of a transaction                                                       |
| Cost                  | Observability at scale, failure handling, schema evolution and event storms | Adding or removing services can break its logic; under load it can become a bottleneck and single point of failure |

Fowler's warning lands on the choreography column: a logical flow spread over event notifications is "not explicit in any program text. Often the only way to figure out this flow is from monitoring a live system." Choreography is not free coupling; it is coupling you cannot grep.

When a business operation spans services that each own their data, one database transaction cannot cover it, and the saga pattern fills the gap. In their 1987 paper [Sagas](https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf), Garcia-Molina and Salem describe a long-lived transaction written as a sequence of transactions, where "either all the transactions in a saga are successfully completed or compensating transactions are run to amend a partial execution". A compensating transaction "undoes, from a semantic point of view" what the original did, but "does not necessarily return the database to the state that existed" before. You do not un-charge a card by deleting a row; you issue a refund.

An illustrative order flow: reserve stock, charge the card, book the shipment. If the carrier rejects the address, the saga refunds the charge, then releases the reservation. Each compensating action is itself an event and must be idempotent, because it is retried until it succeeds.

Debugging needs a thread to follow, and Azure's guidance is that a choreographed system must "consistently use distributed tracing and correlation identifiers". Put a correlation ID on the triggering request and carry it through every event and call. The standard format is [W3C Trace Context](https://www.w3.org/TR/trace-context/) (a W3C Recommendation of 23 November 2021): a `traceparent` header with four fields (version, trace ID, parent ID, trace flags) plus `tracestate` for additional vendor-specific trace information. The CloudEvents [distributed tracing extension](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/extensions/distributed-tracing.md) puts the same values on the event, as in the JSON above, so that traces can "span an event-driven system"; there `traceparent` is required and `tracestate` optional.

## What breaks in production?

Start with the failure that stops a whole lane. A consumer that must process a partition in order cannot move past a record it cannot handle. A Kafka consumer group tracks [one offset per partition](https://kafka.apache.org/43/design/design/), so a poison message holds up everything behind it until the application sets it aside. RabbitMQ [quorum queues](https://www.rabbitmq.com/docs/quorum-queues) drop or dead-letter a message after a delivery limit (default 20), and Kafka's share groups count delivery attempts, which enables automated handling of unprocessable records. Whatever the broker, decide how many attempts a message gets before it is parked; [our dead letter queue guide](https://computese.com/dead-letter-queue/) covers the pattern.

![Three parallel lanes of envelopes flow toward three workers. In the middle lane a torn envelope is stuck at the head and envelopes pile up behind it, while the other lanes flow freely.](https://computese.com/images/blog/event-driven-architecture/poison-partition.d0831a4f8e-1536.webp)

*One poison message blocks its whole partition; the neighbouring lanes keep flowing.*

The other usual failures fit in one table:

| Symptom                                           | Likely cause                                                                                 | What to do                                                                                                                                                                                     |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Group lag climbs at peak and never recovers       | Handling is slower than arrival, and a regular Kafka group allows one consumer per partition | Make the handler faster, use a share group (consumers can outnumber partitions), or [add partitions](https://kafka.apache.org/43/operations/basic-kafka-operations/), which can remap keys     |
| Consumption pauses on every deploy                | The group rebalances when members join or leave                                              | Kafka 4.0's new protocol (KIP-848) says goodbye to "stop-the-world" rebalances; consumers opt in with `group.protocol=consumer`. Static membership (`group.instance.id`) also softens restarts |
| A record is applied twice                         | At-least-once delivery plus a lost acknowledgement                                           | An idempotent consumer keyed on the event ID; a broker's dedup window (SNS FIFO: five minutes) covers only that window                                                                         |
| One customer's events apply out of order          | Parallel consumers with no ordering key                                                      | Key by customer or order: Kafka record keys, Pub/Sub ordering keys, Service Bus sessions, SNS message groups                                                                                   |
| A producer deploy breaks consumers you do not own | An unversioned payload change                                                                | A schema registry with BACKWARD compatibility checks each new schema version before accepting it; upgrade consumers first                                                                      |
| Nobody can say what happens when an order arrives | The flow exists only in the wiring (Fowler's notification trap)                              | Correlation IDs, W3C Trace Context, one AsyncAPI document per service and a diagram someone maintains                                                                                          |
| The database updated but the event never went out | A dual write: two systems, no shared transaction                                             | A transactional outbox, or change data capture from the database log ([our CDC guide](https://computese.com/change-data-capture/))                                                                                  |

Saving a record and publishing its event are two writes to two systems, and either can succeed alone. [AWS Prescriptive Guidance](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html) describes the fix: use an outbox table or change data capture. With an outbox, the event is written in the same transaction as the state change and a separate process publishes it, in the order the database was updated. That process might send duplicates, so the guidance recommends an idempotent consumer that tracks processed messages.

> [!WARNING]
> A flow that loses events without raising an error can look healthy on every dashboard until someone reconciles by hand. Alarm on consumer lag and dead-letter depth, and reconcile counts between systems on a schedule, not after a complaint.

## When should you not go event-driven?

- **The flow is simple CRUD.** One writer, one reader, no reactions worth decoupling: a function call is a dependency you can see, step through and refactor. Microsoft says [broker overhead is not justified for straightforward interactions](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven).
- **The caller needs the answer now.** A payment authorization, a login, a stock check at checkout: an event says something happened, and it cannot answer "did it work?". Make the call synchronously, then publish the event about what you did. Request-reply over queues can imitate a call, but [it effectively turns the flow synchronous](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven), with a broker in the middle.
- **A scheduled batch is enough.** Where trading partners already exchange documents on a schedule (the territory of EDI), a batch job may be all you need.
- **The team cannot operate a broker.** Someone must own upgrades, storage, capacity and on-call. If nobody can, start with a cloud queue or topic. Microsoft lists [teams without experience operating distributed asynchronous systems](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven) as a poor fit.
- **Half-finished states must stay invisible.** Sagas compensate; they do not isolate. The 1987 paper notes that "other transactions might see the effects of a partial saga execution". If a business rule forbids anyone seeing a charged but unshipped order, keep the steps in one database transaction. Microsoft likewise flags [business transactions that need strong consistency across services](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven).

## A migration plan you can start this month

1. **Pick one flow that hurts.** Minutes of latency are acceptable, failures currently page someone, and volume is real but modest, such as orders to accounting.
2. **Write the contract before the code.** A CloudEvents envelope, a schema registered with BACKWARD compatibility, one named owner per event type, and a sample payload reviewed by the people who own the data.
3. **Publish through an outbox.** The event is committed in the same transaction as the state change, so one cannot be saved without the other. If the source is a legacy database nobody may touch, attach change data capture instead.
4. **Wrap the consumer before traffic arrives.** An idempotent handler, bounded retries with backoff, a dead-letter queue with an alarm at one message, and a redrive procedure someone has actually run. [Our DLQ guide](https://computese.com/dead-letter-queue/) is the checklist.
5. **Thread a correlation ID end to end** and prove you can trace one order through every hop, in logs or traces, before you add a second flow.
6. **Cut over gradually.** Run the event flow beside the existing integration, compare outputs against the old path daily, and retire it only after a clean month, in the spirit of the strangler fig pattern. A [legacy Java modernization](https://computese.com/work/legacy-java-modernization/) used the same discipline, comparing each slice's screens, database writes and report files with the old system before accepting it.
7. **Rehearse the bad day.** Replay a day of events into a test environment, dead-letter a poison message on purpose, and write down what happened.

This is the shape of the work in our [integrations service](https://computese.com/services/integrations/): contracts, idempotent consumers, dead-letter queues and a runbook, whether the broker is Kafka, RabbitMQ or a managed service. When the event-driven parts live inside a system we are building for you, that is [custom software development](https://computese.com/services/custom-software-development/).

## Key terms
- **Event**: A record that something already happened, such as order 1042 was paid. It is stated in past tense, carries no instruction, and has no intended recipient: any number of consumers may react to it.
- **Command**: A message that tells one specific component to do something, such as charge order 1042. Unlike an event, a command has an intended recipient and an expected outcome.
- **Message broker**: The middleware that accepts messages from producers and delivers them to consumers, such as RabbitMQ, Apache Kafka, Amazon SQS or Google Pub/Sub. It stores messages in between, so the two sides do not have to send and receive at the same time.
- **Topic**: A named channel of related events that producers publish to and consumers subscribe to. In Kafka a topic is a set of partitions; an Amazon SNS FIFO topic delivers its messages to subscribed SQS queues.
- **Partition**: An ordered shard of a Kafka topic. Records are strictly ordered within a partition, and in a regular consumer group one consumer reads each partition at a time, so partitions cap that group's parallelism.
- **Consumer group**: A set of consumers that share the work of reading a topic. Each subscribing group reads every partition, so two groups consume the same events independently; a new group can be set to start from the earliest offset the broker still holds.
- **Offset**: A consumer's position in a log partition, a single integer per partition. Committing an offset is the log equivalent of acknowledging a message, and resetting it backwards is how a replay works.
- **Pub/sub**: Publish/subscribe: a delivery style where a message published to a topic can be copied to several subscriptions (subject to any filters), rather than going to one competing consumer out of a shared queue.
- **Event sourcing**: Recording every state change as an event and deriving the current state from that log, the way version control derives a working copy from commits. It is separate from event-driven communication and can run synchronously.
- **Saga**: A long-running business transaction written as a sequence of smaller transactions, each paired with a compensating transaction. If a step fails, the completed steps are undone in business terms (refund, restock), not rolled back.

## Common questions

### What is event-driven architecture in simple terms?

It is a way of connecting software where, instead of calling each other, components publish events to a broker (order paid, invoice issued) and every component that cares reacts on its own. The producer does not know which consumers are listening, so new consumers can be added without modifying it.

### What is the difference between a message queue and pub/sub?

A queue hands each message to one of several competing consumers and deletes it once acknowledged. Pub/sub lets several subscribers each receive a copy of a message. A log such as Kafka keeps records for a retention window, so a new consumer group can read the history still inside it; an acknowledged queue message is gone.

### Is Kafka or RabbitMQ better for event-driven architecture?

Neither wins in general. Kafka is a partitioned log with independent consumer groups, and since 4.2 it also has production-ready share groups for queue-style consumption. RabbitMQ is a queue broker with exchange routing, and its streams add a log. Choose on the model (queue or log) first, then on who will operate it.

### What is the difference between event-driven architecture and event sourcing?

Event-driven architecture uses events to communicate between components. Event sourcing uses events as one component's own storage and derives its state by replaying them. Martin Fowler's 2017 article lists them as separate patterns, so you can adopt one without the other.

### Does event-driven architecture guarantee exactly-once delivery?

Not across an arbitrary system boundary. Kafka supports exactly-once for read-process-write loops on Kafka topics, and Pub/Sub offers exactly-once delivery within a region on pull subscriptions. Outside such a boundary, plan for at-least-once delivery and an idempotent consumer.

### What is the difference between choreography and orchestration?

In choreography each service reacts to events and emits its own, so the workflow lives in the wiring between services. In orchestration one central component calls the steps and tracks the status of the transaction. Choreography keeps services decoupled but needs tracing to follow the flow; an orchestrator shows the flow but can become a bottleneck or single point of failure.

### When is event-driven architecture a bad idea?

When the flow is simple CRUD, when the caller needs an immediate answer (a payment authorization, a login), or when the team cannot operate a broker and its monitoring. Sagas also compensate rather than isolate, so a flow that must hide half-finished states belongs in one database transaction.

## Sources
1. [What do you mean by "Event-Driven"?](https://martinfowler.com/articles/201701-event-driven.html), Martin Fowler
2. [Event-driven architecture style](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven), Microsoft Azure Architecture Center
3. [Streams and Superstreams (RabbitMQ 4.3)](https://www.rabbitmq.com/docs/streams), RabbitMQ
4. [Amazon SQS visibility timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html), Amazon Web Services
5. [Amazon SQS standard queues](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html), Amazon Web Services
6. [Queues, topics, and subscriptions in Azure Service Bus](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-queues-topics-subscriptions), Microsoft Learn
7. [Design (Apache Kafka 4.3)](https://kafka.apache.org/43/design/design/), Apache Kafka
8. [Broker configs (Apache Kafka 4.3)](https://kafka.apache.org/43/configuration/broker-configs/), Apache Kafka
9. [Basic Kafka operations (Apache Kafka 4.3)](https://kafka.apache.org/43/operations/basic-kafka-operations/), Apache Kafka
10. [Replay and purge messages with seek](https://cloud.google.com/pubsub/docs/replay-overview), Google Cloud Pub/Sub
11. [Apache Kafka 4.0.0 release announcement](https://kafka.apache.org/blog/2025/03/18/apache-kafka-4.0.0-release-announcement/), Apache Kafka
12. [Apache Kafka 4.2.0 release announcement](https://kafka.apache.org/blog/2026/02/17/apache-kafka-4.2.0-release-announcement/), Apache Kafka
13. [What's new in RabbitMQ 4.0](https://blog.rabbitmq.com/docs/4.0/whats-new), RabbitMQ
14. [Quorum queues (RabbitMQ 4.3)](https://www.rabbitmq.com/docs/quorum-queues), RabbitMQ
15. [Queues (RabbitMQ 4.3)](https://www.rabbitmq.com/docs/queues), RabbitMQ
16. [Amazon SNS message ordering details for FIFO topics](https://docs.aws.amazon.com/sns/latest/dg/fifo-topic-message-ordering.html), Amazon Web Services
17. [Amazon SNS message deduplication for FIFO topics](https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html), Amazon Web Services
18. [Amazon SNS message archiving and replay for FIFO topics](https://docs.aws.amazon.com/sns/latest/dg/fifo-message-archiving-replay.html), Amazon Web Services
19. [Amazon EventBridge event bus](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html), Amazon Web Services
20. [Event retry policy and using dead-letter queues (EventBridge)](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-retry-policy.html), Amazon Web Services
21. [Creating replays of archived events in Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-replay-archived-event.html), Amazon Web Services
22. [Message sessions (Azure Service Bus)](https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-sessions), Microsoft Learn
23. [Duplicate detection (Azure Service Bus)](https://learn.microsoft.com/en-us/azure/service-bus-messaging/duplicate-detection), Microsoft Learn
24. [Event Grid message delivery and retry](https://learn.microsoft.com/en-us/azure/event-grid/delivery-and-retry), Microsoft Learn
25. [Order messages (Google Cloud Pub/Sub)](https://cloud.google.com/pubsub/docs/ordering), Google Cloud Pub/Sub
26. [Exactly-once delivery (Google Cloud Pub/Sub)](https://cloud.google.com/pubsub/docs/exactly-once-delivery), Google Cloud Pub/Sub
27. [Consumer and Share Consumer Configs (Apache Kafka 4.3)](https://kafka.apache.org/43/configuration/consumer-configs/), Apache Kafka
28. [Producer configs (Apache Kafka 4.3)](https://kafka.apache.org/43/configuration/producer-configs/), Apache Kafka
29. [Reliability Guide (RabbitMQ 4.3)](https://www.rabbitmq.com/docs/reliability), RabbitMQ
30. [CloudEvents specification, version 1.0.2](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md), CloudEvents (CNCF)
31. [CloudEvents project](https://www.cncf.io/projects/cloudevents/), Cloud Native Computing Foundation
32. [Schema Registry for Confluent Platform](https://docs.confluent.io/platform/current/schema-registry/index.html), Confluent
33. [Schema evolution and compatibility for Schema Registry](https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html), Confluent
34. [Introduction to AsyncAPI](https://www.asyncapi.com/docs/tutorials/getting-started), AsyncAPI Initiative
35. [AsyncAPI specification v3.1.0 release](https://github.com/asyncapi/spec/releases/tag/v3.1.0), AsyncAPI Initiative (GitHub)
36. [Choreography pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/choreography), Microsoft Azure Architecture Center
37. [Sagas (Garcia-Molina and Salem, 1987)](https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf), ACM SIGMOD (copy hosted by Cornell University)
38. [Trace Context (W3C Recommendation)](https://www.w3.org/TR/trace-context/), W3C
39. [Distributed Tracing extension (CloudEvents 1.0.2)](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/extensions/distributed-tracing.md), CloudEvents (CNCF)
40. [Transactional outbox pattern](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html), AWS Prescriptive Guidance
