diff --git a/app/models/design_management/design_collection.rb b/app/models/design_management/design_collection.rb
index 96d5f4c2419fc36985e3a126804785df73f2e74b..c48b36588c9d1a64defc2bf1c8a1f48a91140e39 100644
--- a/app/models/design_management/design_collection.rb
+++ b/app/models/design_management/design_collection.rb
@@ -6,8 +6,34 @@ class DesignCollection
 
     delegate :designs, :project, to: :issue
 
+    state_machine :copy_state, initial: :ready, namespace: :copy do
+      after_transition any => any, do: :update_stored_copy_state!
+
+      event :start do
+        transition ready: :in_progress
+      end
+
+      event :end do
+        transition in_progress: :ready
+      end
+
+      event :error do
+        transition in_progress: :error
+      end
+
+      event :reset do
+        transition any => :ready
+      end
+    end
+
     def initialize(issue)
+      super() # Necessary to initialize state_machine
+
       @issue = issue
+
+      if stored_copy_state = get_stored_copy_state
+        @copy_state = stored_copy_state
+      end
     end
 
     def ==(other)
@@ -30,5 +56,39 @@ def repository
     def designs_by_filename(filenames)
       designs.current.where(filename: filenames)
     end
+
+    private
+
+    def update_stored_copy_state!
+      # As "ready" is the initial copy state we can clear the cached value
+      # rather than persist it.
+      if copy_ready?
+        unset_store_copy_state!
+      else
+        set_stored_copy_state!
+      end
+    end
+
+    def copy_state_cache_key
+      "DesignCollection/copy_state/issue=#{issue.id}"
+    end
+
+    def get_stored_copy_state
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.get(copy_state_cache_key)
+      end
+    end
+
+    def set_stored_copy_state!
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.set(copy_state_cache_key, copy_state)
+      end
+    end
+
+    def unset_store_copy_state!
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.del(copy_state_cache_key)
+      end
+    end
   end
 end
diff --git a/spec/models/design_management/design_collection_spec.rb b/spec/models/design_management/design_collection_spec.rb
index 33f9d3b988f08c3b94f523c7bedbe450a328402f..8575cc80b5b5df90f4f81d86a56ba9502442a87c 100644
--- a/spec/models/design_management/design_collection_spec.rb
+++ b/spec/models/design_management/design_collection_spec.rb
@@ -3,8 +3,9 @@
 
 RSpec.describe DesignManagement::DesignCollection do
   include DesignManagementTestHelpers
+  using RSpec::Parameterized::TableSyntax
 
-  let_it_be(:issue, reload: true) { create(:issue) }
+  let_it_be(:issue, refind: true) { create(:issue) }
 
   subject(:collection) { described_class.new(issue) }
 
@@ -45,6 +46,61 @@
     end
   end
 
+  describe "#copy_state", :clean_gitlab_redis_shared_state do
+    it "defaults to ready" do
+      expect(collection).to be_copy_ready
+    end
+
+    it "persists its state changes between initializations" do
+      collection.start_copy!
+
+      expect(described_class.new(issue)).to be_copy_in_progress
+    end
+
+    where(:state, :can_start, :can_end, :can_error, :can_reset) do
+      "ready"       | true  | false | true  | true
+      "in_progress" | false | true  | true  | true
+      "error"       | false | false | false | true
+    end
+
+    with_them do
+      it "maintains state machine transition rules", :aggregate_failures do
+        collection.copy_state = state
+
+        expect(collection.can_start_copy?).to eq(can_start)
+        expect(collection.can_end_copy?).to eq(can_end)
+      end
+    end
+
+    describe "clearing the redis cached state when state changes back to ready" do
+      def redis_copy_state
+        Gitlab::Redis::SharedState.with do |redis|
+          redis.get(collection.send(:copy_state_cache_key))
+        end
+      end
+
+      def fire_state_events(*events)
+        events.each do |event|
+          collection.fire_copy_state_event(event)
+        end
+      end
+
+      it "clears the cached state on end_copy!", :aggregate_failures do
+        fire_state_events(:start)
+
+        expect { collection.end_copy! }.to change { redis_copy_state }.from("in_progress").to(nil)
+        expect(collection).to be_copy_ready
+      end
+
+      it "clears the cached state on reset_copy!", :aggregate_failures do
+        fire_state_events(:start, :error)
+
+        expect { collection.reset_copy! }.to change { redis_copy_state }.from("error").to(nil)
+        expect(collection).to be_copy_ready
+      end
+    end
+  end
+
   describe "#versions" do
     it "includes versions for all designs" do
       version_1 = create(:design_version)