ruby

Rails Concerns: To Concern Or Not To Concern

Nikola Đuza

Nikola Đuza on

Last updated:

Rails Concerns: To Concern Or Not To Concern

This post was updated on 4 August 2023 to reference a discussion thread on concerns.

If you've ever used Ruby on Rails, you've probably come across the concept of concerns. Whenever you jumpstart a new Rails project, you get a directory app/controllers/concerns and app/models/concerns. But what are concerns? And why do people from the Rails community sometimes talk badly about them?

Quick Overview

A Rails Concern is any module that extends ActiveSupport::Concern module. You might ask — how are concerns so different from modules? The main difference is that Rails concerns allow you to do a bit of magic, like so:

rb
# app/models/concerns/trashable.rb module Trashable extend ActiveSupport::Concern included do scope :existing, -> { where(trashed: false) } scope :trashed, -> { where(trashed: true) } end def trash update_attribute :trashed, true end end

You see that word included. It is a bit of Rails carbohydrates sprinkled upon a Ruby module. What ActiveSupport::Concern does for you is it allows you to put code that you want evaluated inside the included block. For example, you want to extract the trashing logic out of your model. The included allows you to do what we did and later include your model's concern like so:

rb
class Song < ApplicationRecord include Trashable has_many :authors # ... end

Pretty handy and naive at this point, right? The Model lost a bit of weight and trashing can now be reused throughout other models, not just our Song model. Well, things can get complicated. Let's dive in to find out.

A Classic Example of a Mixin

Before we embark further into the depths of concerns, let's add another explanation of them. When you see include SomeModule or extend AnotherModule, these are called mixins. A mixin is a set of code that can be added to other classes. And, as we all know from the Ruby documentation, a module is a collection of methods and constants. So what we are doing here is including modules with methods and constants into different classes so that they can use them.

That is exactly what we did with the Trashable concern. We extracted common logic around trashing a model object into a module. This module can later be included in other places. So, mixin is a design pattern used not only in Ruby and Rails. But, wherever it's used, people either like it and think it is good, or they hate it and think it can easily spin out of control.

To better understand this, we'll go through a couple of pros and cons of using them. Hopefully, by doing this, we can gain an understanding of when or whether to use concerns.

I Have It All

When you decide to extract something to a concern, like Trashable concern, you now have access to all of the functionality of wherever Trashable is included. This brings great power, but as Richard Schneeman said in his blog post on the topic — "with great power comes great ability to make complicated code". He meant complicating code that you might rely on, something that is supposed to be there in your concerns.

If we take a look at the Trashable once more:

rb
module Trashable extend ActiveSupport::Concern included do scope :existing, -> { where(trashed: false) } scope :trashed, -> { where(trashed: true) } end def trash update_attribute :trashed, true end end

The logic of the concern relies on the fact that the trashed field exists wherever the concern is included. Right? No biggie, this is what we want after all. But, what I see happen is that people get tempted to pull in other stuff from the model into the concern. To paint a picture of how this can happen, let's imagine that the Song model has another method featured_authors:

rb
class Song < ApplicationRecord include Trashable has_many :authors def featured_authors authors.where(featured: true) end # ... end class Album < ApplicationRecord include Trashable has_many :authors def featured_authors authors.where(featured: true) end # ... end

To better illustrate, I added an Album model that also includes Trashable. Let's then say we want to notify featured authors of the song and the album when they get trashed. People will get tempted to put this logic inside the concern like so:

rb
module Trashable extend ActiveSupport::Concern included do scope :existing, -> { where(trashed: false) } scope :trashed, -> { where(trashed: true) } end def trash update_attribute :trashed, true notify(featured_authors) end def notify(authors) # ... end end

Right here, things are starting to get complicated a bit. Since we have trashing logic outside our Song model, we might be tempted to put notifying in the Trashable concern. In there, something "wrong" happens. The featured_authors is taken from the Song model. OK, let's say this passes pull request review and CI checks.

Then, a couple of months down the road, a new requirement is set where the developer needs to change the way we present featured_authors for songs. For example, a new requirement wants to show only featured authors from Europe. Naturally, the developer will find where featured authors are defined and edit them.

rb
class Song < ApplicationRecord include Trashable has_many :authors def featured_authors authors.where(featured: true).where(region: 'Europe') end # ... end class Album < ApplicationRecord include Trashable has_many :authors # ... end

This works nicely wherever we show authors, but after we deploy to production, the folks from other parts of the world won't get notified anymore about their songs. Mistakes like these are easy to make when using concerns. The example above is a simple and artificial one, but the ones that are "in the wild" can be super tricky.

