Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 6.4 of 6.8 · Advanced · 28 min read

Event-driven architecture: brokers, mediators and event streams

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

In an event-driven system, parts announce what has happened, and other parts react. In this lesson you'll learn how that works, and how to keep it reliable when messages are lost or repeated.

Think of a busy restaurant kitchen. A waiter pins an order ticket on the rail, and nobody is told what to do. The grill cook sees a steak to cook, the salad station sees a salad, and the pastry chef sees a dessert for later. Each reacts in its own time. If the pastry chef is on a break, the ticket waits on the rail until they return. And adding a new station doesn't change how waiters write tickets. An event-driven system works the same way.

Three words are often used loosely. An event is a fact that has happened, such as order placed. Any number of services can receive it, and the publisher doesn't know who they are. A command is a request addressed to one handler, such as authorise payment. Unlike an event, it can be refused. A message is simply the envelope that carries either one. A queue delivers each message to one consumer, which suits commands. A topic gives every subscriber its own copy, which suits events.

Here's the broker topology, with no central coordinator. The ordering service publishes an order placed event to the orders topic, and carries on. Every interested service reacts in its own time: payments authorises the card, the restaurant's tablet shows the order, notifications sends a confirmation, and analytics updates its dashboards. Payments then publishes its own event, payment authorised, and dispatch reacts by assigning a courier. The workflow exists only as the sum of these reactions. This is called choreography. Adding a fraud-detection service next year means one new subscription, and no change at all to ordering.

In the mediator topology, an order mediator directs the workflow. It knows the steps, their order and what to do on failure. This is called orchestration. It sends commands to each service in turn, and acts on their replies. Choreography couples least, but the whole flow is hard to see. Orchestration makes it visible, at the risk of a bottleneck. Most real systems mix both.

A log-based broker, such as Apache Kafka, keeps events whether or not they've been read. A topic is split into partitions, each an ordered log, and each event has a position called its offset. Consumers join a group. Within one group, each partition is read by just one consumer at a time, so the group shares the work. Another group, such as analytics, reads the same events independently, with its own offsets. Events with the same key, such as one order's number, all land in one partition, in order. But order is guaranteed only within a partition, never across them. And since each partition has one consumer per group, the number of partitions limits parallelism. Twelve partitions keep at most twelve consumers in a group busy.

When a crash leaves it unclear whether a message was processed, something has to give. At-most-once means never twice, but sometimes not at all. That's fine for courier location pings, where the next update replaces the last. At-least-once means never lost, but sometimes processed twice. It's the usual choice for business events. Exactly-once delivery over an unreliable network isn't achievable in general. Systems can only offer an exactly-once effect within a defined boundary. So assume at-least-once, and make every consumer idempotent. Give each event a unique identifier, record processed identifiers in the same local transaction as the change, and skip any you've already seen.

Once other teams consume an event, its structure is a contract, and consumers may read events written months ago. Give each event an identifier, a type, a version and a timestamp, and change it only in compatible ways. Some failures are temporary, such as a database that's briefly unavailable. Retry those a few times with increasing, randomised delays. Others will fail every time, such as a malformed event. This poison message mustn't stall its partition. Move it to a dead-letter queue, and make sure someone watches it. And publish facts, not commands in disguise. Send confirmation email, published on a topic, is a command with its coupling hidden. Publish order placed, and let notifications decide.

The full section also covers four different ways to use events, and event schemas in more depth. Next, in Section six point five, we compare synchronous and asynchronous communication, and API design. See you there.

What you need to know

Definition: reacting to things that happen

In event-driven architecture (EDA), components communicate by producing and reacting to events: records of facts that have happened in the business, such as an order was placed or a courier was assigned. The component that publishes an event does not call the components that react to it and usually does not know they exist. A message broker carries events from producers to consumers, holds them while consumers are busy or down, and lets new consumers be added without changing the producer.

