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

Promoting open/close principle and loose coupling between components

Intro

I’m a big fun of event-driven architecture and how different components in a system react to different state changes in a domain. Mostly because it promotes loose coupling between components and services. Sometimes it's also a good paradigm to pipe and filter architecture since, for instance, the consumer of an event might just have the responsibility to filter, transform and forward the event to another component of the system. Moreover coming from distributed environments and architectures, an event-driven approach extends horizontal scalability making them more resilient to failures promoting high availability and eventual consistency [6, 7].

Motivation

The initial scope of the article was to avoid ActiveRecord Callbacks mostly because of its unwanted side effects [4] and hard to remove afterward. Other drawbacks would be violating the single responsibility principle and slowing down our test suite. Leaving AR callbacks on the side the next thought would be to decouple a service object from directly calling other services to fulfill a use case as we will see below. The advantage of the last point is that we promote the open-close principle and have a better view of the purpose of a given use case and what are its side-effects.

Example app

For our use case, we will be having a polling app with the following requirements:

  • 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

Let’s start from the controllers to see our use cases. Below we see some controllers one to process a new poll creation request, one to publish or reject a poll from being published to the platform and the last one to vote on a poll.

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
CreatePollAction service
Poll state transition service
Vote poll action
  • 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.

Towards an event-driven design

In the diagram below we re-present the final design that we want to achieve in our system. One of the most important use cases, a domain event should be used to propagate state changes and that the event handlers are implemented in our application layer since it is an application concern.

Handling multiple actions in a domain
Poll state transition service raising a domain event
Create new poll service raising a domain event
Event definition and handlers
class AuthorNotifier
include EventDispatcher
on Events::PollCreated, async: self def self.call(event)
# process event
end
end
Event dispatch and processing implementation

Conclusion

We showed above an approach to process business use cases by decoupling each component and leveraging the publish-subscribe pattern to move towards an event-driven design in our application. Some of the benefits that we gained are by promoting the single responsibility principle, loose coupling between the components and scalability. We also increased the semantics in our application though the domain events which is also the preferred way to trigger side effects across multiple aggregates within a domain or multiple ones. Introducing domain events we now explicitly implement side effects of changes. The key point is the open number of actions to be executed when a domain event occurs. Eventually, the actions and rules in the domain and application will grow. The complexity or number of side-effect actions when something happens will grow, but if we had components coupled together, then every time we need to add a new action you would also need to change working and tested code which will yield to bugs if not changed correctly.

References

[1] https://github.com/kickstarter/event-sourcing-rails-todo-app-demo
[2] https://en.wikipedia.org/wiki/Event-driven_architecture
[3] https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
[4] https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
[5] https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
[6]https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf#page=25
[7] https://www.infoq.com/news/2015/09/domain-events-consistency/
[8] https://github.com/mperham/sidekiq

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store