Skip to content
ExponaBLOG
RUBY ON RAILS · DAY 059 / 100

Current attributes: a Ruby on Rails guide for developers

Request-scoped globals done safely.

Expona Admin6 min read

Introduction

Passing context through your Rails application—who the current user is, which account they belong to, tenant information—is a problem every growing application faces. Most developers reach for dependency injection, thread-local variables, or pass parameters through method signatures until they realize how messy that becomes. Current attributes: a Ruby on Rails guide for developers introduces a cleaner pattern via ActiveSupport::CurrentAttributes, a built-in Rails feature that provides request-scoped globals done safely. This article walks you through how to use it, when it makes sense, and the pitfalls to avoid.

What is ActiveSupport::CurrentAttributes

ActiveSupport::CurrentAttributes is a Rails abstraction for storing request-scoped data that needs to be accessible from anywhere in your application without explicit parameter passing. Think of it as a thread-safe, request-aware holder for context like the current user, current account, or request ID.

Rails ships with this out of the box since version 5.2. It uses thread-local storage internally but abstracts away the complexity. Each new request automatically resets all stored values, preventing accidental cross-request contamination—a common pitfall with raw thread-local variables.

Here's the most basic example:

ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :user
  attribute :account
end

That's it. You've created a container that holds user and account for the lifetime of a single request. Now you can set and access them from anywhere:

ruby
# In a controller
Current.user = @user
Current.account = @user.account

# Anywhere else in the request
puts Current.user.email
puts Current.account.name

Current.user and Current.account in practice

The most common use case is storing the authenticated user and their associated account (for multi-tenant applications). Let's walk through a realistic scenario.

You're building a SaaS product. Each request authenticates a user, and that user belongs to an account. You need this information in models, services, jobs, and background tasks. Passing it through method signatures quickly becomes tedious:

ruby
# Without Current attributes (painful)
class ProjectsController < ApplicationController
  def create
    @project = Project.create(project_params, user: current_user, account: current_account)
    notify_team(@project, current_user, current_account)
    update_analytics(@project, current_user, current_account)
  end
end

Now with ActiveSupport::CurrentAttributes:

ruby
# config/initializers/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
  attribute :account
  attribute :request_id
end
ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_current_context

  private

  def set_current_context
    Current.user = current_user
    Current.account = current_account
    Current.request_id = request.id
  end
end
ruby
# app/models/project.rb
class Project < ApplicationRecord
  belongs_to :account
  belongs_to :user

  before_create :assign_defaults

  private

  def assign_defaults
    self.account ||= Current.account
    self.user ||= Current.user
  end
end
ruby
# app/services/project_service.rb
class ProjectService
  def notify_team(project)
    # No need to pass user or account—they're available
    TeamNotifier.call(
      project: project,
      sender: Current.user,
      account: Current.account
    )
  end
end

This approach keeps your method signatures clean and your code more readable. You still know where the data comes from—it's explicitly set in the controller—so there's no magic happening.

Automatic reset per request

One of the biggest advantages of ActiveSupport::CurrentAttributes over raw thread-local variables is automatic cleanup. Each new HTTP request automatically resets all attributes to nil. This prevents data from one request bleeding into another, which is a real problem in threaded application servers like Puma.

Here's what happens under the hood:

ruby
# Request 1
Current.user = User.find(1)  # User with ID 1
# ... request processes ...
# Request 1 ends, Current is reset

# Request 2 (could run on same thread)
puts Current.user  # => nil, not User 1
Current.user = User.find(2)  # User with ID 2
# ... request processes ...

Rails handles this cleanup automatically via a middleware hook. You don't have to do anything. However, if you're using background jobs or ActiveJob, you need to be aware that Current is not automatically reset when jobs run. This is intentional—many jobs need context from the original request.

If you have a background job that shouldn't inherit the request context, explicitly manage it:

ruby
class SendEmailJob < ApplicationJob
  def perform(user_id)
    Current.reset  # Clear any inherited context
    @user = User.find(user_id)
    # Job runs with no current user/account set
  end
end

Use sparingly: when Current attributes make sense

Just because ActiveSupport::CurrentAttributes exists doesn't mean you should use it everywhere. Here are scenarios where it genuinely helps:

  • Multi-tenant applications where account ID is needed across models, services, and queries without passing it constantly.
  • Request metadata like request ID, correlation ID, or user ID for logging and tracing.
  • Audit trails where you need to know who made a change without modifying every model method signature.
  • Authorization context where policies depend on the current user but are evaluated deep in the call stack.

Scenarios where you shouldn't use it:

  • Database connection parameters or environment-specific configuration (use Rails configuration instead).
  • Data that belongs in a session or cookie (use session middleware).
  • Large objects or collections that vary per request (prefer explicit parameter passing).
  • Mock data for tests (set it explicitly in test setup).

The rule of thumb: if the data is truly request-scoped, needed across multiple layers, and changes per authenticated user/tenant, Current is a good fit. Otherwise, pass it explicitly or use a more targeted solution.

Common mistakes

Not resetting in tests: Tests often fail mysteriously because Current carries over from one test to another. Always reset in your test setup:

ruby
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  def teardown
    Current.reset
    super
  end
end

Setting Current in models or services: This couples those layers to the Current abstraction and makes them harder to test and reuse. Set Current in controllers or middleware only. Let models and services receive what they need as parameters or rely on the currently-set value without setting it themselves.

Storing large or mutable objects: Current is meant for lightweight references like user ID or account ID. Storing entire ActiveRecord collections or complex state objects can lead to unexpected mutations or memory bloat.

Assuming Current is available in background jobs: As mentioned earlier, background jobs inherit the request context but don't automatically reset. Be explicit about job context and don't assume Current is set.

Using Current to avoid dependency injection in tests: Testing with Current set is easier than mocking dependencies, but it can hide poor design. If your service is so tightly coupled to Current that you can't test it without setting it, refactor to accept dependencies explicitly.

Key takeaways

  • ActiveSupport::CurrentAttributes provides request-scoped storage for context like user and account without explicit parameter passing.
  • Create a Current class, define attributes, set them in your application controller, and access them anywhere in the request lifecycle.
  • Rails automatically resets all Current attributes at the end of each request, preventing cross-request contamination.
  • Use Current sparingly—primarily for truly request-scoped data like authenticated user, account ID, or correlation IDs that need to be accessed across multiple layers.
  • Always reset Current in test teardown to prevent test pollution.
  • Avoid setting Current in models or services; keep that responsibility in controllers or middleware.
  • Background jobs inherit request context but don't automatically reset; manage job context explicitly if needed.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles