An event-driven approach to process business use cases in Ruby

Promoting open/close principle and loose coupling between components

Intro

Motivation

Example app

  • A user can create a poll.
  • Once the poll is created a moderator in the backend needs to approve its content and eligibility to be published to the platform.
  • Once the moderator approves the poll it should be published and be visible to the rest of the users.
  • The moderator can reject the poll that a user suggested.
  • The initial creator will be notified by different means on each state of the poll.
  • Other services /components need to be notified about the changes to the poll like a notification service in a distributed environment or an analytics service.

First initial implementation

Private endpoints accessed from admin moderators to publish or reject a poll suggestion
A public endpoint to create a new poll
An endpoint to vote on a poll

The corresponding services implementation can be found below.

CreatePollAction service
Poll state transition service
Vote poll action

In all cases we have to perform some of the following actions once we process a request:

  • Notify the author once the poll is created/published.
  • Notify the moderator once a poll is created.
  • Update some view models once a poll is updated.
  • Update some view models once a poll is voted.

Let’s see some of the pitfalls with the current approach. First of all, each service has to directly call another object to fulfill the use case. This increases the coupling between the components [2, 3]. Each service is not closed for modification. If we need to add or remove a side effect we need to update each action as well. Although the complexity is not big enough in the above code samples, we could also group the related side effects in a proxy object to handle each case: one when a poll is created, published or voted. This would be a simpler approach from the one that we will define below which will add more complexity in our application’s abstraction.

Towards an event-driven design

Handling multiple actions in a domain

Below we will refactor our action services to use an event-driven design instead and raise a domain event once one business action has occurred [6]. This will allow us to decouple each component from knowing what services to call and allow each interested component to subscribe itself to a specific domain event that is raised. We will also add the related supporting code to handle the synchronous or asynchronous processing of a domain event when one occurs.

Below we see a new version of the above code that raises a domain event instead.

Poll state transition service raising a domain event
Create new poll service raising a domain event

Each service is using theraise static method on the EventDispatcher to raise an event. The actual implementation will be shown below but the actual point here is how we decoupled each component. We also added more semantics to our domain by raising an event that tells us what happened in the system. This also opens the possibility to go towards an event driven architecture in a distributed environment which is usually a better architecture in more scalable and highly available systems where we extract different services in self-autonomous deployable units.

The event definition and the event handlers can are shown in the code below:

Event definition and handlers

We could also define an event handler and subscribe directly like so:

class AuthorNotifier
include EventDispatcher
on Events::PollCreated, async: self def self.call(event)
# process event
end
end

The actual event dispatcher implementation is a modified version of Kickstarter's event sourcing demo app [1].

Event dispatch and processing implementation

In the example above we added our own custom code to implement the event dispatching and handling. We could also use some 3rd party libraries to achieve the results like using wisper gem [5, 8]

Conclusion

The biggest drawback though is the increased complexity in our application and the related supporting code that we need to add or implement.

Last but not least, using the design above could be beneficial in more complex use cases where domain driven design is used since the domain events are the preferred way to trigger side effects and communication between multiple root aggregates.

In a future article, we will present a demo app that will use the above concepts in order to achieve an Event-Driven Architecture for our Rails application. We will also follow a component-based architecture in order to simulate the different bounded contexts in our application.

References