Rails ActiveSupport Concern: Simplifying your model logics
08 Jun 2025 - Gagan Shrestha
Master Your Models: A Deep Dive into Rails ActiveSupport::Concern
If you’ve been working with Rails for a while, you’ve likely encountered the infamous “fat model” problem. As an application grows, models can become bloated with business logic, callbacks, and methods that make them difficult to read, test, and maintain.
One of the best ways to combat this is by extracting related, reusable pieces of functionality into modules. In the Rails world, the gold standard for this is ActiveSupport::Concern
.
But what is it, exactly? And how is it better than just using a plain old Ruby module? Let’s dive in.
The Problem: Sharing Behavior Across Models
Imagine we’re building a content management system. We have an Article
model and a NewsItem
model. Both of these can be published, drafted, or scheduled to be published in the future.
This “publishable” behavior would include:
- A scope to find all published records
- A scope to find all scheduled records
- An instance method to check if an item is published
- An instance method to publish an item immediately
- A validation to ensure a
published_at
date is set for scheduled items
Without a shared module, we’d have to duplicate this logic in both Article.rb
and NewsItem.rb
. This is a clear violation of the DRY (Don’t Repeat Yourself) principle.
The “Old Way”: Plain Ruby Mixins
Before ActiveSupport::Concern
became the standard, developers used a common pattern with standard Ruby modules to solve this. It works, but it’s a bit clunky.
Let’s create a Publishable
module the “old way.”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# app/models/concerns/publishable_old.rb
module PublishableOld
# The self.included hook is called when this module is included in a class.
def self.included(base)
# 'base' is the class that includes this module (e.g., Article)
# Add class methods to the base class
base.extend(ClassMethods)
# Add associations, validations, etc. to the base class
base.class_eval do
validates :published_at, presence: true, if: :scheduled?
# We need a status column for this example
# enum status: { draft: 0, published: 1, scheduled: 2 }
end
end
# A nested module to hold the class methods
module ClassMethods
def published
where("published_at <= ?", Time.current)
end
def scheduled
where("published_at > ?", Time.current)
end
end
# Instance methods are defined directly in the module
def published?
published_at.present? && published_at <= Time.current
end
def publish!
update(published_at: Time.current)
end
private
def scheduled?
published_at.present? && published_at > Time.current
end
end
To use it, we would include it in our models:
1
2
3
4
5
6
7
8
9
# app/models/article.rb
class Article < ApplicationRecord
include PublishableOld
end
# app/models/news_item.rb
class NewsItem < ApplicationRecord
include PublishableOld
end
What’s the Problem Here?
This pattern works, but it has some drawbacks:
- Boilerplate: You always have to remember the
self.included(base)
hook and thebase.extend(ClassMethods)
pattern. It’s repetitive and easy to forget. - Readability: The logic is split. Instance methods are at the top level, but class methods are nested inside a
ClassMethods
module. It’s not immediately intuitive. - Dependency Management: If your module depended on another module, managing the inclusion order could become complex.
The “New Way”: The Elegance of ActiveSupport::Concern
ActiveSupport::Concern
was created to clean up this exact pattern. It’s a module that, when extended, gives you a much cleaner and more declarative syntax for building mixins.
Let’s refactor our Publishable
module using the Rails way.
First, create the file: app/models/concerns/publishable.rb
. Rails is configured to automatically load files from this directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# app/models/concerns/publishable.rb
require 'active_support/concern'
module Publishable
extend ActiveSupport::Concern
# Code inside the 'included' block is evaluated in the context
# of the class that includes this concern. This is the perfect
# place for validations, associations, and other macros.
included do
validates :published_at, presence: true, if: :scheduled?
scope :published, -> { where("published_at <= ?", Time.current) }
scope :scheduled, -> { where("published_at > ?", Time.current) }
end
# The 'class_methods' block is a shortcut. Any methods defined
# here will be added as class methods to the including class.
# Note: Scopes defined in the 'included' block are often cleaner.
# This block is great for other types of class methods.
# class_methods do
# def total_published
# published.count
# end
# end
# Instance methods are defined just like in a regular module.
def published?
published_at.present? && published_at <= Time.current
end
def publish!
update(published_at: Time.current)
end
private
def scheduled?
published_at.present? && published_at > Time.current
end
end
Now, our models look exactly the same, but they are much cleaner under the hood.
1
2
3
4
5
6
7
8
9
# app/models/article.rb
class Article < ApplicationRecord
include Publishable
end
# app/models/news_item.rb
class NewsItem < ApplicationRecord
include Publishable
end
Why is This So Much Better?
- No More Boilerplate: The
extend ActiveSupport::Concern
line handles the complexself.included
andextend
logic for you. - Clarity and Intent: The
included
block makes it crystal clear what code is being evaluated in the context of the host class. Defining scopes here is idiomatic and easy to read. - Intuitive Structure: All your instance methods are at the top level, making the module feel more like a natural extension of the class.
- Dependency Resolution:
ActiveSupport::Concern
intelligently handles dependencies between concerns, ensuring they are included in the correct order.
Putting It All Together: A Full Example
Let’s see it in action.
1. Create a migration:
1
rails g migration AddPublishedAtToArticlesAndNewsItems published_at:datetime
This will create a migration file. You can modify it to add the column to both tables.
1
2
3
4
5
6
7
# db/migrate/YYYYMMDDHHMMSS_add_published_at_to_articles_and_news_items.rb
class AddPublishedAtToArticlesAndNewsItems < ActiveRecord::Migration[7.0]
def change
add_column :articles, :published_at, :datetime
add_column :news_items, :published_at, :datetime
end
end
Run rails db:migrate
.
2. Create the concern
Create app/models/concerns/publishable.rb
(as shown above).
3. Include the concern in your models
Update article.rb
and news_item.rb
(as shown above).
4. Use it in the console:
Open the Rails console with rails c
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Create a few records
# A published article
Article.create(title: "Hello Rails", content: "...", published_at: 1.day.ago)
# A scheduled article
Article.create(title: "Upcoming Features", content: "...", published_at: 1.week.from_now)
# A draft article (no published_at)
draft_article = Article.create(title: "Work in Progress", content: "...")
# A published news item
NewsItem.create(headline: "Big News!", published_at: Time.current)
# Check our scopes
Article.published.count
#=> 1
Article.scheduled.count
#=> 1
NewsItem.published.count
#=> 1
# Use the instance methods
draft_article.published?
#=> false
draft_article.publish!
#=> true (updates the record in the DB)
draft_article.published?
#=> true
Article.published.count
#=> 2
When to Use a Concern (and When Not To)
Concerns are powerful, but they aren’t a silver bullet for all code organization problems.
Use a concern when:
- You have a cohesive set of functionalities (methods, scopes, validations) that are directly related to a model’s data and state
- This functionality needs to be shared across multiple models.
Publishable
,Taggable
, andCommentable
are classic examples - You are extracting behavior to make a “fat model” thinner and more focused
Consider alternatives (like Service Objects) when:
- The logic involves coordinating between multiple different models (e.g., creating a User, their Account, and sending a welcome email all at once). This is a job for a Service Object
- The logic represents a single, complex business action that doesn’t feel like a core property of the model itself
Concerns modify and query the state of a single object, while Service Objects orchestrate complex operations.
Conclusion
ActiveSupport::Concern
is a fundamental tool in the modern Rails developer’s toolkit. It provides a clean, readable, and idiomatic way to organize your code, adhere to the DRY principle, and keep your models from becoming unmanageable. By understanding how it improves upon the “old way” of Ruby mixins, you can better appreciate its elegance and use it effectively in your projects.
Go forth and refactor those fat models!