Simplifying Rails Controllers with Concerns - Real-world Examples from Our Projects

Looking to have improve your Rails project structure? Use Controller Concerns to keep our Rails controllers clean and maintainable. Learn from real-world examples Add Jam have in production.

Tuesday February 18th 2025 | 7 min read

Simplifying Rails Controllers with Concerns - Real-world Examples from Our Projects blog post header image

In our previous post about using Ruby on Rails Model Concerns, we explored how using concerns in our Rails projects help keep our codebase models clean and maintainable. Today, we're diving into Controller Concerns - another powerful feature we regularly use at Add Jam to organise our Rails applications.

Why Use Controller Concerns?

We've found that in MVC applications the controllers can quickly become cluttered with logic or we have some core repeatable functionality that we want to use across multiple controllers. With the mantra of keeping our code DRY we utilise Controller Concerns to help us extract common patterns and logic into reusable modules, making our controllers cleaner, more focused on their primary responsibilities and easier to test.

Let's explore some real-world Controller Concerns we've implemented across our Ruby on Rails web projects.

1. Authentication Concern

Authentication logic often spreads across multiple controllers. By extracting it into a concern, we can maintain consistent authentication behaviour throughout our application:

module Authenticatable
  extend ActiveSupport::Concern

  included do
    before_action :authenticate_user!
    helper_method :current_user
    
    private

    def current_user
      @current_user ||= User.find_by(id: session[:user_id])
    end

    def authenticate_user!
      login_user!
      unless current_user.present?
        redirect_to login_path, alert: "Please sign in to continue"
      end
    end
  end
end

This is a really light touch authentication concern example (and importantly assumes you have some kind of actual authentication system in place - ** this is not a comprehensive example**). But here we have a few convenance methods we can use across our controllers simply by including it in your application controller:

class ApplicationController < ActionController::Base
  include Authenticatable
end

And assuming your domain specific controllers in your app are inheriting from ApplicationController we now have authentication logic available to us. Simple.

All the authentication logic can be located in one place making it DRY and therefore more maintainable and easier to test.

2. Using Header values

A common use we have for Controller Concerns is to access header values in a request and do something with it. For example, let's say we have a client that is sending a Version string as a header. Here's a real-world concern we use to access this value and do something with it.

module VersionHeader
  extend ActiveSupport::Concern

  included do
    before_action :check_version
  end

  private

  def check_version
    version = request.get_header("HTTP_X_VERSION")
    return if version.blank?

    unless version_valid?(version)
      render json: { error: "Invalid" }, status: 422
    end
  end

  def version_valid?(version)
    # Add your version validation logic here
  end
end

The VersionHeader concern is designed to automatically get the version header from a request and if the version is not valid we can render an error. Simple approach to API versioning that might come in handy with say a mobile app that where you want to stop older versions of the app from accessing the API.

Rather than pepper that logic through your controllers we can now just include this concern in our application controller and have it available to us.

3. Localisation Management

For modern web applications serving an international audience, handling localisation properly is essential. Rails has really good i18n support (perhaps that's a topic for another post) for managing the locationsation content but how can we set the locale for a request? Enter the Locale concern:

module Locale
  extend ActiveSupport::Concern

  included do
    around_action :switch_locale

    private

    def switch_locale(&action)
      locale = Current.user.try(:locale) || I18n.default_locale
      I18n.with_locale(locale, &action)
    end
  end
end

This concern automatically sets the appropriate locale based on user preferences, ensuring consistent language handling throughout the request lifecycle.

Best Practices for Controller Concerns

Through our experience building Rails applications, we've developed these guidelines for Controller Concerns:

  1. Single Responsibility: Each concern should handle one specific aspect of controller functionality
  2. Keep Them Light: Concerns should contain minimal logic - consider 'services' for complex operations
  3. Consistent Naming: Use clear, descriptive names that indicate the concern's purpose
  4. Documentation: Include clear documentation explaining when and how to use the concern. Easy to be caught out by weird behaviour if you don't know what's going on in ApplicationController.
  5. Testing: Test, test, test. Write dedicated specs for your concerns to ensure reliability

Testing Controller Concerns

Here's how we test our Controller Concerns (like our Model Concerns post we're using RSpec):

RSpec.describe PushToken, type: :controller do
  controller(ApplicationController) do
    include PushToken
    def index
      render plain: "OK"
    end
  end

  describe "#save_user_device" do
    let(:user) { create(:user) }
    
    before do
      allow(Current).to receive(:user).and_return(user)
      request.headers["HTTP_X_PUSH_TOKEN"] = "test-token"
    end

    it "creates a new device token" do
      expect {
        get :index
      }.to change(user.device_tokens, :count).by(1)
    end

    it "updates last_used_at for existing tokens" do
      token = create(:device_token, user: user, token: "test-token")
      get :index
      expect(token.reload.last_used_at).to be_present
    end
  end
end

RSpec.describe VersionHeader, type: :controller do
  controller(ApplicationController) do
    include VersionHeader
    def index
      render plain: "OK"
    end
  end

  describe "#check_version" do
    before do
      allow(controller).to receive(:version_valid?).and_return(true)
    end

    context "when version header is present" do
      before do
        request.headers["HTTP_X_VERSION"] = "1.0"
      end

      it "calls version_valid? with the version" do
        expect(controller).to receive(:version_valid?).with("1.0")
        get :index
      end

      it "renders OK if version is valid" do
        get :index
        expect(response.body).to eq("OK")
      end
    end

    context "when version header is invalid" do
      before do
        allow(controller).to receive(:version_valid?).and_return(false)
        request.headers["HTTP_X_VERSION"] = "invalid"
      end

      it "renders an error if version is invalid" do
        get :index
        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.body).to include("Invalid version")
      end
    end

    context "when version header is not present" do
      it "does not call version_valid?" do
        expect(controller).not_to receive(:version_valid?)
        get :index
      end

      it "renders OK" do
        get :index
        expect(response.body).to eq("OK")
      end
    end
  end
end

Common Controller Concern use cases

We've successfully implemented Controller Concerns for various features in projects including:

  • Request tracking and analytics
  • API version handling
  • Error handling and reporting
  • Rate limiting
  • Response formatting

There's a lot of scope for using Controller Concerns to keep your controllers clean and maintainable.

When NOT to use Controller Concerns

While concerns are powerful, they're not always the right tool for the job. Here are some situations where you might want to consider alternatives:

  1. Complex Business Logic: If your concern is handling complex business rules, consider using a service object instead
  2. Database Operations: Heavy database operations should probably live in a model or service
  3. External API Calls: These are better suited to dedicated service objects
  4. View-specific Logic: This belongs in helpers or view components

Remember, concerns are meant to be lightweight modules that extend controller functionality, not a dumping ground for complex logic.

Debugging Tips

When working with concerns, here are some common debugging approaches we use:

  1. Check load order: Make sure concerns are being loaded in the correct order
  2. Monitor performance: Watch for N+1 queries or slow operations in your concerns, they'll impact all your controllers that include them
  3. Test in isolation: Use RSpec's describe blocks to test concern methods independently

Unsure on when to use Controller Concerns?

Based on our experience, Controller Concerns are ideal when:

  • Multiple controllers share common functionality
  • You need consistent behaviour across controllers
  • The shared logic is relatively simple and focused
  • You want to keep your controllers DRY and maintainable

There's no hard and fast rule on when to use Controller Concerns and the choice of when to use them will come from experience. As a top tip if you find yourself repeating the same logic in multiple controllers then it's probably a good candidate for a concern.

Try it out

Hopefully we've convinced you that Controller Concerns are a powerful tool for organising Rails applications. At Add Jam, we've used them successfully across numerous projects to keep our controllers clean and maintainable while ensuring consistent behaviour throughout our applications. As well as Model Concerns which we previously posted about.

If you're looking to improve your Rails application's architecture or need help implementing these patterns, our team of experienced Rails developers can help. We offer:

  • Custom Ruby on Rails development
  • Code reviews and architectural guidance
  • Performance optimisation
  • Technical consulting

Get in touch with our Rails development team to discuss how we can help improve your Rails application.


Add Jam is a leading Ruby on Rails development agency based in Glasgow, Scotland. We specialise in building robust, scalable applications using Ruby on Rails, React, and React Native for clients across the UK and globally.

Michael Hayes's avatar

Michael Hayes

Co-founder

Recent case studies

Here's a look at some of products we've brought to market recently

Educational Intelligence: Money Matters

Educational Intelligence: Money Matters

How Add Jam partnered with Educational Intelligence to create Money Matters, a digital platform addressing the UK's financial literacy crisis where 12.9 million adults struggle with money management.

Great Glasgow Coffee Run

Great Glasgow Coffee Run

Celebrating Glasgow's vibrant coffee culture and running community through an interactive digital experience that maps out the perfect coffee-fuelled running route through the city.

One Walk A Day

One Walk A Day

During lockdown we rapidly prototyped a health and wellbeing app using React Native then expanded on the concept and redeveloped using SwiftUI

We take products from an idea to revenue

Add Jam is your plug in team of web and mobile developers, designers and product managers. We work with you to create, ship and scale digital products that people use and love.

Hello, let's chat 👋