development

Building a Notification System in Ruby on Rails: DB Design

Hana Mohan Last updated on September 27, 2024

A notification system is an essential feature for any modern-day application. At work, notifications improve our productivity by alerting us to comment mentions or upcoming meetings. Outside work, notifications alert us of important events like a friend's birthday or an upcoming flight.

In this series of articles, we'll design and implement a notification system from scratch. We'll do this in Ruby on Rails, but the concepts are widely applicable to any web application framework. For that reason, we'll try to use more generic concepts where we can. Part 1 (this article) focuses mostly on the database design.

As you will see, building a reliable, multi-tenant notification system is a lot of work. It's an excellent exercise for learning about real-time data communication, database design, and other concepts, but if you are thinking of building this as a feature in your product, we invite you to try MagicBell, a notification inbox you can add to your product in less than an hour!

With that out of the way, let's get started!

Goals

Before we dive in, let's write a few goals that describe the features our notification system must-have. We'd like our notification system to be:

  1. Able to send a notification to a user
  2. Able to send a notification to many users
  3. Able to send a notification to many channels. To start with, it can support two channels, email and an in-app notification center
  4. Be a separate service so we can reuse it in new projects in the future or in other existing projects

An advanced notification system can do more but the goals above are sufficient to get started. In a future post, we can explore adding advanced features to our notification system like User Notification Preferences, Channel Specific Templates etc. You might also want to read our article on how to design a notification system.

Entities

Now that we've written down our goals, let's describe the entities that exist in our notification system and the attributes and relationships those entities will have

Project

The Project entity represents a software application for which a notification is sent for. It can have the attributes

Name Type Description
id UUID A unique identifier to identify the project
name String Name of the project

and the relationships

Name Type Description
notifications Has Many Notifications sent for this project

Notification

A Notification entity represents a notification to be sent. It can have the attributes

Name Type Description
id UUID A unique identifier to identify the notification
title Text Title of the notification
text Text Text of the notification
on_click_url String The URL to redirect a user to when they click on the notification (in channels where clicking on a notification is feasible)
sent_at Time The time at which the notification was sent

and the relationships

Name Type Description
recipient_copies Has Many Copies of the notification sent to individual recipients

User

A User entity represents an entity to whom a notification can be sent to. The User entity can have the attributes

Name Type Description
id UUID A unique identifier to identify a user
first_name String First name of the user
last_name String Last name of the user
email String Email of the user

Notification Recipient Copy

A Notification Recipient Copy represents an instance/copy of a notification that is sent to a specific recipient. This entity is required as a Notification can be sent to many recipients in one API call. This entity can have the attributes

Name Type Description
id UUID A unique identifier to identify a notification recipient copy
seen_at Time The time at the notification was first seen by the recipient
read_at Time The time at the notification was first read by the recipient

and the relationships

Name Type Description
recipient Belongs To The recipient to deliver the notification to
notification Belongs To The notification whose copy this is

Tech Implementation

Tech Stack for our Notification System

We're now ready to pick the technologies to use to implement our notification system.

To store data, let's choose Postgres, a popular, open source, feature-rich and scalable relational database system. To implement the notification system, we will use Ruby and Rails as mentioned earlier.

For creating tables, we only need ActiveRecord, an opinionated and easy-to-use Object Relational Mapping (ORM) Framework, that is part of Rails. While it is easy to use ActiveRecord outside of Rails, it is less time-consuming to use all of Rails.

Even if you're unfamiliar with Rails, the code snippets below will still be very easy to understand as long as you're familiar with a web application framework in any language.

Create a Ruby on Rails project

Install Rails

gem install rails

Create a Rails project. Let's name our Rails project "pigeon".

rails new pigeon
cd pigeon

Install bundler

gem install bundler

Install gems listed in the Gemfile

bundle install

Create tables

Add ActiveRecord migrations to create the projects, notifications, users and notification_recipients_copies tables in Postgres

bundle exec rails g CreateProjects
class CreateProjects < ActiveRecord::Migration[5.1]
  def change
    create_table :projects do
      t.string :name
      
      t.timestamps
    end
  end
end
bundle exec rails g CreateNotifications
class CreateNotifications < ActiveRecord::Migration[5.1]
  def change
    create_table :notifications do
      t.text :title
      t.text :text
      t.string :on_click_url
      t.datetime :sent_at
      
      t.timestamps
    end
  end
end
bundle exec rails g CreateUsers
class CreateUsers < ActiveRecord::Migrations[5.1]
  def change
    create_table :users do
      t.string :email
      
      t.timestamps
    end
  end
