diff --git a/.dockerignore b/.dockerignore
index 0782627230ade073cc2422abaa7c6eb3f8bc61e6..a2d54fa65dd8adb902cf3149abebb1b609d14ae6 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -80,4 +80,3 @@
 /spec/
 /symbol/
 /tmp/
-/vendor/
diff --git a/.gitlab/ci/gitlab-gems.gitlab-ci.yml b/.gitlab/ci/gitlab-gems.gitlab-ci.yml
index adab179d2a2d1b01adf26b0eac6ae8bd1c8858e5..aff272935c51249734d4b149cf8c048e542a4127 100644
--- a/.gitlab/ci/gitlab-gems.gitlab-ci.yml
+++ b/.gitlab/ci/gitlab-gems.gitlab-ci.yml
@@ -5,3 +5,11 @@ gems gitlab-rspec:
   trigger:
     include: gems/gitlab-rspec/.gitlab-ci.yml
     strategy: depend
+
+gems gitlab-utils:
+  extends:
+    - .gems:rules:gitlab-utils
+  needs: []
+  trigger:
+    include: gems/gitlab-utils/.gitlab-ci.yml
+    strategy: depend
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 6acc2dea2e219c2ad125f5bb7b30bbdd15788471..007e5cefb89cc346c84a31e53a87014ea57ca909 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -2135,6 +2135,11 @@
     - <<: *if-merge-request
       changes: ["gems/gitlab-rspec/**/*"]
 
+.gems:rules:gitlab-utils:
+  rules:
+    - <<: *if-merge-request
+      changes: ["gems/gitlab-utils/**/*"]
+
 #######################
 # Vendored gems rules #
 #######################
diff --git a/.rubocop_todo/gitlab/doc_url.yml b/.rubocop_todo/gitlab/doc_url.yml
index 119de2296cea428534e8e146de46d79a9c33f272..13537a142a16db4696f027f9df8be8e53047b779 100644
--- a/.rubocop_todo/gitlab/doc_url.yml
+++ b/.rubocop_todo/gitlab/doc_url.yml
@@ -43,7 +43,6 @@ Gitlab/DocUrl:
     - 'lib/gitlab/pagination/keyset/unsupported_scope_order.rb'
     - 'lib/gitlab/redis/hll.rb'
     - 'lib/gitlab/slash_commands/presenters/help.rb'
-    - 'lib/gitlab/utils/strong_memoize.rb'
     - 'lib/initializer_connections.rb'
     - 'lib/security/ci_configuration/base_build_action.rb'
     - 'lib/tasks/db_obsolete_ignored_columns.rake'
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index 8a0dd63ae7c044df7def0990c0dc90c94fda64d0..4ab42f017900c7bd7d5b6fa1feb2a8c3c5f54534 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -1248,7 +1248,6 @@ Gitlab/NamespacedClass:
     - 'lib/gitlab/user_access.rb'
     - 'lib/gitlab/user_access_snippet.rb'
     - 'lib/gitlab/uuid.rb'
-    - 'lib/gitlab/version_info.rb'
     - 'lib/gitlab/visibility_level_checker.rb'
     - 'lib/gitlab/wiki_file_finder.rb'
     - 'lib/gitlab/workhorse.rb'
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 826e20fb40f796e95558be4d1c35d474934c62e8..e774c081ef41344e0905d90ba8cc9fc4637d0314 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -1820,7 +1820,6 @@ Layout/ArgumentAlignment:
     - 'spec/lib/gitlab/usage_data_queries_spec.rb'
     - 'spec/lib/gitlab/usage_data_spec.rb'
     - 'spec/lib/gitlab/utils/lazy_attributes_spec.rb'
-    - 'spec/lib/gitlab/utils_spec.rb'
     - 'spec/lib/gitlab/workhorse_spec.rb'
     - 'spec/lib/google_api/cloud_platform/client_spec.rb'
     - 'spec/lib/peek/views/detailed_view_spec.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 1deedbfc96e43c1e807de8047c76f89f71dd749c..a5fafa593887c75de501f99776c2a8bbd6cf29a3 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -4230,7 +4230,6 @@ Layout/LineLength:
     - 'spec/lib/gitlab/utils/measuring_spec.rb'
     - 'spec/lib/gitlab/utils/nokogiri_spec.rb'
     - 'spec/lib/gitlab/utils/usage_data_spec.rb'
-    - 'spec/lib/gitlab/utils_spec.rb'
     - 'spec/lib/gitlab/web_ide/config/entry/global_spec.rb'
     - 'spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb'
     - 'spec/lib/gitlab/webpack/file_loader_spec.rb'
diff --git a/.rubocop_todo/lint/ambiguous_regexp_literal.yml b/.rubocop_todo/lint/ambiguous_regexp_literal.yml
index c3b06ede5bedc1bc9b2f367079f26b3353234fdf..4754e381780cab7d07845a5d9f04ce5b040ec4ac 100644
--- a/.rubocop_todo/lint/ambiguous_regexp_literal.yml
+++ b/.rubocop_todo/lint/ambiguous_regexp_literal.yml
@@ -66,7 +66,6 @@ Lint/AmbiguousRegexpLiteral:
     - 'spec/lib/gitlab/pagination/keyset/iterator_spec.rb'
     - 'spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb'
     - 'spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb'
-    - 'spec/lib/gitlab/utils/strong_memoize_spec.rb'
     - 'spec/lib/gitlab/web_ide/config/entry/global_spec.rb'
     - 'spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb'
     - 'spec/lib/object_storage/direct_upload_spec.rb'
diff --git a/.rubocop_todo/lint/symbol_conversion.yml b/.rubocop_todo/lint/symbol_conversion.yml
index 147cd66f993adf1bd6a22d16eb35a6fb091c1d62..8799414c911e11dfbe36cb6dcbdb196d07294e0a 100644
--- a/.rubocop_todo/lint/symbol_conversion.yml
+++ b/.rubocop_todo/lint/symbol_conversion.yml
@@ -117,7 +117,6 @@ Lint/SymbolConversion:
     - 'spec/lib/gitlab/search/abuse_validators/no_abusive_coercion_from_string_validator_spec.rb'
     - 'spec/lib/gitlab/slug/path_spec.rb'
     - 'spec/lib/gitlab/tracking_spec.rb'
-    - 'spec/lib/gitlab/utils_spec.rb'
     - 'spec/lib/google_api/cloud_platform/client_spec.rb'
     - 'spec/lib/service_ping/devops_report_spec.rb'
     - 'spec/models/integrations/prometheus_spec.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index af708ad54e5a35b415e7d6f40c29c4d15efc8fe9..1ce003ecd2f6a0e3d4b5d696524cbaa981923d02 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -2062,9 +2062,7 @@ RSpec/ContextWording:
     - 'spec/lib/gitlab/usage_data_spec.rb'
     - 'spec/lib/gitlab/utils/lazy_attributes_spec.rb'
     - 'spec/lib/gitlab/utils/mime_type_spec.rb'
-    - 'spec/lib/gitlab/utils/strong_memoize_spec.rb'
     - 'spec/lib/gitlab/utils/usage_data_spec.rb'
