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

Leave a Comment

Your email address will not be published. Required fields are marked *