RoR Concerns and Mixins
Concerns are reusable modules that you mix into models or controllers to share behaviour across multiple classes. When several models all need the same methods — like archiving, tagging, or tracking status — you write the logic once in a concern and include it wherever it is needed instead of duplicating code.
The Problem Concerns Solve
WITHOUT concerns — duplicated code:
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :draft, -> { where(published: false) }
def publish!; update(published: true, published_at: Time.current); end
def unpublish!; update(published: false, published_at: nil); end
end
class Product < ApplicationRecord
scope :published, -> { where(published: true) }
scope :draft, -> { where(published: false) }
def publish!; update(published: true, published_at: Time.current); end
def unpublish!; update(published: false, published_at: nil); end
end
WITH concerns — written once, used everywhere:
class Article < ApplicationRecord
include Publishable
end
class Product < ApplicationRecord
include Publishable
end
Creating a Model Concern
Model concerns live in app/models/concerns/:
app/models/concerns/publishable.rb
module Publishable
extend ActiveSupport::Concern
included do
scope :published, -> { where(published: true) }
scope :draft, -> { where(published: false) }
scope :recently_published, -> { published.order(published_at: :desc) }
validates :published_at, presence: true, if: :published?
end
def publish!
update(published: true, published_at: Time.current)
end
def unpublish!
update(published: false, published_at: nil)
end
def published_duration
return nil unless published?
distance_of_time_in_words(published_at, Time.current)
end
end
Include it in any model:
class Article < ApplicationRecord include Publishable end class Product < ApplicationRecord include Publishable end class JobListing < ApplicationRecord include Publishable end
The included Block
The included do ... end block runs code in the context of the including class. Use it for class-level declarations like scopes, validations, callbacks, and associations:
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :taggable, dependent: :destroy
has_many :tags, through: :taggings
scope :with_tag, -> (name) { joins(:tags).where(tags: { name: name }) }
end
def tag_list
tags.map(&:name).join(", ")
end
def add_tag(name)
tags << Tag.find_or_create_by(name: name)
end
def remove_tag(name)
tags.delete(Tag.find_by(name: name))
end
end
A Soft-Delete Concern
app/models/concerns/soft_deletable.rb
module SoftDeletable
extend ActiveSupport::Concern
included do
scope :active, -> { where(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
default_scope { active }
end
def soft_delete
update(deleted_at: Time.current)
end
def restore
update(deleted_at: nil)
end
def deleted?
deleted_at.present?
end
end
Required migration for any model using this concern:
add_column :articles, :deleted_at, :datetime add_index :articles, :deleted_at
Controller Concerns
Controller concerns live in app/controllers/concerns/. Share behaviour across multiple controllers:
app/controllers/concerns/paginatable.rb
module Paginatable
extend ActiveSupport::Concern
included do
before_action :set_pagination_params
end
private
def set_pagination_params
@page = (params[:page] || 1).to_i
@per_page = [(params[:per_page] || 20).to_i, 100].min
end
def paginate(scope)
scope.page(@page).per(@per_page)
end
end
class ArticlesController < ApplicationController
include Paginatable
def index
@articles = paginate(Article.published.recent)
end
end
class ProductsController < ApplicationController
include Paginatable
def index
@products = paginate(Product.available)
end
end
An Authentication Concern
app/controllers/concerns/authenticatable.rb
module Authenticatable
extend ActiveSupport::Concern
included do
before_action :authenticate_user!
before_action :set_current_user
end
private
def set_current_user
Current.user = current_user
end
def require_admin!
redirect_to root_path, alert: "Admin access required." unless current_user.admin?
end
end
Concern vs Inheritance
Use Inheritance when:
Models truly share an IS-A relationship
All behaviour in the parent applies to every subclass
Example: AdminUser < User
Use Concerns when:
Models share specific BEHAVIOUR but are otherwise different types
You need to mix the same behaviour into unrelated models
Example: Article and Product both need Publishable behaviour
but Article is not a type of Product
Concerns Organization
app/models/concerns/ publishable.rb ← published/draft state management soft_deletable.rb ← soft delete behaviour taggable.rb ← tag associations sluggable.rb ← auto-generate URL slugs searchable.rb ← full-text search scope auditable.rb ← track who created/updated recordsapp/controllers/concerns/ paginatable.rb ← shared pagination logic authenticatable.rb ← authentication before_actions json_respondable.rb ← respond_to JSON helper
