diff --git a/app/models/concerns/semantic_versionable.rb b/app/models/concerns/semantic_versionable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..70cc1edae81f683432068a4f5cda33fc9fc55e06
--- /dev/null
+++ b/app/models/concerns/semantic_versionable.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module SemanticVersionable
+  extend ActiveSupport::Concern
+
+  included do
+    # sets the default value for require_valid_semver to false
+    self.require_valid_semver = false
+
+    validate :semver_format, if: :require_valid_semver?
+
+    private
+
+    def semver_format
+      return unless [semver_major, semver_minor, semver_patch].any?(&:nil?)
+
+      errors.add(:base, _('must follow semantic version'))
+    end
+
+    def require_valid_semver?
+      self.class.require_valid_semver
+    end
+  end
+
+  class_methods do
+    attr_accessor :require_valid_semver
+
+    def semver_method(name)
+      define_method(name) do
+        return if [semver_major, semver_minor, semver_patch].any?(&:nil?)
+
+        Packages::SemVer.new(semver_major, semver_minor, semver_patch, semver_prerelease)
+      end
+
+      define_method("#{name}=") do |version|
+        parsed = Packages::SemVer.parse(version)
+
+        return if parsed.nil?
+
+        self.semver_major = parsed.major
+        self.semver_minor = parsed.minor
+        self.semver_patch = parsed.patch
+        self.semver_prerelease = parsed.prerelease
+      end
+    end
+
+    def validate_semver
+      self.require_valid_semver = true
+    end
+  end
+end
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
index 037cf58f532c00770a0067e6f389f69481180ccb..a5f45d542120593e161de4c569aa4a998b4e83ee 100644
--- a/app/models/ml/model_version.rb
+++ b/app/models/ml/model_version.rb
@@ -4,6 +4,9 @@ module Ml
   class ModelVersion < ApplicationRecord
     include Presentable
     include Sortable
+    include SemanticVersionable
+
+    semver_method :semver
 
     validates :project, :model, presence: true
 
diff --git a/db/migrate/20240118191655_add_version_parts_to_model_versions.rb b/db/migrate/20240118191655_add_version_parts_to_model_versions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9758028b4b8e82871998154d9d14af374347b20d
--- /dev/null
+++ b/db/migrate/20240118191655_add_version_parts_to_model_versions.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddVersionPartsToModelVersions < Gitlab::Database::Migration[2.2]
+  disable_ddl_transaction!
+  milestone '16.9'
+
+  def up
+    add_column :ml_model_versions, :semver_major, :integer
+    add_column :ml_model_versions, :semver_minor, :integer
+    add_column :ml_model_versions, :semver_patch, :integer
+    add_column :ml_model_versions, :semver_prerelease, :text # rubocop:disable Migration/AddLimitToTextColumns -- limit is added in 20240118191656_add_text_limit_to_ml_model_versions.rb
+  end
+
+  def down
+    remove_column :ml_model_versions, :semver_major, :integer
+    remove_column :ml_model_versions, :semver_minor, :integer
+    remove_column :ml_model_versions, :semver_patch, :integer
+    remove_column :ml_model_versions, :semver_prerelease, :text
+  end
+end
diff --git a/db/migrate/20240118191656_add_text_limit_to_ml_model_versions.rb b/db/migrate/20240118191656_add_text_limit_to_ml_model_versions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4d110ee3a2add93d9ec077c9a31133bb7989f792
--- /dev/null
+++ b/db/migrate/20240118191656_add_text_limit_to_ml_model_versions.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddTextLimitToMlModelVersions < Gitlab::Database::Migration[2.2]
+  disable_ddl_transaction!
+  milestone '16.9'
+
+  def up
+    add_text_limit :ml_model_versions, :semver_prerelease, 255
+  end
+
+  def down
+    remove_text_limit :ml_model_versions, :semver_prerelease
+  end
+end
diff --git a/db/schema_migrations/20240118191655 b/db/schema_migrations/20240118191655
new file mode 100644
index 0000000000000000000000000000000000000000..b369b05c4e99c1cbf7adb1a4d0d5d09caaecdd00
--- /dev/null
+++ b/db/schema_migrations/20240118191655
@@ -0,0 +1 @@
+4630431cdbbb25db8f507f2c81f0a781bbeaa268e189e94090afda279a7bb4fa
\ No newline at end of file
diff --git a/db/schema_migrations/20240118191656 b/db/schema_migrations/20240118191656
new file mode 100644
index 0000000000000000000000000000000000000000..c41b7aeb545c3af4441fd6dd0b8d42e28036e283
--- /dev/null
+++ b/db/schema_migrations/20240118191656
@@ -0,0 +1 @@
+a7dc207f05ab9ed4f8190dcfdb5ed94c2fbcf2d057cd8df2f1c37a67cb3895fc
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index df072ec496d75e023ff0c9f808c20d6ab5a8dab8..b01eff863d1c7cadb1a8c0efd7a8a20e6f5738bc 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -19893,6 +19893,11 @@ CREATE TABLE ml_model_versions (
     package_id bigint,
     version text NOT NULL,
     description text,
+    semver_major integer,
+    semver_minor integer,
+    semver_patch integer,
+    semver_prerelease text,
+    CONSTRAINT check_246f5048b5 CHECK ((char_length(semver_prerelease) <= 255)),
     CONSTRAINT check_28b2d892c8 CHECK ((char_length(version) <= 255)),
     CONSTRAINT check_caff7d000b CHECK ((char_length(description) <= 500))
 );
