diff --git a/doc/development/reusing_abstractions.md b/doc/development/reusing_abstractions.md
index ccf82dc6c7756427263d68fcba22a508259ff333..f3eb1ebcc0c413e85fceffc55ec893bb4856bda1 100644
--- a/doc/development/reusing_abstractions.md
+++ b/doc/development/reusing_abstractions.md
@@ -190,6 +190,10 @@ Everything in `app/finders`, typically used for retrieving data from a database.
 Finders can not reuse other finders in an attempt to better control the SQL
 queries they produce.
 
+Finders' `execute` method should return `ActiveRecord::Relation`. Exceptions
+can be added to `spec/support/finder_collection_allowlist.yml`.
+See [`#298771`](https://gitlab.com/gitlab-org/gitlab/-/issues/298771) for more details.
+
 ### Presenters
 
 Everything in `app/presenters`, used for exposing complex data to a Rails view,
diff --git a/spec/support/finder_collection.rb b/spec/support/finder_collection.rb
new file mode 100644
index 0000000000000000000000000000000000000000..494dd4bdca140990cf818f55110953da525ca432
--- /dev/null
+++ b/spec/support/finder_collection.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'set'
+
+module Support
+  # Ensure that finders' `execute` method always returns
+  # `ActiveRecord::Relation`.
+  #
+  # See https://gitlab.com/gitlab-org/gitlab/-/issues/298771
+  module FinderCollection
+    def self.install_check(finder_class)
+      return unless check?(finder_class)
+
+      finder_class.prepend CheckResult
+    end
+
+    ALLOWLIST_YAML = File.join(__dir__, 'finder_collection_allowlist.yml')
+
+    def self.check?(finder_class)
+      @allowlist ||= YAML.load_file(ALLOWLIST_YAML).to_set
+
+      @allowlist.exclude?(finder_class.name)
+    end
+
+    module CheckResult
+      def execute(...)
+        result = super
+
+        unless result.is_a?(ActiveRecord::Relation)
+          raise <<~MESSAGE
+            #{self.class}#execute returned `#{result.class}` instead of `ActiveRecord::Relation`.
+            All finder classes are expected to return `ActiveRecord::Relation`.
+
+            Read more at https://docs.gitlab.com/ee/development/reusing_abstractions.html#finders
+          MESSAGE
+        end
+
+        result
+      end
+    end
+  end
+end
+
+RSpec.configure do |config|
+  config.before(:all, type: :finder) do
+    Support::FinderCollection.install_check(described_class)
+  end
+end
diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8f09153afec150fcbf42bb174e5395ddfa864102
--- /dev/null
+++ b/spec/support/finder_collection_allowlist.yml
@@ -0,0 +1,66 @@
+# Allow list for spec/support/finder_collection.rb
+
+# Permenant excludes
+# For example:
+# FooFinder  # Reason: It uses a memory backend
+
+# Temporary excludes (aka TODOs)
+# For example:
+# BarFinder  # See <ISSUE_URL>
+- AccessRequestsFinder
+- Admin::PlansFinder
+- Analytics::CycleAnalytics::StageFinder
+- ApplicationsFinder
+- Autocomplete::GroupFinder
+- Autocomplete::ProjectFinder
+- Autocomplete::UsersFinder
+- BilledUsersFinder
+- Boards::BoardsFinder
+- Boards::VisitsFinder
+- BranchesFinder
+- Ci::AuthJobFinder
+- Ci::CommitStatusesFinder
+- Ci::DailyBuildGroupReportResultsFinder
+- ClusterAncestorsFinder
+- Clusters::AgentAuthorizationsFinder
+- Clusters::KubernetesNamespaceFinder
+- ComplianceManagement::MergeRequests::ComplianceViolationsFinder
+- ContainerRepositoriesFinder
+- ContextCommitsFinder
+- Environments::EnvironmentNamesFinder
+- Environments::EnvironmentsByDeploymentsFinder
+- EventsFinder
+- GroupDescendantsFinder
+- Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershipFinder
+- Groups::ProjectsRequiringAuthorizationsRefresh::OnTransferFinder
+- KeysFinder
+- LfsPointersFinder
+- LicenseTemplateFinder
+- MergeRequests::OldestPerCommitFinder
+- NotesFinder
+- Packages::BuildInfosFinder
+- Packages::Conan::PackageFileFinder
+- Packages::Go::ModuleFinder
+- Packages::Go::PackageFinder
+- Packages::Go::VersionFinder
+- Packages::PackageFileFinder
+- Packages::PackageFinder
+- Packages::Pypi::PackageFinder
+- Projects::Integrations::Jira::ByIdsFinder
+- Projects::Integrations::Jira::IssuesFinder
+- Releases::EvidencePipelineFinder
+- Repositories::BranchNamesFinder
+- Repositories::ChangelogTagFinder
+- Repositories::TreeFinder
+- Security::FindingsFinder
+- Security::PipelineVulnerabilitiesFinder
+- Security::ScanExecutionPoliciesFinder
+- Security::TrainingProviders::BaseUrlFinder
+- Security::TrainingUrlsFinder
+- SentryIssueFinder
+- ServerlessDomainFinder
+- TagsFinder
+- TemplateFinder
+- UploaderFinder
+- UserGroupNotificationSettingsFinder
+- UserGroupsCounter