Ascent

Building a Rails Notification Queue 1: The Database

Many apps live, or die, based on the success of their notifications. Because of this, many apps send notifications about as much as they can. This is not without code cost: notifications end up everywhere in our codebases.

Since they are so important, and numerous, we would benefit from a great way to abstract and track these notifications. After a fair amount of thinking, we found a solution that has worked well in our production environment at MeetMindful. Using it, we have processed millions of notifications without any loss of code quality or tracking.

In this short series, we will look at how to build a great notification queue that you can use in your systems.

The table

Jumping right in, here is the basic structure you will want for your notifications table.

create_table :notifications do |t|
	t.integer   :user_id, null: false
	t.integer   :from_user_id
	t.string    :delivery_method, null: false
	t.string    :reason, null: false
	t.timestamp :scheduled_for, null: false
	t.timestamp :sent_at, null: true
	t.boolean   :cancelled_fl, default: false, null: false
	t.jsonb     :additional_attributes

	t.timestamps null: false
end

Database naming schemes are to-each-their-own, so here is a listing describing the function of each column.

Column Use
user_id the notification recipient
from_user_id the generator of the notification. nullable, as many notifications are triggered by the system instead of a user.
delivery_method the delivery type. this can include things like email, push, etc.
reason the notification type. "Type" itself feels ambiguous as it suggests it is also a delivery_method, so i use reason instead.
scheduled_for the delivery timestamp
sent_at the delivery timestamp
cancelled_fl many notifications have reasons to be cancelled before they are sent, either through the user taking the suggested action or a user modifying their notification preferences. This flag allows that cancellation.
additional_attributes most emails require other information than the recipient and generating user. By leveraging Postgres's `jsonb` column type, we save ourselves lots of nullable columns with minimal impact to relational integrity

jsonb?

Many will note that the table includes a jsonb column type. Think of it as "JSON Binary". It allows us to input parseable JSON while retaining indexable queries against that data. I'm not a big fan of having 10 nullable columns to point to all the objects a notification may, or may not, need to know about. Leveraging the flexibility of JSON without suffering too much relational integrity was a tradeoff i took a leap of faith on. It has paid itself off in spades.

Benefits

The biggest benefit of a notifications table is a centralized place to track every notification a user has received, regardless of delivery method. In our usage, we saw so much value of it that we have begun to leverage it not only for outgoing notifications, but in-app notifications as well.

Another major benefit is the decoupling of all the notification logic from the pages that actually generate them. By allowing our pages to simply create notification database records, you will see the code simplicity we achieve on a larger codebase.

Next steps

So now that we have a table to put these notifications in, there is some work remaining for us to use it productively. In the next post we will look at abstracting the scheduling logic. Finally, we will look into a way to process the unsent notifications via a worker script.

tl;dr: Our apps send lots of notifications. Having an easy to way save and track them is necessary.

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.