diff --git a/doc/development/semver.md b/doc/development/semver.md
new file mode 100644
index 0000000000000000000000000000000000000000..4a591e46cdb8578ffaacbef4e1d67e8a97e91da9
--- /dev/null
+++ b/doc/development/semver.md
@@ -0,0 +1,61 @@
+---
+stage: none
+group: unassigned
+info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
+---
+
+# Semantic Versioning of Database Records
+
+[Semantic Versioning](https://semver.org/) of records in a database introduces complexity when it comes to filtering and sorting. Since the database doesn't natively understand semantic versions it is necessary to extract the version components to separate columns in the database. The [SemanticVersionable](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142228) module was introduced to make this process easier.
+
+## Setup Instructions
+
+In order to use SemanticVersionable you must first create a database migration to add the required columns to your table. The required columns are `semver_major`, `semver_minor`, `semver_patch`, and `semver_prerelease`. An example migration would look like this:
+
+```ruby
+class AddVersionPartsToModelVersions < Gitlab::Database::Migration[2.2]
+  disable_ddl_transaction!
+  milestone '16.9'
+
+  def up
+    add_column :ml_model_versions, :semver_major, :integer
+    add_column :ml_model_versions, :semver_minor, :integer
+    add_column :ml_model_versions, :semver_patch, :integer
+    add_column :ml_model_versions, :semver_prerelease, :text
+  end
+
+  def down
+    remove_column :ml_model_versions, :semver_major, :integer
+    remove_column :ml_model_versions, :semver_minor, :integer
+    remove_column :ml_model_versions, :semver_patch, :integer
+    remove_column :ml_model_versions, :semver_prerelease, :text
+  end
+end
+```
+
+Once the columns are in the database, you can enable the module by including it in your model and configuring it by setting the name of the semver accessor method. For example:
+
+```ruby
+module Ml
+  class ModelVersion < ApplicationRecord
+    include SemanticVersionable
+
+    semver_method :semver
+
+  ...
+  end
+end
+```
+
+The module has two configuation options:
+
+- `semver_method` specifies the name of accessor method that will be added to the objecct
+- `validate_semver` is `true` or `false` (defaults to `false`). If true it will throw a validation error if the provided semver string is not in a valid semver format.
+
+Depending on the use case, you may want to disable the validation during the rollout or backfill process.
+
+Please refer to [this MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142228) as a reference.
+
+## Filtering and Searching
+
+TBD
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 43d3aa783d66eeff6afdb2e4ec141ecc5fd5d424..625ed29bfc09e866544219cd85e8123d5305efc0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -59440,6 +59440,9 @@ msgstr ""
 msgid "must contain only a mastodon username."
 msgstr ""
 
+msgid "must follow semantic version"
+msgstr ""
+
 msgid "must have a repository"
 msgstr ""
 
diff --git a/spec/models/concerns/semantic_versionable_spec.rb b/spec/models/concerns/semantic_versionable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..500b4564fbeff1a4a291e46ae992814288aa6027
--- /dev/null
+++ b/spec/models/concerns/semantic_versionable_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SemanticVersionable, feature_category: :mlops do
+  using RSpec::Parameterized::TableSyntax
+
+  let(:model_class) do
+    Class.new(ActiveRecord::Base) do
+      include SemanticVersionable
+      semver_method :semver
+
+      # we need a table for the dummy class to operate
+      self.table_name = 'ml_model_versions'
+
+      def self.name
+        'Ml::ModelVersion'
+      end
+
+      attr_accessor :major, :minor, :patch, :prerelease
+    end
+  end
+
+  describe '.semver_method' do
+    describe 'setter method' do
+      let(:model_instance) { model_class.new(semver: semver) }
+
+      where(:semver, :major, :minor, :patch, :prerelease) do
+        '1'             | nil | nil | nil | nil
+        '1.2'           | nil | nil | nil | nil
+        '1.2.3'         | 1   | 2   | 3   | nil
+        '1.2.3-beta'    | 1   | 2   | 3   | 'beta'
+        '1.2.3.beta'    | nil | nil | nil | nil
+      end
+      with_them do
+        it do
+          expect(model_instance.semver_major).to be major
+          expect(model_instance.semver_minor).to be minor
+          expect(model_instance.semver_patch).to be patch
+          expect(model_instance.semver_prerelease).to eq prerelease
+        end
+      end
+    end
+
+    describe 'getter method' do
+      let(:model_instance) { model_class.new(semver: semver_input) }
+
+      where(:semver_input, :semver_value) do
+        '1'             | ''
+        '1.2'           | ''
+        '1.2.3'         | '1.2.3'
+        '1.2.3-beta'    | '1.2.3-beta'
+        '1.2.3.beta'    | ''
+      end
+      with_them do
+        it do
+          expect(model_instance.semver.to_s).to eq semver_value
+        end
+      end
+    end
+  end
+
+  describe '.validate_semver' do
+    it 'sets require_valid_semver to true' do
+      model_class.validate_semver
+      expect(model_class.require_valid_semver).to be true
+    end
+
+    it 'defaults to false' do
+      expect(model_class.require_valid_semver).to be false
+    end
+  end
+
+  describe 'semver validation' do
+    let(:model_instance) { model_class.new }
+
+    it 'validates when a valid semver is supplied' do
+      model_class.validate_semver
+      model_instance.semver = '1.2.3'
+      expect(model_instance.valid?).to be true
+    end
+
+    it 'fails validation when an invalid version is supplied' do
+      model_class.validate_semver
+      model_instance.semver = '123'
+      expect(model_instance.valid?).to be false
+      expect(model_instance.errors.count).to be(1)
+      expect(model_instance.errors.first.attribute).to eq(:base)
+      expect(model_instance.errors.first.message).to eq('must follow semantic version')
+    end
+
+    it 'does not validate if the validation is not enabled' do
+      model_instance.semver = '123'
+      expect(model_instance.valid?).to be true
+    end
+  end
+end
diff --git a/spec/models/ml/model_version_spec.rb b/spec/models/ml/model_version_spec.rb
index 1c520e29b52d2dfc556d97140a971e3399ab7795..6f5e250688d3e79e676c7cd6118268c56bcdd226 100644
--- a/spec/models/ml/model_version_spec.rb
+++ b/spec/models/ml/model_version_spec.rb
@@ -270,4 +270,24 @@
       end
     end
   end
+
+  context 'when parsing semver components' do
+    let(:model_version) { build(:ml_model_versions, model: model1, semver: semver, project: base_project) }
+
+    where(:semver, :valid, :major, :minor, :patch, :prerelease) do
+      '1'             | false | nil | nil | nil | nil
+      '1.2'           | false | nil | nil | nil | nil
+      '1.2.3'         | true  | 1   | 2   | 3   | nil
+      '1.2.3-beta'    | true  | 1   | 2   | 3   | 'beta'
+      '1.2.3.beta'    | false | nil | nil | nil | nil
+    end
+    with_them do
+      it do
+        expect(model_version.semver_major).to be major
+        expect(model_version.semver_minor).to be minor
+        expect(model_version.semver_patch).to be patch
+        expect(model_version.semver_prerelease).to eq prerelease
+      end
+    end
+  end
 end