Every seasoned Rails developer remembers the first time they opened a mature codebase. You navigate through the familiar models/
, controllers/
, and views/
with a sense of order. Then, you see it: the concerns/
folder. You open it with a mix of hope and trepidation. What lies within? A collection of elegant, reusable modules that sing with single responsibility? Or a chaotic pile of Notifiable
, Taggable
, SoftDeletable
, and ThatThingWeNeededForTheMarketingReport
?
This folder is more than just a directory; it's a Rorschach test for your application's architecture. It's a journey from the blissful ignorance of "just extract it!" to the hard-won wisdom of intentional design. Let's reframe this not as a debate, but as an artisan's guide to sculpting with concerns.
Act I: The Blank Canvas — The Promise of concerns/
Introduced as a formalization of the module pattern, concessions/
(in both app/models/concerns
and app/controllers/concerns
) was a gift. It promised a way to break the chains of single-table inheritance and tidy up those fat models that haunt our dreams.
The principle was, and still is, beautiful: horizontal composition.
Think of your core model—the User
, the Project
, the Article
—as a vertical pillar, strong and central. Concerns are the horizontal threads you weave through these pillars, creating a rich tapestry of behavior. A User
is Commentable
. A Project
is Searchable
. An Article
is Taggable
and Publishable
.
This is the loom on which we work. When used with intent, it allows us to write code like this:
# app/models/concerns/taggable.rb
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
end
def add_tag(name)
tags << Tag.find_or_create_by(name: name)
end
end
# app/models/article.rb
class Article < ApplicationRecord
include Taggable
include Publishable
# ... clean, expressive, declarative
end
This is the artwork we aspire to create. Modules that are named by behavior, not by data. Modules that are tightly focused and truly reusable across multiple, unrelated models.
Act II: The First Cracks — Where the Junk Drawer Forms
The path to the junk drawer is paved with good intentions. It starts subtly. You have a User
model with a few complex methods for generating avatars. "This doesn't belong in the model!" you proclaim, and rightfully so. You extract it.
# app/models/concerns/avatarable.rb
module Avatarable
def gravatar_url
# ... logic ...
end
def generate_initial_avatar
# ... logic ...
end
end
But then, a question: is Avatarable
a genuine, reusable behavior? Or is it just a namespace for methods that you didn't want to put in User
? If it's only ever included in User
, you haven't composed horizontally; you've just created a separate file for a single class. This is procedural extraction, not object-oriented composition.
This is the first piece of junk in the drawer: The Pseudo-Concern. It looks like a concern, it quacks like a concern, but it's just a procedural grouping of methods for a single class.
The next piece of junk is far more dangerous: The God-Module Concern. You create a Notifiable
concern that handles in-app notifications, emails, and SMS. It has its own callbacks, state machines, and a complex web of private methods. It's not a single thread in the tapestry; it's a thick, knotted rope that's hard to follow and harder to test. It becomes a mini-application hidden inside a module, violating the very isolation it was meant to provide.
Act III: The Artisan's Rules — Disciplining the Loom
So how do we, as senior developers and architects, prevent our masterpiece from becoming a dumping ground? We impose a strict set of artistic constraints.
Rule 1: The "At Least Two" Rule
A true concern must be included in at least two unrelated models. If you find yourself creating a concern for a single model, stop. Ask yourself: "Is this just a User
helper?" If the answer is yes, consider using a plain old Ruby object (PORO) like a UserAvatarService
or a simple mixin within the User
class itself.
Rule 2: The Contract of Inclusion
A concern should define a clear contract. What does it require from the host class? Does it expect certain attributes or relationships? Document this at the top of the module.
# This module requires the including class to have:
# - `first_name` :string
# - `last_name` :string
# - `email` :string
module Nameable
# ... code that uses first_name, last_name, email ...
end
Better yet, use the included
block to declare dependencies explicitly, raising helpful errors if they are missing.
Rule 3: Beware the Callback Spaghetti
ActiveSupport::Concern makes it easy to use included
blocks with callbacks. This is a powerful feature that can quickly create a nightmare. When a model includes three concerns, and each concern adds a before_save
callback, understanding the order of execution becomes a debugging hell.
Guideline: Use callbacks within concerns sparingly and document them heavily. Prefer explicit method calls that the host model can invoke in its own callbacks when possible.
Rule 4: The Namespace of Last Resort
Before creating a concern, exhaust these alternatives first:
- Service Objects: For complex business logic or procedures (
PaymentProcessingService
). - Value Objects: For wrapping simple data structures with behavior (
Money
,EmailAddress
). - View Components / Helpers: For presentation logic.
- Decorators / Presenters: For enriching models with view-specific behavior without polluting the model.
A concern is for reusable, cross-cutting model or controller behavior, not for every piece of extracted code.
Act IV: The Masterpiece — A Well-Woven Tapestry
When you adhere to these rules, the concerns/
folder transforms. It's no longer a junk drawer; it's a curated toolbox. You open it and find a collection of sharp, precise instruments:
-
app/models/concerns/
-
searchable.rb
- Addssearch_by_term(term)
to any model. -
slugable.rb
- Provides URL-friendly slugs. -
soft_deletable.rb
- A canonical pattern for archiving records.
-
-
app/controllers/concerns/
-
authenticable.rb
- Handles API token authentication for a subset of controllers. -
paginatable.rb
- Standardizes pagination params.
-
Each file is a self-contained, well-tested, and genuinely reusable component. The relationship between the host class and the concern is clear, documented, and loose. Your models become declarations of what they are and what they do, not monolithic repositories of how they do it.
The Journey's End: A Question of Intent
The concerns/
folder is neither inherently powerful nor inherently a junk drawer. It is a mirror of the team's architectural discipline.
It becomes a junk drawer when used as a reflex, a place to "shove" code that feels out of place elsewhere. It becomes a powerful tool when used with intentionality, as a specific solution for the problem of horizontal composition.
So the next time your cursor hovers over the "New File" button within concerns/
, pause. Ask yourself the artisan's questions: "Is this a behavior? Will it be shared? Is this the right loom for this thread?"
Your answer will determine whether you're crafting a masterpiece or just filling a drawer.
Top comments (0)