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 captionsListen 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
- An event is a fact that has happened (past tense, many possible receivers); a command is a request to one handler that may be refused; a message is the envelope that carries either.
- Broker topology spreads a workflow across independent reactions (choreography); mediator topology puts the sequence in one coordinator (orchestration). Choreography couples least; orchestration makes workflows visible and failures easier to handle.
- Fowler distinguishes event notification, event-carried state transfer, event sourcing and CQRS; the first two are ways for services to communicate, the last two are ways for one service to store data.
- In Kafka a topic is split into partitions; events with the same key go to the same partition, ordering is guaranteed only within a partition, and each consumer group divides partitions among its members and tracks its own offsets.
- Delivery can be at-most-once, at-least-once or effectively-once; exactly-once delivery is not achievable in general, so assume at-least-once and make every consumer idempotent.
- Published events are long-lived contracts: give them identifiers, versions and timestamps, evolve them compatibly and check them with a schema registry.
- Retry transient failures with back-off, move poison messages to a monitored dead-letter queue, and never let one bad message block a partition.
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
| Event | Command | Message (the envelope) | |
|---|---|---|---|
| Meaning | A fact: something that has happened | A request: please do something | Any data sent from one program to another through a channel |
| Naming | Past tense: OrderPlaced, PaymentAuthorised | Imperative: AuthorisePayment, AssignCourier | Carries an event, a command, a query or a reply |
| Receivers | Zero, one or many; the publisher does not know them | Exactly one intended handler | Depends on the channel: a queue (one) or a topic (many) |
| Can it be refused? | No; it has already happened. Receivers decide how to react | Yes; the handler may reject or fail it | Not applicable |
| Coupling | Publisher does not depend on consumers | Sender depends on the receiver's ability and contract | Depends 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
Mediator topology: the same flow, orchestrated
Choreography or orchestration?
| Question | Choreography (broker) | Orchestration (mediator) |
|---|---|---|
| Coupling | Lowest: publishers do not know consumers | Mediator depends on every participant's contract |
| Seeing the whole workflow | Hard: it is spread across services; needs tracing to reconstruct | Easy: it is written down in one place |
| Error handling | Each participant must detect and react to failures, often by publishing compensating events | The mediator decides retries, timeouts and compensations centrally |
| Adding a step | Add a subscriber; nothing else changes | Change the mediator |
| Risks | Cyclic event chains; nobody notices when a step silently stops | The mediator becomes a bottleneck or accumulates business logic |
| Good fit | Simple, stable flows; many independent reactions to one fact | Long, 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)
| Pattern | What the event carries and why | Benefit | Cost |
|---|---|---|---|
| Event notification | A 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 transfer | Enough 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 sourcing | Every 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. |
| CQRS | Separate 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
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
| Guarantee | What it promises | How it arises | Typical consequence |
|---|---|---|---|
| At-most-once | Each 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-once | Each 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-once | Each 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
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"
}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
| Characteristic | Rating | Reason |
|---|---|---|
| Scalability and elasticity | High | Consumers scale independently and absorb bursts from the broker's buffer; partitions allow parallelism. |
| Responsiveness | High | The publisher returns as soon as the event is durably recorded; slow work happens later. |
| Fault tolerance | High | A consumer can be down for minutes and catch up from the broker; failures do not cascade back to the publisher. |
| Extensibility | High | New consumers subscribe to existing events without any change to the publisher. |
| Data consistency | Low | State spreads across services with a delay; readers see stale data for a while. |
| Testability and debuggability | Low | Behaviour emerges from asynchronous reactions; reproducing a sequence of events across services is hard without good tracing. |
| Workflow visibility | Low (choreography) to medium (mediator) | Nobody owns the end-to-end flow in choreography; a mediator makes it explicit. |
| Simplicity | Low | Brokers, 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.
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?”