Implementing Event-Driven Architecture in Rails with Active Support Instrumentation

slimgee

Given Ncube

Posted on September 17, 2024

Implementing Event-Driven Architecture in Rails with Active Support Instrumentation

TL:DR; You can skip to setup if you just to see the implementation

Background

When I was building Pulse by Welodge I wanted to notify a user when they submit a startup for approval, when it’s accepted/rejected. I also wanted to notify the admins that someone has submitted a startup. The first implementation I simply dispatched a noticed Notifier in the controller when startup was submitted, but this did not have some “rails magic” into it.

After a quick search I found a few articles online about event driven architectures in rails but they all seem to be overly complicated for what I wanted and they seem to rely on 3rd party packages, which is fine but eventmachine wanted to run a separate “event server” or something like which was overkill,

After a deep dive, I landed on Active Support Instrumentation which rails’s own implementation of the observer pattern. Cool, I can now publish an event and have many subscribers which do different things. What the docs don’t say is how to get it to work in userland and that’s what this article is about

Setup

To get this working properly we need to setup a few things, first we need to auto load “subscribers” which I shall call listeners from here on. We want to store listeners in app/listeners/xx_listener.rb where xx is a resource/model in our application. To achieve this let’s hook into the to_prepare config hook to load the event listeners during application boot

module Startuplist
  class Application < Rails::Application
    # rest of your app config
    listeners = "#{Rails.root}/app/listeners"
    Rails.autoloaders.main.ignore(listeners)

    config.to_prepare do
      Dir.glob("#{listeners}/**/*_listener.rb").sort.each do |listener|
        load listener
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now all we have to do is define a listener file in app/listeners/ in this case app/listeners/startup_listener.rb

ActiveSupport::Notifications.subscribe "app.startup.submitted" do |event|
  startup = event.payload[:startup]
  StartupSubmittedNotifier.with(record: startup, message: "Your startup was submitted").deliver(startup.user)

  Rabarber::Role.assignees(:admin).each do |admin|
    SubmissionReceivedNotifier.with(record: startup, message: "A new startup was submitted").deliver(admin)
  end
end

ActiveSupport::Notifications.subscribe "app.startup.accepted" do |event|
  startup = event.payload[:startup]
  StartupAcceptedNotifier.with(record: startup, message: "Your startup was accepted").deliver(startup.user)
end
Enter fullscreen mode Exit fullscreen mode

Here we are simply subscribing to all events related to the startup model, the user_listener.rb could look something like this

ActiveSupport::Notifications.subscribe "app.user.created" do |event|
  SubscribeNewsletterJob.perform_later event.payload[:user].email
end
Enter fullscreen mode Exit fullscreen mode

Which subscribes a newly registered user to a newsletter like ConverKit, MailChimp or Mailerlite

Okay this is cool, how do we then dispatch these events, right, on Pulse the Startup has a status enum so I did something like this to dispatch events each time a status changes

statuses.keys.each do |status|
  after_save :"broadcast_#{status}_changed"

  define_method :"broadcast_#{status}_changed" do
    if saved_change_to_status? && self.status == status
      ActiveSupport::Notifications.instrument "app.startup.#{status}", startup: self
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

This made sure that each time the status changed via a controller or anywhere else in the code, I publish a app.startup.status event which listeners could subscribe to and do what ever they want with the data,

You can broadcast these events anywhere in your app for example we can also do something like for the user model

after_create :broadcast_create
def broadcast_create
  ActiveSupport::Notifications.instrument "app.user.created", user: self
end
Enter fullscreen mode Exit fullscreen mode

On this event we can then send welcome email, subscribe to newsletter, provision a tenant, etc.

I hope you enjoyed this pattern, let me know what you think about this.

💖 💪 🙅 🚩
slimgee
Given Ncube

Posted on September 17, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related