Introduction
ActiveSupport::Concern is Rails' answer to the boilerplate that comes with using included hooks and nested ClassMethods modules. It simplifies the pattern so much that it has become the standard way to write mixins in Rails applications. This lesson shows you the problem it solves and how to use it — and even how to build a minimal version yourself.
Key Concepts
- Concern: A module that extends
ActiveSupport::Concernto gain a cleaner DSL for hooks and class methods includedblock: A block (not a method) that runs in the context of the including classclass_methodsblock: Replaces the manualmodule ClassMethods+base.extendpattern- Dependency resolution: Concerns automatically handle chains of
includeso thatincludedblocks run in the correct order
Real World Context
In a typical Rails application you might have dozens of shared behaviors — Searchable, Taggable, Auditable, SoftDeletable. Without Concern, every one of these modules requires the same self.included(base) boilerplate. With Concern, each module is concise, readable, and correctly handles dependency chains when one concern includes another.
Deep Dive
The Problem
Without Concern, adding class methods and running class-level code from a module requires verbose boilerplate. Here is the traditional approach:
ruby# Without Concern - verbose and error-prone module Taggable def self.included(base) base.extend(ClassMethods) base.class_eval do has_many :tags end end module ClassMethods def most_tagged # ... end end def tag_names tags.map(&:name) end end
Every module that needs class methods repeats this exact same structure, which is tedious and error-prone.
The Solution
ActiveSupport::Concern wraps this boilerplate into a clean DSL:
rubyrequire 'active_support/concern' module Taggable extend ActiveSupport::Concern included do has_many :tags end class_methods do def most_tagged # ... end end def tag_names tags.map(&:name) end end
The included block replaces self.included(base) plus base.class_eval, and class_methods replaces the nested ClassMethods module plus base.extend. The result is more readable and less error-prone.
Dependency Resolution
Concern also handles dependencies between modules correctly. When one concern includes another, the included blocks run in the right order:
rubymodule A extend ActiveSupport::Concern included do # This runs when A is included end end module B extend ActiveSupport::Concern include A # A's included block runs when B is included end class MyClass include B # Both A and B's included blocks run end
Without Concern, including A inside B would immediately trigger A's included hook with B as the base, which is usually not what you want. Concern defers execution until the final class includes the outermost module.
Roll Your Own
To understand the mechanics, here is a simplified version of Concern:
rubymodule SimpleConcern def self.extended(base) base.instance_variable_set(:@_included_block, nil) end def included(base = nil, &block) if block @_included_block = block else base.class_eval(&@_included_block) if @_included_block end end end
This stores the block and replays it when the module is finally included into a class. The real ActiveSupport implementation adds dependency tracking and class method support.
Common Pitfalls
- Forgetting
extend ActiveSupport::Concern— Without this line,included do ... enddefines a regular method calledincludedinstead of registering a callback block. Your code will silently do nothing. - Using
self.includedinstead of theincludedblock — When using Concern, always use the block form. Definingself.includedoverrides the Concern mechanism and breaks dependency resolution.
Best Practices
- One responsibility per concern — Each concern module should encapsulate a single behavior (e.g., Taggable, Searchable). Combining unrelated behaviors in one concern defeats the purpose.
- Use
class_methodsinstead ofClassMethodsmodule — The block-basedclass_methodsDSL is cleaner and avoids the manualextendstep. It also integrates with Concern's dependency resolution.
Summary
- ActiveSupport::Concern removes boilerplate from the
included+ClassMethodspattern - It provides
includedandclass_methodsblocks that run in the correct context automatically - Dependency resolution ensures that chained concerns execute their hooks in the right order
Code Examples
require 'active_support/concern'
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(query) { where("name LIKE ?", "%#{query}%") }
end
class_methods do
def search_fields
[:name, :description]
end
end
def matching_terms(query)
self.class.search_fields.select { |f| send(f)&.include?(query) }
end
end