Do you work on COVID-19 related projects? Was your business significantly affected by the pandemic?

If the answer to previous questions is yes, Hexadecimal will waive the subscription fee for you until the vaccine is discovered.

Sharing notification text between channels

A short story about strings traveling throughout the Rails codebase.

Authored by Jamhur Mustafayev on


When I shipped the initial version of Hexadecimal, the only way to get notified when something went wrong with your websites, was via email. Naturally, all of the text pertaining to notifications was contained inside mailer views.

As I embarked on adding more channels (e.g. Slack, SMS), the challenge I faced was how to share notification text that was previously living inside mailers between all channels?

When I say notification text, I mean the contents of the message that you receive whenever something worthy of your attention happens. For example, when your website goes down, instead of a generic “your website is down, go figure” message, you receive a reason why your website is down, what might have caused it, and how to go about troubleshooting it. When your websites implode, you want to spend as little time as possible on identifying the problem.

Name resolution for has failed.

The most common reason for this failure is the URL that doesn't have any DNS records pointing to it.

To look up the DNS records for, run the following command from your terminal:


If the resulting message is "server can't find NXDOMAIN", then it means your host doesn't have any DNS records associated with it. Note that it might take some time for DNS records to propagate.

If you think that you have mistyped the URL, the best course of action is to delete this check and create a new one (you can't modify the URL after creation, by design).

Head over to [...] to view debug information that can assist you in troubleshooting this incident.


Each notification is a single, standalone Active Record model. These models share some similarities, but also differ in some ways. I looked into polymorphism and Single Table Inheritance (STI), but after a bit of consideration, tradeoffs seemed more costly than alleged benefits. Also, having a separate model for each notification type conformed to the Do The Simplest Thing That Could Possibly Work, which is my preferred way of doing things, at least during the exploratory phase.

Every notification is associated with a single Event object. Notifications can be either local (i.e., you add them manually to each website) or global (i.e., they are automatically added to all websites).

Each notification has a dispatch method that receives data, processes it, and passes on the request to the specific background job that sends the notification. DispatchNotificationsJob background job is responsible for orchestrating the entire show: from gathering all channels for a given website, to dispatch-ing notifications to them.

class DispatchNotificationsJob < ApplicationJob
  queue_as :notifications

  def perform(website, event, **kwargs)
    alert_channels = []

    alert_channels += website.email_alerts.to_a
    alert_channels += website.sms_alerts.to_a
    alert_channels += website.slack_alerts.to_a

    alert_channels.each do |channel|
      channel.dispatch(website, event, kwargs)

First attempt - i18n

My first instinct was to use Rails’ i18n machinery. Briefly, you store your strings in a locale-specific YAML file (for example, en.yml contains strings in English), and retrieve them when necessary.

This approach quickly fell flat on its face because some mailer views contained logic (for example, iterating over missing keywords and listing them in an unordered list), that I didn’t want to remove. After all, that would have resulted in more generic messages, which in turn would have necessitated folks to log in to their dashboards every time a Bad Thing happened. Doubleplusungood.

Second attempt - a service object

Create an intermediary service object that will accept the data and return the appropriate message.

Whenever I need to obtain the message, I would create the object and pass all the data to it, and in return, would magically receive appropriate strings via public methods such as notification_title and notification_body (think junk drawer).

Surprisingly, it worked. I wish it didn’t. It was the epitome of the duct tape and bubble gum. An unashamedly long case statement with squiggly heredocs all over the place. It was one of those Don’t touch it places of the codebase where monsters roam.

As much as I abhorred that piece of code, it was working. I left it alone, hoping that someday I will rip that monster apart.

Third attempt - strings with placeholders

After less than stellar experience with this part of the codebase, I decided to tackle the technical debt once for all.

“What if I could shove those strings in a database”, I thought. To make it happen, I will replace dynamic parts with placeholders and insert necessary data whenever I need to. Those strings should be a part of the event object that this notification is associated with. Whenever I would have to dispatch a message, I would pull that string from the corresponding event object, and pass in the necessary data.

# object access is replaced with a variable
website_url = website.url
event.body % { website_url }

Even though this proof of concept sort of worked, it never made it to production because of how messy the underlying code became. Since I can’t access object methods within those static strings, I need to pass each of those variables separately. Simple object access turned into multiple variable declarations. Yuck.

As much as I wanted to get rid of that despicable service object, I certainly didn’t want to trade one mess with another.

Final attempt - going back home

“If I could place those strings back into mailers, and somehow access them from outside of mailers, my life would certainly become a bit less miserable”, I was thinking to myself. For some unbeknownst reasons, it didn’t occur to me that this is indeed possible before I stumbled upon these two StackOverflow questions.

So I bit the bullet and started making changes. I initialized the mailer class, passed in the necessary params, and dispatched the request to the appropriate mailer (notice public_send method: each event is tied to a single mailer). Then I would simply query the mailer object and get the necessary strings, such as the title and the body of the notification.

class SlackAlert > ApplicationRecord
  def dispatch(website, event, **kwargs)
    params = { website: website, event: event }.merge(kwargs)
    mailer = NotificationMailer.with(params).public_send(

    subject = mailer.subject
    body = mailer.body.encoded

    # Prepare the payload and schedule a background job

Guess what? It worked like a charm. This approach might not conform to the “Single Responsibility Principle”, but in my book, practicality beats purity.

Interested in behind-the-scenes of a one-person software company?

No link tracking, no hidden pixels, no promotional emails, or other nonsense. I will only send you one email when a new article is out. Unsubscribe anytime.

You can also subscribe to the Atom feed (it's like RSS, but better).