Leaky Event-Based Microservices’ Integration

When Events are Passive-Aggressive Commands

Leaky Event-Based Microservices’ Integration
Photo by Daan Mooij on Unsplash

A few years ago, I worked on a microservices system for a banking client. One of the services I inherited was a shared Communication microservice responsible for sending emails.

At the time, event-driven architecture and choreography were in fashion, so they had been applied very broadly across the estate. That included integration with the communication service.

At first glance, that sounds reasonable enough.

Event-based integration can reduce coupling, and reducing coupling is generally a good thing. So the thinking seemed to be: if events reduce coupling, why not use them everywhere?

The rationale was mainly to avoid temporal and behavioural coupling. Temporal coupling is when one service needs another service to be available at the same time. Behavioural coupling is when one service needs to know which other service has the capability to perform the action it wants.

On that basis, the design leaned heavily towards events for service-to-service interaction, including cases where the real intent was to send an email.

The problem: these were not really events

In these scenarios, there was usually a clear intention from the producer: send an email.

There was also a clear expectation: when this message is emitted, something should act on it and the email should be sent.

That is not the same thing as publishing a business fact and allowing interested parties to react in their own way. It is much closer to what Martin Fowler calls a passive-aggressive command:

“...an event is used as a passive-aggressive command. This happens when the source system expects the recipient to carry out an action, and ought to use a command message to show that intention, but styles the message as an event instead.”

That distinction matters.

If the sender’s real intent is to ask for an action to be carried out, hiding that intent behind an event does not eliminate coupling. It just obscures it.

The coupling has not disappeared. It has moved.

This is where blanket thinking around event-driven integration tends to fall down.

Yes, you may reduce temporal coupling because the sender no longer needs the communication service to be available synchronously.

You may also reduce some explicit behavioural coupling because the sender is no longer directly calling a service called Communication.

But the coupling has not disappeared. It has moved.

The communication service now has to understand all the messages across the wider system that imply “an email should be sent”. It has to know which events matter, what they mean, when they should result in an email, and often how to interpret enough data to construct the message.

That is not low coupling in any meaningful design sense. It is a different, and leakier, form of coupling.

Within the boundary of a single service, this kind of thing can sometimes be acceptable. An internal event may trigger internal behaviour, and the implementation coupling stays inside the boundary where it belongs.

Across service boundaries, it is different.

Once a shared communication service becomes responsible for reacting to lots of upstream events, it starts absorbing knowledge that does not belong to it. It must understand the semantics of events from different business areas. Over time, it becomes a central place where unrelated domain knowledge accumulates.

At that point, the communication service is no longer just sending communications. It is deciding which business situations across the estate should result in communication.

That is a very different responsibility.

And because the service is shared, every new email-triggering scenario tends to require a change there. The same happens when an existing email needs to be changed or suppressed.

So instead of isolating change, the design centralises it.

The events start to change shape

There is another problem that often follows.

If the communication service is expected to send emails purely from published events, those events start carrying more and more data “for convenience”. Names, addresses, template data, links, amounts, statuses — whatever the downstream service needs — start finding their way into the event.

So what should have been a clean expression of a business fact becomes an integration payload shaped around a particular consumer.

At that point, the event is no longer being shaped by the publishing service or the business domain it belongs to. It is being shaped by the implementation needs of a shared technical capability elsewhere.

That is another sign that the boundary is wrong.

The real design question is intent

The key question is not “should services communicate via events or commands?”

The real question is: what is the intent of this interaction?

If the purpose is to publish a business fact that other services may legitimately react to in their own way, then an event can be a good fit.

If the purpose is to ask for an action to be carried out, then pretending the request is an event usually creates confusion and misplaced responsibility.

That does not mean the interaction has to be synchronous. A command can still be sent asynchronously. The point is not sync versus async. The point is being honest about intent.

Too many discussions flatten this into a technology choice when it is really a modelling choice.

Cohesion matters as much as coupling

One thing I would say more strongly now than I would have back then is this: reducing technical coupling is not enough. You also need to preserve cohesion.

A service should have a clear responsibility and change for reasons that belong together.

A shared communication service that changes every time an upstream domain introduces a new email-worthy scenario is not especially cohesive. It becomes a grab bag of notification rules, template decisions, and cross-domain knowledge.

That is the opposite of what you want from a well-bounded service.

So even if an event-driven approach appears loosely coupled on the surface, you still need to ask what kind of coupling has been reduced, what kind has been introduced, and whether the responsibility now sits in the right place.

Conclusion

Event-driven integration is useful, but it is not a universal answer.

Used well, it allows services to react autonomously to business facts. Used indiscriminately, it can obscure intent, bloat messages, and turn shared services into leaky central dependencies.

The real question is not whether the interaction should be synchronous or asynchronous. It is whether the interaction is a fact being published or a request for action.

Not every interaction should be modelled as an event just because events are available.

In the post below, I talk about how I redesigned this integration.

Re-designing a Leaky Microservice Integration
Embracing Behavioural Coupling