DEV Community

Alex Aslam
Alex Aslam

Posted on

The `concerns/` Folder: A Loom of Architecture or a Digital Junk Drawer?

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Service Objects: For complex business logic or procedures (PaymentProcessingService).
  2. Value Objects: For wrapping simple data structures with behavior (Money, EmailAddress).
  3. View Components / Helpers: For presentation logic.
  4. 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 - Adds search_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)