Underrated pattern of an inline event dispatcher

by Paweł Świątkowski
01 Mar 2023

In the Events, event, events article I outlined three possible meanings of using the word “event” in the context of software architecture. Today I would like to dive a bit deeper into the third one – using events as a way to organize the code. We will introduce an inline event dispatcher to make the code using this kind of events even better.

The idea of such an inline dispatcher was in my mind for quite some time. However, it usually was met with a laugh or at least raised eyebrows. You can imagine my surprise when I discovered that other people are also promoting it. It is even included in Vaughn Vernons Implementing Domain-driven Design bookThey are called “ligthweight subscribers” there..

We will start with the code from the last time:

def finalize(order_id)
  transaction do
    order = Orders::OrderFinalization.call(order_id)

To remind, I proposed Orders::OrderFinalization to return a specialized even object which is then passed to other contexts instead of Order entity, which should be private to Orders context.

This code is not bad, but has a fundamental flaw: it has to sit somewhere. You need to have some kind of “upper layer” that coordinates data flow between many contexts. And while there’s nothing wrong with that, it bears certain risks. In my experience inevitably this kind of layer will grow and will start to contain its own logic instead of just being a coordinator. We can avoid that by making this coordination part a bit more rigid.

Emitting and handling events

The first step in removing the coordination layer would be to change Orders::OrderFinalization class.

class Orders::OrderFinalization
  def call(order_id)
    # do the ordery things

    event = OrderFinalized.new(order_id: order.id, ...)

Here we create the event as in the example from the previous article, but instead of returning it, we pass it to a mysterious EventDispatcher. We will see what’s inside in a moment, but first, let’s see the other end. We used the event to trigger something in the Inventory and Loyalty contexts. Let’s take the former and see how we can adapt to the new code architecture.

class Inventory::OrderFinalizedHandler
  def handle(event)
    event[:items].each do |item|
      next unless item[:product_id] # might be shipping or something
      # do something decreasing product availability in the warehouse

We know that the event will include an items array and we know how it will look - this is our internal contract for communication between contexts. Based on that, we iterate over this array and decrease the availability in a way that’s only known to the Inventory area. We would do something similar in the Loyalty area to add loyalty points based on order’s… something (total price? presence of special items?).

Now, how do we connect these two?

The dispatcher itself

An inline dispatcher is a really simple moduleCould be a class as well, but it does not have any state, which is better expressed by the module, which connects events to their handlers. It could look like this:

module EventDispatcher
  def self.dispatch(event)
    handlers(event.class).each do |handler|

  def self.handlers(event_class)
    case event_class
    when Orders::OrderFinalized
      [Inventory::OrderFinalizedHandler, Loyalty::OrderFinalizedHandler]
    # ...

If you want, you could use some magic to auto-infer the name of the handler from the name of the event, but to be honest, I like the explicity here. It acts like a “router of dependencies” between different areas of your application.

What did we gain?

If you are not convinced, this style might not be for you. But let me just quickly list why I think this code architecture is better:

  • It creates a clear distinction between “core” business logic (in our example: finalizing the order) and side-effects (adding loyalty points, adjusting values in the warehouse). If we managed to separate it this way it usually means that the side-effects could be handled asynchronously in the future, we’d just have to prepare the interface and infrastructure for that.
  • We got rid of the shady “coordination layer”, which very often feels unnatural to me.
  • Emitting events is done in the domain code itself. In the previous implementation the domain code created an event object but returned it to the coordination layer. It was not clear why to return an event in the first place.
  • We can still wrap everything in a transaction and we keep the full transactionality of a monolith, while at the same time we are one step closer to more independent modules (context). They are only bound together by a single EventDispatcher and its handlers list.
  • If, at some point in the future, your project starts to call for splitting it up into multiple services, you have almost-ready foundation for it. You know the dependencies, you just need to figure out transactionality (good luck!) or realize that with a few extra steps you don’t need the wrapping transaction at all.

Few extra notes

You should never rely on the return value of the handler or on the order of execution of the handlers. If you do, you are still too coupled. In fact, you can enfoce it with calling each handler in a separate thread and just watching if no thread is throwing exceptions.

This is not exactly legal in terms of Domain-Driven Design. DDD principles are quite clear that business logic in different contexts (aggregates) should not be executed under common transaction, instead it should be asynchronous. Good news is that nobody said we are doing proper DDD - this pattern can be used without it. Or if you want to be closer to the principles, you could simply not write a spanning transaction.

That’s it, that’s the whole idea behind the “inline event dispatcher”. I hope I showed you how this can make your codebase less coupled and stating dependencies more clear.

end of the article

Tags: architecture

This article was written by me – Paweł Świątkowski – on 01 Mar 2023. I'm on Fediverse (Ruby-flavoured account, Elixir-flavoured account). Let's talk.

Related posts: