Ascent

Building a Rails Notification Queue 2: The Notification Class

As the title suggests, this is the second post in a series on building a flexible notification queue in Rails. I'd suggest starting with the first post: on the database table (it's quite short).

Our apps create notifications in many places. These notifications are usually tied to lots of different objects or interactions. Unmanaged, this intense coupling becomes a deep dark hole where code quality goes to die.

This post is meant to set up some guiding abstractions so you can keep notifications from becoming a plague on your codebase. A theme you will find is that, by design, this solution is flexible. I urge you to think critically about my decisions as you read through it. While the details below have worked wonderfully for me, your implementation will benefit from some customization.

The class

Enough chat, here is the heart of the app/models/notification.rb file.

class Notification < ActiveRecord::Base
	# Notification Reasons
	ACCOUNT_CREATED = 'account_created'.freeze
	NEW_MESSAGE = 'new_message'.freeze

	belongs_to :user

	scope :unsent, -> {
		 where( cancelled_fl: false, sent_at: nil )
	}

	def self.schedule( method, to_user_id, reason, scheduled_for, from_user_id=nil, additional_attributes=nil )
		Notification.create!(
			method: method,
			reason: reason,
			user_id: to_user_id,
			from_user_id: from_user_id,
			scheduled_for: scheduled_for,
			additional_attributes: additional_attributes
		)
	end

	# Cancels any notifications of a specified reason
	#	 - Use when a user turns off a notification preference
	def self.cancel( user_id, reason )
		unsent
			.where( user_id: user_id, reason: reason )
			.update_all( cancelled_fl: true )
	end

	# Cancels all incoming or outgoing emails from a user
	#	 - Best used when a user deletes
	def self.cancel_all_for( user_id )
		unsent
			.where( 'user_id = ? OR from_user_id = ?', user_id, user_id )
			.update_all( cancelled_fl: true )
	end

	# Cancel a notification instance. ymmv
	def cancel!()
		self.update_attributes( cancelled_fl: true )
	end

	def send()
		case reason.to_s
		when ACCOUNT_CREATED
			MailerService.unconfirmed_email( self )
		when NEW_MESSAGE
			if method == 'push'
				PushService.send_new_message( self )
			elsif method == 'email'
				MailerService.send_new_message( self )
			end
		else
			raise "Unsupported Notification reason: #{reason.inspect}"
		end

		self.update_attributes( sent_at: Time.now )
	end
end

Implementation notes

Skipping the helper methods for cancellations, you will find the real meat of this class is in 2 methods. Notification.schedule() and notification.send() do all the heavy listing, so let's explore them.

Notification.schedule() should handle all inserts into the notification table. Not only is the call shorter than typing Notification.create( {big ass hash} ), it will give you a single function call that every schedule action goes through. This will pay dividends in the future. The rest of the function is pretty self explanatory.

Also notable is that, in my systems, the from_user_id is nullable and thus optional while scheduling notifications. Your system may not have a nullable sender, or may not have a sender at all. Adjust this function accordingly.

notification.send() handles the shepherding of all notifications to the appropriate notification service. This does not actually send emails or pushes. Remember that this is a queue we are trying to process. This class is not responsible for managing vendor api implementations.

Actually scheduling notifications

So now that we have a class to process these notifications, how about a couple of examples of scheduling notifications.

# Send the account created email after a bit
Notification.schedule( :email, user.id, Notification::ACCOUNT_CREATED, Time.now + 30.minutes )

# Send a message email with included message relation
Notification.schedule( :push, user.id, Notification::NEW_MESSAGE, Time.now,
						sender.id, {message_id: message.id} )

The first example shows us the power of being able to very easily schedule a notification for arbitrary times in the future. The second example we start to see the power of the additional_attributes column. Moving forward, new notifications that attach to other tables or objects will not need any database migrations.

Also, does your app tend to use emails and pushes in unison? Make Notification.schedule() accept an array as the method. I think you can start to see the flexibility this abstraction provides.

Is that it?

Mostly. The beauty of this class is it's flexibility and how cleanly it organizes the entire notification landscape. In some projects i have found it so useful that i have leveraged it for in app notification and alerts instead of simply outgoing messages.

In the final and all but certainly shorter, post, we will look at building a simple task we can schedule to process this queue in the background, or on another server entirely. You many have been wondering why we haven't called send() yet. We will.

td;dr: Using a notification class we can abstract all of the scheduling and cancelling logic to a central place.

Get the latest posts delivered right to your inbox.
Author image
Written by Ben
Ben is the co-founder of Skyward. He has spent the last 10 years building products and working with startups.