diff --git a/app/models/import/source_user_placeholder_reference.rb b/app/models/import/source_user_placeholder_reference.rb
new file mode 100644
index 0000000000000000000000000000000000000000..66c6647e3d1386f1b793f4828889626c8740a901
--- /dev/null
+++ b/app/models/import/source_user_placeholder_reference.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Import
+  class SourceUserPlaceholderReference < ApplicationRecord
+    self.table_name = 'import_source_user_placeholder_references'
+
+    belongs_to :source_user, class_name: 'Import::SourceUser'
+    belongs_to :namespace
+
+    validates :model, :namespace_id, :source_user_id, :user_reference_column, presence: true
+    validates :numeric_key, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
+    validates :composite_key,
+      json_schema: { filename: 'import_source_user_placeholder_reference_composite_key' },
+      allow_nil: true
+    validate :validate_numeric_or_composite_key_present
+
+    attribute :composite_key, :ind_jsonb
+
+    private
+
+    def validate_numeric_or_composite_key_present
+      return if numeric_key.present? ^ composite_key.present?
+
+      errors.add(:base, :blank, message: 'numeric_key or composite_key must be present')
+    end
+  end
+end
diff --git a/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json b/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json
new file mode 100644
index 0000000000000000000000000000000000000000..ccfbe0ec527bc7c5c18c20601abb7d002b190264
--- /dev/null
+++ b/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json
@@ -0,0 +1,19 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "description": "Stores composite_key data for imported records that are mapped to placeholder users",
+  "type": "object",
+  "minProperties": 1,
+  "patternProperties": {
+    ".*": {
+      "oneOf": [
+        {
+          "type": "string",
+          "pattern": "^[0-9]+$"
+        },
+        {
+          "type": "integer"
+        }
+      ]
+    }
+  }
+}
diff --git a/db/docs/import_source_user_placeholder_references.yml b/db/docs/import_source_user_placeholder_references.yml
new file mode 100644
index 0000000000000000000000000000000000000000..87e1f72fc44ec82b800c7eb10708f2a3fc8b789c
--- /dev/null
+++ b/db/docs/import_source_user_placeholder_references.yml
@@ -0,0 +1,14 @@
+---
+table_name: import_source_user_placeholder_references
+classes:
+- Import::SourceUserPlaceholderReference
+feature_categories:
+- importers
+description: Used to map placeholder user references from imported data to real users
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156241
+milestone: '17.2'
+gitlab_schema: gitlab_main_cell
+allow_cross_foreign_keys:
+- gitlab_main_clusterwide
+sharding_key:
+  namespace_id: namespaces
diff --git a/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb b/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0364d6322006ff0febefc497e41b9e627c5f7e31
--- /dev/null
+++ b/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class CreateImportSourceUserPlaceholderReference < Gitlab::Database::Migration[2.2]
+  milestone '17.2'
+
+  enable_lock_retries!
+
+  INDEX_NAME = 'index_import_source_user_placeholder_references_on_source_user_'
+
+  def change
+    create_table :import_source_user_placeholder_references do |t|
+      t.references :source_user,
+        index: { name: INDEX_NAME },
+        null: false,
+        foreign_key: { to_table: :import_source_users, on_delete: :cascade }
+      t.references :namespace, null: false, index: true, foreign_key: { on_delete: :cascade }
+      t.bigint :numeric_key, null: true
+      t.datetime_with_timezone :created_at, null: false
+      t.text :model, limit: 150, null: false
+      t.text :user_reference_column, limit: 50, null: false
+      t.jsonb :composite_key, null: true
+    end
+  end
+end
diff --git a/db/schema_migrations/20240612034702 b/db/schema_migrations/20240612034702
new file mode 100644
index 0000000000000000000000000000000000000000..4c11c17c15d8fef5a4c47fff54bf7a3728030c8f
--- /dev/null
+++ b/db/schema_migrations/20240612034702
@@ -0,0 +1 @@
+91e467973c28e98ed562c70ae108f7b5cb1ed0353e3ce0c8b13f052077c75d5a
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6688cf76c18065d4a40852d01e51395c87671c98..60ad4eb31d5d73a59786e47fddfa00a0b536b7de 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11167,6 +11167,28 @@ CREATE SEQUENCE import_failures_id_seq
 
 ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id;
 
+CREATE TABLE import_source_user_placeholder_references (
+    id bigint NOT NULL,
+    source_user_id bigint NOT NULL,
+    namespace_id bigint NOT NULL,
+    numeric_key bigint,
+    created_at timestamp with time zone NOT NULL,
+    model text NOT NULL,
+    user_reference_column text NOT NULL,
+    composite_key jsonb,
+    CONSTRAINT check_782140eb9d CHECK ((char_length(user_reference_column) <= 50)),
+    CONSTRAINT check_d17bd9dd4d CHECK ((char_length(model) <= 150))
+);
+
+CREATE SEQUENCE import_source_user_placeholder_references_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE import_source_user_placeholder_references_id_seq OWNED BY import_source_user_placeholder_references.id;
+
 CREATE TABLE import_source_users (
     id bigint NOT NULL,
     placeholder_user_id bigint,
@@ -20801,6 +20823,8 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo
 
 ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass);
 
+ALTER TABLE ONLY import_source_user_placeholder_references ALTER COLUMN id SET DEFAULT nextval('import_source_user_placeholder_references_id_seq'::regclass);
+
 ALTER TABLE ONLY import_source_users ALTER COLUMN id SET DEFAULT nextval('import_source_users_id_seq'::regclass);
 
 ALTER TABLE ONLY incident_management_escalation_policies ALTER COLUMN id SET DEFAULT nextval('incident_management_escalation_policies_id_seq'::regclass);
@@ -22987,6 +23011,9 @@ ALTER TABLE ONLY import_export_uploads
 ALTER TABLE ONLY import_failures
     ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id);
 
+ALTER TABLE ONLY import_source_user_placeholder_references
+    ADD CONSTRAINT import_source_user_placeholder_references_pkey PRIMARY KEY (id);
+
 ALTER TABLE ONLY import_source_users
     ADD CONSTRAINT import_source_users_pkey PRIMARY KEY (id);
 
@@ -27322,6 +27349,10 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI
 
 CREATE INDEX index_import_failures_on_user_id_not_null ON import_failures USING btree (user_id) WHERE (user_id IS NOT NULL);
 
+CREATE INDEX index_import_source_user_placeholder_references_on_namespace_id ON import_source_user_placeholder_references USING btree (namespace_id);
+
+CREATE INDEX index_import_source_user_placeholder_references_on_source_user_ ON import_source_user_placeholder_references USING btree (source_user_id);
+
 CREATE INDEX index_import_source_users_on_namespace_id ON import_source_users USING btree (namespace_id);
 
 CREATE INDEX index_import_source_users_on_placeholder_user_id ON import_source_users USING btree (placeholder_user_id);
@@ -33352,6 +33383,9 @@ ALTER TABLE ONLY namespaces_storage_limit_exclusions
 ALTER TABLE ONLY users_security_dashboard_projects
     ADD CONSTRAINT fk_rails_150cd5682c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY import_source_user_placeholder_references
+    ADD CONSTRAINT fk_rails_158995b934 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY import_source_users
     ADD CONSTRAINT fk_rails_167f82fd95 FOREIGN KEY (reassign_to_user_id) REFERENCES users(id) ON DELETE SET NULL;
 
@@ -34720,6 +34754,9 @@ ALTER TABLE ONLY upload_states
 ALTER TABLE ONLY epic_metrics
     ADD CONSTRAINT fk_rails_d071904753 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY import_source_user_placeholder_references
+    ADD CONSTRAINT fk_rails_d0b75c434e FOREIGN KEY (source_user_id) REFERENCES import_source_users(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY subscriptions
     ADD CONSTRAINT fk_rails_d0c8bda804 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
 
diff --git a/spec/factories/import_source_user_placeholder_references.rb b/spec/factories/import_source_user_placeholder_references.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7aa19ad0a5a69eacbe4a81affed69bd6086518fb
--- /dev/null
+++ b/spec/factories/import_source_user_placeholder_references.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :import_source_user_placeholder_reference, class: 'Import::SourceUserPlaceholderReference' do
+    source_user factory: :import_source_user
+    namespace
+    model { 'Note' }
+    user_reference_column { 'author_id' }
+    numeric_key { 1 }
+  end
+end
diff --git a/spec/models/import/source_user_placeholder_reference_spec.rb b/spec/models/import/source_user_placeholder_reference_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e441e065f59d4f97fd0bc73a2517eabeb4c16195
--- /dev/null
+++ b/spec/models/import/source_user_placeholder_reference_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::SourceUserPlaceholderReference, feature_category: :importers do
+  describe 'associations' do
+    it { is_expected.to belong_to(:source_user).class_name('Import::SourceUser') }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:user_reference_column) }
+    it { is_expected.to validate_presence_of(:model) }
+    it { is_expected.to validate_presence_of(:namespace_id) }
+    it { is_expected.to validate_presence_of(:source_user_id) }
+    it { is_expected.to validate_numericality_of(:numeric_key).only_integer.is_greater_than(0) }
+    it { expect(described_class).to validate_jsonb_schema(['composite_key']) }
+    it { is_expected.to allow_value({ id: 1 }).for(:composite_key) }
+    it { is_expected.to allow_value({ id: '1' }).for(:composite_key) }
+    it { is_expected.to allow_value({ foo: '1', bar: 2 }).for(:composite_key) }
+    it { is_expected.not_to allow_value({}).for(:composite_key) }
+    it { is_expected.not_to allow_value({ id: 'foo' }).for(:composite_key) }
+    it { is_expected.not_to allow_value(1).for(:composite_key) }
+
+    describe '#validate_numeric_or_composite_key_present' do
+      def validation_errors(...)
+        described_class.new(...).tap(&:validate)
+          .errors
+          .where(:base)
+      end
+
+      it 'must have numeric_key or composite_key present', :aggregate_failures do
+        expect(validation_errors).to be_present
+        expect(validation_errors(numeric_key: 1)).to be_blank
+        expect(validation_errors(composite_key: { id: 1 })).to be_blank
+        expect(validation_errors(numeric_key: 1, composite_key: { id: 1 })).to be_present
+      end
+    end
+  end
+
+  it 'is destroyed when source user is destroyed' do
+    reference = create(:import_source_user_placeholder_reference)
+
+    expect { reference.source_user.destroy! }.to change { described_class.count }.by(-1)
+  end
+end