end
bundle exec rails g CreateNotificationRecipientCopies
class CreateNotificationRecipientCopies < ActiveRecord::Migrations[5.1]
  def change
    create_table :notification_recipient_copies do
      t.references :recipient
      t.references :email
      
      t.timestamps
    end
  end
end

Add models

Add the Project, Notification, User and the NotificationRecipientCopy models

# In models/project.rb

class Project < ApplicationRecord
  has_many :notifications
  
  validates_presence_of :name
end
# In models/notification.rb

class Notification < ApplicationRecord
  belongs_to :project

  has_many :recipient_copies, class: "NotificationRecipientCopy"
  
  validates_presence_of :title
  validates_presence_of :text
  validates_presence_of :recipient_emails
end
# In models/user.rb

class User < ApplicationRecord
  has_many :recipient_copies, class: "NotificationRecipientCopy"
end
# In models/notification_recipient_copy.rb

class NotificationRecipientCopy < ApplicationRecord
  belongs_to :user
  belongs_to :recipient, :class => "User"
end

Now, modify the Notification and NotificationRecipientCopy models so they deliver a Notification to recipients when one is created

# In app/models/notification.rb

class Notification < ApplicationRecord
  # ...
  
  before_create :associate_recipients
  after_commit :deliver, :on => :create
  
  attr_accessor :recipient_emails
  
  def associate_recipients
    recipient_emails.each do |recipient_email|
      recipient = User.where(email: recipient_email).first
      unless recipient
        recipient = User.create(recipient_email: recipient_email).
      end
      
      recipients << recipient
    end
  end
  
  def deliver
    recipients.each do |recipient|
      recipient_notification_copy = RecipientNotificationCopy.create(
        notification: notification
        recipient: recipient,
      )
    end
  end
end
# In app/models/notification_recipient_copy.rb

require "lib/channels/email"
require "lib/channels/in_app_notification_center"

class NotificationRecipientCopy
  # ...

  after_create :deliver

  def deliver
    channels.each do |channel_class|
      channel_class.new(self).deliver
    end
  end

  private

  def channels
    [Channels::Email, Channels::InAppNotificationCenter]
  end
end

Add channels

We can use Sendgrid to deliver emails. They offer a Free plan. Signup for Sendgrid and obtain an API key. Configure Rails to deliver emails via Sendgrid

# Borrowed from https://sendgrid.com/docs/for-developers/sending-email/rubyonrails/
ActionMailer::Base.smtp_settings = {
  :user_name => 'your_sendgrid_api_key',
  :password => 'your_sendgrid_api_key',
  :domain => 'pigeon.io',
  :address => 'smtp.sendgrid.net',
  :port => 587,
  :authentication => :plain,
  :enable_starttls_auto => true
}

and add the Email channel

class Channels
  class Email
    def initialize(notification_recipient_copy)
      @notification_recipient_copy = notification_recipient_copy
    end

    def deliver
      mail.deliver
    end

    def mail
      Mail.new do
        from "[email protected]"
        to @notification_recipient_copy.recipient.email
        subject @notification_recipient_copy.title
        body @notification_recipient_copy.text
      end
    end
  end
end

To deliver notifications in real-time to your web application's JavaScript (which can render an in-app notification center), we can use a service like Ably. Like Sendgrid, Ably offers a Free plan as well.

Add the ably gem to the Rails project's Gemfile

gem 'ably'

and install it

bundle install

Implement the InAppNotificationCenter channel

class Channels
  class InAppNotificationCenter
    def initialize(notification_recipient_copy)
      @notification_recipient_copy = notification_recipient_copy
    end

    def deliver
      event_name = "new_notification"
      event_data = {
        event: {
          name: event_name,
          data: {
            notification: {
              id: notification_recipient_copy.id
            }
          }
        }
      }
      ably.publish(event_name, event_data)
    end

    private

    def ably_channel
      "users/#{recipient.id}"
    end

    def ably
      @ably ||= Ably::Rest.new(key: ENV['ABLY_PRIVATE_KEY'])
    end

    def recipient
      notification_recipient_copy.recipient
    end
  end
end

Demo

That's it 😅! Let's try our notification system. Open a rails console

bundle exec rails console

and copy paste the code below and send a test notification to yourself

project = Project.create(name: "Test project")
project.notifications.create(
  title: "First notification!",
  text: "This is our first notification!",
  recipient_emails: ["[email protected]"]
)

In a few minutes, Sendgrid should deliver you the email.

In the next article, we'll explore adding API endpoints to create notifications, fetch a user's notifications, mark a notification as read etc. Explore all MagicBell integrations and docs.

Thanks to Nisanth Chunduru for this blog post!

Related articles:

  1. A Guide to Notification System Design
  2. Building a Notification System in Angular with MagicBell
  3. How to Implement React Native Push Notifications with Firebase