-    - 'spec/lib/gitlab/utils_spec.rb'
     - 'spec/lib/gitlab/view/presenter/base_spec.rb'
     - 'spec/lib/gitlab/visibility_level_checker_spec.rb'
     - 'spec/lib/gitlab/visibility_level_spec.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index e3829a97b02007b139d853e6a945174cda0eae58..3cf0ed5c38cef64118a932c8f6326fbd2ec332e3 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -4413,7 +4413,6 @@ RSpec/MissingFeatureCategory:
     - 'spec/lib/gitlab/utils/safe_inline_hash_spec.rb'
     - 'spec/lib/gitlab/utils/sanitize_node_link_spec.rb'
     - 'spec/lib/gitlab/utils/usage_data_spec.rb'
-    - 'spec/lib/gitlab/utils_spec.rb'
     - 'spec/lib/gitlab/uuid_spec.rb'
     - 'spec/lib/gitlab/verify/job_artifacts_spec.rb'
     - 'spec/lib/gitlab/verify/lfs_objects_spec.rb'
diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml
index 2e03bbf455798cb66cc409b7d97f3690a54fa9fa..bb29652fe9653f528a7520c9e767037536ea62f3 100644
--- a/.rubocop_todo/style/percent_literal_delimiters.yml
+++ b/.rubocop_todo/style/percent_literal_delimiters.yml
@@ -843,7 +843,6 @@ Style/PercentLiteralDelimiters:
     - 'spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
     - 'spec/lib/gitlab/usage_data_spec.rb'
     - 'spec/lib/gitlab/utils/log_limited_array_spec.rb'
-    - 'spec/lib/gitlab/utils_spec.rb'
     - 'spec/lib/gitlab/webpack/graphql_known_operations_spec.rb'
     - 'spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb'
     - 'spec/lib/object_storage/config_spec.rb'
diff --git a/.rubocop_todo/style/string_concatenation.yml b/.rubocop_todo/style/string_concatenation.yml
index 4e3b17fe4c097e9021958b7ff79e63fae641326e..34e4549a987a0b1b597abea7df0501b53b61aa6d 100644
--- a/.rubocop_todo/style/string_concatenation.yml
+++ b/.rubocop_todo/style/string_concatenation.yml
@@ -212,7 +212,6 @@ Style/StringConcatenation:
     - 'spec/lib/gitlab/themes_spec.rb'
     - 'spec/lib/gitlab/throttle_spec.rb'
     - 'spec/lib/gitlab/tree_summary_spec.rb'
-    - 'spec/lib/gitlab/utils_spec.rb'
     - 'spec/lib/gitlab/visibility_level_spec.rb'
     - 'spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb'
     - 'spec/lib/gitlab/workhorse_spec.rb'
diff --git a/Gemfile b/Gemfile
index faada71223925e01df4ea64bbb3a2e44ddac2116..d746892802ee111299770cb95d5bd81b8c1863cc 100644
--- a/Gemfile
+++ b/Gemfile
@@ -20,6 +20,11 @@ gem 'bootsnap', '~> 1.16.0', require: false
 gem 'openssl', '~> 3.0'
 gem 'ipaddr', '~> 1.2.5'
 
+# GitLab Monorepo Gems
+group :monorepo do
+  gem 'gitlab-utils', path: 'gems/gitlab-utils'
+end
+
 # Responders respond_to and respond_with
 gem 'responders', '~> 3.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 8844732d35ee2f56a8f0be19f70e9cb6da8c5cad..395293f17a200dde9ca5254c88d974c8b7c03e98 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,6 +4,16 @@ PATH
     gitlab-rspec (0.1.0)
       rspec (~> 3.0)
 
+PATH
+  remote: gems/gitlab-utils
+  specs:
+    gitlab-utils (0.1.0)
+      actionview (>= 6.1.7.2)
+      activesupport (>= 6.1.7.2)
+      addressable (~> 2.8)
+      nokogiri (~> 1.15.2)
+      rake (~> 13.0)
+
 PATH
   remote: vendor/gems/attr_encrypted
   specs:
@@ -1778,6 +1788,7 @@ DEPENDENCIES
   gitlab-rspec!
   gitlab-sidekiq-fetcher!
   gitlab-styles (~> 10.0.0)
+  gitlab-utils!
   gitlab_chronic_duration (~> 0.10.6.2)
   gitlab_omniauth-ldap (~> 2.2.0)
   gitlab_quality-test_tooling (~> 0.8.1)
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 76c733b1c0bdc74b6d9e8b5399dfa8c9925a22f8..c70100c03c8c6f9d3440a4072dbdf1d5a6976453 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -4,7 +4,7 @@
 #
 # After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
 # fields to a new table "project_features", support for the old fields is still needed in the API.
-require 'gitlab/utils'
+require 'gitlab/utils/all'
 
 module ProjectFeaturesCompatibility
   extend ActiveSupport::Concern
diff --git a/bin/audit-event-type b/bin/audit-event-type
index fec34724c7c5d44ccac972908d798176bf8f5d53..e9d72aaba46d10a5b380e1fe06dc7cd22e17eb70 100755
--- a/bin/audit-event-type
+++ b/bin/audit-event-type
@@ -11,9 +11,10 @@ require 'yaml'
 require 'fileutils'
 require 'uri'
 require 'readline'
+require_relative '../config/bundler_setup'
+require 'gitlab/utils/all'
 
 require_relative '../lib/gitlab/audit/type/shared' unless defined?(::Gitlab::Audit::Type::Shared)
-require_relative '../lib/gitlab/utils' unless defined?(::Gitlab::Utils)
 
 module AuditEventTypeHelpers
   Abort = Class.new(StandardError)
diff --git a/config/application.rb b/config/application.rb
index 06153b377f35dca6e8e9d744c4e23dfde161d216..c8bb56ce956f4ba47eb7f7ae36df14f85d2c92ed 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -12,6 +12,8 @@
 require 'action_cable/engine'
 require 'rails/test_unit/railtie'
 
+require 'gitlab/utils/all'
+
 Bundler.require(*Rails.groups)
 
 module Gitlab
@@ -49,7 +51,6 @@ class Application < Rails::Application
     ActiveSupport.to_time_preserves_timezone = false
 
     require_dependency Rails.root.join('lib/gitlab')
-    require_dependency Rails.root.join('lib/gitlab/utils')
     require_dependency Rails.root.join('lib/gitlab/action_cable/config')
     require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
     require_dependency Rails.root.join('lib/gitlab/redis/cache')
diff --git a/config/environments/test.rb b/config/environments/test.rb
index da91752549ea29dcd7deccb2d2bbf328c5bdb18c..b919df45214bcf40f5faa4d988c4a5da1a21ecc1 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -4,7 +4,7 @@
 require 'gitlab/testing/robots_blocker_middleware'
 require 'gitlab/testing/request_inspector_middleware'
 require 'gitlab/testing/clear_process_memory_cache_middleware'
-require 'gitlab/utils'
+require 'gitlab/utils/all'
 
 Rails.application.configure do
   # Make sure the middleware is inserted first in middleware chain
diff --git a/doc/development/gems.md b/doc/development/gems.md
index 4d364242efd1d7720ba554bc372bae1204d78710..55963438347e92c0c11c6fb5f6958ebd130d1e5e 100644
--- a/doc/development/gems.md
+++ b/doc/development/gems.md
@@ -107,7 +107,7 @@ You can see example adding new Gem: [!121676](https://gitlab.com/gitlab-org/gitl
      rspec:
        image: "ruby:${RUBY_VERSION}"
        cache:
-         key: gitlab-<name-of-gem>
+         key: gitlab-<name-of-gem>-${RUBY_VERSION}
          paths:
            - gitlab-<name-of-gem>/vendor/ruby
        before_script:
diff --git a/gems/gitlab-rspec/.gitlab-ci.yml b/gems/gitlab-rspec/.gitlab-ci.yml
index 0932753d1e7b452e8d91e8bd44be0db20fd3cc77..95bdc51cb7d7fb9d29685258fb249a885c486842 100644
--- a/gems/gitlab-rspec/.gitlab-ci.yml
+++ b/gems/gitlab-rspec/.gitlab-ci.yml
@@ -12,7 +12,7 @@ workflow:
 rspec:
   image: "ruby:${RUBY_VERSION}"
   cache:
-    key: gitlab-rspec
+    key: gitlab-rspec-${RUBY_VERSION}
     paths:
       - gitlab-rspec/vendor/ruby
   before_script:
diff --git a/gems/gitlab-utils/.gitignore b/gems/gitlab-utils/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..b04a8c840df1a534cfd67449e31919721b410986
--- /dev/null
+++ b/gems/gitlab-utils/.gitignore
@@ -0,0 +1,11 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+# rspec failure tracking
+.rspec_status
diff --git a/gems/gitlab-utils/.gitlab-ci.yml b/gems/gitlab-utils/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ab92953b57dc4db21ea92f16d0e3664359789a01
--- /dev/null
+++ b/gems/gitlab-utils/.gitlab-ci.yml
@@ -0,0 +1,30 @@
+# You can override the included template(s) by including variable overrides
+# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+# Note that environment variables can be set in several places
+# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+workflow:
+  rules:
+    - if: $CI_MERGE_REQUEST_ID
+
+rspec:
+  image: "ruby:${RUBY_VERSION}"
+  cache:
+    key: gitlab-utils-${RUBY_VERSION}
+    paths:
+      - gitlab-utils/vendor/ruby
+  before_script:
+    - cd gems/gitlab-utils
+    - ruby -v                                   # Print out ruby version for debugging
+    - gem install bundler --no-document         # Bundler is not installed with the image
+    - bundle config set --local path 'vendor'   # Install dependencies into ./vendor/ruby
+    - bundle config set with 'development'
+    - bundle config set --local frozen 'true'   # Disallow Gemfile.lock changes on CI
+    - bundle config                             # Show bundler configuration
+    - bundle install -j $(nproc)
+  script:
+    - bundle exec rspec
+  parallel:
+    matrix:
+      - RUBY_VERSION: ["2.7", "3.0", "3.1", "3.2"]
diff --git a/gems/gitlab-utils/.rspec b/gems/gitlab-utils/.rspec
new file mode 100644
index 0000000000000000000000000000000000000000..34c5164d9b56c7d528f061c97f2d2fe02c834bdd
--- /dev/null
+++ b/gems/gitlab-utils/.rspec
@@ -0,0 +1,3 @@
+--format documentation
+--color
+--require spec_helper
diff --git a/gems/gitlab-utils/.rubocop.yml b/gems/gitlab-utils/.rubocop.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7f0d48d1b2bc3ca1b0fe1d65dcc6b29788d30ddf
--- /dev/null
+++ b/gems/gitlab-utils/.rubocop.yml
@@ -0,0 +1,31 @@
+inherit_from:
+  - ../../.rubocop.yml
+
+CodeReuse/ActiveRecord:
+  Enabled: false
+
+Gitlab/DocUrl:
+  Enabled: false
+
+Gitlab/NamespacedClass:
+  Enabled: false
+
+AllCops:
+  TargetRubyVersion: 3.0
+
+Naming/FileName:
+  Exclude:
+    - spec/**/*.rb
+    - lib/gitlab/utils/all.rb
+
+Lint/AmbiguousRegexpLiteral:
+  Exclude:
+    - spec/**/*.rb
+
+RSpec/InstanceVariable:
+  Exclude:
+    - spec/**/*.rb
+
+Lint/BinaryOperatorWithIdenticalOperands:
+  Exclude:
+    - spec/**/*.rb
diff --git a/gems/gitlab-utils/Gemfile b/gems/gitlab-utils/Gemfile
new file mode 100644
index 0000000000000000000000000000000000000000..2c7228c874c980a290fbf4ec764ca105c60844d9
--- /dev/null
+++ b/gems/gitlab-utils/Gemfile
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in gitlab-utils.gemspec
+gemspec
+
+group :development, :test do
+  gem 'gitlab-rspec', path: '../gitlab-rspec'
+end
diff --git a/gems/gitlab-utils/Gemfile.lock b/gems/gitlab-utils/Gemfile.lock
new file mode 100644
index 0000000000000000000000000000000000000000..e7f954eeea3e837e9e871a8615cad4ed30f4ae8d
--- /dev/null
+++ b/gems/gitlab-utils/Gemfile.lock
@@ -0,0 +1,193 @@
+PATH
+  remote: ../gitlab-rspec
+  specs:
+    gitlab-rspec (0.1.0)
+      rspec (~> 3.0)
+
+PATH
+  remote: .
+  specs:
+    gitlab-utils (0.1.0)
+      actionview (>= 6.1.7.2)
+      activesupport (>= 6.1.7.2)
+      addressable (~> 2.8)
+      nokogiri (~> 1.15.2)
+      rake (~> 13.0)
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    actionpack (7.0.5)
+      actionview (= 7.0.5)
+      activesupport (= 7.0.5)
+      rack (~> 2.0, >= 2.2.4)
+      rack-test (>= 0.6.3)
+      rails-dom-testing (~> 2.0)
+      rails-html-sanitizer (~> 1.0, >= 1.2.0)
+    actionview (7.0.5)
+      activesupport (= 7.0.5)
+      builder (~> 3.1)
+      erubi (~> 1.4)
+      rails-dom-testing (~> 2.0)
+      rails-html-sanitizer (~> 1.1, >= 1.2.0)
+    activesupport (7.0.5)
+      concurrent-ruby (~> 1.0, >= 1.0.2)
+      i18n (>= 1.6, < 2)
+      minitest (>= 5.1)
+      tzinfo (~> 2.0)
+    addressable (2.8.1)
+      public_suffix (>= 2.0.2, < 6.0)
+    ast (2.4.2)
+    benchmark-malloc (0.2.0)
+    benchmark-perf (0.6.0)
+    benchmark-trend (0.4.0)
+    binding_of_caller (1.0.0)
+      debug_inspector (>= 0.0.1)
+    builder (3.2.4)
+    coderay (1.1.3)
+    concurrent-ruby (1.2.2)
+    crass (1.0.6)
+    debug_inspector (1.1.0)
+    diff-lcs (1.5.0)
+    erubi (1.12.0)
+    factory_bot (6.2.1)
+      activesupport (>= 5.0.0)
+    factory_bot_rails (6.2.0)
+      factory_bot (~> 6.2.0)
+      railties (>= 5.0.0)
+    gitlab-styles (10.0.0)
+      rubocop (~> 1.43.0)
+      rubocop-graphql (~> 0.18)
+      rubocop-performance (~> 1.15)
+      rubocop-rails (~> 2.17)
+      rubocop-rspec (~> 2.18)
+    i18n (1.14.1)
+      concurrent-ruby (~> 1.0)
+    json (2.6.3)
+    loofah (2.21.3)
+      crass (~> 1.0.2)
+      nokogiri (>= 1.12.0)
+    method_source (1.0.0)
+    mini_portile2 (2.8.2)
+    minitest (5.18.1)
+    nokogiri (1.15.2)
+      mini_portile2 (~> 2.8.2)
+      racc (~> 1.4)
+    parallel (1.22.1)
+    parser (3.2.0.0)
+      ast (~> 2.4.1)
+    proc_to_ast (0.1.0)
+      coderay
+      parser
+      unparser
+    public_suffix (5.0.0)
+    racc (1.7.1)
+    rack (2.2.7)
+    rack-test (2.1.0)
+      rack (>= 1.3)
+    rails-dom-testing (2.0.3)
+      activesupport (>= 4.2.0)
+      nokogiri (>= 1.6)
+    rails-html-sanitizer (1.6.0)
+      loofah (~> 2.21)
+      nokogiri (~> 1.14)
+    railties (7.0.5)
+      actionpack (= 7.0.5)
+      activesupport (= 7.0.5)
+      method_source
+      rake (>= 12.2)
+      thor (~> 1.0)
+      zeitwerk (~> 2.5)
+    rainbow (3.1.1)
+    rake (13.0.6)
+    regexp_parser (2.6.0)
+    rexml (3.2.5)
+    rspec (3.12.0)
+      rspec-core (~> 3.12.0)
+      rspec-expectations (~> 3.12.0)
+      rspec-mocks (~> 3.12.0)
+    rspec-benchmark (0.6.0)
+      benchmark-malloc (~> 0.2)
+      benchmark-perf (~> 0.6)
+      benchmark-trend (~> 0.4)
+      rspec (>= 3.0)
+    rspec-core (3.12.0)
+      rspec-support (~> 3.12.0)
+    rspec-expectations (3.12.2)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.12.0)
+    rspec-mocks (3.12.3)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.12.0)
+    rspec-parameterized (1.0.0)
+      rspec-parameterized-core (< 2)
+      rspec-parameterized-table_syntax (< 2)
+    rspec-parameterized-core (1.0.0)
+      parser
+      proc_to_ast
+      rspec (>= 2.13, < 4)
+      unparser
+    rspec-parameterized-table_syntax (1.0.0)
+      binding_of_caller
+      rspec-parameterized-core (< 2)
+    rspec-rails (6.0.1)
+      actionpack (>= 6.1)
+      activesupport (>= 6.1)
+      railties (>= 6.1)
+      rspec-core (~> 3.11)
+      rspec-expectations (~> 3.11)
+      rspec-mocks (~> 3.11)
+      rspec-support (~> 3.11)
+    rspec-support (3.12.0)
+    rubocop (1.43.0)
+      json (~> 2.3)
+      parallel (~> 1.10)
+      parser (>= 3.2.0.0)
+      rainbow (>= 2.2.2, < 4.0)
+      regexp_parser (>= 1.8, < 3.0)
+      rexml (>= 3.2.5, < 4.0)
+      rubocop-ast (>= 1.24.1, < 2.0)
+      ruby-progressbar (~> 1.7)
+      unicode-display_width (>= 2.4.0, < 3.0)
+    rubocop-ast (1.24.1)
+      parser (>= 3.1.1.0)
+    rubocop-capybara (2.18.0)
+      rubocop (~> 1.41)
+    rubocop-graphql (0.19.0)
+      rubocop (>= 0.87, < 2)
+    rubocop-performance (1.18.0)
+      rubocop (>= 1.7.0, < 2.0)
+      rubocop-ast (>= 0.4.0)
+    rubocop-rails (2.19.1)
+      activesupport (>= 4.2.0)
+      rack (>= 1.1)
+      rubocop (>= 1.33.0, < 2.0)
+    rubocop-rspec (2.18.1)
+      rubocop (~> 1.33)
+      rubocop-capybara (~> 2.17)
+    ruby-progressbar (1.11.0)
+    thor (1.2.2)
+    tzinfo (2.0.6)
+      concurrent-ruby (~> 1.0)
+    unicode-display_width (2.4.2)
+    unparser (0.6.7)
+      diff-lcs (~> 1.3)
+      parser (>= 3.2.0)
+    zeitwerk (2.6.8)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  factory_bot_rails (~> 6.2.0)
+  gitlab-rspec!
+  gitlab-styles (~> 10.0.0)
+  gitlab-utils!
+  rspec-benchmark (~> 0.6.0)
+  rspec-parameterized (~> 1.0)
+  rspec-rails (~> 6.0.1)
+  rubocop (~> 1.21)
+  rubocop-rspec (~> 2.18.1)
+
+BUNDLED WITH
+   2.4.4
diff --git a/gems/gitlab-utils/README.md b/gems/gitlab-utils/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f7c7d83888bb70103710793f97d011cee8061484
--- /dev/null
+++ b/gems/gitlab-utils/README.md
@@ -0,0 +1,8 @@
+# Gitlab::Utils
+
+This Gem contains all code that is not dependent on application code
+or business logic and provides a generic functions like:
+
+- safe parsing of YAML
+- version comparisions
+- `strong_memoize`
diff --git a/gems/gitlab-utils/Rakefile b/gems/gitlab-utils/Rakefile
new file mode 100644
index 0000000000000000000000000000000000000000..cca7175449300ac09160bf9df759076b53696d25
--- /dev/null
+++ b/gems/gitlab-utils/Rakefile
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+require "rubocop/rake_task"
+
+RuboCop::RakeTask.new
+
+task default: %i[spec rubocop]
diff --git a/gems/gitlab-utils/gitlab-utils.gemspec b/gems/gitlab-utils/gitlab-utils.gemspec
new file mode 100644
index 0000000000000000000000000000000000000000..15e40ecc279788b49ec0f8cf5988e3693180fc14
--- /dev/null
+++ b/gems/gitlab-utils/gitlab-utils.gemspec
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "lib/gitlab/utils/version"
+
+Gem::Specification.new do |spec|
+  spec.name = "gitlab-utils"
+  spec.version = Gitlab::Utils::Version::VERSION
+  spec.authors = ["group::tenant scale"]
+  spec.email = ["engineering@gitlab.com"]
+
+  spec.summary = "GitLab's common helper methods"
+  spec.description = "A set of useful helpers methods to perform various conversions and checks."
+  spec.homepage = 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-utils'
+  spec.license = 'MIT'
+  spec.required_ruby_version = ">= 3.0"
+
+  spec.files = Dir['lib/**/*.rb']
+  spec.test_files = Dir['spec/**/*']
+  spec.require_paths = ["lib"]
+
+  spec.add_runtime_dependency 'actionview', '>= 6.1.7.2'
+  spec.add_runtime_dependency 'activesupport', '>= 6.1.7.2'
+  spec.add_runtime_dependency 'addressable', '~> 2.8'
+  spec.add_runtime_dependency 'nokogiri', '~> 1.15.2'
+  spec.add_runtime_dependency 'rake', '~> 13.0'
+
+  spec.add_development_dependency 'factory_bot_rails', '~> 6.2.0'
+  spec.add_development_dependency 'gitlab-styles', '~> 10.0.0'
+  spec.add_development_dependency 'rspec-benchmark', '~> 0.6.0'
+  spec.add_development_dependency 'rspec-parameterized', '~> 1.0'
+  spec.add_development_dependency 'rspec-rails', '~> 6.0.1'
+  spec.add_development_dependency 'rubocop', '~> 1.21'
+  spec.add_development_dependency 'rubocop-rspec', '~> 2.18.1'
+end
diff --git a/lib/gitlab/utils.rb b/gems/gitlab-utils/lib/gitlab/utils.rb
similarity index 93%
rename from lib/gitlab/utils.rb
rename to gems/gitlab-utils/lib/gitlab/utils.rb
index dc0112c14d6b6f285bb434ea562e6f5601f81336..4e08ee8fcaf2b684d64d44e12b294bddc55bc0aa 100644
--- a/lib/gitlab/utils.rb
+++ b/gems/gitlab-utils/lib/gitlab/utils.rb
@@ -1,9 +1,13 @@
 # frozen_string_literal: true
 
