diff --git a/app/models/protected_branch/cache_key.rb b/app/models/protected_branch/cache_key.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3274fad32e4244bfdc1eb59bef814c62f70612a4
--- /dev/null
+++ b/app/models/protected_branch/cache_key.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+class ProtectedBranch::CacheKey # rubocop:disable Style/ClassAndModuleChildren -- Same problem as in push_access_level.rb
+  include Gitlab::Utils::StrongMemoize
+
+  CACHE_ROOT_KEY = 'cache:gitlab:protected_branch'
+
+  def initialize(entity)
+    @entity = entity
+  end
+
+  def to_s
+    need_to_scope? ? scoped_key : unscoped_key
+  end
+
+  private
+
+  attr_reader :entity
+
+  def need_to_scope?
+    Feature.enabled?(:group_protected_branches, group) ||
+      Feature.enabled?(:allow_protected_branches_for_group, group)
+  end
+
+  def scoped_key
+    [CACHE_ROOT_KEY, entity_scope, entity.id].join(':')
+  end
+
+  def unscoped_key
+    [CACHE_ROOT_KEY, entity.id].join(':')
+  end
+
+  def group
+    return entity if entity.is_a?(Group)
+    return entity.group if entity.is_a?(Project)
+
+    nil
+  end
+  strong_memoize_attr :group
+
+  def entity_scope
+    case entity
+    when Group
+      'group'
+    when Project
+      'project'
+    else
+      entity.class.name.downcase
+    end
+  end
+end
diff --git a/app/services/protected_branches/cache_service.rb b/app/services/protected_branches/cache_service.rb
index 9d4aff6a345f742368e4348a3b7bfe8cd1e3f83d..cebc1eda0a630c1e87452bba6cf7da03238a72f4 100644
--- a/app/services/protected_branches/cache_service.rb
+++ b/app/services/protected_branches/cache_service.rb
@@ -4,7 +4,6 @@ module ProtectedBranches
   class CacheService < ProtectedBranches::BaseService
     include Gitlab::Utils::StrongMemoize
 
-    CACHE_ROOT_KEY = 'cache:gitlab:protected_branch'
     TTL_UNSET = -1
     CACHE_EXPIRE_IN = 1.day
     CACHE_LIMIT = 1000
@@ -84,25 +83,10 @@ def check_and_log_discrepancy(cached_value, real_value, ref_name)
 
     def redis_key(entity = project_or_group)
       strong_memoize_with(:redis_key, entity) do
-        scope_redis_key?(entity) ? scoped_redis_key(entity) : unscoped_redis_key(entity)
+        ProtectedBranch::CacheKey.new(entity).to_s
       end
     end
 
-    def scope_redis_key?(entity)
-      group = entity.is_a?(Group) ? entity : entity.group
-
-      Feature.enabled?(:group_protected_branches, group) ||
-        Feature.enabled?(:allow_protected_branches_for_group, group)
-    end
-
-    def scoped_redis_key(entity)
-      [CACHE_ROOT_KEY, entity.class.name, entity.id].join(':')
-    end
-
-    def unscoped_redis_key(entity)
-      [CACHE_ROOT_KEY, entity.id].join(':')
-    end
-
     def metrics
       @metrics ||= Gitlab::Cache::Metrics.new(cache_metadata)
     end
diff --git a/spec/models/protected_branch/cache_key_spec.rb b/spec/models/protected_branch/cache_key_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..14cdf68c61d9f03c4ecbea033a01c6090ba2a05b
--- /dev/null
+++ b/spec/models/protected_branch/cache_key_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProtectedBranch::CacheKey, feature_category: :source_code_management do
+  subject(:cache_key) { described_class.new(entity) }
+
+  let_it_be(:project) { create(:project) }
+  let_it_be(:group) { create(:group) }
+  let_it_be(:user) { create(:user) }
+
+  describe '#to_s' do
+    subject { cache_key.to_s }
+
+    shared_examples 'group feature flags are disabled' do
+      context 'when feature flags are disabled' do
+        before do
+          stub_feature_flags(group_protected_branches: false)
+          stub_feature_flags(allow_protected_branches_for_group: false)
+        end
+
+        it 'returns an unscoped key' do
+          is_expected.to eq "cache:gitlab:protected_branch:#{entity.id}"
+        end
+      end
+    end
+
+    context 'with entity project' do
+      let(:entity) { project }
+
+      it 'returns a scoped key' do
+        is_expected.to eq "cache:gitlab:protected_branch:project:#{project.id}"
+      end
+
+      context 'when a project presenter is provided' do
+        let(:entity) { ProjectPresenter.new(project) }
+
+        it 'returns the same key as a project' do
+          is_expected.to eq "cache:gitlab:protected_branch:project:#{project.id}"
+        end
+      end
+
+      it_behaves_like 'group feature flags are disabled'
+    end
+
+    context 'with entity group' do
+      let(:entity) { group }
+
+      it 'returns a scoped key' do
+        is_expected.to eq "cache:gitlab:protected_branch:group:#{group.id}"
+      end
+
+      it_behaves_like 'group feature flags are disabled'
+    end
+
+    context 'with an unsupported entity' do
+      let(:entity) { user }
+
+      it 'returns a scoped key' do
+        is_expected.to eq "cache:gitlab:protected_branch:user:#{user.id}"
+      end
+
+      it_behaves_like 'group feature flags are disabled'
+    end
+  end
+end