
Notifications are everywhere in modern web applications. Users expect to be notified when someone comments on their post, shares their content, or invites them to collaborate in a post. But as your Rails application grows, managing different types of notifications can quickly become a maintenance nightmare.
Recently, while adding new notifications to a feedback management system, I encountered a common pattern that many Rails developers face: multiple notification types sharing similar behavior but with subtle differences. The original implementation used an enumerator column called event
to differentiate between notification types, leading to scattered conditional logic and repeated code. Here’s how Single Table Inheritance (STI) provided a cleaner, more maintainable solution.
The Problem: Event-Driven Chaos
The initial notification system relied on an event
column with values like ‘publish’, ‘comment’, ‘collaborate’, and ‘praise’. Each notification type required different logic for titles, descriptions, and email templates, resulting in a bloated model full of case statements:
# app/models/notification.rb
def title
case event
when 'publish'
I18n.t('notification.feedback.shared.title')
when 'comment'
I18n.t('notification.feedback.commented.title')
# ... more cases
end
end
This approach violated the Open/Closed Principle and made adding new notification types cumbersome. Every new feature required modifications to the core Notification
model, increasing the risk of introducing bugs.
Enter Single Table Inheritance
STI allows multiple classes to share the same database table while maintaining their own specific behaviors. The base Notification
class defines the common interface and shared behavior:
#app/models/notification.rb
class Notification < ApplicationRecord
belongs_to :user
belongs_to :notifiable, polymorphic: true
scope :by_recent, -> { order(created_at: :desc) }
scope :unread, -> { where(read_at: nil) }
def self.for(user, opts = {})
where(opts.merge!(user_id: user.id)).order(updated_at: :desc)
end
def mark_as_read
update(read_at: Time.current)
end
def read?
read_at.present?
end
def unread?
!read?
end
def title
raise NotImplementedError, 'Subclass must implement #title'
end
def description
raise NotImplementedError, 'Subclass must implement #description'
end
def send_email
NotifierMailer.send(mailer_action, user, notifiable).deliver_later
end
def self.create_notification(user, notifiable)
create(user: user, notifiable: notifiable)
end
private
def mailer_action
raise NotImplementedError, 'Subclass must implement #mail'
end
end
In our case, each notification type became its own class inheriting from this base model:
# app/models/notifications/feedback_shared.rb
class Notifications::FeedbackShared < Notification
def title
I18n.t('notification.feedback.shared.title')
end
def description
I18n.t('notification.feedback.shared.description',
author: feedback.author.first_name)
end
def notifiable_path
feedback_path(feedback)
end
def self.notify(user, feedback)
create_notification(user, feedback).send_email
end
private
def feedback
notifiable
end
def mailer_action
:feedback_shared
end
end
Each notification class encapsulates its own logic, making the codebase more modular and easier to test. With this approach, we can add new notification types without modifying existing code. For example, we can create a dedicated class for comment notifications or for collaboration invitations — the possibilities are virtually unlimited!
Using the New Notification System
With STI in place, creating notifications becomes straightforward and type-safe:
# When feedback is shared
Notifications::FeedbackShared.notify(user, feedback)
# When someone comments
Notifications::Commented.notify(user, comment)
# Querying notifications remains simple
user.notifications.unread.by_recent
Each notification type handles its own email delivery, and text rendering. Adding a new notification type is as simple as creating a new class that inherits from Notification
.
Why STI Works Perfectly Here
STI proved to be an ideal fit for our notification system because we leverage Rails’ I18n system for all notification content. Instead of storing titles, descriptions, or email content in the database, we generate them dynamically from translation files. This means all notification types share the same database schema — they only differ in their behavior and the I18n keys they reference.
The polymorphic notifiable
association allows us to attach notifications to any model (feedback, comments, reviews), while the type
column handles the class-specific logic. We’re not dealing with sparse columns or varying data structures that would make STI problematic—just different ways of presenting the same core notification data.
Benefits Beyond Clean Code
STI brought several unexpected advantages. Testing became more straightforward since each notification type could be tested in isolation. Adding new notification types no longer required touching existing code — just create a new class following the established pattern. The polymorphic association with notifiable
remained intact, preserving the flexibility to attach notifications to any model.
Performance also improved slightly. Instead of Rails evaluating case statements at runtime, method dispatch happens at the class level, and specific behavior is defined directly in each subclass.
When STI Isn’t the Answer
STI works well when subclasses share most of their structure but differ in behavior. However, it’s not suitable when notification types require significantly different database columns. In such cases, separate tables with a shared interface might be more appropriate.
Also, be mindful of table bloat. If you have many notification types with sparse data, you might end up with numerous null columns, which can impact query performance.
Moving from event-driven conditionals to STI transformed our notification system from a maintenance burden into an extensible, clean architecture. Each notification type now has its own space to grow, and the codebase tells a clearer story about what each notification does. Have you encountered similar challenges in your Rails applications? I’d love to hear how you’ve approached notification system design in the comments.