DEV Community

Cover image for Scopes vs class methods in Rails
Hassan Farooq
Hassan Farooq

Posted on

Scopes vs class methods in Rails

Scopes and class methods in Rails do almost the same job, which is exactly why the question trips people up. The honest answer is that a scope is basically a class method that returns a query, with one safety feature bolted on. Once you know what that feature is, you know when to use which. The whole thing comes down to a single nil.

What a scope actually is

A scope is a named, reusable piece of a query. You define it with scope, hand it a lambda, and it gives you back an ActiveRecord::Relation.

class Post < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent,    -> { order(created_at: :desc) }
  scope :by_author, ->(author_id) { where(author_id: author_id) }
end
Enter fullscreen mode Exit fullscreen mode

Two things make that useful. Because every scope returns a relation, you can chain them, and because a relation is lazy, no SQL runs until you actually read the results.

Post.published.recent.limit(10)
Post.published.by_author(42).recent
Enter fullscreen mode Exit fullscreen mode

Each call hands the next one a relation to build on, so all of that collapses into a single SELECT at the end. You're composing one query out of small named parts, not running three.

They're closer than you think

Here's the part that surprises people. A scope and a class method that returns a relation are nearly identical. These two are equivalent:

scope :published, -> { where(published: true) }

def self.published
  where(published: true)
end
Enter fullscreen mode Exit fullscreen mode

Both give you Post.published. Both return a relation. Both chain. So if they're the same, why have both? The difference only shows up when the logic returns nothing useful.

The nil that breaks the chain

A scope has one guarantee a plain method doesn't. If the body of a scope returns nil, Rails quietly swaps in all, so the chain keeps working. A class method has no such net. Return nil from it and the next method in the chain blows up.

Picture a search with a guard clause for a blank term:

def self.search(term)
  return all if term.blank?

  where("title ILIKE ?", "%#{sanitize_sql_like(term)}%")
end
Enter fullscreen mode Exit fullscreen mode

That return all is the whole point. If you wrote return nil instead, then Post.search("").recent would call .recent on nil and you'd get a NoMethodError. The scope version would have rescued you by falling back to all automatically, but inside a class method you're responsible for returning a relation yourself.

So the rule I follow:

Use a scope for simple, always-chainable fragments, especially one-liners like the three on the Post model above.

Use a class method when there's real logic, a conditional, a guard clause, or several steps, because then you want to control the relation by hand and return all on purpose.

One thing scopes are not for

Scopes are for shaping queries, not for doing work. A scope should be a where, an order, a joins, something that narrows or sorts records. The moment you catch a scope sending an email, updating rows, or calling an API, it's in the wrong place. That isn't query composition anymore, and it belongs in a service object.

The short version

Scopes are named, chainable queries that always hand back a relation. A class method does the same job but gives you room for branching and conditionals. Reach for a scope when it's a clean one-liner, reach for a class method when there's logic involved, and whichever you pick, return all instead of nil so the next link in the chain doesn't snap.

Top comments (0)