diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 8fded701d65f361f91540008aa1583fffb0abaa0..c8731e87107840534eb4f027dbd05332f94f66ee 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -251,6 +251,22 @@ static-analysis as-if-foss:
     - .static-analysis:rules:as-if-foss
     - .as-if-foss
 
+zeitwerk-check:
+  extends:
+    - .rails-cache
+    - .default-before_script
+    - .rails:rules:ee-and-foss-unit
+  variables:
+    BUNDLE_WITHOUT: ""
+    SETUP_DB: "false"
+  needs: []
+  stage: test
+  script:
+    - sed -i -e "s/config\.autoloader = :classic/config\.autoloader = :zeitwerk/" config/application.rb
+    - RAILS_ENV=test bundle exec rake zeitwerk:check
+    - RAILS_ENV=development bundle exec rake zeitwerk:check
+    - RAILS_ENV=production bundle exec rake zeitwerk:check
+
 rspec migration pg12:
   extends:
     - .rspec-base-pg12
diff --git a/config/application.rb b/config/application.rb
index 5e350cb80ddd9509433ba0eb96dfc044dbdb7b09..181b216f44413863d4b7bb2975f62c9b3a2b9dfe 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -32,6 +32,8 @@ class Application < Rails::Application
     require_dependency Rails.root.join('lib/gitlab/middleware/rack_multipart_tempfile_factory')
     require_dependency Rails.root.join('lib/gitlab/runtime')
 
+    config.autoloader = :classic
+
     # Settings in config/environments/* take precedence over those specified here.
     # Application configuration should go into files in config/initializers
     # -- all .rb files in that directory are automatically loaded.
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 50d394859bcb533c33261bb0ddfe45ae384ee7fc..92de88394c6bfbc7649fd14ce8db20cb7a6619b9 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -50,7 +50,7 @@
   config.action_mailer.raise_delivery_errors = true
   # Don't make a mess when bootstrapping a development environment
   config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1')
-  config.action_mailer.preview_path = 'app/mailers/previews'
+  config.action_mailer.preview_path = "#{Rails.root}{/ee,}/app/mailers/previews"
 
   config.eager_load = false
 
diff --git a/config/initializers/0_runtime_identify.rb b/config/initializers/9_runtime_identify.rb
similarity index 88%
rename from config/initializers/0_runtime_identify.rb
rename to config/initializers/9_runtime_identify.rb
index e6be19ffb79dbacf161629c5367229d59f687462..c5efc9b17cbb2ce2d8a8780a462749d5b9a3e339 100644
--- a/config/initializers/0_runtime_identify.rb
+++ b/config/initializers/9_runtime_identify.rb
@@ -7,7 +7,7 @@
   \n!! RUNTIME IDENTIFICATION FAILED: #{e}
   Runtime based configuration settings may not work properly.
   If you continue to see this error, please file an issue via
-  https://gitlab.com/gitlab-org/gitlab/issues/new
+  https://gitlab.com/gitlab-org/gitlab/-/issues/new
   NOTICE
   Gitlab::AppLogger.error(message)
   Gitlab::ErrorTracking.track_exception(e)
diff --git a/config/initializers/global_id.rb b/config/initializers/global_id.rb
index 8f1b8f2aeebd35240d05d83480a69fbcba20deab..c3ab9f10c087c250d67fff010a1a17cb7cf59f30 100644
--- a/config/initializers/global_id.rb
+++ b/config/initializers/global_id.rb
@@ -1,3 +1,3 @@
 # frozen_string_literal: true
 
