Find my posts on IT strategy, enterprise architecture, and digital transformation at ArchitectElevator.com.
Event-driven architectures (EDA) are seeing a marked renaissance, especially in the context of serverless applications. That's great, but it's also not a new topic by any means. Besides my EDA presentation at Distributed Event-Based Systems (DEBS) from 2007, you might recall my ambitious plans to write an EIP volume on Event Patterns about a decade ago, right after Conversation Patterns, ahem.
As EDA isn't a new topic, it behooves us to not be caught celebrating our new discovery without acknowledging past challenges and understanding architecture trade-offs. One such trade-off is coupling. "Decoupled" is a frequently cited benefit of EDAs, routinely showing up in many definitions of EDA: "Decoupled systems that run in response to events". The assumption behind such statements is that "decoupled" is a simple and universally positive thing. But architects know that life isn't that simple. Coupling is a sliding scale and has many dimensions, so perhaps there's more behind events and coupling!
Before we can discuss the magical properties of events, we first want to gain clarity on whether events are something entirely unique, or whether the benefits commonly associated with events exist more broadly. "My chicken laid an egg" is mighty interesting for a person who's only ever seen a chicken but less so for people who know about birds in general (apologies for my poor attempt at making a non-car analogy).
Events are commonly described as a notification of a change in state, or something that took place. Such notifications must be conveyed to other systems, and the natural way to do so is via a message. Whereas some narratives contrast events with messages, EIP takes a clear stance on the relationship between events and messages (Akka and the reactive manifesto appear to agree):
Events are Messages
Denoted in a UML Class Diagram, the relationship looks as follows:
EIP underlines this is-a relationship by calling events Event Message. In hindsight, this naming sets an appropriate context. As Eric Evans of DDD fame pointed out in a recent conversation, an event could be something different depending on the domain. In the domain of integration, it denotes a message. The Message Construction Introduction clarifies the distinction between the interaction style and message semantics (there are actually quite a few hidden gems in these introductions):
Messaging is an interaction style whereas "Event" describes the semantics of a message.
Therefore:
When assessing the properties of event-based solutions, the discussion splits into two parts: 1) properties of messaging in general and 2) those of events in particular (contrasted with commands, for example).
To stretch our poor analogy, events are chickens and messages are birds. They both lay eggs but the chickens may lay the most tasty ones.
Event-based architectures are associated with Publish-Subscribe Channels because multiple recipients might be interested in the occurrence of an event. This stands in contrast to the Point-to-Point Channels commonly used for commands or document messages.
Again, we must delineate which characteristics of an event-driven system derive from the properties of the channel rather than the message intent (such as being an Event). For example, if I place a Command Message on a Publish-Subscribe Channel, would I gain the same benefits?
Now some people might think that placing a command message on a Publish-Subscribe Channel is non-sensical, but life isn't that simple (crocodiles also lay eggs--I promise I'll stop pushing the analogy). Consider the following example:
Does naming a message OrderPlaced
(indicating an event has occurred) or ProcessOrder
(instructing recipients to process the order) make a system more or less coupled?
If an e-mail should be sent to a customer as part of processing an order, a recipient
could be added to perform this function regardless of the event semantics. We commonly
think that the schema of a command is defined by the recipient of the command, e.g.
the e-mail sender has an interface for sending e-mails, but to me that is just common
usage and not an inherent characteristic. So, it may be less about the semantics of
the message than which entity defines the message schema. Hold that thought--we will
come back to it later.
If events on a queue still seem odd to you, consider that Publish-Subscribe Channels are commonly coupled with Point-to-Point Channels to scale out event consumption via a Competing Consumers approach. This combination is so common that GCP's Pub/Sub bundles it into a single service:
Frequently Publish-Subscribe Channels define the application message flow, whereas Point-to-Point Channels are used for operational aspects.
So, you'll find events being transported via a Point-to-Point Channel more frequently than you might expect. And if you use an event-router like AWS EventBridge, you are already sending Events via a Point-to-Point Channel because the service uses several internally.
Now that we better understand what events are and how they're related to channels, we can have a much richer conversation about coupling. My previous blog post laid useful groundwork by enumerating the facets of coupling:
Along these dimensions, we can now compare the coupling of event-based communication with other types of integration. Based on the discussion above, we don't separate by message semantics but by interaction style:
Recalling that coupling indicates change propagation, the table includes typical changes that are relevant for each dimension of coupling. A quick scan reveals that the level of coupling for general message-oriented solutions, which may send Command Messages, is identical for most dimensions.
Thanks to breaking down the problem and the solution space into multiple dimensions, we come to our main insights. First, while there is a big difference between synchronous and asynchronous interaction styles, the difference between messaging in general and event-driven communication appears relatively minor:
Event-Driven Architectures are loosely coupled, but very similar to any asynchronous, message-oriented interactions.
Second, the additional topology decoupling for adding recipients is due to the use of a Publish-Subscribe, not from using event semantics.
Most of the decoupling properties of EDAs derive from the use of Publish-Subscribe Channels, not event semantics.
The key difference in coupling lies in being able to add recipients without affecting existing recipients nor the sender. Two main scenarios make this ability appealing:
Let's look at each one.
Being able to add message recipients without side effects is great for a system that is growing, for example, because it is still under construction or because adding recipients is a natural occurrence over the life of the system. But that convenience comes at a price:
The risk is that EDAs demo very well, but become messy as they grow over time. The second risk is related:
Folks pitching EDA as decoupled may be the ones early in the lifecycle (like vendors) and won't have to live with the consequences of their (coupling) decisions.
Having event hierarchies and distinguishing between internal and external events can help place some sanity back into the event cloud, and some cohesion back into the application.
Having dependencies between components all over the system is likely a poor design choice and should not be considered "loose coupling".
Architects are known to zoom in, so if we look yet more closely at coupling, we find that it not only has dimensions but also a direction. Coupling can be asymmetric, meaning change propagation has a direction. Here we find the real magic of Publish-Subscribe channels: adding subscribers doesn't affect the sender:
In contrast, a Recipient List, which also sends messages to multiple recipients, requires a change to the source to add a recipient. This makes the source subject to change propagation.
In both cases, sending a message (and likewise changing its format or semantics) travels from left to right, meaning a change in the source very much impacts the recipients (sender to recipient coupling). For stable sources (like system services) that's less of a concern, but it can put a major monkey wrench into your "loosely coupled" applications. In short, a Recipient List propagates changes both ways, whereas a Publish-Subscribe Channel does so only in one direction (from sender to receiver).
We do encounter Recipient Lists rather frequently in EDAs. For example, AWS EventBridge rules and targets act as recipient list. So, in a way, using an event broker like EventBridge negates some of the "right-to-left" decoupling benefits of Publish-Subscribe channels. You won't have to modify the sender, but you have to modify a central element nonetheless.
We conclude that:
If you don't have control over the source, Publish-Subscribe channels are ideal.
This property also explains the origin of services like AWS EventBridge. EventBridge evolved from CloudWatch Events, which made system events available for rule-based alerting. Naturally, the sources being AWS services, adding listeners had to free of side effects to the producers.
In your own application (where you have control over sender and receiver), adding recipients free of side effects is a less-pronounced benefit.
One last observation before we wrap up: decisions about topology and interaction styles are closely related to an application's architecture. Message formats and semantics are associated with application and domain-driven design. So could it be that we're back at Architect's Dream, Developer's Nightmare??
Share: Tweet
Follow: Follow @ghohpe SUBSCRIBE TO FEED
More On: INTEGRATION CLOUD EVENTS ALL RAMBLINGS