Event-driven designs are not new: graphical user interfaces, operating systems and trading systems have worked this way for decades, and enterprise messaging products predate the web. What changed in the 2010s was scale and durability. Log-based brokers such as Apache Kafka, created at LinkedIn and open-sourced in 2011, made it practical to keep every business event for days or years and to replay them. EDA can be the main style of a system or, more commonly, the way the services of a microservices or service-based system talk to each other.

Events, commands and messages

Three words are often used loosely, and the difference matters. A message is the envelope: any data sent from one program to another through a channel. What the message carries can be an event (a fact, named in the past tense), a command (a request addressed to one handler, named as an instruction) or a query and its reply. Gregor Hohpe and Bobby Woolf's Enterprise Integration Patterns (2003) catalogued these message types and the channels that carry them, and their vocabulary is still the standard one.

Event, command and message

EventCommandMessage (the envelope)
MeaningA fact: something that has happenedA request: please do somethingAny data sent from one program to another through a channel
NamingPast tense: OrderPlaced, PaymentAuthorisedImperative: AuthorisePayment, AssignCourierCarries an event, a command, a query or a reply
ReceiversZero, one or many; the publisher does not know themExactly one intended handlerDepends on the channel: a queue (one) or a topic (many)
Can it be refused?No; it has already happened. Receivers decide how to reactYes; the handler may reject or fail itNot applicable
CouplingPublisher does not depend on consumersSender depends on the receiver's ability and contractDepends on what it carries

Messages travel through two kinds of channel. A queue delivers each message to one of its consumers, which suits commands and work distribution. A topic delivers a copy of each message to every subscriber, which is publish-subscribe (pub-sub) and suits events. A design that sends OrderPlaced to a topic lets Payments, Restaurant, Notifications and Analytics all react, each in its own time; adding a fraud-detection service next year means one new subscription and no change to Ordering.

Broker topology and mediator topology

Richards and Ford describe two topologies for event-driven systems. In the broker topology there is no central coordinator. An initiating event is published; every interested processor reacts and may publish further events, which others react to in turn. The workflow exists only as the sum of these reactions. This is choreography: like dancers who each know their own steps, no one directs the whole.

In the mediator topology an event mediator receives the initiating event and directs the workflow, sending commands to processors in a defined order and acting on their replies. This is orchestration: a conductor knows the score. Mediators range from a simple service with a state machine to a workflow engine such as Temporal, AWS Step Functions or a BPMN engine.

Broker topology: the OrderPlaced flow

The food-delivery OrderPlaced flow in broker topology: Ordering publishes OrderPlaced to an orders topic; Payments, Restaurant, Notifications and Analytics each consume it independently; Payments publishes PaymentAuthorised, which Dispatch consumes to assign a courierOrderingserviceEvent brokerorderstopicpaymentstopicOrderPlacedPaymentAuthorisedpublishPaymentsauthorise cardRestaurantshow on kitchen tabletNotificationssend confirmationAnalyticsupdate dashboardspublishDispatchassign courierDashed: events delivered to subscribers. Each consumer has its own copy and its own position.
In broker topology Ordering only announces the fact; each consumer reacts independently, and Payments' own event drives the next step.

Mediator topology: the same flow, orchestrated

Mediator topology: an order mediator receives the initiating event and sends commands to Payments, Restaurant, Dispatch and Notifications in a defined order, receiving their repliesCustomerplaces orderOrder mediatorknows the steps,their order andwhat to do on failurePayments1 Authorise paymentRestaurant2 Notify restaurantDispatch3 Assign courierNotifications4 Send confirmationCommands go out; replies orevents come back to the mediator
In mediator topology one component owns the sequence, so the workflow is visible and failures are handled in one place.

Choreography or orchestration?

