DEV Community

loading...
Cover image for Intuitive Ruby Event Router

Intuitive Ruby Event Router

ahmadelassuty profile image Ahmad Elassuty ・3 min read

This article explains the motives behind building ruby EventRouter gem. It starts with an example to demonstrate the problem. Then presents how EventRouter can help solve the problem in a clean easy way.


Imagine we are building GitHub Pull Request (PR) page where you can update the PR description, title and state. We are also going to keep track of all of these updates on the PR activity timeline, and make sure we notify the reviewers of any change. For demonstration purposes, the update action might look like:

We first persist the user values into the database. And that is essentially what this action is responsible for. Then, we are performing multiple actions one after another. However, the update action itself should not be responsible for the side effects of a pull request being updated.

Lets inspect what those actions are doing. They are acting upon the fact that a pull request got updated. Each has a different responsibility and work to do. Which means they should be isolated in terms of execution. If one failed, the rest should not. We also need to retry each action independently and take the necessary steps on failure.

One way to refactor this, is to emit a single domain event PullRequestUpdated and we create one consumer per each use case. Lets do a bit of refactoring…

and PullRequestUpdated event class can be defined as:

We moved the logic to another class and that approach has couple of benefits:

  • The new class has only one responsibility, which is to deliver the event to specific set of handlers.

  • Our controller now is smaller and easier to test.

  • Easy to test the PullRequestUpdated class independently.

  • The event logic is now reusable, so if we want to emit the same event when a commit is submitted, well you know what to call.

The controller still needs to wait for the event to be delivered to its destinations, right? But for that specific case, we want to return the response back once the update is finished. So lets create a new publish_async method and use it in the controller instead. We can use a background processing library like Sidekiq but to keep it simple lets use Ruby Thread:

Since the event is processed in background, the previous approach avoids having multiple points of failure while processing the update action.


EventRouter helps you build a similar flow. It supports multiple delivery adapters out of the box and it is also easy to extend and add new delivery backends, e.g RabbitMQ. The controller stays the same and the event can be changed to:

EventRouter will handle the publishing logic for your events. Also it abstracts the delivery backend which you can change when your app needs an upgrade! Here are a few features of EventRouter:

  • Publish events in sync or async way.

  • Deliver events to their destinations using sync or async adapters.

  • Build events with arbitrary arguments.

  • Using Oj serializer you can maintain the arguments structure.

  • Enrich events with extra payload before they are delivered to their destinations.

  • More to come so make sure to check the repo Wiki for more features and details.

Let me know what you think about EventRouter. You can start a discussion on EventRouter Discussions page. Bug reports and pull requests are also welcome on GitHub repo.

Constructive feedback is always welcome. Hope it helps! 👋

Discussion (6)

Collapse
kinnalru profile image
Samoilenko Yuri • Edited

Nice one.
Is there a global listener to catch all events?

Collapse
ahmadelassuty profile image
Ahmad Elassuty Author

Thanks! 🙏

There is not at the moment. Would be great if you explain for what use-case a global listener is needed, and we can drive an API for it 🙂

Collapse
kinnalru profile image
Samoilenko Yuri

It's very useful (and important) to be able to subscribe to all event especially with filtering. It can be used for debugging or logging particular events.
Github ex: log all events(any kind) for specific user, or catch all notifications for particular PullRequest

Thread Thread
ahmadelassuty profile image
Ahmad Elassuty Author

It makes sense to have better support for debugging and logging for sure, but not necessary through global listeners that has filtering functionality, as the filtering can happen as part of the destination handlers (business logic).

Generally, I see the value of having global listeners, but what I'm not sure of is creating global destinations that subscribe to Event instead of Event routing to destinations as adopted by the current API. That will require adopting two completely different APIs. Also as you mentioned, there is already gems doing so, not sure if there is any gem that is backed with a storage system, e.g Redis/Rabbitmq like EventRouter. What do you think?

Collapse
kinnalru profile image
Samoilenko Yuri

Often publish/subsribe systems like ActiveSupport::Instrumentation or whisper gem using string event identifier. So it is very simple to implement global subscription and filtering with regexp matchers or something else.

Forem Open with the Forem app