+require "addressable/uri"
+require "active_support/all"
+require "action_view"
+
 module Gitlab
   module Utils
     extend self
-    DoubleEncodingError ||= Class.new(StandardError)
+    DoubleEncodingError = Class.new(StandardError)
 
     def allowlisted?(absolute_path, allowlist)
       path = absolute_path.downcase
@@ -15,7 +19,7 @@ def allowlisted?(absolute_path, allowlist)
 
     def decode_path(encoded_path)
       decoded = CGI.unescape(encoded_path)
-      if decoded != CGI.unescape(decoded)
+      if decoded != CGI.unescape(decoded) # rubocop:disable Style/IfUnlessModifier
         raise DoubleEncodingError, "path #{encoded_path} is not allowed"
       end
 
@@ -31,7 +35,7 @@ def ensure_utf8_size(str, bytes:)
       raise ArgumentError, 'Negative string size provided!' if bytes < 0
 
       truncated = str.each_char.each_with_object(+'') do |char, object|
-        if object.bytesize + char.bytesize > bytes
+        if object.bytesize + char.bytesize > bytes # rubocop:disable Style/GuardClause
           break object
         else
           object.concat(char)
@@ -43,7 +47,7 @@ def ensure_utf8_size(str, bytes:)
 
     # Append path to host, making sure there's one single / in between
     def append_path(host, path)
-      "#{host.to_s.sub(%r{\/+$}, '')}/#{remove_leading_slashes(path)}"
+      "#{host.to_s.sub(%r{\/+$}, '')}/#{remove_leading_slashes(path)}" # rubocop:disable Style/RedundantRegexpEscape
     end
 
     def remove_leading_slashes(str)
@@ -128,7 +132,7 @@ def ensure_array_from_string(string_or_array)
     def deep_indifferent_access(data)
       case data
       when Array
-        data.map(&method(:deep_indifferent_access))
+        data.map { |item| deep_indifferent_access(item) }
       when Hash
         data.with_indifferent_access
       else
@@ -139,7 +143,7 @@ def deep_indifferent_access(data)
     def deep_symbolized_access(data)
       case data
       when Array
-        data.map(&method(:deep_symbolized_access))
+        data.map { |item| deep_symbolized_access(item) }
       when Hash
         data.deep_symbolize_keys
       else
@@ -242,7 +246,7 @@ def valid_brackets?(string = '', allow_nested: true)
 
       unless allow_nested
         # nested brackets check
-        return false if brackets.include?('[[') || brackets.include?(']]')
+        return false if brackets.include?('[[') || brackets.include?(']]') # rubocop:disable Style/SoleNestedConditional
       end
 
       # open / close brackets coherence check
diff --git a/gems/gitlab-utils/lib/gitlab/utils/all.rb b/gems/gitlab-utils/lib/gitlab/utils/all.rb
new file mode 100644
index 0000000000000000000000000000000000000000..200a21aad88f447ed1c26ec7452387e81a487507
--- /dev/null
+++ b/gems/gitlab-utils/lib/gitlab/utils/all.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "../utils"
+require_relative "../version_info"
+require_relative "version"
+require_relative "strong_memoize"
diff --git a/lib/gitlab/utils/strong_memoize.rb b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
similarity index 100%
rename from lib/gitlab/utils/strong_memoize.rb
rename to gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
diff --git a/gems/gitlab-utils/lib/gitlab/utils/version.rb b/gems/gitlab-utils/lib/gitlab/utils/version.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a9afe5bf8455d3f105da2acd21412efdc925e0c4
--- /dev/null
+++ b/gems/gitlab-utils/lib/gitlab/utils/version.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Utils
+    module Version
+      VERSION = "0.1.0"
+    end
+  end
+end
diff --git a/lib/gitlab/version_info.rb b/gems/gitlab-utils/lib/gitlab/version_info.rb
similarity index 92%
rename from lib/gitlab/version_info.rb
rename to gems/gitlab-utils/lib/gitlab/version_info.rb
index 0351c9b30b3adfd1a3570cf808e44c256a083e63..0f94aea04adc46e2c3dd4f705f3f88acfa16a23e 100644
--- a/lib/gitlab/version_info.rb
+++ b/gems/gitlab-utils/lib/gitlab/version_info.rb
@@ -6,7 +6,7 @@ class VersionInfo
 
     attr_reader :major, :minor, :patch
 
-    VERSION_REGEX = /(\d+)\.(\d+)\.(\d+)/.freeze
+    VERSION_REGEX = /(\d+)\.(\d+)\.(\d+)/
     # To mitigate ReDoS, limit the length of the version string we're
     # willing to check
     MAX_VERSION_LENGTH = 128
@@ -21,7 +21,7 @@ def self.parse(str, parse_suffix: false)
       end
     end
 
-    def initialize(major = 0, minor = 0, patch = 0, suffix = nil)
+    def initialize(major = 0, minor = 0, patch = 0, suffix = nil) # rubocop:disable Metrics/ParameterLists
       @major = major
       @minor = minor
       @patch = patch
@@ -59,7 +59,7 @@ def <=>(other)
 
     def to_s
       if valid?
-        "%d.%d.%d%s" % [@major, @minor, @patch, @suffix_s]
+        "%d.%d.%d%s" % [@major, @minor, @patch, @suffix_s] # rubocop:disable Style/FormatString
       else
         'Unknown'
       end
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb
similarity index 95%
rename from spec/lib/gitlab/utils/strong_memoize_spec.rb
rename to gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb
index ea8083e7d7f40eb38b92245d24a2fde454ed9684..05b256810434956d85868eefbffc56965a113527 100644
--- a/spec/lib/gitlab/utils/strong_memoize_spec.rb
+++ b/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb
@@ -1,13 +1,9 @@
 # frozen_string_literal: true
 
-require 'fast_spec_helper'
-require 'rspec-benchmark'
-require 'rspec-parameterized'
-require 'active_support/testing/time_helpers'
+# rubocop:disable GitlabSecurity/PublicSend
 
-RSpec.configure do |config|
-  config.include RSpec::Benchmark::Matchers
-end
+require 'spec_helper'
+require 'active_support/testing/time_helpers'
 
 RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
   include ActiveSupport::Testing::TimeHelpers
@@ -89,8 +85,8 @@ def public_method; end
     let(:member_name) { described_class.normalize_key(method_name) }
 
     it 'only calls the block once' do
-      value0 = object.send(method_name)
-      value1 = object.send(method_name)
+      value0 = object.public_send(method_name)
+      value1 = object.public_send(method_name)
 
       expect(value0).to eq(value)
       expect(value1).to eq(value)
@@ -98,7 +94,7 @@ def public_method; end
     end
 
     it 'returns and defines the instance variable for the exact value' do
-      returned_value = object.send(method_name)
+      returned_value = object.public_send(method_name)
       memoized_value = object.instance_variable_get(:"@#{member_name}")
 
       expect(returned_value).to eql(value)
@@ -125,7 +121,7 @@ def public_method; end
       end
     end
 
-    context "memory allocation", type: :benchmark do
+    context "with memory allocation", type: :benchmark do
       let(:value) { 'aaa' }
 
       before do
@@ -171,7 +167,7 @@ def public_method; end
       end
     end
 
-    context 'value memoization test' do
+    context 'with value memoization test' do
       let(:value) { 'value' }
 
       it 'caches the value for specified number of seconds' do
@@ -265,7 +261,7 @@ def public_method; end
       context "with value '#{value}'" do
         let(:value) { value }
 
-        context 'memoized after method definition' do
+        context 'with memoized after method definition' do
           let(:method_name) { :method_name_attr }
 
           it_behaves_like 'caching the value'
@@ -372,3 +368,5 @@ def method_with_parameters(params); end
     end
   end
 end
+
+# rubocop:enable GitlabSecurity/PublicSend
diff --git a/spec/lib/gitlab/utils_spec.rb b/gems/gitlab-utils/spec/gitlab/utils_spec.rb
similarity index 96%
rename from spec/lib/gitlab/utils_spec.rb
rename to gems/gitlab-utils/spec/gitlab/utils_spec.rb
index 7b9504366ec6fffc7dcb4854c23e600a367919ed..53593190eeadaab5733c36d683867202f3e670bb 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/gems/gitlab-utils/spec/gitlab/utils_spec.rb
@@ -2,13 +2,14 @@
 
 require 'spec_helper'
 
-RSpec.describe Gitlab::Utils do
+RSpec.describe Gitlab::Utils, feature_category: :shared do
   using RSpec::Parameterized::TableSyntax
+  include StubENV
 
   delegate :to_boolean, :boolean_to_yes_no, :slugify, :which,
-           :ensure_array_from_string, :bytes_to_megabytes,
-           :append_path, :remove_leading_slashes, :allowlisted?,
-           :decode_path, :ms_to_round_sec, to: :described_class
+    :ensure_array_from_string, :bytes_to_megabytes,
+    :append_path, :remove_leading_slashes, :allowlisted?,
+    :decode_path, :ms_to_round_sec, to: :described_class
 
   describe '.allowlisted?' do
     let(:allowed_paths) { ['/home/foo', '/foo/bar', '/etc/passwd'] }
@@ -225,7 +226,7 @@
   end
 
   describe '.ensure_utf8_size' do
-    context 'string is has less bytes than expected' do
+    context 'with string is has less bytes than expected' do
       it 'backfills string with null characters' do
         transformed = described_class.ensure_utf8_size('a' * 10, bytes: 32)
 
@@ -234,7 +235,7 @@
       end
     end
 
-    context 'string size is exactly the one that is expected' do
+    context 'with string size is exactly the one that is expected' do
       it 'returns original value' do
         transformed = described_class.ensure_utf8_size('a' * 32, bytes: 32)
 
@@ -247,7 +248,7 @@
       it 'backfills string with null characters' do
         transformed = described_class.ensure_utf8_size('❤' * 6, bytes: 32)
 
-        expect(transformed).to eq '❤❤❤❤❤❤' + ('0' * 14)
+        expect(transformed).to eq '❤❤❤❤❤❤' + ('0' * 14) # rubocop:disable Style/StringConcatenation
         expect(transformed.bytesize).to eq 32
       end
     end
@@ -368,7 +369,7 @@
       nil                          | { b: 3, a: 2 }     | '?a=2&b=3'
       'https://gitlab.com'         | nil                | 'https://gitlab.com'
       'https://gitlab.com'         | { b: 3, a: 2 }     | 'https://gitlab.com?a=2&b=3'
-      'https://gitlab.com?a=1#foo' | { b: 3, 'a': 2 }   | 'https://gitlab.com?a=2&b=3#foo'
+      'https://gitlab.com?a=1#foo' | { b: 3, 'a' => 2 } | 'https://gitlab.com?a=2&b=3#foo'
       'https://gitlab.com?a=1#foo' | [[:b, 3], [:a, 2]] | 'https://gitlab.com?a=2&b=3#foo'
     end
 
@@ -391,7 +392,8 @@
     end
 
     it 'returns string with filtered access_token param' do
-      expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token')).to eq('http://gitlab.com/auth.html#access_token=filtered')
+      expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token'))
+        .to eq('http://gitlab.com/auth.html#access_token=filtered')
     end
 
     it 'returns string with filtered access_token param but other params preserved' do
diff --git a/spec/lib/gitlab/version_info_spec.rb b/gems/gitlab-utils/spec/gitlab/version_info_spec.rb
similarity index 98%
rename from spec/lib/gitlab/version_info_spec.rb
rename to gems/gitlab-utils/spec/gitlab/version_info_spec.rb
index 99c7a762392b3e6175c8cde7b3011b73d2d54969..2b5f6bcb4c13253bfd1d138fc20dd758ba365d7c 100644
--- a/spec/lib/gitlab/version_info_spec.rb
+++ b/gems/gitlab-utils/spec/gitlab/version_info_spec.rb
@@ -1,8 +1,8 @@
 # frozen_string_literal: true
 
-require 'fast_spec_helper'
+require 'spec_helper'
 
-RSpec.describe Gitlab::VersionInfo do
+RSpec.describe Gitlab::VersionInfo, feature_category: :shared do
   before do
     @unknown = described_class.new
     @v0_0_1 = described_class.new(0, 0, 1)
diff --git a/gems/gitlab-utils/spec/spec_helper.rb b/gems/gitlab-utils/spec/spec_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5dc3859f77d9ff44f9fa2c1b2c42c2796e625bf1
--- /dev/null
+++ b/gems/gitlab-utils/spec/spec_helper.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails'
+require 'rspec/mocks'
+require 'rspec-benchmark'
+require 'rspec-parameterized'
+
+require 'gitlab/rspec/all'
+require 'gitlab/utils/all'
+
+RSpec.configure do |config|
+  config.include RSpec::Benchmark::Matchers
+
+  # Enable flags like --only-failures and --next-failure
+  config.example_status_persistence_file_path = ".rspec_status"
+
+  # Disable RSpec exposing methods globally on `Module` and `main`
+  config.disable_monkey_patching!
+
+  config.expect_with :rspec do |c|
+    c.syntax = :expect
+  end
+end
diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
index b39d2a02f0285ce7c628d31280b6055fc901c82b..c6ce0aa6160ad8a9b1c21f8840c4707f789428fc 100644
--- a/lib/gitlab/cluster/lifecycle_events.rb
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require_relative '../utils' # Gitlab::Utils
+require 'gitlab/utils/all' # Gitlab::Utils
 
 module Gitlab
   module Cluster
diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb
index b9800a4db730f42bdb6aa522cd78b575dc8e2f19..f756d229ba1ff49d00f1dd4869eb83e3499ead80 100644
--- a/lib/gitlab/task_helpers.rb
+++ b/lib/gitlab/task_helpers.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 require 'rainbow/ext/string'
-require_relative 'utils/strong_memoize'
+require 'gitlab/utils/all'
 
 # rubocop:disable Rails/Output
 module Gitlab
diff --git a/lib/gitlab/utils/override.rb b/lib/gitlab/utils/override.rb
index 1d02bcbb2d2474d2e1c253cdceb0adc6a599d6d5..10370811bb595a0e02e2c98872ea2bd5e81477ea 100644
--- a/lib/gitlab/utils/override.rb
+++ b/lib/gitlab/utils/override.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require_relative '../utils'
+require 'gitlab/utils/all'
 require_relative '../environment'
 
 module Gitlab
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 825388461bcd4f4febd093001b5fd0aeb8880b73..1a659a930abf5f9dd992e5a2c134d6f1743645c5 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -42,7 +42,7 @@ namespace :gettext do
   desc 'Lint all po files in `locale/'
   task lint: :environment do
     require 'simple_po_parser'
-    require 'gitlab/utils'
+    require 'gitlab/utils/all'
     require 'parallel'
 
     FastGettext.silence_errors
diff --git a/metrics_server/dependencies.rb b/metrics_server/dependencies.rb
index 233511eb505e4f11b3267ddcd67db166bf18e218..c96fecd7cb7c28a34346ba60de67ab732895e650 100644
--- a/metrics_server/dependencies.rb
+++ b/metrics_server/dependencies.rb
@@ -11,11 +11,11 @@
 require 'prometheus/client'
 require 'rack'
 
+require 'gitlab/utils/all'
+
 require_relative 'settings_overrides'
 
 require_relative '../lib/gitlab/daemon'
-require_relative '../lib/gitlab/utils'
-require_relative '../lib/gitlab/utils/strong_memoize'
 require_relative '../lib/prometheus/cleanup_multiproc_dir_service'
 require_relative '../lib/gitlab/metrics/prometheus'
 require_relative '../lib/gitlab/metrics'
diff --git a/qa/Dockerfile b/qa/Dockerfile
index e5308d78f83b8c1631bb20bdc8fc0b837b10026e..213ec3450cba5bd164c58170355283077fe68046 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -40,6 +40,8 @@ WORKDIR /home/gitlab/qa
 # Install qa dependencies or fetch from cache if unchanged
 #
 COPY ./qa/Gemfile* /home/gitlab/qa/
+COPY ./vendor/gems/ /home/gitlab/vendor/gems/
+COPY ./gems/ /home/gitlab/gems/
 RUN bundle config set --local without development \
     && bundle install --retry=3
 
@@ -47,9 +49,7 @@ COPY ./config/initializers/0_inject_enterprise_edition_module.rb /home/gitlab/co
 COPY ./config/feature_flags /home/gitlab/config/feature_flags
 COPY ./config/bundler_setup.rb /home/gitlab/config/
 COPY ./lib/gitlab_edition.rb /home/gitlab/lib/
-COPY ./lib/gitlab/utils.rb /home/gitlab/lib/gitlab/
 COPY ./INSTALLATION_TYPE ./VERSION /home/gitlab/
-COPY ./gems /home/gitlab/
 
 COPY ./qa /home/gitlab/qa
 
diff --git a/qa/Gemfile b/qa/Gemfile
index c907240333fd984011897acf5b15e1a8b23e403c..2a7f221f91f813080d70d89181ecf9cbfa8e42a6 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
 
 gem 'gitlab-qa', '~> 11', '>= 11.2.0', require: 'gitlab/qa'
 gem 'gitlab_quality-test_tooling', '~> 0.8.2', require: false
+gem 'gitlab-utils', path: '../gems/gitlab-utils'
 gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile
 gem 'allure-rspec', '~> 2.20.0'
 gem 'capybara', '~> 3.39.2'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 94eae5f80bb4f7daed7e397a1d2ff280b23c7fcd..861bb1e0def5995eaeb19d00a59ce27d740dc91e 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -1,6 +1,22 @@
+PATH
+  remote: ../gems/gitlab-utils
+  specs:
+    gitlab-utils (0.1.0)
+      actionview (>= 6.1.7.2)
+      activesupport (>= 6.1.7.2)
+      addressable (~> 2.8)
+      nokogiri (~> 1.15.2)
+      rake (~> 13.0)
+
 GEM
   remote: https://rubygems.org/
   specs:
+    actionview (6.1.7.2)
+      activesupport (= 6.1.7.2)
+      builder (~> 3.1)
+      erubi (~> 1.4)
+      rails-dom-testing (~> 2.0)
+      rails-html-sanitizer (~> 1.1, >= 1.2.0)
     activesupport (6.1.7.2)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 1.6, < 2)
@@ -55,6 +71,7 @@ GEM
     confiner (0.4.0)
       gitlab (>= 4.17)
       zeitwerk (>= 2.5, < 3)
+    crass (1.0.6)
     debug_inspector (1.1.0)
     declarative (0.0.20)
     deprecation_toolkit (2.0.3)
@@ -62,6 +79,7 @@ GEM
     diff-lcs (1.3)
     domain_name (0.5.20190701)
       unf (>= 0.0.5, < 1.0.0)
+    erubi (1.12.0)
     excon (0.92.4)
     faker (3.2.0)
       i18n (>= 1.8.11, < 2)
@@ -176,6 +194,9 @@ GEM
     llhttp-ffi (0.4.0)
       ffi-compiler (~> 1.0)
       rake (~> 13.0)
+    loofah (2.21.3)
+      crass (~> 1.0.2)
+      nokogiri (>= 1.12.0)
     macaddr (1.7.2)
       systemu (~> 2.6.5)
     matrix (0.4.2)
@@ -214,10 +235,15 @@ GEM
       byebug (~> 11.0)
       pry (>= 0.13, < 0.15)
     public_suffix (5.0.1)
-    racc (1.6.2)
+    racc (1.7.1)
     rack (2.2.3.1)
     rack-test (1.1.0)
       rack (>= 1.0, < 3)
+    rails-dom-testing (2.0.3)
+      activesupport (>= 4.2.0)
+      nokogiri (>= 1.6)
+    rails-html-sanitizer (1.5.0)
+      loofah (~> 2.19, >= 2.19.1)
     rainbow (3.1.1)
     rake (13.0.6)
     regexp_parser (2.1.1)
@@ -324,6 +350,7 @@ DEPENDENCIES
   fog-core (= 2.1.0)
   fog-google (~> 1.19)
   gitlab-qa (~> 11, >= 11.2.0)
+  gitlab-utils!
   gitlab_quality-test_tooling (~> 0.8.2)
   influxdb-client (~> 2.9)
   knapsack (~> 4.0)