QuestionChoreography (broker)Orchestration (mediator)
CouplingLowest: publishers do not know consumersMediator depends on every participant's contract
Seeing the whole workflowHard: it is spread across services; needs tracing to reconstructEasy: it is written down in one place
Error handlingEach participant must detect and react to failures, often by publishing compensating eventsThe mediator decides retries, timeouts and compensations centrally
Adding a stepAdd a subscriber; nothing else changesChange the mediator
RisksCyclic event chains; nobody notices when a step silently stopsThe mediator becomes a bottleneck or accumulates business logic
Good fitSimple, stable flows; many independent reactions to one factLong, conditional workflows with compensation, such as the order saga

Most real systems mix both: choreography between bounded contexts (Ordering announces facts and does not care who listens) and orchestration inside a context where a multi-step process needs a clear owner.

Four ways to use events

In a 2017 article Martin Fowler pointed out that 'event-driven' is used for at least four quite different patterns, and that conversations go wrong when people mean different ones. Telling them apart is one of the most useful habits an architect can have.

Fowler's four event patterns (2017)

PatternWhat the event carries and whyBenefitCost
Event notificationA small signal that something changed, often just an identifier: OrderPlaced {order_id}. Receivers call back for details if they need them.Very low coupling; the publisher's model stays private.Callbacks add load and a runtime dependency on the publisher; the flow across many events is hard to see.
Event-carried state transferEnough data for receivers to keep their own copy: the full order with items and totals.Receivers work without calling the publisher, even when it is down; lower latency.Data is copied widely and is eventually consistent; larger events; schemas become contracts.
Event sourcingEvery change to an entity's state is stored as an event; the event log is the source of truth and current state is derived by replaying it.Full audit trail; rebuild past states; new views from old history.Complex; replay is hard when results depended on external systems; event schemas must evolve for ever.
CQRSSeparate models (often separate stores) for writing and for reading, frequently fed by events.Each side optimised for its job; suits read-heavy systems with complex queries.Two models to keep in step; often applied where one model would do.

Event notification and event-carried state transfer are about communication between services and belong in this chapter. Event sourcing and CQRS are about how one service stores its data; they are often combined with EDA but are independent choices, covered in Chapter 7.

Event streams and logs: how Kafka works

Traditional message queues delete a message once a consumer acknowledges it. A log-based broker such as Apache Kafka instead stores events in an append-only log and keeps them for a configured retention period (or for ever) whether or not they have been read. Several things follow from that design, and they explain both Kafka's strengths and its traps.

A topic is split into partitions, each an ordered log stored and replicated across brokers. A producer chooses the partition, normally by hashing the event's key, so all events with the same key (the same order ID, say) go to the same partition. Each event in a partition has a sequential position called its offset. Consumers pull events and record the offset they have reached; they can also rewind and replay.

Consumers join a consumer group. Within one group, each partition is read by exactly one consumer at a time, so the group shares the work; different groups read independently, each with its own offsets, which is how pub-sub is achieved. The number of partitions therefore limits the parallelism of one group: a topic with 12 partitions can keep at most 12 consumers in a group busy. (Kafka 4.2, released in 2026, added share groups as a queue-like alternative: several consumers can share one partition and acknowledge messages individually, but ordering within the partition is then not preserved. This section describes classic consumer groups.)

Kafka guarantees ordering only within a partition. Events for one order arrive in the order they were written if they share a key; events for different orders, in different partitions, have no defined order relative to each other. Designs that need a global order must use a single partition (giving up parallelism) or, better, be redesigned so that only per-entity order matters.

Partitions, offsets and consumer groups

A topic with three partitions, each an append-only log with numbered offsets; consumer group payments splits the partitions between two consumers, while group analytics reads all three with one consumerTopic order-events, 3 partitions (key = order ID)P0012345P1012345P2012345offsets: 0, 1, 2 ... new events appended at the rightGroup: paymentsConsumer Areads P0, P1Consumer Breads P2Group: analyticsConsumer Creads P0, P1, P2Order guaranteed within a partitionall events for order 4711 share a key, sothey land in one partition, in orderGroups are independenteach group keeps its own committed offsets;within a group one consumer per partition
Each partition is an ordered log; a consumer group divides partitions among its members, and each group tracks its own offsets.

