Evil Martians

Crafting user notifications in Rails with Active Delivery

palkan_tula profile image Vladimir Dementyev Originally published at evilmartians.com ・4 min read

Rails framework is a like a Swiss-army knife, providing a lot of useful functionality out-of-the-box (and it's becoming even more Swiss-er).

It's built on top of the sub-frameworks, such as, to name a few, ActiveRecord, ActiveJob, ActionCable (❤️), ActionMailer... Ok, let's stop at this point.
What is the purpose of ActionMailer?

ActionMailer is an abstraction to send emails (and receive too, though now we have ActionMailbox).

It abstracts the delivery mechanism and provides a Railsy API to build messages.

So, sending email notifications to users is not a big problem for Rails apps.

The problem is that in a modern world we have many different ways to send notifications, not only emails: push notifications, chatbots, SMS, pigeons.

NOTE: DDH mentioned some "action notifier" framework, "yet to be extracted" from Basecamp, which sounds like a solution; but we're not there yet.

It's pretty common to have a code like this:

def notify_user(user)
  MyMailer.with(user: user).some_action.deliver_later if user.receive_emails?
  SmsSender.send_message(user, "Something happened") if user.receive_sms?
  NotifyService.send_notification(user, "action") if whatever_else?

And there could be dozens of such places in the codebase. Good luck with maintaining and testing this code!

How can we refactor this code? Maybe, we need an another layer of abstraction?)

Here comes Active Delivery–a new gem I wrote to solve this puzzle.

Active Delivery is a framework providing an entry point for all types of notifications: mailers, push notifications, whatever you want.

It helps you to rewrite the code above in the following way:

def notify_user(user)
  MyDelivery.with(user: user).notify(:some_action)

And even more–you can now test it elegantly:

# my_something_spec.rb
expect { subject }.to have_delivered_to(MyDelivery, :some_action).
  with(user: user)

How does it work?

In the simplest case, a delivery is just a wrapper over a mailer:

# suppose that you have a mailer class
class MyMailer < ApplicationMailer
  def some_action
    # ...

# the corresponding delivery could look like this
class MyDelivery < ActiveDelivery::Base
  # here we can also apply "delivery rules"
  before_notify :ensure_receive_emails, on: :mailer

  def ensure_receive_emails
    # returning `false` halts the execution

# when you call
MyDelivery.with(user: user).notify(:some_action)

# it invokes under the hood (only if user receives emails)
MyMailer.with(user: user).some_action.deliver_later

We rely on convention over configuration to infer the corresponding mailer class.

OK. We've just wrapped our mailer. What's the deal? How to handle other delivery methods?

Let's take a look at the architecture of the framework:

Active Delivery architecture

Notice that we have an internal layer here–lines. Each line is a connector between the delivery and the actual notification channel (e.g., mailer).

Active Delivery provides an API to add custom delivery lines–that's how you can implement pretty much any type of notifications!

And to make it even easier, we've built another micro-framework–Abstract Notifier.

It's a very abstract framework: all it does is provides an Action Mailer-like API for describing notifier classes, pure Ruby abstraction, zero knowledge of "how to send notifications."

Why Action Mailer-like interface? It's a familiar and continent API, first of all. And I like it's parameterized classes feature (which we heavily use in Active Delivery).

To "teach" Abstract Notifier how to send notifications, you must implement a driver (any callable object).

For example, we use Twilio Notify for push notifications, and that's how our driver, ApplicationDelivery and ApplicationNotifier classes look like:

class TwilioDriver
  attr_reader :service

  def initialize(service_id)
    client = build_twilio_api_client
    @service = client.notify.services(service_id)

  def call(params)

class ApplicationDelivery < ActiveDelivery::Base
  # NOTE: abstract_notifier automatically registers its default line,
  # you don't have to do that
  # Default notifier infers notifier classes replacing "*Delivery* with
  # "*Notifier"
  register_line :notifier, ActiveDelivery::Lines::Notifier

class ApplicationNotifier < AbstractNotifier::Base
  self.driver = TwilioDriver.new(Rails.application.config.twilio_notify_id)

Now let's define our delivery, mailer and notifier classes:

class PostsDelivery < ApplicationDelivery
  # here we can define callbacks, for example,
  # we want to enforce passing a target user as a param
  before_notify :ensure_user_provided  

  def ensure_user_provided
    raise ArgumentError, "User must be passed as a param" unless params[:user].is_a?(User)

  # in our case we have a convenient params-reader method
  def user

class PostsMailer < ApplicationMailer
  def published(post)
      to: user.email,
      subject: "Post #{post.title} has been published"

class PostsNotifier < ApplicationNotifier
  # Btw, we can specify default notification fields
  default action: "POSTS"

  def published(post)
      body: "Post #{post.title} has been published",
      identity: user.twilio_notify_id
      # you can pass here any fields supported by your driver

And, finally, that's how we trigger the notification:

PostsDelivery.with(user: user).notify(:published, post)

What if need one more notification channel? We can add another notifier line to our ApplicationDelivery:

class ApplicationDelivery < ActiveDelivery::Base
  register_line :notifier, ActiveDelivery::Lines::Notifier

  register_line :pigeon,
                # resolver is responsible for inferring
                # the notifier class from
                # the delivery class name
                resolver: ->(name) { name.gsub(/Delivery$/, "Pigeon").safe_constantize }


class PigeonNotifier < AbstractNotifier::Base
  self.driver = PigeonDelivery.new

class PostsPigeon < PigeonNotifier
  def published(post)
      to: user.pigeon_nest_id,
      message: "coo-chi coo-chi coo"

That's it 🐦!

Check out Active Delivery and Abstract Notifier repos for more technical information.

Read more dev articles on https://evilmartians.com/chronicles!

Posted on by:

palkan_tula profile

Vladimir Dementyev


A mathematician found his happiness in programming Ruby and Erlang, contributing to open source and being an Evil Martian.

Evil Martians

Evil Martians is a distributed product development consultancy that works with startups and established businesses, and creates open source-based products and services.


markdown guide

Really loved the article.

I just have a question about how do you organize your files, where do you recommend to place*_delivery.rb & *_driver.rb files in a rails project folder structure?

I'm asking because I have an existing project, with tons of files, and I want to organize them better.



I put deliveries under app/deliveries.

Drivers could be stored in different places: somewhere in lib/ folder (if it's a general driver, does not rely on the application code) or in app/services (if it depends on the application itself).


Thanks @vladimir for this article.
What about notifier classes? Where should we store them?
If I've understood the component workflow, we need to create delivery class, notifier class and driver class. Right?


I store notifiers in app/notifiers.

In the end, my structure looks like this: