diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index fba14f0100cd1527525e753ce40cfa10ad19cfc0..3dacd6a6224b67248e60c29767a000b75b1162c3 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -12,7 +12,6 @@ class Build < CommitStatus
     include Presentable
     include Importable
     include Gitlab::Utils::StrongMemoize
-    include Deployable
     include HasRef
 
     BuildArchivedError = Class.new(StandardError)
@@ -417,10 +416,6 @@ def environment_action
       self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
     end
 
-    def has_deployment?
-      !!self.deployment
-    end
-
     def outdated_deployment?
       success? && !deployment.try(:last?)
     end
diff --git a/app/models/concerns/deployable.rb b/app/models/concerns/deployable.rb
deleted file mode 100644
index 957b72f3721eccbd97f5fb5fd011db3bdb0ba93a..0000000000000000000000000000000000000000
--- a/app/models/concerns/deployable.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module Deployable
-  extend ActiveSupport::Concern
-
-  included do
-    after_create :create_deployment
-
-    def create_deployment
-      return unless starts_environment? && !has_deployment?
-
-      environment = project.environments.find_or_create_by(
-        name: expanded_environment_name
-      )
-
-      # If we failed to persist envirionment record by validation error, such as name with invalid character,
-      # the job will fall back to a non-environment job.
-      return unless environment.persisted?
-
-      create_deployment!(
-        cluster_id: environment.deployment_platform&.cluster_id,
-        project_id: environment.project_id,
-        environment: environment,
-        ref: ref,
-        tag: tag,
-        sha: sha,
-        user: user,
-        on_stop: on_stop)
-    end
-  end
-end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 338495ba030ee81825a38799530aaebfb2cdb051..7a5e33c61babca3e3a19eb6e3d9612859ec25bba 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -39,9 +39,18 @@ def reprocess!(build)
           .where(name: build.name)
           .update_all(retried: true)
 
-        project.builds.create!(Hash[attributes])
+        create_build!(attributes)
       end
     end
     # rubocop: enable CodeReuse/ActiveRecord
+
+    private
+
+    def create_build!(attributes)
+      build = project.builds.new(Hash[attributes])
+      build.deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment.new(build).to_resource
+      build.save!
+      build
+    end
   end
 end
diff --git a/changelogs/unreleased/deployment-iid-transaction-improvement.yml b/changelogs/unreleased/deployment-iid-transaction-improvement.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aefc3349c350144d3a0f96938f6d9194cd95a0a8
--- /dev/null
+++ b/changelogs/unreleased/deployment-iid-transaction-improvement.yml
@@ -0,0 +1,6 @@
+---
+title: Reduce lock contention of deployment creation by allocating IID outside
+  of the pipeline transaction
+merge_request: 17696
+author:
+type: performance
diff --git a/ee/spec/policies/ci/build_policy_spec.rb b/ee/spec/policies/ci/build_policy_spec.rb
index 2fc56e2e221dc70bf514198fdc63719d19b69f6d..f71d90d693a1677a651ff753d494d7f4c647f2e6 100644
--- a/ee/spec/policies/ci/build_policy_spec.rb
+++ b/ee/spec/policies/ci/build_policy_spec.rb
@@ -17,7 +17,7 @@
     it_behaves_like 'protected environments access'
 
     context 'when a pipeline has manual deployment job' do
-      let!(:build) { create(:ee_ci_build, :manual, :deploy_to_production, pipeline: pipeline) }
+      let!(:build) { create(:ee_ci_build, :with_deployment, :manual, :deploy_to_production, pipeline: pipeline) }
 
       before do
         project.add_developer(user)
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 1f6b38530690c7c1a6c963fca12b297ff8636210..fc9c540088b7adabbb9aa446b84571eb9671b97e 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -73,7 +73,9 @@ def to_resource
               if bridge?
                 ::Ci::Bridge.new(attributes)
               else
-                ::Ci::Build.new(attributes)
+                ::Ci::Build.new(attributes).tap do |job|
+                  job.deployment = Seed::Deployment.new(job).to_resource
+                end
               end
             end
           end
