.ai/principles/distilled/backend-architecture.md
Prerequisite: If you haven't already, also read .ai/principles/distilled/backend-ruby.md - it contains foundational rules that apply to all backend work.
ProjectsFinder) inside other Finders; use the underlying query primitives directlyActiveRecord methods (e.g., where, find_by) directly from controllers, API endpoints, service classes, finders, presenters, or serializers — only from model class/instance methodsSomeWorker.new.perform; use SomeWorker.perform_async or SomeWorker.perform_innil?, present?, or boolean attribute checks into a helper, presenter, or ViewComponentcurrent_user: as a keyword argument#execute method takes no arguments (all data passed via initializer)#execute returns a ServiceResponse object when a return value is neededServiceResponse.success / ServiceResponse.error with a message: and optional payload: or reason:reason: symbols in ServiceResponse.error (e.g., :job_not_retriable, :duplicate_package); use Rails HTTP status symbols only for common failures like :not_found or :forbiddenBaseContainerService, BaseProjectService, or BaseGroupService where appropriate#execute methods return ActiveRecord::Relation; add exceptions to spec/support/finder_collection_allowlist.yml only when necessaryEpic::AddExistingIssueService not EpicIssues::CreateService)Projects::CreateService)self.table_name= when model name diverges from table nameconfig/bounded_contexts.yml; resolve Gitlab/BoundedContexts RuboCop offenses by nesting into an existing contextProjects:: or Groups:: namespaces unless the concept is strictly about projects or groups themselveslib/gitlab/bounded_contexts/subscriptions/[context]_subscriptions.rb; EE-only in ee/lib/gitlab/event_store/subscriptions/[context]_subscriptions.rbProject, User, MergeRequest, Ci::Pipeline, or any class >1000 LOC); create a dedicated class insteadAntiAbuse::UserTrustScore.new(user)) over adding methods to large models<DomainObject><Action>Event (e.g., Ci::PipelineCreatedEvent, not Ci::CreatePipelineEvent); elide the domain object when obvious from the bounded context (e.g., MergeRequest::ApprovedEvent not MergeRequest::MergeRequestApprovedEvent)required and all other properties as optionalActiveRecord callbacksif: lambda) only for cheap synchronous checks; handle complex conditions inside handle_eventpublish_event RSpec matcher to test publishers; use it_behaves_like 'subscribes to event' shared example to test subscribers@var ||= value single-assignment pattern when memoizing in a modulelocal_assigns.fetch(:key)Gitlab::AbstractMethodError (not NotImplementedError, NoMethodError, or a generic string raise) for abstract methods that subclasses must implementplan_limits with a non-null default, then fine-tune per plan using create_or_update_plan_limit in a separate migrationdefault, free, premium, premium_trial, ultimate, ultimate_trial, ultimate_trial_paid_customer, opensource) in limit migrations; omitting a plan causes those customers to receive the default (possibly 0/unlimited)PlanLimits#exceeded? or the Limitable concern to enforce limits; DO NOT implement ad-hoc count checksRack::Attack for middleware-level rate limiting and Gitlab::ApplicationRateLimiter for controller/API-level throttlingNOT NULL column constraint without a default value when old application nodes are still inserting rows without that columnee/) must not directly reference EE:: namespaced classesprepend_mod pattern in CE filesprepend_mod hooks or Gitlab.ee? guardsEE:: namespaced classes in CE code (prevents FOSS build failures)authorize! / authorize_admin! call and verify what permission it currently enforces; the required fix may be documentation- or test-only with no code change neededFor the full picture, see: