Excerpt
I’ve recently built a favoriting system for an app. While the code isn’t novel, it reminded me how clean, flexible, and fun Rails can be when you lean into the framework’s conventions. Here’s how I did it - I hope you’ll learn something along the way.
If you want to skip the details and just see the code, you can check out this gist.
My team wanted users to mark records as favorite in the system. This would make those records handy for quick access. We needed to support several existing models—and likely more in the future—so it had to be flexible and reusable. Also, multiple users should be able to favorite the same record (some records are “public”, but we’ll skip how that works for this post).
So we need a way to mark a record as favorite. Instead of a boolean field on each favoritable model, I created a separate model to represent the relationship between a user and a favoritable. That way, any record with an associated FavoriteRecord is considered favorited. While a bit more co
I’ve recently built a favoriting system for an app. While the code isn’t novel, it reminded me how clean, flexible, and fun Rails can be when you lean into the framework’s conventions. Here’s how I did it - I hope you’ll learn something along the way.
If you want to skip the details and just see the code, you can check out this gist.
My team wanted users to mark records as favorite in the system. This would make those records handy for quick access. We needed to support several existing models—and likely more in the future—so it had to be flexible and reusable. Also, multiple users should be able to favorite the same record (some records are “public”, but we’ll skip how that works for this post).
So we need a way to mark a record as favorite. Instead of a boolean field on each favoritable model, I created a separate model to represent the relationship between a user and a favoritable. That way, any record with an associated FavoriteRecord is considered favorited. While a bit more complex, this lets us make any model favoritable without writing new migrations. We’ll use a polymorphic association to allow any model to be favoritable.
The database structure will look like this:

A diagram showing the database structure for a favoriting system. It shows: A favorite_records table with a owner_id, favoritable_id and favoritable_type columns; A user table with an id column; A "favoritable_model" table with an id column; Both the user and favoritable_model tables have a has_many relationship with the favorite_records table.
And the migration looks like this:
```plain text
class CreateFavoriteRecords < ActiveRecord::Migration[7.2]
def change
create_table :favorite_records, id: :uuid do |t|
t.references :owner, foreign_key: { to_table: :users }, null: false, type: :uuid
t.references :favoritable, polymorphic: true, null: false, type: :uuid
t.timestamps
end
add_index :favorite_records, [:owner_id, :favoritable_type, :favoritable_id], unique: true, name: 'index_favorite_records_uniqueness'
end
end
```
And here’s our humble FavoriteRecord model:
```plain text
class FavoriteRecord < ApplicationRecord
belongs_to :owner, class_name: 'User'
belongs_to :favoritable, polymorphic: true
# a user can only favorite a record once
validates :owner, uniqueness: { scope: [:favoritable_type, :favoritable_id] }
end
```
Let’s work on the other side of the association now. First, we make users have favorite records:
```plain text
class User < ApplicationRecord
has_many :favorite_records, foreign_key: :owner_id, dependent: :destroy
end
```
Then, let’s make a model favoritable. For this example, I’ll use a Note model. Instead of writing this directly in Note, I’ll create a Favoritable concern so we can share this behavior with other models.
```plain text
module Favoritable
extend ActiveSupport::Concern
included do
has_many :favorite_records, as: :favoritable, dependent: :destroy
scope :favorite, -> { favorited_by(Current.user) }
scope :not_favorite, -> { where.missing(:favorite_records) }
scope :favorited_by, ->(owner) {
joins(:favorite_records)
.where(favorite_records: { owner: })
}
end
def favorite!
favorite_records.create!(owner: Current.user)
end
def unfavorite!
favorite_record_for(Current.user)&.destroy
end
def favoritable_sgid
to_sgid(for: :favoritable, expires_in: nil)
end
private
def favorite_record_for(user)
favorite_records.find { it.owner_id == user.id }
end
end
```
So now Note just includes that concern and gets all that functionality.
Note that a favoritable has many favorite records, because multiple people can favorite the same record in this system.
We can mark notes as favorite now and we have a few helpful scopes. Let’s work on the next level of abstraction: the controller.
We could create a FavoriteNotesController, but since we want to stay model-agnostic, we’ll go with a generic FavoriteRecordsController. Similarly, instead of using model_id and model_type params, we’ll use a favoritable_sgid Signed Global ID. This prevents users from messing with the params and trying to favorite records that they shouldn’t be able to.
Global IDs are a cool Rails feature that allows you to have a unique identifier for any record in your system. Exactly what we want. We’re using a signed version, which is a more secure version that prevents tampering with the ID.
```plain text
class FavoriteRecordsController < ApplicationController
before_action :set_favoritable
def create
@favoritable.favorite!
redirect_back_or_to @favoritable
end
def destroy
@favoritable.unfavorite!
redirect_back_or_to @favoritable
end
private
def set_favoritable
@favoritable = GlobalID::Locator.locate_signed(
params[:favoritable_sgid],
for: :favoritable
)
end
end
```
Signed IDs help us make this controller look just like any other Rails controller. Our helpers in Favoritable take care of the rest. Pretty neat!
And the route will look like this:
```plain text
Rails.application.routes.draw do
resource :favorite_records,
param: :favoritable_sgid,
only: %i[create destroy]
end
```
When rendering each record in an index view, we can add a button to favorite/unfavorite it. Let’s create a partial for this, so we can reuse it in several models:
```plain text
<%# app/views/application/_favorite_record_button.html.erb %>
<% if favoritable.favorited_by? Current.user %>
<%= button_to 'Unfavorite',
favorite_records_path(favoritable_sgid: favoritable.favoritable_sgid),
method: :delete %>
<% else %>
<%= button_to 'Favorite',
favorite_records_path(favoritable_sgid: favoritable.favoritable_sgid),
method: :post %>
<% end %>
```
So in app/views/notes/_note.html.erb, we can render this partial:
```plain text
<%= render 'favorite_record_button', favoritable: note %>
```
How we display the favorite records will depend on your app. We could add an index action to our FavoriteRecordsController to list all favorite records for a user, or we could add a “Favorite” tab to the index action of each favoritable model, or even just make favorite records be displayed at the top of the list. I’ll just leave this up to you.
I hope this highlighted how Rails (and Ruby!) helps you build flexible code when you lean into its conventions. We modeled our task into something that looks like it came out of a Rails generator, so it fits smoothly into the framework.
When you’re designing a new feature, think about how you can use Rails conventions to make it easier to build and maintain. Modeling work into the MVC pattern makes it easier to build it in Rails, because the framework is optimized for that.
## If you enjoyed this post, you might also like:
## About thoughtbot
We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.

One-on-one mentoring with a thoughtbot developer