Rails concerns get a bad reputation. Developers either avoid them entirely or stuff every shared method into a concern, creating a tangled mess. The truth lies in the middle: concerns are powerful when used for specific patterns and problematic when used as a dumping ground.
What Concerns Actually Solve
Concerns address horizontal code sharing—behavior that cuts across multiple models or controllers without fitting into inheritance. Think of trackable behavior (who created/updated a record), searchable functionality, or soft deletion. These features don't define what a model is, but rather what it can do.
The key distinction: inheritance answers "what is this?" while concerns answer "what can this do?" A Post is a type of content, but it can be searchable, trackable, and soft-deletable.
Pattern 1: Behavior Modules with Callbacks
The most common use case wraps callbacks and instance methods into a reusable package. Consider tracking who creates and updates records across multiple models:
# app/models/concerns/trackable.rb
module Trackable
extend ActiveSupport::Concern
included do
belongs_to :created_by, class_name: 'User', optional: true
belongs_to :updated_by, class_name: 'User', optional: true
before_create :set_created_by
before_update :set_updated_by
end
private
def set_created_by
self.created_by ||= Current.user
end
def set_updated_by
self.updated_by = Current.user if Current.user
end
endApply this to any model that needs audit tracking:
# app/models/post.rb
class Post < ApplicationRecord
include Trackable
validates :title, presence: true
validates :body, presence: true
end
# app/models/comment.rb
class Comment < ApplicationRecord
include Trackable
belongs_to :post
validates :content, presence: true
endThis pattern works because the behavior is self-contained. The concern doesn't need to know anything about the including class beyond the existence of the required columns.
Pattern 2: Query Interface Extensions
Concerns excel at adding scopes and class methods that models can opt into. A searchable concern demonstrates this pattern:
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
class_methods do
def search(query)
return all if query.blank?
columns = searchable_columns.map { |col| "#{table_name}.#{col}" }
conditions = columns.map { |col| "#{col} LIKE :query" }.join(' OR ')
where(conditions, query: "%#{sanitize_sql_like(query)}%")
end
def searchable_columns
raise NotImplementedError, "#{name} must define searchable_columns"
end
end
end
# app/models/article.rb
class Article < ApplicationRecord
include Searchable
def self.searchable_columns
%i[title summary body]
end
end
# app/models/product.rb
class Product < ApplicationRecord
include Searchable
def self.searchable_columns
%i[name description sku]
end
endThe concern defines the interface, and each model specifies its searchable columns. This creates a contract: include the concern, implement the required method, and gain the functionality.
Pattern 3: Stateful Behavior with Validations
Complex state management benefits from concern extraction. Soft deletion with restoration capabilities makes a good example:
# app/models/concerns/soft_deletable.rb
module SoftDeletable
extend ActiveSupport::Concern
included do
scope :kept, -> { where(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
default_scope { kept }
end
def soft_delete
update(deleted_at: Time.current)
end
def restore
update(deleted_at: nil)
end
def deleted?
deleted_at.present?
end
class_methods do
def with_deleted
unscope(where: :deleted_at)
end
def only_deleted
with_deleted.deleted
end
end
endUsage in a controller shows the clean interface this provides:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def destroy
@post = Post.find(params[:id])
@post.soft_delete
redirect_to posts_path, notice: 'Post moved to trash'
end
def restore
@post = Post.with_deleted.find(params[:id])
@post.restore
redirect_to posts_path, notice: 'Post restored'
end
def trash
@posts = Post.only_deleted
end
endCommon Mistakes to Avoid
Several patterns indicate concern misuse. Watch for these warning signs:
- Concerns that require specific columns without documenting them. Always add a comment listing required database columns or raise helpful errors when they're missing.
- Concerns that include other concerns. This creates hidden dependencies and makes debugging difficult. Keep concerns flat and independent.
- Concerns with more than 100 lines. Large concerns often indicate a missing abstraction. Consider whether a service object or separate class would work better.
- Concerns used by only one class. If only one model uses it, the code belongs in that model. Extract to a concern when the second use case appears.
When to Skip Concerns Entirely
Some situations call for different solutions. Service objects handle complex business logic better than concerns. Form objects manage multi-model forms more cleanly. Query objects encapsulate complex database queries with better testability.
The decision framework: concerns work for declarative behavior ("this model has these capabilities"), while objects work for imperative processes ("perform this operation").
Testing Concerns in Isolation
Concerns deserve their own tests using anonymous classes:
# spec/models/concerns/trackable_spec.rb
RSpec.describe Trackable do
let(:model_class) do
Class.new(ApplicationRecord) do
self.table_name = 'posts'
include Trackable
end
end
let(:user) { create(:user) }
before { Current.user = user }
after { Current.user = nil }
describe 'on create' do
it 'sets created_by to current user' do
record = model_class.create!(title: 'Test', body: 'Content')
expect(record.created_by).to eq(user)
end
end
describe 'on update' do
it 'sets updated_by to current user' do
record = model_class.create!(title: 'Test', body: 'Content')
record.update!(title: 'Updated')
expect(record.updated_by).to eq(user)
end
end
endSummary
Effective concerns share specific characteristics: they're self-contained, well-documented, and focused on a single responsibility. The three patterns covered—behavior modules, query extensions, and stateful behavior—represent the most maintainable uses of concerns in Rails applications.
Start with code in the model. Extract to a concern when a second model needs the same behavior. Keep concerns small, flat, and independent. Following these guidelines turns concerns from a source of confusion into a powerful tool for code organization.