---
title: "RR010 – Define Reusable Query Scopes in Models"
impact: medium
impactDescription: "Inline where() chains scattered across controllers make conditions hard to reuse, test, and change in one place."
tags: [ruby, rails, code-quality, database]
---

# RR010 – Define Reusable Query Scopes in Models

## Rule

Encapsulate frequently used `where`, `order`, and `joins` conditions as named scopes on the model. Controllers call scopes by name — they do not build query logic inline.

## Why

Query conditions duplicated across controllers and services violate DRY and make maintenance hazardous. A scope is a named, composable, chainable query fragment that belongs to the model (the owner of the data).

## Wrong

```ruby
# Controller 1
@posts = Post.where(published: true).where('created_at > ?', 30.days.ago).order(created_at: :desc)

# Controller 2 (duplicate, slightly different)
@featured = Post.where(published: true).where(featured: true).order(created_at: :desc)

# Controller 3 (wrong date, bug introduced by copy-paste)
@recent = Post.where(published: true).where('created_at > ?', 7.days.ago).order(created_at: :asc)
```

## Correct

```ruby
# app/models/post.rb
class Post < ApplicationRecord
  scope :published,  -> { where(published: true) }
  scope :featured,   -> { where(featured: true) }
  scope :recent,     -> { where('created_at > ?', 30.days.ago) }
  scope :by_newest,  -> { order(created_at: :desc) }

  # Scopes with parameters
  scope :created_after, ->(date) { where('created_at > ?', date) }
  scope :by_author,     ->(author_id) { where(author_id: author_id) }
end

# Controllers compose scopes — clean, readable, no SQL inline
@posts    = Post.published.recent.by_newest
@featured = Post.published.featured.by_newest
@mine     = Post.published.by_author(current_user.id).recent
```

## Notes

- Scopes return `ActiveRecord::Relation` — they are chainable by design.
- Scopes should be tested independently in model specs: `Post.published` should only return published posts.
- For complex multi-table logic, consider a Query Object (like a service object) that wraps the relation.
- Avoid scopes with side effects (no writes, callbacks, or network calls).
