Heuristics for choosing bounded context for an event handler
… and check why 5600+ Rails engineers read also this
Heuristics for choosing bounded context for an event handler
Some time ago I was implementing a feature. As part of this I was, of course, writing a bunch of event handlers. At some point, I’ve realized I didn’t put much thought when choosing the bounded context to which the event handlers should belong. It was mostly driven by intuition or some mechanical routine that upfront design.
The code
Let’s consider the popular example, of “Order with shipping” and few different approaches.
Of course, one solution is:
module Shipping
# handler reacting to Ordering::OrderCompleted
class ShipOrderHandler
def call(fact)
service = Shipping::Service.new
service.call(
Shipping::PrepareShipment.new(
order_id: fact.data.fetch(:order_id),
# ...
)
)
end
end
end
The other however is
module Ordering
# handler reacting to Ordering::OrderCompleted
class ShipOrderHandler
def call(fact)
service = Shipping::Service.new
service.call(
Shipping::PrepareShipment.new(
order_id: fact.data.fetch(:order_id),
# ...
)
)
end
end
end
Which one to choose? Both fact and commands are something we usually consider a “public API” (by a public, I mean public for bounded contexts within an organization, not public to the whole world. Or published language, how some would probably call it).
Context maps
The first suggestion of my colleagues was: do what your context map tells you.
In that popular case of some eCommerce, there’s a high chance that Ordering
and Shipping
are in upstream-downstream relation, Ordering
being upstream one.
Therefore, the Shipping should “adjust” to Ordering, so it makes sense that the handler is in the Shipping
bounded context.
On the other hand, we can imagine that we want refund the order when we receive information from the shipping company that the package was destroyed, so we make a handler:
module Shipping
# handler reacting to Shipping::PackageDestroyed
class RefundOrderAfterPackageBeingDestroyedHandler
def call(fact)
service = Ordering::Service.new
service.call(
Ordering::RefundOrder.new(
order_id: fact.data.fetch(:order_id),
# ...
)
)
end
end
end
In this case, we have an event handler in the same BC as published domain event, because again, we want Ordering
to know as little as possible (preferably nothing) about Shipping
.
Process managers
The other suggestion given by Andrzej was: none of them.
Instead of scheduling PrepareShipment
command in either one of these bounded contexts, we can extract a process manager which manages the whole “order flow”.
Why would we like to do that?
Firstly, you end up with no coupling between Ordering
and Shipping
(at least when it comes to that particular flow). The whole coupling is in the OrderFlow
process manager, and this is a place you want to go when you want to understand how the whole flow is working.
Secondly, as told nicely in a talk by Bernd Rucker about process managers, it allows you to achieve less coupled code in more complex scenarios.
Imagine that you want to add pretty packaging if the buyer had a “VIP status”.
In that case, you either need to have information about which buyers are VIP in the Shipping
BC (which sounds like a lot of work to do and adding complexity only to make one conditional work) or you add a conditional in the handler, like so:
module Shipping
class ShipOrderHandler #reacting to Ordering::OrderCompleted
def call(fact)
service = Shipping::Service.new
if fact.data.fetch(:vip_status)
service.call(
Shipping::PreparePrettyShipment.new(
order_id: fact.data.fetch(:order_id),
# ...
)
)
else
service.call(
Shipping::PrepareRegularShipment.new(
order_id: fact.data.fetch(:order_id),
# ...
)
)
end
end
end
end
As a result, you end up with domain logic in the event handler (which is sometimes fine, but it’s always best to have as little of it as possible in the handlers).
By having a process manager for the order flow, process manager have to know about the VIP status of the buyer, but it sounds far more reasonable than forcing Shipping
to know it (especially that there can be some additional actions in the other BCs done only if the buyer is a VIP).
Other …?
Having said that, these were two heuristics, there are possibly more. What are your heuristics when deciding about a place where a given event handler resides? Do you use the ones mentioned above? Share your opinion :)
Thanks to @pawelpacana, @szymonfiedler, and @andrzejkrzywda for the discussion