Delivery guarantees, and what 'exactly once' really means

Because networks and processes fail (Section 6.1), a broker and its clients must choose what happens when a crash or timeout leaves it unclear whether a message was processed. There are three possible guarantees, and the choice is made by when acknowledgements and offsets are recorded.

Delivery guarantees

GuaranteeWhat it promisesHow it arisesTypical consequence
At-most-onceEach message is processed once or not at all; never twice.The consumer records its position (commits the offset or acknowledges) before processing, or the producer never retries.Messages can be lost after a crash. Acceptable for metrics samples or courier location pings, where the next update supersedes the last.
At-least-onceEach message is processed one or more times; never lost.The producer retries until acknowledged, and the consumer commits its position only after processing.Duplicates after crashes, retries and rebalances. The default for most brokers and the usual choice for business events.
Effectively-onceEach message's effect is applied exactly once, even though it may be delivered more than once.At-least-once delivery plus an idempotent consumer, or a transaction that stores the result and the consumer's position together.The practical meaning of 'exactly once' in business systems; requires deliberate design in every consumer.

'Exactly once' needs careful definition. Exactly-once delivery over an unreliable network is not achievable in general: a sender that gets no acknowledgement cannot know whether to resend. What systems can provide is exactly-once effect within a defined boundary. Kafka, for example, has offered an idempotent producer and transactions since version 0.11 (2017), so that a read-process-write cycle from Kafka topics to Kafka topics commits its output and its consumer offsets atomically. That guarantee stops at Kafka's edge. If the consumer charges a card, sends an email or writes to another database, a crash between that action and the offset commit will lead to the action being repeated, unless the consumer itself is idempotent or stores its offsets in the same transaction as its results.

The practical rule for architects is therefore simple: assume at-least-once delivery and make every consumer an idempotent consumer. Give each event a unique identifier; in the consumer, record processed identifiers in the same local transaction as the state change, and skip any event already recorded. Where possible, also make the operation naturally idempotent, for example 'set status to PAID if it is AWAITING_PAYMENT' rather than 'add one payment'.

# Idempotent consumer: processing the same event twice has the effect of once
def handle_payment_authorised(event, db):
    with db.transaction():                       # one local ACID transaction
        inserted = db.execute(
            "INSERT INTO processed_events (event_id) VALUES (%s) "
            "ON CONFLICT (event_id) DO NOTHING",
            [event["event_id"]],
        ).rowcount
        if inserted == 0:
            return                               # seen before: do nothing
        db.execute(
            "UPDATE orders SET status = 'PAID' WHERE order_id = %s "
            "AND status = 'AWAITING_PAYMENT'",
            [event["order_id"]],
        )
    # commit the broker offset only after the transaction has committed
An idempotent consumer: the processed-events record and the state change commit together, so a redelivered event changes nothing.

Event schemas and versioning

Once other teams consume an event, its structure is a published contract, just like an API, and it lasts longer: with a log-based broker, consumers may read events written months ago. A good event has a unique identifier, a type, a schema version, the time it occurred, the identifiers of the entities involved and the business data consumers need. Money is stored in minor units with a currency, never as a floating-point number (Chapter 0).

{
  "event_id": "9f1c2e7a-4b0d-4e55-9a51-0c3d8f2b6a10",
  "type": "OrderPlaced",
  "schema_version": 2,
  "occurred_at": "2026-10-22T18:42:07Z",
  "order_id": "ord-4711",
  "customer_id": "cus-208",
  "restaurant_id": "res-31",
  "items": [{"dish_id": "d-5", "qty": 2, "price_minor": 1150}],
  "total_minor": 2450,
  "currency": "GBP"
}
An OrderPlaced event using event-carried state transfer: identifiers, version, time and the data consumers need.

