Chapter 14. Event-Driven Architecture Style
A distributed, asynchronous style built from decoupled event processors that receive and react to events. Highly scalable and performant, adaptable from small apps to large complex ones. Used standalone or embedded in other styles (e.g. event-driven microservices).
Request-based vs event-based
- Request-based model: requests go to a request orchestrator (usually a UI, sometimes an API layer or ESB), which deterministically and synchronously routes them to request processors that read/update data. Good example: “retrieve my order history for the last six months” — a data-driven, deterministic request.
- Event-based model: reacts to a situation and acts on it. Good example: submitting a bid in an online auction — not a request but an event the system must react to (comparing it against other simultaneous bids).
Topology
Two primary topologies:
- Broker — no central mediator; used when you need high responsiveness and dynamic control, with a simple processing flow.
- Mediator — a central coordinator; used when you need control over the workflow.
Broker topology
Message flow is distributed across event processors in a chain-like, broadcasting fashion through a lightweight message broker (RabbitMQ, ActiveMQ, HornetQ). No central orchestration. Four components:
- Initiating event — starts the flow (e.g. “place a bid”, or a life event like changing jobs). Sent to a channel in the broker.
- Event broker — usually federated (multiple domain-based clustered instances), each holding all event channels for its domain. Because the flow is decoupled, async, and fire-and-forget, it typically uses topics in a publish/subscribe model.
- Event processor — accepts an event, performs its task, then asynchronously advertises what it did as a processing event.
- Processing event — broadcast to anyone interested; other processors react and advertise their own processing events, and so on until no one cares about the last one.
Always advertise what you did, even if nobody currently listens. This gives architectural extensibility: a new requirement (e.g. “analyse emails sent to customers”) can hook into the existing email-sent topic with no infrastructure changes and no changes to other processors.
Worked example — retail order entry: OrderPlacement receives PlaceOrder, inserts the order, returns an order ID, and advertises order-created. Three processors react in parallel: Notification (emails the customer, then advertises email-sent that nobody listens to), Inventory (decrements stock, advertises inventory-updated, picked up by Warehouse to reorder), and Payment (charges the card, advertising either payment-applied or payment-denied — Notification listens for the latter to ask the customer to fix their payment). payment-applied triggers OrderFulfillment (pick/pack → order-fulfilled), which Notification and Shipping both react to, and so on through order-shipped.
Think of it as a relay race: once a processor hands off the baton (event), it’s done and free to handle other events. Each processor scales independently, and topics provide back-pressure if a processor slows or fails.
But the broker topology’s weaknesses are significant:
- No workflow control — the flow is dynamic and nobody knows when the business transaction (placing an order) is actually complete.
- Error handling — with no mediator watching, a crashed
Paymentprocessor goes unnoticed; the process gets stuck while everything else (e.g.Inventorydecrementing) carries on as if fine. - No recoverability/restart — no component owns the state of the original request, so the initiating event can’t be resubmitted from where it left off.
| Broker — advantages | Broker — disadvantages |
|---|---|
| Highly decoupled event processors | Workflow control |
| High scalability | Error handling |
| High responsiveness | Recoverability |
| High performance | Restart capabilities |
| High fault tolerance | Data inconsistency |
Mediator topology
Addresses the broker’s shortcomings with a central event mediator that controls the workflow. Components: initiating event, an event queue, the mediator, event channels, and event processors.
The initiating event goes to an initiating-event queue, picked up by the mediator. The mediator knows the steps, generating processing events sent to dedicated channels (usually queues, point-to-point). Processors handle their event and respond back to the mediator — they do not advertise to the rest of the system (unlike broker). There are usually multiple mediators per domain/grouping (reducing single-point-of-failure risk and improving throughput), e.g. a customer mediator and an order mediator.
Mediator implementation depends on event complexity:
- Simple error handling/orchestration → Apache Camel, Mule ESB, Spring Integration (routes written in code).
- Complex conditional/dynamic workflows with rich error handling → BPEL-based engines (Apache ODE, Oracle BPEL Process Manager). BPEL is powerful but complex, usually built with graphical tools.
- Long-running workflows needing human intervention (e.g. a large trade needing manual senior-trader approval) → a BPM engine like jBPM.
Choosing right matters — Camel for long human-driven flows is painful; a BPM engine for simple flows wastes months. Practical recommendation: classify events as simple, hard, or complex, route everything through a simple mediator first, and have it either handle the event or delegate to a more complex mediator.
Worked example — the same order entry, but the Customer mediator drives explicit, mostly serial steps (with parallelism inside a step): step 1 create-order (→ order ID), step 2 concurrently email-customer + apply-payment + adjust-inventory (mediator waits for all three acknowledgements before continuing), step 3 concurrently fulfill-order + order-stock, step 4 ship-order + notify, step 5 final notification — then the mediator marks the flow complete and discards its state.
Because the mediator owns the workflow and state, it can do error handling, recoverability, and restart — e.g. on an expired card it stops the flow, persists state, and resumes from step 3 once payment succeeds.
A key semantic difference: broker processing events are things that have happened (order-created, payment-applied) that others may ignore; mediator processing events are commands (place-order, fulfill-order) — things that need to happen and must be processed.
Downsides: hard to declaratively model complex dynamic flows (often a hybrid broker+mediator handles the dynamic parts); the mediator itself must scale and can bottleneck; processors are less decoupled and performance is lower than broker.
| Mediator — advantages | Mediator — disadvantages |
|---|---|
| Workflow control | More coupling of event processors |
| Error handling | Lower scalability |
| Recoverability | Lower performance |
| Restart capabilities | Lower fault tolerance |
| Better data consistency | Modeling complex workflows |
The choice comes down to workflow control + error handling (mediator) vs higher performance + scalability (broker). Mediator’s performance/scalability are still good, just not as high as broker’s.
Asynchronous capabilities
EDA relies solely on asynchronous communication — both fire-and-forget and request/reply. This separates responsiveness from performance. Example: posting a comment that takes 3,000 ms to run through bad-word/grammar/context checkers. Synchronously, the user waits ~3,100 ms (50 ms in + 3,000 ms + 50 ms out). Asynchronously, the user gets an acknowledgement in ~25 ms while the 3,000 ms work happens in the background. Nothing made the comment service faster (that would be performance — parallelising the parsers, caching); the async path improved responsiveness. The caveat: the sync path guarantees the post; the async path only promises it. If a bad word is found later, you can’t respond inline — though a registered user can be notified out-of-band. Harder cases (e.g. an async stock trade) make error handling the main issue with async.
Error handling — the workflow event pattern
A reactive-architecture pattern giving resiliency without sacrificing responsiveness, via delegation, containment, and repair using a workflow delegate. The event consumer, on hitting an error, immediately delegates it to a workflow processor and moves to the next message — so the queue keeps flowing. The workflow processor tries to repair the message programmatically (deterministic rules or ML to spot anomalies) and resubmits it to the original queue (the consumer treats it as new). If it can’t, it forwards the message to a dashboard queue where a person manually fixes and resubmits it.
Worked example — a trading firm receives a basket of trade orders shaped ACCOUNT,SIDE,SYMBOL,SHARES. One row is malformed: 2WE35HF6DHF,BUY,AAPL,8756 SHARES (the word SHARES appended), causing a NumberFormatException. Since it’s asynchronous there’s no user to fix it inline. The Trade Placement service delegates the error to a Trade Placement Error service, which strips the offending word and resubmits the corrected trade for successful reprocessing.
Consequence: repaired messages are reprocessed out of order. If order matters (a SELL must precede a BUY in the same brokerage account), maintaining order is complex — one approach is to queue any later trades for the same account in a temporary FIFO queue until the errored trade is fixed and processed.
Preventing data loss
Async messaging has several places to lose a message (a dropped message or one that never reaches its destination). Three classic loss points and their fixes, for Processor A → queue → Processor B → database:
- Message never reaches the queue / broker dies before B retrieves it → persistent queues (guaranteed delivery: the broker persists to disk, not just memory) plus synchronous send (the producer blocks until the broker acknowledges persistence).
- B de-queues then crashes before processing → client acknowledge mode (keep the message in the queue and attach the client ID so no other consumer reads it; if B crashes the message survives), instead of the default auto acknowledge.
- B can’t persist to the database → ACID transaction via DB commit, plus last participant support (LPS) which removes the message from the queue only after confirming processing and persistence completed.
Broadcast capabilities
A producer can broadcast an event with no knowledge of who (if anyone) receives it or what they do with it — the highest level of decoupling. Essential to eventual-consistency patterns, complex event processing (CEP), and more. Example: a service publishing the latest stock ticker price simply broadcasts it; many subscribers may react, but the publisher knows nothing of them.
Request-reply
For synchronous needs (an order ID when ordering a book, a confirmation number when booking a flight), EDA uses request-reply messaging (a.k.a. pseudo-synchronous). Each channel has a request queue and a reply queue: the producer sends to the request queue and does a blocking wait on the reply queue; the consumer processes and replies. Two implementation techniques:
- Correlation ID (most common) — the reply’s correlation ID is set to the original request’s message ID, and the producer’s blocking wait uses a message selector matching it. Lets multiple in-flight requests share one reply queue without crossing wires.
- Temporary queue — a dedicated reply queue created per request and deleted when done; no correlation ID needed since the queue is known only to that producer. Simpler, but creating/deleting a queue per request can heavily load the broker at high volume — so the book recommends the correlation ID technique.
Choosing between request-based and event-based
- Request-based — for well-structured, data-driven requests (e.g. retrieving customer profile data) where you need certainty and control over the workflow.
- Event-based — for flexible, action-based events needing high responsiveness, scale, and complex/dynamic processing.
| Event-based advantages | Event-based trade-offs |
|---|---|
| Better response to dynamic user content | Only supports eventual consistency |
| Better scalability and elasticity | Less control over processing flow |
| Better agility and change management | Less certainty over outcome of event flow |
| Better adaptability and extensibility | Difficult to test and debug |
| Better responsiveness and performance | |
| Better real-time decision making | |
| Better reaction to situational awareness |
Hybrid event-driven architectures
EDA is often embedded in other styles rather than used alone — commonly with microservices and space-based architecture, but also event-driven microkernel or pipeline. Adding EDA removes bottlenecks, provides a back-pressure point when requests back up, and improves user responsiveness. Both microservices and space-based architecture use messaging for data pumps (asynchronously sending data to a processor that updates a database) and for programmatic scalability.
Trade-offs
Primarily a technically partitioned architecture — a domain is spread across many event processors tied together by mediators/queues/topics, so a domain change usually touches many of them (hence not domain-partitioned). Number of quanta ranges from one to many, depending on database sharing and request-reply: processors sharing a database belong to one quantum, and a synchronous request-reply dependency ties processors into the same quantum even though messages are async (if the order-ID provider is down, the caller can’t proceed).
Characteristic ratings (1 = poorly supported, 5 = defining strength):
| Characteristic | Rating |
|---|---|
| Performance | 5 |
| Scalability | 5 |
| Fault tolerance | 5 |
| Evolvability | 5 |
| Simplicity | Low |
| Testability | Low |
| Number of quanta | 1 to many |
Reasoning:
- Performance (5) via async communication plus highly parallel processing.
- Scalability (5) via programmatic load balancing of event processors (competing consumers) — add processors as load rises.
- Fault tolerance (5) via highly decoupled async processors with eventual consistency; promises/futures let work be deferred when downstream processors are unavailable.
- Simplicity & testability rate low — non-deterministic, dynamic event flows are hard to test. Request-based flows have known paths; here, “event tree diagrams” can run to hundreds or thousands of scenarios.
- Evolvability (5) — new features slot in via new/existing processors, especially in the broker topology where published messages already expose the data with no infrastructure changes.
Source
Mark Richards & Neal Ford, Fundamentals of Software Architecture, Chapter 14.