Event-Driven Architecture: Benefits, Challenges, CQRS & Event Storming
Dean Jain
Senior Staff Software Engineer · Enterprise AI, Data & Cloud Architect
· 7 min read
---
config:
theme: dark
fontSize: 17
themeVariables:
fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart LR
P["📤 Producer"]:::server -->|"event (a fact)"| B[("🔀 Event broker")]:::gate
B --> C1["📥 Consumer"]:::obs
B --> C2["📥 Consumer"]:::obs
classDef server fill:#A8E6D0,stroke:#2FA37C,stroke-width:2px,color:#0F172A
classDef gate fill:#D7C3F2,stroke:#8E5BD0,stroke-width:2px,color:#0F172A
classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
Figure 1: EDA's core shape. Producers publish facts to a broker; consumers react, with no direct knowledge of each other.
Event-driven architecture is, at its heart, an exchange of facts. Instead of services calling each other directly (“do this for me, now”), a service announces something happened and anyone interested reacts. That one change, from commands between services to facts published to a broker, buys genuine resilience and team agility.
It also hands you a new bill. Eventual consistency, harder debugging, and the ever-present risk of an unplanned mess. Understanding both sides is what separates EDA that scales from EDA that becomes a tangle.
TL;DR
- EDA is publishing facts, not making calls. Producers emit events; consumers react through a broker, loosely coupled.
- Three patterns: event notification (something happened, here’s the ID), event-carried state transfer (push the data, consumers cache it), event sourcing (store changes as an append-only event stream).
- The payoff: resilience + agility. A crashed consumer doesn’t take down the producer; new components drop in with little cross-team coordination.
- The price: eventual consistency, dual writes, and debuggability plus the Big Ball of Mud if you don’t identify events well.
- CQRS splits reads from writes so each side scales and evolves independently.
- Find the events before you build anything. Event Storming is the workshop for it, and the clusters it produces are a candidate service map.
1. Three flavors of “event-driven”
“EDA” actually names three patterns that can be used alone or together and conflating them causes confusion:
- Event notification. An event that says something happened, carrying the bare minimum: maybe just an entity ID and a timestamp. Interested consumers then go fetch what they need. Lightweight, but it creates callbacks to the source.
- Event-carried state transfer. The asynchronous cousin of REST. Where REST is pull on demand, this is a push model: data changes are broadcast, and consumers keep their own local cached copies. They never need to call back to the originator to do their work. More data on the wire, but maximum decoupling.
- Event sourcing. Instead of overwriting a record, store every change as an event in an append-only stream. To get an entity’s current state, you replay its events in order. There are no deletes only reversing events which makes the model remarkably easy to scale and audit.
Pick the pattern by how much the consumer needs to know: notification when they’ll fetch details themselves, state transfer when you want them fully independent, sourcing when the history of changes is itself valuable.
2. The infrastructure: queue → stream → store
The three patterns run on three kinds of infrastructure, distinguished by one thing how long events live:
---
config:
theme: dark
fontSize: 17
themeVariables:
fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart LR
Q["📨 Message queue<br/>no retention consumed & gone"]:::obs --> S["🌊 Event stream<br/>+ retention replay from any point"]:::gate
S --> ST["🗄️ Event store<br/>append-only, millions of streams, strong consistency"]:::good
classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
classDef gate fill:#D7C3F2,stroke:#8E5BD0,stroke-width:2px,color:#0F172A
classDef good fill:#BFEFC8,stroke:#3FA34D,stroke-width:2px,color:#0F172A
Figure 2: Retention is the dividing line. A queue forgets, a stream remembers (replayable), an event store is the append-only system of record.
- Message queue. No retention. Events have a limited life; once consumed or expired, they’re gone. Good for fire-and-forget work.
- Event stream. A queue plus retention. Consumers can read from the earliest event, from their last position, or only the new ones, so they can replay. (Kafka is the canonical example.)
- Event store. An append-only repository holding potentially millions of individual streams, with optimistic concurrency for strong per-stream consistency. The system of record for event sourcing.
The benefits flow from loose coupling. Resilience, because a consumer crash doesn’t touch the producer with the broker sitting between them. And agility, because a new feature team drops in a consumer without negotiating an API with everyone else. You also get easy analytics and auditing from the async event trail.
The challenges are the flip side of that same coupling. The system is correct eventually, not instantly. The dual-write problem is genuinely hard, because updating your database and publishing an event atomically is not something you get for free. Debugging is worse too, since a request’s path is now spread across async hops.
Then you have to choose how components collaborate. Choreography, where each component knows its own next step, is decentralized and flexible. Orchestration, where a central orchestrator directs the dance, is centralized and easier to follow.
And the lurking risk is a Big Ball of Mud.
Events make it easier to build a tangle, not harder, if you don’t identify behaviours deliberately.
3. CQRS: split the reads from the writes
Command and Query Responsibility Segregation (CQRS) is EDA’s natural partner. The idea: stop using one model for both writing and reading, and split the object into a command side and a query side:
---
config:
theme: dark
fontSize: 17
themeVariables:
fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart LR
CMD["✍️ Command side<br/>writes → append events"]:::server -->|"events"| PROJ["🔄 Projections"]:::gate
PROJ --> QRY["🔎 Query side<br/>read-optimized store(s)"]:::obs
classDef server fill:#A8E6D0,stroke:#2FA37C,stroke-width:2px,color:#0F172A
classDef gate fill:#D7C3F2,stroke:#8E5BD0,stroke-width:2px,color:#0F172A
classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
Figure 3: CQRS. Writes append events on the command side, and those events build the read-optimized projections the query side serves.
Splitting them lets each side evolve and scale on its own terms. The command side appends events: idempotent, ordered, with no deletes, only reversing transactions. The query side reads from projections you can move to whatever store fits the load, whether that is NoSQL, document or graph.
You get more than scale from that. Different security models per side, reads and writes scaled independently, and different teams owning each with different tech stacks.
Event sourcing has one catch. Replaying every event from the beginning is slow. Rolling snapshots fix it: store the aggregate’s state at a point in time, every N events or on a pivotal event, and replay only forward from there.
4. Finding the events: Event Storming
Everything above assumes you already know what your events are. That is the assumption that quietly sinks most event-driven projects.
Get the events wrong and none of the rest helps. Your topics carry the wrong facts, your consumers subscribe to the wrong things, and the loose coupling you paid for turns into the Big Ball of Mud this article keeps warning about. Bad events are worse than direct calls, because now the mess is asynchronous.
Event Storming is Alberto Brandolini’s workshop format for finding them. It is deliberately low-tech: a long roll of paper, a wall, sticky notes, and everyone who knows something about the domain in the same room. The point is that domain experts and engineers build one picture together instead of trading documents.
It runs in three passes, and the order is the method.
- Domain events, on orange. Everything that happens in the business, written in the past tense: Order Placed, Payment Declined, Shipment Dispatched. Past tense is not a style note. It forces people to describe facts that already happened rather than features they would like, which is exactly the distinction EDA runs on.
- Commands, on blue. What caused each event. Place Order causes Order Placed. Now you can see which actor or system triggers what.
- Aggregates, on yellow. The domain object each command lands on and each event comes out of. Cluster the stickies around these, and the clusters are your bounded contexts.
---
config:
theme: dark
fontSize: 16
themeVariables:
fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
flowchart:
wrappingWidth: 170
nodeSpacing: 22
rankSpacing: 30
---
flowchart LR
C["🟦 Command<br/><i>Place Order</i>"]:::cmd --> A["🟨 Aggregate<br/><i>Order</i>"]:::agg
A --> E["🟧 Domain event<br/><i>Order Placed</i>"]:::evt
E -.->|"clusters become"| BC["🧱 Bounded context<br/>= your service<br/>and its topics"]:::ctx
classDef cmd fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
classDef agg fill:#FFE6A8,stroke:#E0A106,stroke-width:2px,color:#0F172A
classDef evt fill:#F5CBA7,stroke:#CA6F1E,stroke-width:2px,color:#0F172A
classDef ctx fill:#E0D6F5,stroke:#9B7EDE,stroke-width:2px,color:#0F172A
Figure 4: One sticky each way. A command lands on an aggregate and produces a past-tense event, and the clusters that form become your bounded contexts.
That last step is the payoff for an EDA team. The clusters on the wall are a candidate service map, and the orange stickies are a candidate topic list. You are not guessing at boundaries any more; you are reading them off a wall that the business helped build.
Brandolini’s site names four formats, and they answer different questions. Improve picks apart an existing process. Envision explores a new business model. Explore designs a service across business and IT. Design works out software behaviour for one critical process, which is the one to reach for when the output has to become code.
One distinction worth carrying out of the room. Three kinds of event get called “event” and they are not the same. Domain events come from DDD and describe the business. Event-sourcing events record aggregate state changes and are your write model. Integration events cross a service boundary. Conflating them is how a team ends up publishing its internal state changes to the whole company and can never change them again.
5. What you are actually trading
EDA isn’t free architecture. It’s a trade. You swap synchronous certainty for loose coupling, and in return get resilient, independently-evolvable systems at the cost of eventual consistency and harder debugging. Reach for it when that trade pays: when teams need to move independently, and when components must survive each other’s failures. Then lean on Event Storming to find your events, CQRS to separate concerns, and deliberate event design to keep the whole thing from becoming a mud ball.
Further reading
- Building Event-Driven Microservices Adam Bellemare on EDA patterns and infrastructure
- CQRS (Martin Fowler) · Event Sourcing the canonical write-ups
- Introducing EventStorming Alberto Brandolini’s DDD workshop method