-GlobalID.prepend(Gitlab::Patch::GlobalID)
+GlobalID.prepend(Gitlab::Patch::GlobalId)
diff --git a/config/initializers_before_autoloader/004_zeitwerk.rb b/config/initializers_before_autoloader/004_zeitwerk.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d576e21f173cad4ac445c41a073267ecacafe99e
--- /dev/null
+++ b/config/initializers_before_autoloader/004_zeitwerk.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+Rails.autoloaders.each do |autoloader|
+  # We need to ignore these since these are non-Ruby files
+  # that do not define Ruby classes / modules
+  autoloader.ignore(Rails.root.join('lib/support'))
+
+  # Ignore generators since these are loaded manually by Rails
+  # https://github.com/rails/rails/blob/v6.1.3.2/railties/lib/rails/command/behavior.rb#L56-L65
+  autoloader.ignore(Rails.root.join('lib/generators'))
+  autoloader.ignore(Rails.root.join('ee/lib/generators')) if Gitlab.ee?
+
+  # Mailer previews are also loaded manually by Rails
+  # https://github.com/rails/rails/blob/v6.1.3.2/actionmailer/lib/action_mailer/preview.rb#L121-L125
+  autoloader.ignore(Rails.root.join('app/mailers/previews'))
+  autoloader.ignore(Rails.root.join('ee/app/mailers/previews')) if Gitlab.ee?
+
+  autoloader.inflector.inflect(
+    'api' => 'API',
+    'api_authentication' => 'APIAuthentication',
+    'api_guard' => 'APIGuard',
+    'group_api_compatibility' => 'GroupAPICompatibility',
+    'project_api_compatibility' => 'ProjectAPICompatibility',
+    'ast' => 'AST',
+    'cte' => 'CTE',
+    'recursive_cte' => 'RecursiveCTE',
+    'cidr' => 'CIDR',
+    'cli' => 'CLI',
+    'dn' => 'DN',
+    'global_id_type' => 'GlobalIDType',
+    'global_id_compatibility' => 'GlobalIDCompatibility',
+    'hll' => 'HLL',
+    'hll_redis_counter' => 'HLLRedisCounter',
+    'redis_hll_metric' => 'RedisHLLMetric',
+    'hmac_token' => 'HMACToken',
+    'html' => 'HTML',
+    'html_parser' => 'HTMLParser',
+    'html_gitlab' => 'HTMLGitlab',
+    'http' => 'HTTP',
+    'http_connection_adapter' => 'HTTPConnectionAdapter',
+    'http_clone_enabled_check' => 'HTTPCloneEnabledCheck',
+    'hangouts_chat_http_override' => 'HangoutsChatHTTPOverride',
+    'chunked_io' => 'ChunkedIO',
+    'http_io' => 'HttpIO',
+    'json_formatter' => 'JSONFormatter',
+    'json_web_token' => 'JSONWebToken',
+    'as_json' => 'AsJSON',
+    'jwt_token' => 'JWTToken',
+    'ldap_key' => 'LDAPKey',
+    'mr_note' => 'MRNote',
+    'pdf' => 'PDF',
+    'rsa_token' => 'RSAToken',
+    'san_extension' => 'SANExtension',
+    'sca' => 'SCA',
+    'spdx' => 'SPDX',
+    'sql' => 'SQL',
+    'sse_helpers' => 'SSEHelpers',
+    'ssh_key' => 'SSHKey',
+    'ssh_key_with_user' => 'SSHKeyWithUser',
+    'ssh_public_key' => 'SSHPublicKey',
+    'git_ssh_proxy' => 'GitSSHProxy',
+    'git_user_default_ssh_config_check' => 'GitUserDefaultSSHConfigCheck',
+    'binary_stl' => 'BinarySTL',
+    'text_stl' => 'TextSTL',
+    'svg' => 'SVG',
+    'function_uri' => 'FunctionURI',
+    'uuid' => 'UUID',
+    'vulnerability_uuid' => 'VulnerabilityUUID',
+    'vs_code_extension_activity_unique_counter' => 'VSCodeExtensionActivityUniqueCounter'
+  )
+end
diff --git a/ee/lib/gitlab/analytics/cycle_analytics/summary/group/stage_time_summary.rb b/ee/lib/gitlab/analytics/cycle_analytics/summary/group/stage_time_summary.rb
deleted file mode 100644
index b44657e6c7e630a68100a2a71980f4ec5e8d2c88..0000000000000000000000000000000000000000
--- a/ee/lib/gitlab/analytics/cycle_analytics/summary/group/stage_time_summary.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module Analytics
-    module CycleAnalytics
-      module Summary
-        class StageTimeSummary
-          attr_reader :stage, :current_user, :options
-
-          def initialize(stage, options:)
-            @stage = stage
-            @current_user = options[:current_user]
-            @options = options
-          end
-
-          def data
-            [lead_time, cycle_time]
-          end
-
-          private
-
-          def lead_time
-            serialize(
-              Summary::LeadTime.new(
-                stage: stage, current_user: current_user, options: options
-              ),
-              with_unit: true
-            )
-          end
-
-          def cycle_time
-            serialize(
-              Summary::CycleTime.new(
-                stage: stage, current_user: current_user, options: options
-              ),
-              with_unit: true
-            )
-          end
-
-          def serialize(summary_object, with_unit: false)
-            AnalyticsSummarySerializer.new.represent(
-              summary_object, with_unit: with_unit)
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/patch/global_id.rb b/lib/gitlab/patch/global_id.rb
index e99f36c7dca39394352ec5b8b464bc371f3e8daa..145a7bfe842d8fe3c613b10ea7eaeec3714830a2 100644
--- a/lib/gitlab/patch/global_id.rb
+++ b/lib/gitlab/patch/global_id.rb
@@ -4,7 +4,7 @@
 # we alter GlobalID so it will correctly find the record with its new model name.
 module Gitlab
   module Patch
-    module GlobalID
+    module GlobalId
       def initialize(gid, options = {})
         super
 
diff --git a/spec/initializers/global_id_spec.rb b/spec/initializers/global_id_spec.rb
index 63bfa32d74f82ea55a4df13b5d2163a36a10e691..4deb183399903cb5d03ae3eb423b2361cfbd0db9 100644
--- a/spec/initializers/global_id_spec.rb
+++ b/spec/initializers/global_id_spec.rb
@@ -3,8 +3,8 @@
 require 'spec_helper'
 
 RSpec.describe 'global_id' do
-  it 'prepends `Gitlab::Patch::GlobalID`' do
-    expect(GlobalID.ancestors).to include(Gitlab::Patch::GlobalID)
+  it 'prepends `Gitlab::Patch::GlobalId`' do
+    expect(GlobalID.ancestors).to include(Gitlab::Patch::GlobalId)
   end
 
   it 'patches GlobalID to find aliased models when a deprecation exists' do