diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index 6cae36c2d9ed9c387eefcd424441941e453ed553..4edd42ce4096c72013e0dbd837d732dfed7df519 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -32,6 +32,7 @@
   (:require [cheshire.core :as json]
             [clojure
              [core :as core]
+             [data :as data]
              [string :as str]]
             [clojure.data.csv :as csv]
             [clojure.tools.logging :as log]
@@ -82,7 +83,12 @@
    :tag         (s/maybe Class)  ; type annotation, e.g. ^String, to be applied. Defaults to tag based on :type
    :sensitive?  s/Bool           ; is this sensitive (never show in plaintext), like a password? (default: false)
    :internal?   s/Bool           ; should the API never return this setting? (default: false)
-   :cache?      s/Bool})         ; should the getter always fetch this value "fresh" from the DB? (default: false)
+   :cache?      s/Bool           ; should the getter always fetch this value "fresh" from the DB? (default: false)
+
+  ;; called whenever setting value changes, whether from update-setting! or a cache refresh. used to handle cases
+  ;; where a change to the cache necessitates a change to some value outside the cache, like when a change the
+  ;; `:site-locale` setting requires a call to `java.util.Locale/setDefault`
+  :on-change   (s/maybe clojure.lang.IFn)})
 
 
 (defonce ^:private registered-settings
@@ -98,6 +104,18 @@
                   (tru "Setting {0} does not exist.\nFound: {1}" k (sort (keys @registered-settings)))))))))
 
 
+(defn- call-on-change
+  "Cache watcher that applies `:on-change` callback for all settings that have changed."
+  [_key _ref old new]
+  (let [rs      @registered-settings
+        [d1 d2] (data/diff old new)]
+    (doseq [changed-setting (into (set (keys d1))
+                                  (set (keys d2)))]
+      (when-let [on-change (get-in rs [(keyword changed-setting) :on-change])]
+        (on-change (clojure.core/get old changed-setting) (clojure.core/get new changed-setting))))))
+
+(add-watch @#'cache/cache* :call-on-change call-on-change)
+
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                                      get                                                       |
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -382,6 +400,7 @@
                :description nil
                :type        setting-type
                :default     default
+               :on-change   nil
                :getter      (partial (default-getter-for-type setting-type) setting-name)
                :setter      (partial (default-setter-for-type setting-type) setting-name)
                :tag         (default-tag-for-type setting-type)
diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj
index 1c852be0b37b43a39035355e463565ccb297c42e..c73cf77e1506c97c22dadbc914043a0bb3f42cb6 100644
--- a/src/metabase/public_settings.clj
+++ b/src/metabase/public_settings.clj
@@ -73,11 +73,9 @@
   (str  (deferred-tru "The default language for this Metabase instance.")
         " "
         (deferred-tru "This only applies to emails, Pulses, etc. Users'' browsers will specify the language used in the user interface."))
-  :type    :string
-  :setter  (fn [new-value]
-             (setting/set-string! :site-locale new-value)
-             (set-locale new-value))
-  :default "en")
+  :type      :string
+  :on-change (fn [_ new-value] (when new-value (set-locale new-value)))
+  :default   "en")
 
 (defsetting admin-email
   (deferred-tru "The email address users should be referred to if they encounter a problem."))
diff --git a/test/metabase/models/setting/cache_test.clj b/test/metabase/models/setting/cache_test.clj
index 1cc922cd229fc47255d985ebe037ef1b75e2d32d..706e1b2d662035bbf36641eb66ebbb14fa39fc00 100644
--- a/test/metabase/models/setting/cache_test.clj
+++ b/test/metabase/models/setting/cache_test.clj
@@ -2,7 +2,9 @@
   (:require [clojure.core.memoize :as memoize]
             [expectations :refer [expect]]
             [honeysql.core :as hsql]
-            [metabase.db :as mdb]
+            [metabase
+             [db :as mdb]
+             [public-settings :as public-settings]]
             [metabase.models
              [setting :refer [Setting]]
              [setting-test :as setting-test]]
@@ -122,3 +124,28 @@
     ;; detect a cache out-of-date situation and flush the cache as appropriate, giving us the updated value when we
     ;; call! :wow:
     (setting-test/toucan-name)))
+
+;; sets site locale setting
+(expect
+  "fr"
+  (let [original-locale (java.util.Locale/getDefault)]
+    (try (let [new-language (do (clear-cache!)
+                                (public-settings/site-locale "en")
+                                (simulate-another-instance-updating-setting! :site-locale "fr")
+                                (flush-memoized-results-for-should-restore-cache!)
+                                (public-settings/site-locale))]
+           new-language)
+         (finally (java.util.Locale/setDefault original-locale)))))
+
+;; sets java util locale
+(expect
+  "fr"
+  (let [original-locale (java.util.Locale/getDefault)]
+    (try (let [new-language (do (clear-cache!)
+                                (public-settings/site-locale "en")
+                                (simulate-another-instance-updating-setting! :site-locale "fr")
+                                (flush-memoized-results-for-should-restore-cache!)
+                                (public-settings/site-locale)
+                                (.getLanguage (java.util.Locale/getDefault)))]
+           new-language)
+         (finally (java.util.Locale/setDefault original-locale)))))