diff --git a/qa/gdk/Dockerfile.gdk b/qa/gdk/Dockerfile.gdk
index cdb693841cb8c269b84f75e900a180cc3baa6a0f..2b296535622942352654d5952ea1585068c5a072 100644
--- a/qa/gdk/Dockerfile.gdk
+++ b/qa/gdk/Dockerfile.gdk
@@ -98,8 +98,8 @@ RUN set -eux; \
 # Install gitlab gem dependencies
 #
 COPY --chown=gdk:gdk Gemfile Gemfile.lock ./gitlab/
-COPY --chown=gdk:gdk vendor/gems ./gitlab/vendor/gems
-COPY --chown=gdk:gdk gems ./gitlab/gems
+COPY --chown=gdk:gdk vendor/gems/ ./gitlab/vendor/gems/
+COPY --chown=gdk:gdk gems/ ./gitlab/gems/
 RUN make .gitlab-bundle && rm -rf ${GEM_HOME}/cache
 
 # Install gitlab npm dependencies
diff --git a/qa/qa.rb b/qa/qa.rb
index f6fba30c0796c9c208b80ebb130c747978940395..0e3d343b8616f86e882be20018800b7bc6c9e8b7 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -2,8 +2,9 @@
 
 Encoding.default_external = 'UTF-8'
 
+require 'gitlab/utils/all'
+
 require_relative '../lib/gitlab_edition'
-require_relative '../lib/gitlab/utils'
 require_relative '../config/initializers/0_inject_enterprise_edition_module'
 
 require_relative 'lib/gitlab'
diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build
index 6901593009ad64f40ee4a5e75430eae72466f889..cfa089b327ec1931d01893ec5490ead8d1838748 100755
--- a/scripts/gitaly-test-build
+++ b/scripts/gitaly-test-build
@@ -1,6 +1,7 @@
 #!/usr/bin/env ruby
 # frozen_string_literal: true
 
+require_relative '../config/bundler_setup'
 require 'fileutils'
 
 require_relative '../spec/support/helpers/gitaly_setup'
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index 475c7715bddf85d8b124e8c34afaf94349c6d06f..9285b561ae09d6b2f41dff8352c43c27dca66440 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -3,6 +3,7 @@
 
 # This script is used both in CI and in local development 'rspec' runs.
 
+require_relative '../config/bundler_setup'
 require_relative '../spec/support/helpers/gitaly_setup'
 
 class GitalyTestSpawn
diff --git a/scripts/merge-simplecov b/scripts/merge-simplecov
index 24be731549ba435cf0a87b81e13a4f971e509b17..7db12839382cbd3998675ea783a8c8727f27017b 100755
--- a/scripts/merge-simplecov
+++ b/scripts/merge-simplecov
@@ -1,6 +1,7 @@
 #!/usr/bin/env ruby
 # frozen_string_literal: true
 
+require_relative '../config/bundler_setup'
 require_relative '../spec/simplecov_env'
 SimpleCovEnv.configure_profile
 SimpleCovEnv.configure_formatter
diff --git a/scripts/setup-test-env b/scripts/setup-test-env
index ae00b569ce39aa9284fcc6ea3e421d5d28b2c161..1c39483bb7aac6b0628fa9f84e9f0360457cee33 100755
--- a/scripts/setup-test-env
+++ b/scripts/setup-test-env
@@ -25,8 +25,8 @@ require_relative '../lib/system_check/helpers'
 require 'omniauth'
 require 'omniauth-github'
 require 'etc'
+require 'gitlab/utils/all'
 require_relative '../lib/gitlab/access'
-require_relative '../lib/gitlab/utils'
 
 unless defined?(License)
   # This is needed to allow use of `Gitlab::ImportSources.values` in `1_settings.rb`.
diff --git a/sidekiq_cluster/cli.rb b/sidekiq_cluster/cli.rb
index 0d24f70c37d5a6713e374378794bac19808787ef..fc065d799d487abb70417a8a3acba43013972d7c 100644
--- a/sidekiq_cluster/cli.rb
+++ b/sidekiq_cluster/cli.rb
@@ -5,11 +5,11 @@
 require 'optparse'
 require 'logger'
 require 'time'
+require 'gitlab/utils/all'
 
 # In environments where code is preloaded and cached such as `spring`,
 # we may run into "already initialized" warnings, hence the check.
 require_relative '../lib/gitlab'
-require_relative '../lib/gitlab/utils'
 require_relative '../lib/gitlab/sidekiq_config/cli_methods'
 require_relative '../lib/gitlab/sidekiq_config/worker_matcher'
 require_relative '../lib/gitlab/sidekiq_logging/json_formatter'
diff --git a/spec/deprecation_warnings.rb b/spec/deprecation_warnings.rb
index 45fed5fecca0bbb2c66e3d25f3e21587a1f78085..abdd13ee8e7a841bc072bf07280142b526e9d64c 100644
--- a/spec/deprecation_warnings.rb
+++ b/spec/deprecation_warnings.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require_relative '../lib/gitlab/utils'
+require 'gitlab/utils/all'
 return if Gitlab::Utils.to_boolean(ENV['SILENCE_DEPRECATIONS'], default: false)
 
 # Enable deprecation warnings by default and make them more visible
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 03c8919912ce86da71275169e0c1f4e6ccf0ca25..47a90efab1ec93f0a22ee0bf320dce09a28635a2 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -18,13 +18,12 @@
 
 require 'active_support/all'
 require 'pry'
+require 'gitlab/utils/all'
 require_relative 'rails_autoload'
 
 require_relative '../config/settings'
 require_relative 'support/rspec'
 require_relative '../lib/gitlab'
-require_relative '../lib/gitlab/utils'
-require_relative '../lib/gitlab/utils/strong_memoize'
 
 require_relative 'simplecov_env'
 SimpleCovEnv.start!
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index bea312369f74a68a4e8ed1b13910e3646f1179d4..af722f45ae5ebbcbed50da7549372f7851c02ee7 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -3,7 +3,7 @@
 require 'simplecov'
 require 'simplecov-cobertura'
 require 'simplecov-lcov'
-require_relative '../lib/gitlab/utils'
+require 'gitlab/utils/all'
 
 module SimpleCovEnv
   extend self
diff --git a/spec/support/ability_check.rb b/spec/support/ability_check.rb
index 213944506bb6cf8c65bd6b7f196e618f94c0b17b..5b56b9925f66cc82706af5bcf860fcc61049b096 100644
--- a/spec/support/ability_check.rb
+++ b/spec/support/ability_check.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'gitlab/utils/strong_memoize'
+require 'gitlab/utils/all'
 
 module Support
   module AbilityCheck
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 7db9e0aaf093ebe1772d2a83ef83421dad8733da..06390406efc989d0134e719b19cd319389598c22 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -10,8 +10,7 @@
 require 'socket'
 require 'logger'
 require 'fileutils'
-
-require_relative '../../../lib/gitlab/utils'
+require 'gitlab/utils/all'
 
 module GitalySetup
   extend self
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index b34f8fe9a22fef8a615572951dba82f5bc4e537e..4479e679d670880e7efe3bd36365467b351abbe8 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -8,6 +8,7 @@
 require_relative "helpers/fast_rails_root"
 
 require 'gitlab/rspec/all'
+require 'gitlab/utils/all'
 
 RSpec::Expectations.configuration.on_potential_false_positives = :raise