One common pattern in Domain-Driven Design is the use of publish/subscribe messaging to communicate between domains. When Domain Events are created from within a domain, other domains are able to subscribe to these events and take action within their own domains, respectively.
This is not a common pattern in Rails, particularly because of Ruby’s lack of language support for functional programming paradigms that exist in other languages. However, with a nifty framework and the help of Sidekiq, we can get just a little bit closer.
What is a Domain Event?
A domain event is a recorded property in the system that tracks an action that the system performs, and the factors/properties that lead to its creation.
Imagine that we are writing an endpoint that our users will hit, indicating that they want to hail a time-traveling cab. Now the logic to hail a cab is rather complicated and lives in an entirely different area of the codebase, perhaps even in another application. How should we call the other code and ensure that our code is cleanly decoupled?
With our Domain-Driven powers, we’ve been smart enough to segregate our code into different subdomains and bounded contexts, denoted by these two Ruby modules
Example 1: In-process pub-sub event modeling, with a service object.
A simple way to use Wisper is to use it to implement your service objects with Wisper, calling the service from the controller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Note that the
HailDelorean class has powers of event subscriptions now. Our calling code does not have to concern itself with the implementation details of the
HailDelorean service - it merely needs to register handlers for the two possible outcomes,
could_not_hail. Here’s how the service class is implemented:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Handling side effects in subscriber classes
Other side-effects can subscribe to the
HailDelorean events. Let’s say we want to fire an event to Segment analytics tracking. I can create a plain Ruby object that simply needs to implement a method with the same name as the event.
could_not_hail methods on this subscriber class:
1 2 3 4 5 6 7 8 9
And we hook it up by subscribing it to the command handler:
1 2 3 4 5 6 7 8 9 10 11 12
OK, that was a little awkward, doing all that wiring up in the controller. What if we did the wiring globally, within an app initializer?
1 2 3 4 5
This registers a global subscriber for all future instances of
Example 2: Asynchronous events with subscription handlers and Sidekiq
Here’s the real power of Wisper - we can decouple our application domain responsibilities by modeling effects as subscription objects and do them out-of-band of the primary web request thread.
Note that with the
wisper-sidekiq gem, all subscriptions given with an
async: true option flag will automatically execute in an external thread as a Sidekiq job. Let’s take advantage of that now.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
Finally, we add handlers (subscribers) to these domain objects:
1 2 3 4 5 6 7 8 9 10 11
Now let’s link it together with subscriptions:
1 2 3 4
Now our messages between our domains are pulled out of the main request thread, and operate in an asynchronous fashion with Sidekiq as the runner.
Code in our domains are kept clean - note that there are no direct references to the other subdomains within each subdomain. Our app more cleanly segregates the responsibilities between each app, heavy workloads are naturally balanced as they move to worker threads.
Caveats: Beware of overbuilding
If you are on a small app, you probably should go with approach #1. The weight of indirection can be a cognitive load on development, unless you truly need to build async code in #2. The overhead and conceptual complexities of the approach can only be justified with large codebases, or in apps where a domain-centric view (and segregation) of code is present.
Caveats: Event subscriptions can be a tangled mess
Note that the act of wiring can quickly fan out into a spidery mess of handlers - you could even further decouple your handlers by modeling a global event bus as a publisher, and having each domain tap into the bus’ events and figure out how to handle each event on its own.
Caveats: transactional consistency!
If you implement this asynchronously, you’ll have to think about how to deal with transactional consistency. Can you design your data models (and database schema) to support independent updates without any dependencies? How will you handle the case when one domain action fails and the other completes?
You may have to roll your own two-phase commit here, the specifics of which I won’t delve into. However, for most of our applications, we may want to skip the asynchronous and keep our events synchronous.