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.
Jumping right in, here is the basic structure you will want for your
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.
|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
|reason||the notification type. "Type" itself feels ambiguous as it suggests it is also a
|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|
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.
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.
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.