What is risky here is that the concern (mixin) knows a lot about the model it gets included in. It is what is called a circular dependency. Song and Album depend on Trashable for trashing, Trashable depends on both of them for featured_authors definition. The same can be said for the fact that a trashed field needs to exist in both models in order to have the Trashable concern working.

This is why a no-concern club might be against, and the pro-concern club is for. I'd say, the first version of Trashable is the one I'd go with in my codebase. Let's see how we can make the second version with notifying better.

Where Do Y'all Come From

Looking back at our Trashable with notifying, we have to do something about it. Another thing that happens when using concerns is that we tend to over-DRY things. Let's try to do that, for demonstration purposes, to our existing models by creating another concern (bear with me on this one):

rb
module Authorable has_many :authors def featured_authors authors.where(featured: true) end end

Then, our Song and Album will look like this:

rb
class Song < ApplicationRecord include Trashable include Authorable # ... end class Album < ApplicationRecord include Trashable include Authorable # ... end

We dried everything up, but now the requirement for featured authors from Europe is not fulfilled. To make things worse, now the Trashable concern and the models depend on the Authorable. What the hell? Exactly my question when I was dealing with concerns some time ago. It's hard to track down where methods are coming from.

My solution to all of this would be to keep featured_authors as close to the models as possible. The notify method should not be a part of Trashable concern at all. Each model should take care of that on its own, especially if they tend to notify different subgroups. Let's see how to do it less painfully:

rb
# Concerns module Trashable extend ActiveSupport::Concern included do scope :existing, -> { where(trashed: false) } scope :trashed, -> { where(trashed: true) } end def trash update_attribute :trashed, true end end module Authorable has_many :authors # Other useful methods that relate to authors across models. # If there are none, ditch the concern. end # Models class Song < ApplicationRecord include Trashable include Authorable def featured_authors authors.where(featured: true).where(region: 'Europe') end # ... end class Album < ApplicationRecord include Trashable include Authorable def featured_authors authors.where(featured: true) end # ... end

Concerns like these are manageable and not too complex. I skipped the notify functionality I described earlier since that can be a topic for another day.

The Final Boss

As DHH, Rails' creator, so perfectly illustrated in this discussion thread, it is completely fine to reference concerns within other concerns. For example, take a look at the code snippet which is referenced in the discussion and extracted from Basecamp, a Rails project built and run by DHH and his team:

ruby
class Identity < ApplicationRecord include Accessor, Authenticable, Boxes, ClearancePreapproval, Clipper, Creator, Eventable, Examiner, Filer, Member, PasswordReset, Poster, Reaching, Regional, TopicMerger end module Identity::Accessor def accessible_topics Topic.joins(:accesses).where(accesses: { contact: contacts }) end def accessible_entries Entry.joins(:topic).merge(accessible_topics) end end

Looking at the code snippet, you are either opening your mouth in awe or you are completely appalled. I feel there is no in-between here. If I got a chance to edit this code, I would envision it as the "Final Concern Boss Fight". But jokes aside, the interesting thing here is that there are comments that say which concern depends on which. Take a look at:

rb
# ... include Subscribable # Depends on Readable include Eventable # Depends on Recordables # ...

Putting comments like these can be helpful, but it's still set up for doing something sketchy, especially if you are new to the codebase. Being new and not being aware of all the "gotchas" a code has can certainly send you on a concern downward spiral.

So how do you deal with the "final boss of concerns" fight? DHH offers this workaround in the same thread:

"...It’s a writing style. Like using subheads to explain subservient ideas within a broader context. You could probably extract all those subheads out, and turn them into little essays of their own. But that’s often just not the right level of extraction. As you saw, the Accessor role starts life as literally just two methods! It doesn’t warrant being turned into it’s own class. It doesn’t have an independent center of gravity."

It's my feeling that taking this into consideration can help you do your concerns better. But at the end of the day, when it comes to how you use concerns, it will really depend on whether you are more comfortable with multiple inheritances from modules, or if you prefer composition. It's your call.

Wrapping Up

As we've seen, concerns are nothing more than modules that provide some useful syntax sugar to extract and DRY up your code. If you have more useful tools under your belt, maybe you shouldn't reach out for concerns right away. Behavior like handling file attachments and the trashing logic we showed in the examples might be good candidates to extract into modules (concerns).

Hopefully, you get to see the possible good and bad things when dealing with concerns and modules in general. Bear in mind that no code is perfect. And in the end, how can you learn what is good and what is bad for you if you don't try and possibly fail or succeed?

No solution is perfect, and I hope you got to understand the Rails concerns way of doing things in the blog post. As always, use your judgment and be aware of the pros and cons.

Until the next one, cheers!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Nikola Đuza

Nikola Đuza

Nikola helps developers improve their productivity by sharing pragmatic advice & applicable knowledge on JavaScript and Ruby.

All articles by Nikola Đuza

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps