Skip to content

Beware! Anti-patterns in Event-Driven Architecture

Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


Event-driven architecture has patterns and common practices that are solutions for various problems. The issue arises when you apply these patterns when you don’t have the problem they solve, or you can avoid having the problem in the first place. Here are some patterns and why the might become anti-patterns in a given context.

YouTube

Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.

Leaking Internals

If each service has it’s own storage/database, then we need to be cautious about leaking schema and data that we don’t want outside our service boundary. We’ve established in the industry that coupling at the database level is a bad idea because it limits our ability to evolve.

We do not want one service to communicate and query another database directly from another service. Rather, we’ll expose some APIs that we can use to version, rate limit, monitor, etc.

But with Event-Driven architecture, it’s not quite as obvious that you’re also coupling at the database if you’re exposing your internal data structures by leaking them out into events.

There’s a difference between inside and outside events.

Inside events are private within a service boundary. We do not want to expose them. In this case, inside events are for within a service boundary. The service will be the producer and the sole consumer of these events. Because of this, you have more flexibility for evolving and changing.

Outside events are public and intended for use by other service boundaries. They are contracts. You will need a very different versioning strategy with them if you do not want to break existing clients. Events become your API, and you need to treat them like one.

Often, inside events are what get published to other service boundaries and you end up leaking internal schema details of your data. This is usually because we’re thinking of events in a data-centric way.

If your UI is very CRUD (Create-Read-Update-Delete) driven, you’ll likely end up publishing events that are derived from CRUD. I call this CRUD by Entity, which is a more coarse/fat event. This means that the event is full of data about all the properties related to an entity, such as CustomerChanged.

Or it can be more granular, and it’s CRUD by Property. So you’re still data-centric, but the event is slim and only contains data related to the properties that changed on the entity. For example, CustomerNameChanged.

This has become more prominent with the rise of CDC (Change Data Capture) tooling. Any changes to your database are monitored by a CDC tool, which then generates an event from those changes and publishes it to a broker (or log).

Then, other service boundaries consume these events. Often, it’s used to keep a local cache of another services data.

This has turned into using events as a form of data distribution.

So when does this become an anti-pattern? If you’re leaking internal implementation details (schema) and don’t realize the coupling you’re creating.

CRUD-driven events are not explicit. CustomerChanged or CustomerNameChanged does not indicate to other consumers why something changed; it simply indicates that data changed. If you’re trying to derive the why from the changed data, you may imply the wrong cause for the event.

If you’re trying to use events for workflow, be explicit with events.

Commands

When you’re only using publish-subscribe pattern, and that’s what your tooling supports, it’s easy to make everything an event. But not the case. Commands are distinct from events. Just because you have a hammer, not everything is a nail… as the saying goes.

Commands invoke behavior, while events notify us that something has occurred. A command can be thought of as a request, while an event can be thought of as a notification. The result of commands are often events.

Commands only have a single consumer. There must be a single consumer. That’s it. They do not use the publish-subscribe pattern. Trying to force everything into publish-subscribe when that’s not the pattern you want will lead you to apply more patterns incorrectly.

Ordering

It’s common to want to process messages in order, especially if you’re using events as a form of data distribution, as I mentioned above. This is because you want to ensure you’re not overwriting data from an old event because it was processed after a new event.

There are different brokers that have different ways of handling this. However, they all have one limitation that you need to be aware of. If you want to process messages in order, then it comes at the cost of concurrency.

A broker might use partitions to manage this (for example, Kafka). This means that a partition only has a single consumer. That consumer may be responsible for more than one partition, but a partition only has a single consumer. This allows the single consumer to process/consume messages in order.

This can limit scale and throughput as you have no means to process messages concurrently. Typically, this is done with the Competing consumer’s pattern. For more, check out my post Competing Consumers Pattern for Scalability.

There are many situations where you might think you need ordered event processing, but you don’t. There are other ways to handle this, such as using sagas, which allow you to handle messages out of order. It’s actually fairly common in the real world, and it’s really a matter of embracing asynchrony. For an example, check out my post Message Ordering in Pub/Sub or Queues.

Queries

When you start getting into Event-Driven Architecture and asynchronous messaging, you start wanting to turn everything into an event. That’s when you start getting into anti-patterns. This is similar to what I mentioned with commands. But not everything is asynchronous. Especially queries.

There’s a pattern used with commands/queues called Request-Reply, but I’ve also noticed it with topics and events. The idea is you have a separate queue (in the case of events, topics) that is used as the reply channel. The initial publisher of the event is then listening to that other channel for the response.

The problem is, it’s a square peg – round hole. Don’t force it.

Queries are naturally request-response. You want to know right now and get a response. Using events and publish-subscribe does not fit this at all.

Anti-patterns in Event-Driven Architecture

Not everything is an event. It’s easy to fall into the trap of these anti-patterns in event-driven architecture. Be cautious when using events as a form of data distribution and the coupling that occurs because of it. Leaking those internal data schemas is no different than having another service interact with your private database and understanding its schema. You’re coupled at the database level, just with events.

Not everything needs to use publish-subscribe or be asynchronous. Commands might be synchronous request responses, or they might be asynchronous. But commands have a different utility than events and different semantics.

Queries are naturally synchronous request responses.

Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.

Leave a Reply

Your email address will not be published. Required fields are marked *