Default Scopes
The default_scope macro in ActiveRecord, which silently appends a condition or ordering to every query on the model, breaking predictability because developers writing Model.count or Model.find(id) get filtered results without realizing it.
Before / After
class Post < ApplicationRecord
default_scope { where(published: true) }
end
# Developer new to the codebase expects 1,000
Post.count
# => 240 (silently filtered)
# Admin panel "missing" draft posts: devs waste
# an afternoon debugging why admin queries
# skip unpublished content.
Post.unscoped.count # => 1,000 class Post < ApplicationRecord
scope :published, -> { where(published: true) }
scope :draft, -> { where(published: false) }
end
# Explicit everywhere
Post.published.count # => 240
Post.count # => 1,000
Post.draft.count # => 760
# Admin panel uses Post.all, public uses
# Post.published. Intent is local and obvious. Why this hurts
Default scopes modify the base relation used by every public query method on the model, which means find, find_by, count, where, exists?, and every association traversal that walks back to the model all inherit the filter. Developers reading code cannot know from the call site whether Post.count returns the true row count or a filtered subset, because the scope is declared in a file they may never open. The ActiveRecord console lies to operators trying to verify production state, and scripts written by support staff return wrong numbers in customer tickets.
The interaction with query merging is worse than unpredictable, it is actively wrong in specific cases. Chained scopes that target the same column silently drop one of the conditions: Post.published.where(published: false) returns no results on some Rails versions and all draft posts on others, depending on how the query builder resolves the conflict. Joins through the model carry the default scope into the join condition, filtering parent records by child attributes in ways that break naive expectations.
Performance suffers because the default scope forces a WHERE clause on every index lookup, even primary-key lookups. Post.find(1) runs SELECT ... WHERE id = 1 AND published = true instead of the single-row primary-key fetch the developer expected, and the additional column in the WHERE clause defeats the query planner’s fast-path for unique lookups. The only escape is Post.unscoped.find(1), which must be sprinkled across the codebase and is trivially forgotten in admin tools, migrations, and data-quality checks. Reports, exports, and batch operations built on top of the model produce results silently filtered by the default scope for months before anyone notices the discrepancy with source systems.
Get Expert Help
Inheriting a legacy Rails codebase with this problem? Request a Technical Debt Audit.