Pretty image
People often ask how we do what we do. This series explains…
This month's question: How do you send email, RSS, and Twitter notifications?

Our online store can send notifications (such as “thank you for your order”) via email, RSS, and Twitter. It was important to us that we encapsulated this functionality behind a tidy, abstracted, interface—when new ways of sending notifications come along in the future, we don’t want to have to go changing the application in lots of places. In the end, all of the notification stuff is hidden inside one library class and one Rails model.

Let’s track that “new order” message as it makes its way from the code to the user.

When the checkout process is complete, we convert what was the shopping cart into a full-blown order. At the end of that process, there’s the following line of code.

 Notifier.new_order(cart.email, user, cart)

The Notifier is the central dispatcher for all notifications. It contains a public method for each of the different notifications we send out. These methods are basically responsible for extracting all the various parameters needed to construct the notification before calling a common, private method that does the work. The new_order method looks like this:

 def new_order(email, user, order)
  do_notify(:new_order, email, user,
  :title => "Your Order ##{order.number}",
  :link => homepage_url(user),
  :category => RssCategory::ORDERS,
  :order => order,
  :download_url => order_downloads_url(:order_id=>order,
  :token=>order.token))
 end

The first parameter is the base name of the template we use to generate the actual notification message. For each message, we typically have both HTML and plain text variants. We use the Rails ActionMailer to format a multipart email using these templates. Inside the do_mailer method, you’d find the code

 mail = NotificationMailer.send("create_#{event}", email, params)

The first parameter creates an email based on the name of the template, and the last parameter is the hash passed to do_notifier. These are the values to be substituted into the template.

The good news is that we don’t actually have to write separate create_xxx methods inside the mailer model for each message type—Ruby metaprogramming to the rescue. Here’s our entire mailer:

 class NotificationMailer < ActionMailer::Base
  helper :application
 
  def method_missing(name, *params)
  email = params[0]
  assigns = params[1]
  from "The Pragmatic Bookstore <support@pragprog.com>"
  sent_on Time.now
  recipients email
  subject "[Pragmatic Bookstore] #{assigns[:title]}"
  body(assigns)
  end
 end

Back in the Notifier class, we then look at the user’s preferences to see whether to actually send the email we just created. If the email is wanted, we just call NotificationMailer.deliver and off it goes. But why go to the trouble of creating the email if the user doesn’t want us to send it? It’s because ActionMailer is a great way to render Rails templates without going to the trouble of faking out a whole controller environment. And we want the HTML version of the message, because we’ll use it to create the RSS notification for the user.

 html_part = mail.parts.find {|p| p.content_type == "text/html"}
 
 user.add_rss_entry(params.merge(:description=>html_part.body))
 
 ActionController::Base.expire_page(rss_feed_url(user, true))

Finally, we see if the user has registered a Twitter name with us. If so, and if the notification says it’s appropriate, we send off a quick tweet.

When we first started doing this, our volumes were fairly low, and we sent email in the foreground. That didn’t scale, so Michael Koziarksi (Koz) added a wonderful hack to the notifier. He changed all the notification methods into instance methods. He then implemented this:

 def self.method_missing(name, *args)
  send_later(:send_notification, name, *args)
 end
 
 def self.send_notification(name, *args)
  new.send(name, *args)
 end

When you call the class method new_order, the method_missing intercepts and uses the wonderful DelayedJob plugin to invoke the send_notification method at some point in the future. When that happens, a new instance of the Notifier is created and its new_order instance method is invoked. Because all the notification code is encapsulated inside this one class, the switch to making all notifications asynchronous didn’t affect any of the main application code.

As well as handling email, RSS, and Twitter notifications, we also use the notifier to update a couple of private Campfire rooms with a log of internal application events.

The notification system in our store is a great example of how the flexibility of Ruby can lead to some nicely decoupled, easy-to-maintain code.

Dave Thomas is one of the Pragmatic Programmers.