diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b32459fdabf2ca8e2637bc428373f65c0d4920bf
--- /dev/null
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -0,0 +1,54 @@
+module CacheableAttributes
+  extend ActiveSupport::Concern
+
+  included do
+    after_commit { self.class.expire }
+  end
+
+  class_methods do
+    # Can be overriden
+    def current_without_cache
+      last
+    end
+
+    def cache_key
+      "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json".freeze
+    end
+
+    def defaults
+      {}
+    end
+
+    def build_from_defaults(attributes = {})
+      new(defaults.merge(attributes))
+    end
+
+    def cached
+      json_attributes = Rails.cache.read(cache_key)
+      return nil unless json_attributes.present?
+
+      build_from_defaults(JSON.parse(json_attributes))
+    end
+
+    def current
+      cached_record = cached
+      return cached_record if cached_record.present?
+
+      current_without_cache.tap { |current_record| current_record&.cache! }
+    rescue
+      # Fall back to an uncached value if there are any problems (e.g. Redis down)
+      current_without_cache
+    end
+
+    def expire
+      Rails.cache.delete(cache_key)
+    rescue
+      # Gracefully handle when Redis is not available. For example,
+      # omnibus may fail here during gitlab:assets:compile.
+    end
+  end
+
+  def cache!
+    Rails.cache.write(self.class.cache_key, attributes.to_json)
+  end
+end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index c5498d0da1a50471c19112fd5b53633145d41ffb..31119471b8e24dcf0ce66df7540c5771ba687591 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -9,6 +9,10 @@ module Gitlab
     Settings
   end
 
+  def self.migrations_hash
+    @_migrations_hash ||= Digest::MD5.hexdigest(ActiveRecord::Migrator.get_all_versions.to_s)
+  end
+
   COM_URL = 'https://gitlab.com'.freeze
   APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
   SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..49e4b23ebc7e5c14499059c5e873c4c3794d5d31
--- /dev/null
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -0,0 +1,153 @@
+require 'spec_helper'
+
+describe CacheableAttributes do
+  let(:minimal_test_class) do
+    Class.new do
+      include ActiveModel::Model
+      extend ActiveModel::Callbacks
+      define_model_callbacks :commit
+      include CacheableAttributes
+
+      def self.name
+        'TestClass'
+      end
+
+      def self.first
+        @_first ||= new('foo' => 'a')
+      end
+
+      def self.last
+        @_last ||= new('foo' => 'a', 'bar' => 'b')
+      end
+
+      attr_accessor :attributes
+
+      def initialize(attrs = {})
+        @attributes = attrs
+      end
+    end
+  end
+
+  shared_context 'with defaults' do
+    before do
+      minimal_test_class.define_singleton_method(:defaults) do
+        { foo: 'a', bar: 'b', baz: 'c' }
+      end
+    end
+  end
+
+  describe '.current_without_cache' do
+    it 'defaults to last' do
+      expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last)
+    end
+
+    it 'can be overriden' do
+      minimal_test_class.define_singleton_method(:current_without_cache) do
+        first
+      end
+
+      expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.first)
+    end
+  end
+
+  describe '.cache_key' do
+    it 'excludes cache attributes' do
+      expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json")
+    end
+  end
+
+  describe '.defaults' do
+    it 'defaults to {}' do
+      expect(minimal_test_class.defaults).to eq({})
+    end
+
+    context 'with defaults defined' do
+      include_context 'with defaults'
+
+      it 'can be overriden' do
+        expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' })
+      end
+    end
+  end
+
+  describe '.build_from_defaults' do
+    include_context 'with defaults'
+
+    context 'without any attributes given' do
+      it 'intializes a new object with the defaults' do
+        expect(minimal_test_class.build_from_defaults).not_to be_persisted
+      end
+    end
+
+    context 'without attributes given' do
+      it 'intializes a new object with the given attributes merged into the defaults' do
+        expect(minimal_test_class.build_from_defaults(foo: 'd').attributes[:foo]).to eq('d')
+      end
+    end
+  end
+
+  describe '.current', :use_clean_rails_memory_store_caching do
+    context 'redis unavailable' do
+      it 'returns an uncached record' do
+        allow(minimal_test_class).to receive(:last).and_return(:last)
+        expect(Rails.cache).to receive(:read).and_raise(Redis::BaseError)
+
+        expect(minimal_test_class.current).to eq(:last)
+      end
+    end
+
+    context 'when a record is not yet present' do
+      it 'does not cache nil object' do
+        # when missing settings a nil object is returned, but not cached
+        allow(minimal_test_class).to receive(:last).twice.and_return(nil)
+
+        expect(minimal_test_class.current).to be_nil
+        expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(false)
+      end
+
+      it 'cache non-nil object' do
+        # when the settings are set the method returns a valid object
+        allow(minimal_test_class).to receive(:last).and_call_original
+
+        expect(minimal_test_class.current).to eq(minimal_test_class.last)
+        expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(true)
+
+        # subsequent calls retrieve the record from the cache
+        last_record = minimal_test_class.last
+        expect(minimal_test_class).not_to receive(:last)
+        expect(minimal_test_class.current.attributes).to eq(last_record.attributes)
+      end
+    end
+  end
+
+  describe '.cached', :use_clean_rails_memory_store_caching do
+    context 'when cache is cold' do
+      it 'returns nil' do
+        expect(minimal_test_class.cached).to be_nil
+      end
+    end
+
+    context 'when cached settings do not include the latest defaults' do
+      before do
+        Rails.cache.write(minimal_test_class.cache_key, { bar: 'b', baz: 'c' }.to_json)
+        minimal_test_class.define_singleton_method(:defaults) do
+          { foo: 'a', bar: 'b', baz: 'c' }
+        end
+      end
+
+      it 'includes attributes from defaults' do
+        expect(minimal_test_class.cached.attributes[:foo]).to eq(minimal_test_class.defaults[:foo])
+      end
+    end
+  end
+
+  describe '#cache!', :use_clean_rails_memory_store_caching do
+    let(:appearance_record) { create(:appearance) }
+
+    it 'caches the attributes' do
+      appearance_record.cache!
+
+      expect(Rails.cache.read(Appearance.cache_key)).to eq(appearance_record.attributes.to_json)
+    end
+  end
+end