Schemas should evolve in backward-compatible ways: add optional fields, never remove or rename fields or change their meaning without a new version, and never reuse a field name for something different. Many teams enforce this with a schema format such as Avro, Protocol Buffers or JSON Schema and a schema registry that rejects incompatible changes at publish time. The CloudEvents specification, a graduated Cloud Native Computing Foundation project, standardises the common envelope attributes such as id, source, type and time. Compatibility modes and migration techniques are covered in schema evolution.

Error handling: retries, poison messages and dead-letter queues

Consumers fail for two very different reasons, and they need different treatment. Transient failures, such as a database that is briefly unavailable or a timeout calling another service, usually succeed on retry; retry a few times with exponential back-off and jitter (increasing, randomised delays) so that a struggling dependency is not overwhelmed. Permanent failures, such as a malformed event or one that violates a business rule, will fail every time. Such a 'poison message' must not block the consumer for ever, which on a partitioned log would stall every later event in that partition.

The standard answer is a dead-letter queue (DLQ): after a set number of attempts the message is moved to a separate channel, with the error recorded, and the consumer moves on. A DLQ is only useful if someone watches it: alert on its size, give a named team ownership, provide tools to inspect, fix and replay messages, and decide in advance what ordering guarantees are lost when a message is replayed late. Some teams add one or more intermediate retry topics with longer delays before the DLQ.

Characteristics of event-driven architecture

CharacteristicRatingReason
Scalability and elasticityHighConsumers scale independently and absorb bursts from the broker's buffer; partitions allow parallelism.
ResponsivenessHighThe publisher returns as soon as the event is durably recorded; slow work happens later.
Fault toleranceHighA consumer can be down for minutes and catch up from the broker; failures do not cascade back to the publisher.
ExtensibilityHighNew consumers subscribe to existing events without any change to the publisher.
Data consistencyLowState spreads across services with a delay; readers see stale data for a while.
Testability and debuggabilityLowBehaviour emerges from asynchronous reactions; reproducing a sequence of events across services is hard without good tracing.
Workflow visibilityLow (choreography) to medium (mediator)Nobody owns the end-to-end flow in choreography; a mediator makes it explicit.
SimplicityLowBrokers, schemas, retries, dead-letter handling, ordering and duplicates must all be designed.

When to use it, and common mistakes

Event-driven architecture fits when many parts of a business must react to the same facts, when work can happen after the user's request returns, when load is bursty and must be smoothed, and when new consumers are expected over time. It fits less well when a user needs an immediate, definitive answer that depends on several services (although a synchronous call for the answer plus events for the follow-up is a common hybrid), or when strong consistency across services is required.

Check your understanding

Is ReserveCourier an event or a command, and how would you know?

A command: it is an instruction addressed to one handler (Dispatch), which may refuse it. An event would be named in the past tense, such as CourierReserved.

A Kafka topic has 6 partitions and a consumer group has 10 consumers. How many consumers will be busy?

At most 6. Within a group each partition is assigned to one consumer, so 4 consumers sit idle.

Why does Kafka's exactly-once support not prevent a consumer from sending a confirmation email twice?

Its transactions cover reading from and writing to Kafka topics and committing offsets; an email sent to an external system is outside that boundary, so a crash before the offset commit causes reprocessing unless the consumer is idempotent.

What is the difference between event notification and event-carried state transfer?

Notification sends a small signal, often only an identifier, so receivers call back for details; state transfer includes the data receivers need, so they can keep their own copy and avoid calling the publisher.

Next · 6.5Communication and APIsHow services talk: the coupling created by synchronous and asynchronous communication, REST and HTTP semantics, gRPC, GraphQL and messaging, and the API design practices that let interfaces evolve without breaking consumers.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “Our order events go through Kafka and some customers occasionally get two confirmation emails. What is going on, and how should we design the consumer?”