diff --git a/lib/gitlab/ci/pipeline/seed/deployment.rb b/lib/gitlab/ci/pipeline/seed/deployment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..238cd845a0e2900cf675b7ae0bc6daad9b13f972
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/deployment.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    module Pipeline
+      module Seed
+        class Deployment < Seed::Base
+          attr_reader :job, :environment
+
+          def initialize(job)
+            @job = job
+            @environment = Seed::Environment.new(@job)
+          end
+
+          def to_resource
+            return job.deployment if job.deployment
+            return unless job.starts_environment?
+
+            deployment = ::Deployment.new(attributes)
+            deployment.environment = environment.to_resource
+
+            # If there is a validation error on environment creation, such as
+            # the name contains invalid character, the job will fall back to a
+            # non-environment job.
+            return unless deployment.valid? && deployment.environment.valid?
+
+            deployment.cluster_id =
+              deployment.environment.deployment_platform&.cluster_id
+
+            # Allocate IID for deployments.
+            # This operation must be outside of transactions of pipeline creations.
+            deployment.ensure_project_iid!
+
+            deployment
+          end
+
+          private
+
+          def attributes
+            {
+              project: job.project,
+              user: job.user,
+              ref: job.ref,
+              tag: job.tag,
+              sha: job.sha,
+              on_stop: job.on_stop
+            }
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/pipeline/seed/environment.rb b/lib/gitlab/ci/pipeline/seed/environment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..813a9e9e399a6b0a492ffed337da24a5cc1f4c51
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/environment.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    module Pipeline
+      module Seed
+        class Environment < Seed::Base
+          attr_reader :job
+
+          def initialize(job)
+            @job = job
+          end
+
+          def to_resource
+            find_environment || ::Environment.new(attributes)
+          end
+
+          private
+
+          def find_environment
+            job.project.environments.find_by_name(expanded_environment_name)
+          end
+
+          def expanded_environment_name
+            job.expanded_environment_name
+          end
+
+          def attributes
+            {
+              project: job.project,
+              name: expanded_environment_name
+            }
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index c0f7948f96392a81a59f63eb73e83725dae373ad..0bd39d4cdcfe0a6bd8c98ed85c1c85d062912c75 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -211,6 +211,17 @@
       build.project ||= build.pipeline.project
     end
 
+    trait :with_deployment do
+      after(:build) do |build, evaluator|
+        ##
+        # Build deployment/environment relations if environment name is set
+        # to the job. If `build.deployment` has already been set, it doesn't
+        # build a new instance.
+        build.deployment =
+          Gitlab::Ci::Pipeline::Seed::Deployment.new(build).to_resource
+      end
+    end
+
     trait :tag do
       tag { true }
     end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 023d7530b4bbe858484dfb3a12690568367fa2f5..945baf47b7bcc0079efa3c42ed9c652cab515229 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -117,6 +117,24 @@
     context 'when job is not a bridge' do
       it { is_expected.to be_a(::Ci::Build) }
       it { is_expected.to be_valid }
+
+      context 'when job has environment name' do
+        let(:attributes) { { name: 'rspec', ref: 'master', environment: 'production' } }
+
+        it 'returns a job with deployment' do
+          expect(subject.deployment).not_to be_nil
+          expect(subject.deployment.deployable).to eq(subject)
+          expect(subject.deployment.environment.name).to eq('production')
+        end
+
+        context 'when the environment name is invalid' do
+          let(:attributes) { { name: 'rspec', ref: 'master', environment: '!!!' } }
+
+          it 'returns a job without deployment' do
+            expect(subject.deployment).to be_nil
+          end
+        end
+      end
     end
 
     context 'when job is a bridge' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4e63f60ea6bc1d6d0cf3e156b88d724f83c62be4
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Deployment do
+  let_it_be(:project) { create(:project) }
+  let(:job) { build(:ci_build, project: project) }
+  let(:seed) { described_class.new(job) }
+  let(:attributes) { {} }
+
+  before do
+    job.assign_attributes(**attributes)
+  end
+
+  describe '#to_resource' do
+    subject { seed.to_resource }
+
+    context 'when job has environment attribute' do
+      let(:attributes) do
+        {
+          environment: 'production',
+          options: { environment: { name: 'production' } }
+        }
+      end
+
+      it 'returns a deployment object with environment' do
+        expect(subject).to be_a(Deployment)
+        expect(subject.iid).to be_present
+        expect(subject.environment.name).to eq('production')
+        expect(subject.cluster).to be_nil
+      end
+
+      context 'when environment has deployment platform' do
+        let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+        it 'returns a deployment with cluster id' do
+          expect(subject.cluster).to eq(cluster)
+        end
+      end
+
+      context 'when environment has an invalid URL' do
+        let(:attributes) do
+          {
+            environment: '!!!',
+            options: { environment: { name: '!!!' } }
+          }
+        end
+
+        it 'returns nothing' do
+          is_expected.to be_nil
+        end
+      end
+
+      context 'when job has already deployment' do
+        let(:job) { build(:ci_build, :with_deployment, project: project, environment: 'production') }
+
+        it 'returns the persisted deployment' do
+          is_expected.to eq(job.deployment)
+        end
+      end
+    end
+
+    context 'when job has environment attribute with stop action' do
+      let(:attributes) do
+        {
+          environment: 'production',
+          options: { environment: { name: 'production', action: 'stop' } }
+        }
+      end
+
+      it 'returns nothing' do
+        is_expected.to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..92ad20f30a05e74521957f6b46a0984671d8f56e
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Environment do
+  let_it_be(:project) { create(:project) }
+  let(:job) { build(:ci_build, project: project) }
+  let(:seed) { described_class.new(job) }
+  let(:attributes) { {} }
+
+  before do
+    job.assign_attributes(**attributes)
+  end
+
+  describe '#to_resource' do
+    subject { seed.to_resource }
+
+    context 'when job has environment attribute' do
+      let(:attributes) do
+        {
+          environment: 'production',
+          options: { environment: { name: 'production' } }
+        }
+      end
+
+      it 'returns an environment object' do
+        expect(subject).to be_a(Environment)
+        expect(subject).not_to be_persisted
+        expect(subject.project).to eq(project)
+        expect(subject.name).to eq('production')
+      end
+
+      context 'when environment has already existed' do
+        let!(:environment) { create(:environment, project: project, name: 'production') }
+
+        it 'returns the existing environment object' do
+          expect(subject).to be_persisted
+          expect(subject).to eq(environment)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index bf09bf7f4b50a159f7bfa4a9c2f1572c291ff4f8..de90e4c2fbaef51023de5a4299579cc6bc60505f 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -957,7 +957,7 @@
   end
 
   describe 'state transition as a deployable' do
-    let!(:build) { create(:ci_build, :start_review_app) }
+    let!(:build) { create(:ci_build, :with_deployment, :start_review_app) }
     let(:deployment) { build.deployment }
     let(:environment) { deployment.environment }
 
@@ -1043,20 +1043,6 @@
   end
 
   describe 'deployment' do
-    describe '#has_deployment?' do
-      subject { build.has_deployment? }
-
-      context 'when build has a deployment' do
-        let!(:deployment) { create(:deployment, deployable: build) }
-
-        it { is_expected.to be_truthy }
-      end
-
-      context 'when build does not have a deployment' do
-        it { is_expected.to be_falsy }
-      end
-    end
-
     describe '#outdated_deployment?' do
       subject { build.outdated_deployment? }
 
@@ -1955,7 +1941,7 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
     end
 
     context 'when build has a start environment' do
-      let(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
+      let(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
 
       it 'does not expand environment name' do
         expect(build).not_to receive(:expanded_environment_name)
diff --git a/spec/models/concerns/deployable_spec.rb b/spec/models/concerns/deployable_spec.rb
deleted file mode 100644
index ad2c0770a2cc19f33f46311acee671c156bcb57e..0000000000000000000000000000000000000000
--- a/spec/models/concerns/deployable_spec.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Deployable do
-  describe '#create_deployment' do
-    let(:deployment) { job.deployment }
-    let(:environment) { deployment&.environment }
-
-    context 'when the deployable object will deploy to production' do
-      let!(:job) { create(:ci_build, :start_review_app) }
-
-      it 'creates a deployment and environment record' do
-        expect(deployment.project).to eq(job.project)
-        expect(deployment.ref).to eq(job.ref)
-        expect(deployment.tag).to eq(job.tag)
-        expect(deployment.sha).to eq(job.sha)
-        expect(deployment.user).to eq(job.user)
-        expect(deployment.deployable).to eq(job)
-        expect(deployment.on_stop).to eq('stop_review_app')
-        expect(environment.name).to eq('review/master')
-      end
-    end
-
-    context 'when the deployable object will deploy to a cluster' do
-      let(:project) { create(:project) }
-      let!(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
-      let!(:job) { create(:ci_build, :start_review_app, project: project) }
-
-      it 'creates a deployment with cluster association' do
-        expect(deployment.cluster).to eq(cluster)
-      end
-    end
-
-    context 'when the deployable object will stop an environment' do
-      let!(:job) { create(:ci_build, :stop_review_app) }
-
-      it 'does not create a deployment record' do
-        expect(deployment).to be_nil
-      end
-    end
-
-    context 'when the deployable object has already had a deployment' do
-      let!(:job) { create(:ci_build, :start_review_app, deployment: race_deployment) }
-      let!(:race_deployment) { create(:deployment, :success) }
-
-      it 'does not create a new deployment' do
-        expect(deployment).to eq(race_deployment)
-      end
-    end
-
-    context 'when the deployable object will not deploy' do
-      let!(:job) { create(:ci_build) }
-
-      it 'does not create a deployment and environment record' do
-        expect(deployment).to be_nil
-        expect(environment).to be_nil
-      end
-    end
-
-    context 'when environment scope contains invalid character' do
-      let(:job) do
-        create(
-          :ci_build,
-          name: 'job:deploy-to-test-site',
-          environment: '$CI_JOB_NAME',
-          options: {
-            environment: {
-              name: '$CI_JOB_NAME',
-              url: 'http://staging.example.com/$CI_JOB_NAME',
-              on_stop: 'stop_review_app'
-            }
-          })
-      end
-
-      it 'does not create a deployment and environment record' do
-        expect(deployment).to be_nil
-        expect(environment).to be_nil
-      end
-    end
-  end
-end
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index e2836420df9b7f4699803fb173d13b6e492bbd29..01d331f518bbfe0641c7393f0861cb614d0c6d74 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -95,7 +95,7 @@
   describe '.build_environments_status' do
     subject { described_class.send(:build_environments_status, merge_request, user, pipeline) }
 
-    let!(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
+    let!(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
     let(:environment) { build.deployment.environment }
     let(:user) { project.owner }
 
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index b142942a0a75b90352960bfe6d856c67a4ede777..b146c767f824b7e3c1871e74263f54eb45f36635 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2366,7 +2366,7 @@ def set_compare(merge_request)
         merge_requests_as_head_pipeline: [merge_request])
     end
 
-    let!(:job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
+    let!(:job) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline, project: project) }
 
     it 'returns environments' do
       is_expected.to eq(pipeline.environments)
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 281c7438eeead98fe534f2b3a0e7f2cdd39c7d37..b1368f7776ba968dd1384017591c42f9ffec5960 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -204,6 +204,16 @@
         expect(build).to be_retried
         expect(build.reload).to be_retried
       end
+
+      context 'when build with deployment is retried' do
+        let!(:build) do
+          create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline, stage_id: stage.id)
+        end
+
+        it 'creates a new deployment' do
+          expect { new_build }.to change { Deployment.count }.by(1)
+        end
+      end
     end
 
     context 'when user does not have ability to execute build' do
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 890fa5bc00943c111c87d26c2c876ab2290969fd..ed92625a2cce42a10c6634fcab108fa36c8f3f1d 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -121,8 +121,8 @@
         merge_requests_as_head_pipeline: [merge_request])
     end
 
-    let!(:review_job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
-    let!(:stop_review_job) { create(:ci_build, :stop_review_app, :manual, pipeline: pipeline, project: project) }
+    let!(:review_job) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline, project: project) }
+    let!(:stop_review_job) { create(:ci_build, :with_deployment, :stop_review_app, :manual, pipeline: pipeline, project: project) }
 
     before do
       review_job.deployment.success!
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb
index 7dc52f6816a8886d78565994b492fbbb7cae91f7..343dab8a9747b4b614f7b94fa54406c1c617a551 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/update_deployment_service_spec.rb
@@ -9,6 +9,7 @@
 
   let(:job) do
     create(:ci_build,
+      :with_deployment,
       ref: 'master',
       tag: false,
       environment: 'production',
@@ -114,6 +115,7 @@
     context 'when yaml environment uses $CI_COMMIT_REF_NAME' do
       let(:job) do
         create(:ci_build,
+               :with_deployment,
                ref: 'master',
                environment: 'production',
                project: project,
@@ -126,6 +128,7 @@
     context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do
       let(:job) do
         create(:ci_build,
+               :with_deployment,
                ref: 'master',
                environment: 'prod-slug',
                project: project,
@@ -138,6 +141,7 @@
     context 'when yaml environment uses yaml_variables containing symbol keys' do
       let(:job) do
         create(:ci_build,
+               :with_deployment,
                yaml_variables: [{ key: :APP_HOST, value: 'host' }],
                environment: 'production',
                project: project,
@@ -148,7 +152,7 @@
     end
 
     context 'when yaml environment does not have url' do
-      let(:job) { create(:ci_build, environment: 'staging', project: project) }
+      let(:job) { create(:ci_build, :with_deployment, environment: 'staging', project: project) }
 
       it 'returns the external_url from persisted environment' do
         is_expected.to be_nil
@@ -174,6 +178,7 @@
         context 'when job deploys to staging' do
           let(:job) do
             create(:ci_build,
+              :with_deployment,
               ref: 'master',
               tag: false,
               environment: 'staging',
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index b2817f9c14a9a63ae89249d6b546b6612dd3047c..a604359942f46becca876bd80501fc74134fd018 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -113,6 +113,7 @@ def dummy_pipeline(project)
 
   def new_dummy_job(user, project, environment)
     create(:ci_build,
+      :with_deployment,
       project: project,
       user: user,
       environment: environment,