class ArticlesController < ApplicationController
def index
# Bounded queries: without preload, article.author in the view raises.
@articles = Article
.published
.preload(:author, :tags)
.order(published_at: :desc)
.limit(50)
end
def feed
# Legit escape hatch: this relation is intentionally lazy-loaded.
@articles = current_user
.followed_articles
.strict_loading(false)
.recent
end
end
class StrictLoadingLogger
def call(_name, _start, _finish, _id, payload)
owner = payload[:owner]
reflection = payload[:reflection]
Rails.logger.warn(
"[strict_loading] #{owner.class}##{reflection.name} " \
"was accessed without preloading (record id=#{owner.id.inspect})"
)
end
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
if Rails.configuration.x.enforce_strict_loading
self.strict_loading_by_default = true
self.strict_loading_mode = :all
end
end
Rails.application.configure do
config.x.enforce_strict_loading = true
# Raise instead of silently logging so N+1s cannot slip through.
config.active_record.action_on_strict_loading_violation = :raise
config.action_controller.raise_on_strict_loading_violation = true
config.after_initialize do
ActiveSupport::Notifications.subscribe(
"strict_loading_violation.active_record",
StrictLoadingLogger.new
)
end
end
This snippet shows how Rails' strict_loading feature turns silent N+1 queries into loud, catchable failures during development instead of shipping them to production as slow endpoints. The N+1 problem happens when code iterates a collection and lazily triggers one query per record for an association it forgot to preload; strict_loading flips lazy loading off so any un-preloaded association access raises ActiveRecord::StrictLoadingViolationError.
In application_record.rb, strict loading is enabled globally for every model when the environment configures it, and the mode is set to :all so that even associations reachable through already-loaded records are guarded. The strict_loading_by_default flag is the model-level switch; setting it on ApplicationRecord cascades to all subclasses. Because forcing this on in production would be dangerous, it is gated behind a config flag.
In environments/development.rb, that flag is turned on together with action_controller.raise_on_strict_loading_violation. The key trade-off is choosing between raising and logging: raising via strict_loading_violation = :raise fails fast and forces developers to add includes or preload, while :log merely records the violation. Development uses raise so mistakes cannot be ignored, but tests could use log to avoid brittle failures on legacy code paths.
ArticlesController demonstrates the fix in practice. The naive index would blow up the moment the view calls article.author.name, so the action uses preload(:author, :tags) to load everything up front in a bounded number of queries. The feed action shows a scoped escape hatch: strict_loading(false) re-enables lazy loading for a specific relation where the eager load genuinely cannot be expressed cleanly.
Finally, StrictLoadingLogger subscribes to the strict_loading_violation.active_record notification so violations are surfaced with the owner class and association name even when running in :log mode. This instrumentation is useful in CI or staging where raising is too aggressive but visibility is still required. Together these files make N+1 regressions a build-time concern rather than a runtime surprise, with per-query escape valves for the rare legitimate case.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
<form data-controller="query-sync" data-action="change->query-sync#apply">
<select name="status" class="rounded border p-2">
<option value="">Any</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
Filter UI that syncs query params via Stimulus (no front-end router)
Share this code
Here's the card — post it anywhere.