.ai/principles/distilled/backend-ruby.md
attr_reader for public attributes only when accessed outside the class; maintain consistency for internal access.for_ prefix for scopes filtering by belongs_to associations (e.g., scope :for_project).with_ prefix for scopes using joins, includes, or filtering by has_one/has_many/boolean conditions.including_ prefix for scopes that eager-load associations via includes without changing the result set; use preload_ when loading multiple has_many associations or when a separate subquery is explicitly required.order_by_ prefix for scopes that apply order.CONSTANT = 'value'.freeze).excluding (alias without) over hand-written where.not(id: record) when excluding specific records already loaded in memory; DO NOT use excluding as a replacement for where.not(id: relation) — pass the relation directly to avoid loading IDs into memory.has_many through: or has_one through: associations; overriding changes destroy() behavior and can cause data loss.db:drop and db:test:prepare will fail if an active session is held.Gitlab::Json in place of all calls to the default JSON class, .to_json, and similar methods.Gitlab::Json::LimitedEncoder when JSON output size must be bounded.Rails.logger; use a structured JSON logger instead.$stdout.puts, $stderr.puts, $stdout.print, $stderr.print, or equivalent STDOUT/STDERR calls in application code; use a structured JSON logger or existing wrapper methods (e.g., SystemCheck::Helpers) for Rake/CLI output.Gitlab::JsonLogger for new log files; call exclude_context! if the logger is used outside of a request context.logger.info(message: "...", project_id: id)).class attribute in structured log payloads; use Gitlab::Loggable and build_structured_payload to add it automatically._s and include duration in the key name (e.g., view_duration_s).Gitlab::ErrorTracking.track_exception or Gitlab::ErrorTracking.track_and_raise_exception with additional context parameters.labkit-ruby lib/labkit/fields.rb._(), s_(), or n_() helpers; use __(), s__(), n__() in JavaScript/Vue.safe_format with tag_pair in Ruby/HAML or GlSprintf in Vue.downcase or toLocaleLowerCase() on translatable strings; let translators control casing.|) to all UI strings to provide translator context; prefer granular subcategories over broad ones.%{named} placeholders rather than positional %d in strings where the number adds no value to the singular form; use n_/n__ with %{count} named placeholders for counted strings.n_/n__ only to select between plural forms of the same string, not to switch between entirely different strings.one slot of a plural string; handle the zero state as a separate string outside the plural call.count argument.n__() calls and combine with a non-pluralized connector string.:base with a complete sentence rather than to a specific attribute when the message is a full sentence, to avoid Rails prepending the humanized attribute name.locale/gitlab.pot by running tooling/bin/gettext_extractor locale/gitlab.pot before pushing changes to translated strings.have_content(_('...'))); DO NOT hard-code translated strings.__() — externalization is mocked and expectations should use plain string literals.{} when multiple keys must reside on the same Redis shard (hash-tags for Redis Cluster compatibility).Gitlab::Redis::Cache only for truly ephemeral, regenerable data; always set a TTL explicitly (no default TTL is set; consider 8 hours to match a workday).Gitlab::Redis::SharedState for data that must persist until its expiration; always set a TTL.Rails.cache for data that must be reliably persisted; use Gitlab::Redis::SharedState instead.RedisCommands::Recorder in tests to detect Redis N+1 call problems and assert expected call counts.Gitlab::EtagCaching::Router, set the polling interval header via Gitlab::PollingInterval.set_header, and invalidate ETags via Gitlab::EtagCaching::Store on resource changes./-/ scope./-/ scope, except where a Git client or other external software requires otherwise./o/:organization_path/*path) for organization-level resources.Gitlab::Routing.redirect_legacy_paths when adding /-/ scope to previously unscoped routes, and create a technical debt issue to remove deprecated routes in a later release.db:drop and db:test:prepare will fail if an active session is held.config.autoload_paths or Zeitwerk inflections) in config/initializers_before_autoloader instead of config/initializers.@param, @return) when documenting method arguments or return values.@return [void] and explicitly return nil at the end.app/assets in application code; use lib/assets for assets that must be accessed by application code but not served directly.expect_any_instance_of or allow_any_instance_of in RSpec; use expect_next_instance_of, allow_next_instance_of, expect_next_found_instance_of, or allow_next_found_instance_of instead.rescue Exception; rescue specific exception classes.:javascript filter).OpenStruct; prefer Struct for new code.Regexp or Range instances in RSpec — they are frozen in Ruby 3 and cannot be stubbed; stub the method that returns the range/regexp instead.&method_ref to avoid Ruby 3 argument-count errors (Hash#each consistently yields a 2-element array to lambdas in Ruby 3).f(k: v) or f(**{k: v}); DO NOT use f({k: v}) — it is only valid in Ruby 3 if f takes a positional Hash.with matchers, pass an explicit Hash literal { a: 42 } (with braces) when the method under test takes a positional options hash, not keyword arguments, to avoid Ruby 3 matcher failures.rubocop:todo (not rubocop:disable) for temporary inline disables, and link the follow-up issue or epic.gitlab-styles gem; if it only applies to the main GitLab application, add it to the GitLab repository.bundle exec rake rubocop:todo:generate rather than adding inline disables throughout the codebase..rubocop.yml, generating TODOs, creating an issue to fix TODOs (with ~"quick win" / ~"Seeking community contributions" labels), and creating an issue to remove the grace period after 1 week of silence in #f_rubocop.For the full picture, see: