Anti-patterns in event modelling - Passive-Aggressive Events
On why event-driven communication should not be only about events.
Have you ever heard phrases like.
Just an update, the milk ran out. Someone finished it and put the empty carton back.
Or
So everyone is aware, the meeting started 15 minutes ago.
Or
Heads up: the coffee machine is empty again.
I’m sure you either heard or used such phrases.
We all know that there’s some hidden intention behind it.
The intention is not to inform, but to trigger a certain action.
Formally, we’re reporting on events to announce the facts, but in practice, we’re using passive-aggressive words. The real intention is to command someone.
We don’t want to inform that the trash bin is full, but we want someone to take it out. We don’t want to inform that the coffee machine requires coffee beans refill, but we want someone to do it.
Passive-aggressive tone is the worst. It’s toxic for both sides of the communication. Usually, it’s just better to ask someone to do it.
The same rule applies in event-driven modelling. We should avoid passive-aggressive communication at all costs.
We should watch out for Passive-Agressive Events. So events that should be commands.
I already warned you in past not to let Event-Driven Architecture buzzwords fool us.
Event-Driven Architecture is an integration architecture style. We’re trying to model our business processes to run smoothly. To achieve that, we prefer a non-blocking communication flow, with things happening in parallel at their own pace. The goal is to achieve autonomous components, reducing the time needed to understand them. That helps maintain, or even replace them as your business evolves.
And events are enablers for that. They notify of what has happened, allowing other components to interpret facts and take the next steps.
But… Let me show one more photo.
It’s parliament, per the official definition: a room full of angry, shouting people.
If we model our communication only in terms of events, our system will look just like that. We’d just announce new facts in a passive-aggressive style and not be interested in what happens next. Oh, wait, are we really not interested? Actually, we are. If someone won’t do what we expect with our information, we’ll be even angrier.
What’s the difference between a command and an event? Both are messages. They convey specific information: a command indicating intent to do something, an event describes what has happened. From the computer’s point of view, they are no different. Only the business logic and the interpretation of the message can distinguish between an event and a command.
And that’s the main difference: commands can be rejected by the command handler. Events can only be ignored.
If we publish an event, we expect one or more consumers to be interested in it. Yet, we don’t know which components will do it. We just broadcast information.
This can easily change into passive-aggressive:
I did my work, now it’s your turn.
And here’s the crucial part. If we always have a single consumer for an event that needs to run the specific logic and expect to get the particular event back, then it should be a command. It’s not an event, we don’t inform. We want some component to take the next specific step and let us know when it’s finished.
Aren’t we making our communication synchronous?
What does it even mean, synchronous or asynchronous?
That’s what Sam Newman discussed in his great talk. The main conclusion is that synchronous vs asynchronous discussion is actually about blocking or non-blocking processing. And that’s much broader topic than the technical solution (so whether we call something in-process via an HTTP endpoint or a messaging system).
It’s a common misconception that events are published asynchronously through a messaging system (e.g. Kafka, RabbitMQ, SQS, WhateverQueue) and commands are sent through synchronous WebAPI. That can be true for a specific solution, but not as the general rule. As said, both events and commands are messages; we can send them through a messaging system or via HTTP (e.g. events via webhooks).
This misleading split came out from our expectation about handling. We expected the command handler to give us the result. For event handler, we don’t expect a specific result. At least in theory.
If we publish a specific event to the messaging system and expect a specific critical path of follow-up events, then we’re not making our communication non-blocking. It’s still sequential. We cannot proceed until the expected sequence occurs.
Whether something is blocking or not is not established by the tools we use, but by how our business process looks.
Speaking about it.
Let’s get back to our favourite E-Commerce Order scenario (read more in Predictable Identifiers: Enabling True Module Autonomy in Distributed Systems).
We could model it so we just publish the OrderConfirmed event and passively-aggressively expect that others will take it from there. So:
The payment module will initiate the payment.
Inventory will start completing shipments.
The notification module will send a confirmation e-mail.
Fraud detection module will check if the order is not rigged.
Once we receive information about a successful shipment or payment registration, we can complete the order.
You may notice two paths for order processing:
Blocking - We need to wait for information about payments and shipments. This is our critical path.
Non-blocking- Order process shouldn’t stop if the notification wasn’t sent or the data warehouse wasn’t able to process events. We’d like that to happen, but it’s expected rather than critical.
Now, both payments may fail (if our customer doesn’t have enough money), and the shipment may not be completed (if it’s Black Friday, and multiple people are competing for the same product).
If that happens, ordering module needs to take action, for instance, do reimbursement if the shipment wasn’t completed, and eventually cancel the order.
If we don’t foresee that and stay in passive-aggressive mode, we tend to forget about “negative” scenarios. it’s too easy to stay in I-Alread-Did-My-Job mode. This will have severe consequences: blocked orders, missed communication, and dissatisfied customers.
We may lear too late that another module can actually say no:
Payment module can say: Man, that’s not going to happen, you’ve already run out of money.
Shipment module can say: Man, I’m sorry, but you weren’t fast enough and we’ve run out of product.
And both of those scenarios will block successful order completion.
How to find such cases? Doing Example Mapping during modelling can be a good option for that.
Most importantly, we need to embrace the fact that some processes require direct, blocking communication, and others don’t. Just like in real life, sometimes it’s just more effective to tell someone to do something. We should avoid micromanagement and aim for autonomy, but not end up with anarchy.
In our case, it’d be better to have a coordinator (workflow, saga, process manager, To-Do List etc.) that publishes the OrderConfirmed event for modules not on the critical path and sends commands like RecordPayment and InitiateShipment.
By that, we’re separating responsibilities and making explicit what should be explicit. This also helps in understanding the business process, as you have a central place to see the critical flow and get proper observability.
Lacking tracing and observability of the business process is one of the most common issues I see in my clients’ projects. As said, if we don’t want to end up with parliament instead of proper communication in our system, we need to be explicit about our intention.
Is that all? Not quite, there’s one more message type we model as events that should not be events.
Gregor Hohpe, in “Enterprise Integration Patterns”, besides Event and Command defines one more message type: Document.
What’s the Document? It’s a state. Or to be precise: self-contained data we have at a certain point in time. We can store it, but we can also publish information about its new value.
That’s probably why Martin Fowler frames it as Event-Carried State Transfer, and I don’t like that term. For me, it’s extremely misleading as, it doesn’t tell what has happened, but what has changed. It just gathers the new version of the state (or the diff).
In my opinion, it’s a variation of State Obsession anti-pattern. Many people fell into that and believe it’s fine to connect the messaging system to the database, use tools like Change Data Capture, and publish it automatically to others. They end up with passive-aggressive communication style:
You have all you need. The whole state is in the events, just interpret it.
How can you reason about what has happened if instead of OrderConfirmed you get OrderCreated, OrderUpdated, OrderDeleted? You’d need to do the diff, compare with previous values, and do the guess about the reason of the specific change.
You deal with Clickbait Events and have a leaking business abstraction. All consumers need to understand the internals of your processing to detect a specific type of change. I wrote about it in detail in Internal and external events, or how to design event-driven API.
Again, the loose coupling of the event-driven processing is only loose for producers; consumers need to adapt. This can lead to hidden coupling, where a change in the producer breaks consumer flows. And that’s the worst type of coupling you can get.
If we’re making commands explicit, we’re also making an explicit relationship between components. It’s no longer flattened to producer <=> consumer, where the producer always shapes the communication. Now, if the other component exposes a command, that’s the driving force behind its behaviour. This helps to shape autonomy. In our case, we could make the Payment Module a generic module with a stable public API for registering payments, and an ordering module that requests them, in accordance with the Shipment Module. Fraud Detection could continue subscribing to events, as it already does. Context Mapping can greatly help in finding those relationships.
TLDR
We tend to be all about events these days, but they’re not the only message types. In our systems, messages take various forms: Events, Commands, and Documents, each serving distinct purposes:
Documents are all about state transitions, which are essential for syncing data across services but missing deeper business insights.
Commands represent a clear intent to act, directed with an expectation of execution, and can be accepted or rejected.
Events are immutable facts, announced without waiting for a response. They’re like broadcasting news, hoping it catches the right ears.
Event-Driven Architectures enable loose coupling, but only for producers. To make consumers loosely coupled, we need to take extra steps, embrace different message types, and have them participate in modelling business processes.
If we go too far with an event-all-the-things communication style, we’ll make our system a room full of shouting people, with a passive-aggressive communication style. Or just aggressive.
In consequence, we won’t know what’s happening in our system, will see only noise, and will have a hard time making it reliable, observable and predictable. We should treat our messages as a communication contract, API and model their flow in a way that shapes our regular communication.
So next time, ask yourself if your event shouldn’t be a command. If it has always had a single consumer and you expect a specific event back, then it’s probably so. It’s all about being clear about the intention, not lying to yourself and others.
I hope this article will equip you with the knowledge to fix that.
If you’re dealing with such issues, I’m happy to help you through consulting, training or mentoring. Contact me and we’ll find a way to unblock you!
See also more in series about event modelling anti-patterns:
Check also more general considerations:
Cheers!
Oskar
p.s. Ukraine is still under brutal Russian invasion. A lot of Ukrainian people are hurt, without shelter and need help. You can help in various ways, for instance, directly helping refugees, spreading awareness, putting pressure on your local government or companies. You can also support Ukraine by donating e.g. to Red Cross, Ukraine humanitarian organisation or donate Ambulances for Ukraine.




