This is an automated email from the ASF dual-hosted git repository.

emaynard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new b93e97b9c Introduce behavior-change configs as an alternative to 
feature configs (#1124)
b93e97b9c is described below

commit b93e97b9c5c97cb813696df50aee7550f21b155c
Author: Eric Maynard <[email protected]>
AuthorDate: Wed Mar 12 10:50:06 2025 -0700

    Introduce behavior-change configs as an alternative to feature configs 
(#1124)
    
    * builds
    
    * builds, tests fail
    
    * autolint
    
    * some fixes
    
    * autolint
    
    * style
    
    * working with duplicated code
    
    * autolint
    
    * working with duplicated code
    
    * revert
    
    * autolint
    
    * autolint
    
    * add basic test
    
    * autolint
    
    * better comment
    
    * autolint
---
 .../PolarisEclipseLinkMetaStoreManagerTest.java    |   2 +-
 .../it/test/PolarisApplicationIntegrationTest.java |   5 +-
 .../PolarisManagementServiceIntegrationTest.java   |   5 +-
 .../it/test/PolarisRestCatalogIntegrationTest.java |  11 +-
 .../PolarisRestCatalogViewIntegrationBase.java     |  10 +-
 .../it/test/PolarisSparkIntegrationTest.java       |   5 +-
 .../apache/polaris/core/PolarisCallContext.java    |   1 +
 .../polaris/core/auth/PolarisAuthorizerImpl.java   |   6 +-
 .../core/config/BehaviorChangeConfiguration.java   |  46 ++++++
 .../FeatureConfiguration.java}                     | 175 +++++----------------
 .../polaris/core/config/PolarisConfiguration.java  | 141 +++++++++++++++++
 .../{ => config}/PolarisConfigurationStore.java    |   3 +-
 .../storage/PolarisStorageConfigurationInfo.java   |   4 +-
 .../aws/AwsCredentialsStorageIntegration.java      |   4 +-
 .../azure/AzureCredentialsStorageIntegration.java  |   4 +-
 .../core/storage/cache/StorageCredentialCache.java |  11 +-
 ...TreeMapAtomicOperationMetaStoreManagerTest.java |   2 +-
 .../PolarisTreeMapMetaStoreManagerTest.java        |   2 +-
 .../storage/InMemoryStorageIntegrationTest.java    |   2 +-
 .../storage/PolarisConfigurationStoreTest.java     |  53 ++++++-
 .../polaris/admintool/config/QuarkusProducers.java |   2 +-
 .../QuarkusBehaviorChangesConfiguration.java       |  91 +++++++++++
 .../config/QuarkusFeaturesConfiguration.java       |   9 +-
 .../service/quarkus/config/QuarkusProducers.java   |   2 +-
 .../quarkus/admin/PolarisAuthzTestBase.java        |   6 +-
 .../quarkus/admin/PolarisOverlappingTableTest.java |   4 +-
 .../quarkus/catalog/BasePolarisCatalogTest.java    |  34 ++--
 .../catalog/BasePolarisCatalogViewTest.java        |   8 +-
 .../PolarisCatalogHandlerWrapperAuthzTest.java     |   2 +-
 .../quarkus/test/PolarisIntegrationTestHelper.java |   2 +-
 .../polaris/service/admin/PolarisAdminService.java |   6 +-
 .../polaris/service/admin/PolarisServiceImpl.java  |   4 +-
 .../service/catalog/BasePolarisCatalog.java        |  52 +++---
 .../service/catalog/IcebergCatalogAdapter.java     |   2 +-
 .../catalog/PolarisCatalogHandlerWrapper.java      |  10 +-
 .../service/catalog/io/DefaultFileIOFactory.java   |   2 +-
 .../polaris/service/catalog/io/FileIOUtil.java     |   8 +-
 .../catalog/io/WasbTranslatingFileIOFactory.java   |   2 +-
 .../service/config/DefaultConfigurationStore.java  |   2 +-
 .../context/DefaultCallContextResolver.java        |   2 +-
 .../org/apache/polaris/service/TestServices.java   |   2 +-
 .../service/catalog/io/MeasuredFileIOFactory.java  |   2 +-
 42 files changed, 499 insertions(+), 247 deletions(-)

diff --git 
a/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java
 
b/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java
index 9415696da..8e63a77db 100644
--- 
a/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java
+++ 
b/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java
@@ -36,9 +36,9 @@ import java.util.Comparator;
 import java.util.Objects;
 import java.util.stream.Stream;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
 import org.apache.polaris.core.persistence.BasePolarisMetaStoreManagerTest;
 import org.apache.polaris.core.persistence.PolarisTestMetaStoreManager;
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java
index 638e4b582..0be4312c0 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java
@@ -89,10 +89,11 @@ import org.junit.jupiter.api.io.TempDir;
 /**
  * @implSpec This test expects the server to be configured with the following 
features configured:
  *     <ul>
- *       <li>{@link 
org.apache.polaris.core.PolarisConfiguration#ALLOW_OVERLAPPING_CATALOG_URLS}:
+ *       <li>{@link
+ *           
org.apache.polaris.core.config.FeatureConfiguration#ALLOW_OVERLAPPING_CATALOG_URLS}:
  *           {@code true}
  *       <li>{@link
- *           
org.apache.polaris.core.PolarisConfiguration#SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION}:
+ *           
org.apache.polaris.core.config.FeatureConfiguration#SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION}:
  *           {@code true}
  *     </ul>
  *     The server must also be configured to reject request body sizes larger 
than 1MB (1000000
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
index e830a18ab..d743351d9 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
@@ -90,10 +90,11 @@ import org.testcontainers.shaded.org.awaitility.Awaitility;
  * @implSpec @implSpec This test expects the server to be configured with the 
following features
  *     configured:
  *     <ul>
- *       <li>{@link 
org.apache.polaris.core.PolarisConfiguration#ALLOW_OVERLAPPING_CATALOG_URLS}:
+ *       <li>{@link
+ *           
org.apache.polaris.core.config.FeatureConfiguration#ALLOW_OVERLAPPING_CATALOG_URLS}:
  *           {@code true}
  *       <li>{@link
- *           
org.apache.polaris.core.PolarisConfiguration#ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING}:
+ *           
org.apache.polaris.core.config.FeatureConfiguration#ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING}:
  *           {@code true}
  *     </ul>
  */
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationTest.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationTest.java
index 2a31f2f11..a832553cf 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationTest.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationTest.java
@@ -58,7 +58,6 @@ import org.apache.iceberg.io.ResolvingFileIO;
 import org.apache.iceberg.rest.RESTCatalog;
 import org.apache.iceberg.rest.responses.ErrorResponse;
 import org.apache.iceberg.types.Types;
-import org.apache.polaris.core.PolarisConfiguration;
 import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
 import org.apache.polaris.core.admin.model.Catalog;
 import org.apache.polaris.core.admin.model.CatalogGrant;
@@ -76,6 +75,8 @@ import org.apache.polaris.core.admin.model.TableGrant;
 import org.apache.polaris.core.admin.model.TablePrivilege;
 import org.apache.polaris.core.admin.model.ViewGrant;
 import org.apache.polaris.core.admin.model.ViewPrivilege;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfiguration;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PolarisEntityConstants;
 import org.apache.polaris.service.it.env.CatalogApi;
@@ -449,7 +450,7 @@ public class PolarisRestCatalogIntegrationTest extends 
CatalogTests<RESTCatalog>
     Catalog catalog = managementApi.getCatalog(currentCatalogName);
     Map<String, String> catalogProps = new 
HashMap<>(catalog.getProperties().toMap());
     catalogProps.put(
-        
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), 
"false");
+        
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), 
"false");
     managementApi.updateCatalog(catalog, catalogProps);
 
     restCatalog.createNamespace(Namespace.of("ns1"));
@@ -477,7 +478,7 @@ public class PolarisRestCatalogIntegrationTest extends 
CatalogTests<RESTCatalog>
     Catalog catalog = managementApi.getCatalog(currentCatalogName);
     Map<String, String> catalogProps = new 
HashMap<>(catalog.getProperties().toMap());
     catalogProps.put(
-        
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), 
"false");
+        
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), 
"false");
     managementApi.updateCatalog(catalog, catalogProps);
 
     restCatalog.createNamespace(Namespace.of("ns1"));
@@ -514,7 +515,7 @@ public class PolarisRestCatalogIntegrationTest extends 
CatalogTests<RESTCatalog>
     Catalog catalog = managementApi.getCatalog(currentCatalogName);
     Map<String, String> catalogProps = new 
HashMap<>(catalog.getProperties().toMap());
     catalogProps.put(
-        
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), 
"false");
+        
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), 
"false");
     managementApi.updateCatalog(catalog, catalogProps);
 
     restCatalog.createNamespace(Namespace.of("ns1"));
@@ -563,7 +564,7 @@ public class PolarisRestCatalogIntegrationTest extends 
CatalogTests<RESTCatalog>
             .isInstanceOf(ForbiddenException.class)
             .hasMessageContaining("Access Delegation is not enabled for this 
catalog")
             .hasMessageContaining(
-                
PolarisConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING.catalogConfig());
+                
FeatureConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING.catalogConfig());
       } finally {
         resolvingFileIO.deleteFile(fileLocation);
       }
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogViewIntegrationBase.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogViewIntegrationBase.java
index 1fb24f5fe..28c38c99e 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogViewIntegrationBase.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogViewIntegrationBase.java
@@ -24,12 +24,12 @@ import java.lang.reflect.Method;
 import java.util.Map;
 import org.apache.iceberg.rest.RESTCatalog;
 import org.apache.iceberg.view.ViewCatalogTests;
-import org.apache.polaris.core.PolarisConfiguration;
 import org.apache.polaris.core.admin.model.Catalog;
 import org.apache.polaris.core.admin.model.CatalogProperties;
 import org.apache.polaris.core.admin.model.PolarisCatalog;
 import org.apache.polaris.core.admin.model.PrincipalWithCredentials;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
+import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.service.it.env.ClientCredentials;
 import org.apache.polaris.service.it.env.IcebergHelper;
@@ -50,8 +50,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
  * client.
  *
  * @implSpec This test expects the server to be configured with {@link
- *     
org.apache.polaris.core.PolarisConfiguration#SUPPORTED_CATALOG_STORAGE_TYPES} 
set to the
- *     appropriate storage type.
+ *     
org.apache.polaris.core.config.FeatureConfiguration#SUPPORTED_CATALOG_STORAGE_TYPES}
 set to
+ *     the appropriate storage type.
  */
 @ExtendWith(PolarisIntegrationTestExtension.class)
 public abstract class PolarisRestCatalogViewIntegrationBase extends 
ViewCatalogTests<RESTCatalog> {
@@ -99,9 +99,9 @@ public abstract class PolarisRestCatalogViewIntegrationBase 
extends ViewCatalogT
         CatalogProperties.builder(defaultBaseLocation)
             .addProperty(
                 
CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:")
-            
.addProperty(PolarisConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(),
 "true")
+            
.addProperty(FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(),
 "true")
             .addProperty(
-                
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
+                
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
             .build();
     Catalog catalog =
         PolarisCatalog.builder()
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisSparkIntegrationTest.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisSparkIntegrationTest.java
index 3625c69e8..7333baa39 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisSparkIntegrationTest.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisSparkIntegrationTest.java
@@ -64,9 +64,10 @@ import org.slf4j.LoggerFactory;
  * @implSpec This test expects the server to be configured with the following 
features enabled:
  *     <ul>
  *       <li>{@link
- *           
org.apache.polaris.core.PolarisConfiguration#SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION}:
+ *           
org.apache.polaris.core.config.FeatureConfiguration#SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION}:
  *           {@code true}
- *       <li>{@link 
org.apache.polaris.core.PolarisConfiguration#ALLOW_OVERLAPPING_CATALOG_URLS}:
+ *       <li>{@link
+ *           
org.apache.polaris.core.config.FeatureConfiguration#ALLOW_OVERLAPPING_CATALOG_URLS}:
  *           {@code true}
  *     </ul>
  */
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/PolarisCallContext.java 
b/polaris-core/src/main/java/org/apache/polaris/core/PolarisCallContext.java
index 3b5e96251..510cd0f6c 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/PolarisCallContext.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/PolarisCallContext.java
@@ -21,6 +21,7 @@ package org.apache.polaris.core;
 import jakarta.annotation.Nonnull;
 import java.time.Clock;
 import java.time.ZoneId;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.persistence.BasePersistence;
 
 /**
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
index 96f449acb..564be49da 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
@@ -98,8 +98,8 @@ import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.iceberg.exceptions.ForbiddenException;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.entity.PolarisBaseEntity;
 import org.apache.polaris.core.entity.PolarisEntityConstants;
@@ -510,7 +510,7 @@ public class PolarisAuthorizerImpl implements 
PolarisAuthorizer {
     boolean enforceCredentialRotationRequiredState =
         featureConfig.getConfiguration(
             CallContext.getCurrentContext().getPolarisCallContext(),
-            
PolarisConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING);
+            
FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING);
     if (enforceCredentialRotationRequiredState
         && authenticatedPrincipal
             .getPrincipalEntity()
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java
new file mode 100644
index 000000000..a64c59402
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.config;
+
+import java.util.Optional;
+
+/**
+ * Internal configuration flags for non-feature behavior changes in Polaris. 
These flags control
+ * subtle behavior adjustments and bug fixes, not user-facing catalog 
settings. They are intended
+ * for internal use only, are inherently unstable, and may be removed at any 
time. When introducing
+ * a new flag, consider the trade-off between maintenance burden and the risk 
of an unguarded
+ * behavior change. Flags here are generally short-lived and should either be 
removed or promoted to
+ * stable feature flags before the next release.
+ *
+ * @param <T> The type of the configuration
+ */
+public class BehaviorChangeConfiguration<T> extends PolarisConfiguration<T> {
+
+  protected BehaviorChangeConfiguration(
+      String key, String description, T defaultValue, Optional<String> 
catalogConfig) {
+    super(key, description, defaultValue, catalogConfig);
+  }
+
+  public static final BehaviorChangeConfiguration<Boolean> 
VALIDATE_VIEW_LOCATION_OVERLAP =
+      PolarisConfiguration.<Boolean>builder()
+          .key("STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS")
+          .description("If true, validate that view locations don't overlap 
when views are created")
+          .defaultValue(true)
+          .buildBehaviorChangeConfiguration();
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/PolarisConfiguration.java 
b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
similarity index 61%
rename from 
polaris-core/src/main/java/org/apache/polaris/core/PolarisConfiguration.java
rename to 
polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
index ca1962e3c..087d0525e 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/PolarisConfiguration.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
@@ -16,115 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.core;
+package org.apache.polaris.core.config;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
-import org.apache.polaris.core.context.CallContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-public class PolarisConfiguration<T> {
-
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(PolarisConfiguration.class);
-
-  public final String key;
-  public final String description;
-  public final T defaultValue;
-  private final Optional<String> catalogConfigImpl;
-  private final Class<T> typ;
-
-  @SuppressWarnings("unchecked")
-  public PolarisConfiguration(
+/**
+ * Configurations for features within Polaris. These configurations are 
intended to be customized
+ * and many expose user-facing catalog-level configurations. These 
configurations are stable over
+ * time.
+ *
+ * @param <T>
+ */
+public class FeatureConfiguration<T> extends PolarisConfiguration<T> {
+  protected FeatureConfiguration(
       String key, String description, T defaultValue, Optional<String> 
catalogConfig) {
-    this.key = key;
-    this.description = description;
-    this.defaultValue = defaultValue;
-    this.catalogConfigImpl = catalogConfig;
-    this.typ = (Class<T>) defaultValue.getClass();
-  }
-
-  public boolean hasCatalogConfig() {
-    return catalogConfigImpl.isPresent();
-  }
-
-  public String catalogConfig() {
-    return catalogConfigImpl.orElseThrow(
-        () ->
-            new IllegalStateException(
-                "Attempted to read a catalog config key from a configuration 
that doesn't have one."));
-  }
-
-  T cast(Object value) {
-    return this.typ.cast(value);
-  }
-
-  public static class Builder<T> {
-    private String key;
-    private String description;
-    private T defaultValue;
-    private Optional<String> catalogConfig = Optional.empty();
-
-    public Builder<T> key(String key) {
-      this.key = key;
-      return this;
-    }
-
-    public Builder<T> description(String description) {
-      this.description = description;
-      return this;
-    }
-
-    @SuppressWarnings("unchecked")
-    public Builder<T> defaultValue(T defaultValue) {
-      if (defaultValue instanceof List<?>) {
-        // Type-safe handling of List
-        this.defaultValue = (T) new ArrayList<>((List<?>) defaultValue);
-      } else {
-        this.defaultValue = defaultValue;
-      }
-      return this;
-    }
-
-    public Builder<T> catalogConfig(String catalogConfig) {
-      this.catalogConfig = Optional.of(catalogConfig);
-      return this;
-    }
-
-    public PolarisConfiguration<T> build() {
-      if (key == null || description == null || defaultValue == null) {
-        throw new IllegalArgumentException("key, description, and defaultValue 
are required");
-      }
-      return new PolarisConfiguration<>(key, description, defaultValue, 
catalogConfig);
-    }
-  }
-
-  /**
-   * Returns the value of a `PolarisConfiguration`, or the default if it 
cannot be loaded. This
-   * method does not need to be used when a `CallContext` is already available
-   */
-  public static <T> T loadConfig(PolarisConfiguration<T> configuration) {
-    var callContext = CallContext.getCurrentContext();
-    if (callContext == null) {
-      LOGGER.warn(
-          String.format(
-              "Unable to load current call context; using %s = %s",
-              configuration.key, configuration.defaultValue));
-      return configuration.defaultValue;
-    }
-    return callContext
-        .getPolarisCallContext()
-        .getConfigurationStore()
-        .getConfiguration(callContext.getPolarisCallContext(), configuration);
-  }
-
-  public static <T> Builder<T> builder() {
-    return new Builder<>();
+    super(key, description, defaultValue, catalogConfig);
   }
 
-  public static final PolarisConfiguration<Boolean>
+  public static final FeatureConfiguration<Boolean>
       ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING =
           PolarisConfiguration.<Boolean>builder()
               .key("ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING")
@@ -132,9 +43,9 @@ public class PolarisConfiguration<T> {
                   "If set to true, require that principals must rotate their 
credentials before being used "
                       + "for anything else.")
               .defaultValue(false)
-              .build();
+              .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> 
SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION =
+  public static final FeatureConfiguration<Boolean> 
SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION =
       PolarisConfiguration.<Boolean>builder()
           .key("SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION")
           .description(
@@ -147,9 +58,9 @@ public class PolarisConfiguration<T> {
                   + "   \"credential-vending\" and can use server-default 
environment variables or credential config\n"
                   + "   files for all storage access, or in test/dev 
scenarios.")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> 
ALLOW_TABLE_LOCATION_OVERLAP =
+  public static final FeatureConfiguration<Boolean> 
ALLOW_TABLE_LOCATION_OVERLAP =
       PolarisConfiguration.<Boolean>builder()
           .key("ALLOW_TABLE_LOCATION_OVERLAP")
           .catalogConfig("allow.overlapping.table.location")
@@ -157,58 +68,58 @@ public class PolarisConfiguration<T> {
               "If set to true, allow one table's location to reside within 
another table's location. "
                   + "This is only enforced within a given namespace.")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> 
ALLOW_NAMESPACE_LOCATION_OVERLAP =
+  public static final FeatureConfiguration<Boolean> 
ALLOW_NAMESPACE_LOCATION_OVERLAP =
       PolarisConfiguration.<Boolean>builder()
           .key("ALLOW_NAMESPACE_LOCATION_OVERLAP")
           .description(
               "If set to true, allow one namespace's location to reside within 
another namespace's location. "
                   + "This is only enforced within a parent catalog or 
namespace.")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> 
ALLOW_EXTERNAL_METADATA_FILE_LOCATION =
+  public static final FeatureConfiguration<Boolean> 
ALLOW_EXTERNAL_METADATA_FILE_LOCATION =
       PolarisConfiguration.<Boolean>builder()
           .key("ALLOW_EXTERNAL_METADATA_FILE_LOCATION")
           .description(
               "If set to true, allows metadata files to be located outside the 
default metadata directory.")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> 
ALLOW_OVERLAPPING_CATALOG_URLS =
+  public static final FeatureConfiguration<Boolean> 
ALLOW_OVERLAPPING_CATALOG_URLS =
       PolarisConfiguration.<Boolean>builder()
           .key("ALLOW_OVERLAPPING_CATALOG_URLS")
           .description("If set to true, allows catalog URLs to overlap.")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> 
ALLOW_UNSTRUCTURED_TABLE_LOCATION =
+  public static final FeatureConfiguration<Boolean> 
ALLOW_UNSTRUCTURED_TABLE_LOCATION =
       PolarisConfiguration.<Boolean>builder()
           .key("ALLOW_UNSTRUCTURED_TABLE_LOCATION")
           .catalogConfig("allow.unstructured.table.location")
           .description("If set to true, allows unstructured table locations.")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> 
ALLOW_EXTERNAL_TABLE_LOCATION =
+  public static final FeatureConfiguration<Boolean> 
ALLOW_EXTERNAL_TABLE_LOCATION =
       PolarisConfiguration.<Boolean>builder()
           .key("ALLOW_EXTERNAL_TABLE_LOCATION")
           .catalogConfig("allow.external.table.location")
           .description(
               "If set to true, allows tables to have external locations 
outside the default structure.")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> 
ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING =
+  public static final FeatureConfiguration<Boolean> 
ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING =
       PolarisConfiguration.<Boolean>builder()
           .key("ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING")
           .catalogConfig("enable.credential.vending")
           .description("If set to true, allow credential vending for external 
catalogs.")
           .defaultValue(true)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<List<String>> 
SUPPORTED_CATALOG_STORAGE_TYPES =
+  public static final FeatureConfiguration<List<String>> 
SUPPORTED_CATALOG_STORAGE_TYPES =
       PolarisConfiguration.<List<String>>builder()
           .key("SUPPORTED_CATALOG_STORAGE_TYPES")
           .catalogConfig("supported.storage.types")
@@ -219,34 +130,34 @@ public class PolarisConfiguration<T> {
                   StorageConfigInfo.StorageTypeEnum.AZURE.name(),
                   StorageConfigInfo.StorageTypeEnum.GCS.name(),
                   StorageConfigInfo.StorageTypeEnum.FILE.name()))
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> CLEANUP_ON_NAMESPACE_DROP =
+  public static final FeatureConfiguration<Boolean> CLEANUP_ON_NAMESPACE_DROP =
       PolarisConfiguration.<Boolean>builder()
           .key("CLEANUP_ON_NAMESPACE_DROP")
           .catalogConfig("cleanup.on.namespace.drop")
           .description("If set to true, clean up data when a namespace is 
dropped")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> CLEANUP_ON_CATALOG_DROP =
+  public static final FeatureConfiguration<Boolean> CLEANUP_ON_CATALOG_DROP =
       PolarisConfiguration.<Boolean>builder()
           .key("CLEANUP_ON_CATALOG_DROP")
           .catalogConfig("cleanup.on.catalog.drop")
           .description("If set to true, clean up data when a catalog is 
dropped")
           .defaultValue(false)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Boolean> DROP_WITH_PURGE_ENABLED =
+  public static final FeatureConfiguration<Boolean> DROP_WITH_PURGE_ENABLED =
       PolarisConfiguration.<Boolean>builder()
           .key("DROP_WITH_PURGE_ENABLED")
           .catalogConfig("drop-with-purge.enabled")
           .description(
               "If set to true, allows tables to be dropped with the purge 
parameter set to true.")
           .defaultValue(true)
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Integer> 
STORAGE_CREDENTIAL_DURATION_SECONDS =
+  public static final FeatureConfiguration<Integer> 
STORAGE_CREDENTIAL_DURATION_SECONDS =
       PolarisConfiguration.<Integer>builder()
           .key("STORAGE_CREDENTIAL_DURATION_SECONDS")
           .description(
@@ -254,22 +165,22 @@ public class PolarisConfiguration<T> {
                   + " longer (or shorter) durations is dependent on the 
storage provider. GCS"
                   + " current does not respect this value.")
           .defaultValue(60 * 60) // 1 hour
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Integer> 
STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS =
+  public static final FeatureConfiguration<Integer> 
STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS =
       PolarisConfiguration.<Integer>builder()
           .key("STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS")
           .description(
               "How long to store storage credentials in the local cache. This 
should be less than "
                   + STORAGE_CREDENTIAL_DURATION_SECONDS.key)
           .defaultValue(30 * 60) // 30 minutes
-          .build();
+          .buildFeatureConfiguration();
 
-  public static final PolarisConfiguration<Integer> 
MAX_METADATA_REFRESH_RETRIES =
+  public static final FeatureConfiguration<Integer> 
MAX_METADATA_REFRESH_RETRIES =
       PolarisConfiguration.<Integer>builder()
           .key("MAX_METADATA_REFRESH_RETRIES")
           .description(
               "How many times to retry refreshing metadata when the previous 
error was retryable")
           .defaultValue(2)
-          .build();
+          .buildFeatureConfiguration();
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java
new file mode 100644
index 000000000..bcb380988
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.apache.polaris.core.context.CallContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An ABC for Polaris configurations that alter the service's behavior
+ *
+ * @param <T> The type of the configuration
+ */
+public abstract class PolarisConfiguration<T> {
+
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(PolarisConfiguration.class);
+
+  public final String key;
+  public final String description;
+  public final T defaultValue;
+  private final Optional<String> catalogConfigImpl;
+  private final Class<T> typ;
+
+  @SuppressWarnings("unchecked")
+  protected PolarisConfiguration(
+      String key, String description, T defaultValue, Optional<String> 
catalogConfig) {
+    this.key = key;
+    this.description = description;
+    this.defaultValue = defaultValue;
+    this.catalogConfigImpl = catalogConfig;
+    this.typ = (Class<T>) defaultValue.getClass();
+  }
+
+  public boolean hasCatalogConfig() {
+    return catalogConfigImpl.isPresent();
+  }
+
+  public String catalogConfig() {
+    return catalogConfigImpl.orElseThrow(
+        () ->
+            new IllegalStateException(
+                "Attempted to read a catalog config key from a configuration 
that doesn't have one."));
+  }
+
+  T cast(Object value) {
+    return this.typ.cast(value);
+  }
+
+  public static class Builder<T> {
+    private String key;
+    private String description;
+    private T defaultValue;
+    private Optional<String> catalogConfig = Optional.empty();
+
+    public Builder<T> key(String key) {
+      this.key = key;
+      return this;
+    }
+
+    public Builder<T> description(String description) {
+      this.description = description;
+      return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Builder<T> defaultValue(T defaultValue) {
+      if (defaultValue instanceof List<?>) {
+        // Type-safe handling of List
+        this.defaultValue = (T) new ArrayList<>((List<?>) defaultValue);
+      } else {
+        this.defaultValue = defaultValue;
+      }
+      return this;
+    }
+
+    public Builder<T> catalogConfig(String catalogConfig) {
+      this.catalogConfig = Optional.of(catalogConfig);
+      return this;
+    }
+
+    public FeatureConfiguration<T> buildFeatureConfiguration() {
+      if (key == null || description == null || defaultValue == null) {
+        throw new IllegalArgumentException("key, description, and defaultValue 
are required");
+      }
+      return new FeatureConfiguration<>(key, description, defaultValue, 
catalogConfig);
+    }
+
+    public BehaviorChangeConfiguration<T> buildBehaviorChangeConfiguration() {
+      if (key == null || description == null || defaultValue == null) {
+        throw new IllegalArgumentException("key, description, and defaultValue 
are required");
+      }
+      if (catalogConfig.isPresent()) {
+        throw new IllegalArgumentException(
+            "catalogConfig is not valid for behavior change configs");
+      }
+      return new BehaviorChangeConfiguration<>(key, description, defaultValue, 
catalogConfig);
+    }
+  }
+
+  /**
+   * Returns the value of a `PolarisConfiguration`, or the default if it 
cannot be loaded. This
+   * method does not need to be used when a `CallContext` is already available
+   */
+  public static <T> T loadConfig(PolarisConfiguration<T> configuration) {
+    var callContext = CallContext.getCurrentContext();
+    if (callContext == null) {
+      LOGGER.warn(
+          String.format(
+              "Unable to load current call context; using %s = %s",
+              configuration.key, configuration.defaultValue));
+      return configuration.defaultValue;
+    }
+    return callContext
+        .getPolarisCallContext()
+        .getConfigurationStore()
+        .getConfiguration(callContext.getPolarisCallContext(), configuration);
+  }
+
+  public static <T> Builder<T> builder() {
+    return new Builder<>();
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/PolarisConfigurationStore.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfigurationStore.java
similarity index 98%
rename from 
polaris-core/src/main/java/org/apache/polaris/core/PolarisConfigurationStore.java
rename to 
polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfigurationStore.java
index a3c902206..65e613351 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/PolarisConfigurationStore.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfigurationStore.java
@@ -16,13 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.core;
+package org.apache.polaris.core.config;
 
 import com.google.common.base.Preconditions;
 import jakarta.annotation.Nonnull;
 import jakarta.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.List;
+import org.apache.polaris.core.PolarisCallContext;
 import org.apache.polaris.core.entity.CatalogEntity;
 
 /**
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
index 6b0638e83..9631d95b4 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
@@ -36,9 +36,9 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import org.apache.polaris.core.PolarisConfiguration;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.admin.model.Catalog;
+import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PolarisEntity;
@@ -168,7 +168,7 @@ public abstract class PolarisStorageConfigurationInfo {
                       .getConfiguration(
                           
CallContext.getCurrentContext().getPolarisCallContext(),
                           catalog,
-                          
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION);
+                          
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION);
               if (!allowEscape
                   && catalog.getCatalogType() != Catalog.TypeEnum.EXTERNAL
                   && baseLocation != null) {
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
index 591a67f15..9b1c64900 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
@@ -18,8 +18,8 @@
  */
 package org.apache.polaris.core.storage.aws;
 
-import static 
org.apache.polaris.core.PolarisConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS;
-import static org.apache.polaris.core.PolarisConfiguration.loadConfig;
+import static 
org.apache.polaris.core.config.FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS;
+import static org.apache.polaris.core.config.PolarisConfiguration.loadConfig;
 
 import jakarta.annotation.Nonnull;
 import java.net.URI;
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java
index 4013cbd81..62e4fc4dc 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java
@@ -45,8 +45,8 @@ import java.util.EnumMap;
 import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
-import org.apache.polaris.core.PolarisConfiguration;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.storage.InMemoryStorageIntegration;
 import org.apache.polaris.core.storage.PolarisCredentialProperty;
 import org.slf4j.Logger;
@@ -126,7 +126,7 @@ public class AzureCredentialsStorageIntegration
     // clock skew between the client and server,
     OffsetDateTime startTime = 
start.truncatedTo(ChronoUnit.SECONDS).atOffset(ZoneOffset.UTC);
     int intendedDurationSeconds =
-        
PolarisConfiguration.loadConfig(PolarisConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS);
+        
FeatureConfiguration.loadConfig(FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS);
     OffsetDateTime intendedEndTime =
         start.plusSeconds(intendedDurationSeconds).atOffset(ZoneOffset.UTC);
     OffsetDateTime maxAllowedEndTime =
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
index bfc75214c..a80b38aa4 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
@@ -29,7 +29,8 @@ import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import org.apache.iceberg.exceptions.UnprocessableEntityException;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfiguration;
 import org.apache.polaris.core.entity.PolarisEntity;
 import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult;
@@ -96,15 +97,15 @@ public class StorageCredentialCache {
   private static long maxCacheDurationMs() {
     var cacheDurationSeconds =
         PolarisConfiguration.loadConfig(
-            PolarisConfiguration.STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS);
+            FeatureConfiguration.STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS);
     var credentialDurationSeconds =
-        
PolarisConfiguration.loadConfig(PolarisConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS);
+        
PolarisConfiguration.loadConfig(FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS);
     if (cacheDurationSeconds >= credentialDurationSeconds) {
       throw new IllegalArgumentException(
           String.format(
               "%s should be less than %s",
-              
PolarisConfiguration.STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS.key,
-              PolarisConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS.key));
+              
FeatureConfiguration.STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS.key,
+              FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS.key));
     } else {
       return cacheDurationSeconds * 1000L;
     }
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapAtomicOperationMetaStoreManagerTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapAtomicOperationMetaStoreManagerTest.java
index 42e61f7d4..274841c52 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapAtomicOperationMetaStoreManagerTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapAtomicOperationMetaStoreManagerTest.java
@@ -22,9 +22,9 @@ import static 
org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RAND
 
 import java.time.ZoneId;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import 
org.apache.polaris.core.persistence.transactional.PolarisTreeMapMetaStoreSessionImpl;
 import org.apache.polaris.core.persistence.transactional.PolarisTreeMapStore;
 import org.mockito.Mockito;
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java
index 65f8080d9..d7491e038 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java
@@ -22,9 +22,9 @@ import static 
org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RAND
 
 import java.time.ZoneId;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import 
org.apache.polaris.core.persistence.transactional.PolarisMetaStoreManagerImpl;
 import 
org.apache.polaris.core.persistence.transactional.PolarisTreeMapMetaStoreSessionImpl;
 import org.apache.polaris.core.persistence.transactional.PolarisTreeMapStore;
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java
index aa5317e99..0f1f4f151 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java
@@ -26,9 +26,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
 import org.assertj.core.api.Assertions;
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java
index a0da2c5d8..6abb76b8a 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java
@@ -20,9 +20,12 @@ package org.apache.polaris.service.storage;
 
 import jakarta.annotation.Nullable;
 import java.util.List;
+import java.util.function.Supplier;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.BehaviorChangeConfiguration;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -91,6 +94,50 @@ public class PolarisConfigurationStoreTest {
         .key(key)
         .description("")
         .defaultValue(defaultValue)
-        .build();
+        .buildFeatureConfiguration();
+  }
+
+  private static class PolarisConfigurationConsumer {
+
+    private final PolarisCallContext polarisCallContext;
+    private final PolarisConfigurationStore configurationStore;
+
+    public PolarisConfigurationConsumer(
+        PolarisCallContext polarisCallContext, PolarisConfigurationStore 
configurationStore) {
+      this.polarisCallContext = polarisCallContext;
+      this.configurationStore = configurationStore;
+    }
+
+    public <T> T consumeConfiguration(
+        PolarisConfiguration<Boolean> config, Supplier<T> code, T defaultVal) {
+      if (configurationStore.getConfiguration(polarisCallContext, config)) {
+        return code.get();
+      }
+      return defaultVal;
+    }
+  }
+
+  @Test
+  public void testBehaviorAndFeatureConfigs() {
+    PolarisConfigurationConsumer consumer =
+        new PolarisConfigurationConsumer(null, new PolarisConfigurationStore() 
{});
+
+    FeatureConfiguration<Boolean> featureConfig =
+        PolarisConfiguration.<Boolean>builder()
+            .key("example")
+            .description("example")
+            .defaultValue(true)
+            .buildFeatureConfiguration();
+
+    BehaviorChangeConfiguration<Boolean> behaviorChangeConfig =
+        PolarisConfiguration.<Boolean>builder()
+            .key("example")
+            .description("example")
+            .defaultValue(true)
+            .buildBehaviorChangeConfiguration();
+
+    consumer.consumeConfiguration(behaviorChangeConfig, () -> 21, 22);
+
+    consumer.consumeConfiguration(featureConfig, () -> 42, 43);
   }
 }
diff --git 
a/quarkus/admin/src/main/java/org/apache/polaris/admintool/config/QuarkusProducers.java
 
b/quarkus/admin/src/main/java/org/apache/polaris/admintool/config/QuarkusProducers.java
index 6788742ba..07ad02362 100644
--- 
a/quarkus/admin/src/main/java/org/apache/polaris/admintool/config/QuarkusProducers.java
+++ 
b/quarkus/admin/src/main/java/org/apache/polaris/admintool/config/QuarkusProducers.java
@@ -25,9 +25,9 @@ import jakarta.enterprise.inject.Any;
 import jakarta.enterprise.inject.Instance;
 import jakarta.enterprise.inject.Produces;
 import java.time.Clock;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
 import org.apache.polaris.core.storage.PolarisStorageIntegration;
diff --git 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusBehaviorChangesConfiguration.java
 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusBehaviorChangesConfiguration.java
new file mode 100644
index 000000000..852529a0c
--- /dev/null
+++ 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusBehaviorChangesConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.quarkus.config;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.runtime.annotations.StaticInitSafe;
+import io.smallrye.config.ConfigMapping;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.service.config.FeaturesConfiguration;
+
+@StaticInitSafe
+@ConfigMapping(prefix = "polaris.behavior-changes")
+// FIXME: this should extend FeatureConfiguration, but that causes conflicts 
with
+// QuarkusFeaturesConfiguration
+public interface QuarkusBehaviorChangesConfiguration {
+
+  Map<String, String> defaults();
+
+  Map<String, ? extends FeaturesConfiguration.RealmOverrides> realmOverrides();
+
+  default Map<String, Object> parseDefaults(ObjectMapper objectMapper) {
+    return convertMap(objectMapper, defaults());
+  }
+
+  default Map<String, Map<String, Object>> parseRealmOverrides(ObjectMapper 
objectMapper) {
+    Map<String, Map<String, Object>> m = new HashMap<>();
+    for (String realm : realmOverrides().keySet()) {
+      m.put(realm, convertMap(objectMapper, 
realmOverrides().get(realm).overrides()));
+    }
+    return m;
+  }
+
+  private static Map<String, Object> convertMap(
+      ObjectMapper objectMapper, Map<String, String> properties) {
+    Map<String, Object> m = new HashMap<>();
+    for (String configName : properties.keySet()) {
+      String json = properties.get(configName);
+      try {
+        JsonNode node = objectMapper.readTree(json);
+        m.put(configName, configValue(node));
+      } catch (JsonProcessingException e) {
+        throw new RuntimeException(
+            "Invalid JSON value for feature configuration: " + configName, e);
+      }
+    }
+    return m;
+  }
+
+  private static Object configValue(JsonNode node) {
+    return switch (node.getNodeType()) {
+      case BOOLEAN -> node.asBoolean();
+      case STRING -> node.asText();
+      case NUMBER ->
+          switch (node.numberType()) {
+            case INT, LONG -> node.asLong();
+            case FLOAT, DOUBLE -> node.asDouble();
+            default ->
+                throw new IllegalArgumentException("Unsupported number type: " 
+ node.numberType());
+          };
+      case ARRAY -> {
+        List<Object> list = new ArrayList<>();
+        node.elements().forEachRemaining(n -> list.add(configValue(n)));
+        yield List.copyOf(list);
+      }
+      default ->
+          throw new IllegalArgumentException(
+              "Unsupported feature configuration JSON type: " + 
node.getNodeType());
+    };
+  }
+}
diff --git 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFeaturesConfiguration.java
 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFeaturesConfiguration.java
index 76ba04968..85858440a 100644
--- 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFeaturesConfiguration.java
+++ 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFeaturesConfiguration.java
@@ -20,7 +20,6 @@ package org.apache.polaris.service.quarkus.config;
 
 import io.quarkus.runtime.annotations.StaticInitSafe;
 import io.smallrye.config.ConfigMapping;
-import io.smallrye.config.WithParentName;
 import java.util.Map;
 import org.apache.polaris.service.config.FeaturesConfiguration;
 
@@ -32,11 +31,5 @@ public interface QuarkusFeaturesConfiguration extends 
FeaturesConfiguration {
   Map<String, String> defaults();
 
   @Override
-  Map<String, QuarkusRealmOverrides> realmOverrides();
-
-  interface QuarkusRealmOverrides extends RealmOverrides {
-    @WithParentName
-    @Override
-    Map<String, String> overrides();
-  }
+  Map<String, FeaturesConfiguration.RealmOverrides> realmOverrides();
 }
diff --git 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java
 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java
index 6ec24ac42..c017c6135 100644
--- 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java
+++ 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java
@@ -33,12 +33,12 @@ import jakarta.ws.rs.container.ContainerRequestContext;
 import jakarta.ws.rs.core.Context;
 import java.time.Clock;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java
index 2b28e9f7f..de0e03fa8 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java
@@ -47,8 +47,6 @@ import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.iceberg.types.Types;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.admin.model.FileStorageConfigInfo;
 import org.apache.polaris.core.admin.model.PrincipalWithCredentials;
@@ -57,6 +55,8 @@ import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.CatalogEntity;
@@ -164,7 +164,7 @@ public abstract class PolarisAuthzTestBase {
       new PolarisAuthorizerImpl(
           new DefaultConfigurationStore(
               Map.of(
-                  
PolarisConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING.key,
+                  
FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING.key,
                   true)));
 
   @Inject protected MetaStoreManagerFactory managerFactory;
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisOverlappingTableTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisOverlappingTableTest.java
index 878c35e82..f2e929484 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisOverlappingTableTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisOverlappingTableTest.java
@@ -18,8 +18,8 @@
  */
 package org.apache.polaris.service.quarkus.admin;
 
-import static 
org.apache.polaris.core.PolarisConfiguration.ALLOW_TABLE_LOCATION_OVERLAP;
-import static 
org.apache.polaris.core.PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION;
+import static 
org.apache.polaris.core.config.FeatureConfiguration.ALLOW_TABLE_LOCATION_OVERLAP;
+import static 
org.apache.polaris.core.config.FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION;
 import static 
org.apache.polaris.service.quarkus.admin.PolarisAuthzTestBase.SCHEMA;
 import static org.assertj.core.api.Assertions.assertThat;
 
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
index fbf6db504..29401e266 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
@@ -72,13 +72,13 @@ import org.apache.iceberg.inmemory.InMemoryFileIO;
 import org.apache.iceberg.io.FileIO;
 import org.apache.iceberg.types.Types;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.CatalogEntity;
@@ -258,9 +258,9 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
                 .setDefaultBaseLocation(storageLocation)
                 .setReplaceNewLocationPrefixWithCatalogDefault("file:")
                 .addProperty(
-                    
PolarisConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "true")
+                    
FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "true")
                 .addProperty(
-                    
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
+                    
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
                 .setStorageConfigurationInfo(storageConfigModel, 
storageLocation)
                 .build());
 
@@ -686,9 +686,9 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
         List.of(PolarisEntity.toCore(catalogEntity)),
         new CatalogEntity.Builder(CatalogEntity.of(catalogEntity))
             .addProperty(
-                
PolarisConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "false")
+                
FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "false")
             .addProperty(
-                
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
+                
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
             .build());
     BasePolarisCatalog catalog = catalog();
     TableMetadata tableMetadata =
@@ -743,9 +743,9 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
         List.of(PolarisEntity.toCore(catalogEntity)),
         new CatalogEntity.Builder(CatalogEntity.of(catalogEntity))
             .addProperty(
-                
PolarisConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "false")
+                
FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "false")
             .addProperty(
-                
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
+                
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
             .build());
     BasePolarisCatalog catalog = catalog();
     TableMetadata tableMetadata =
@@ -797,9 +797,9 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
         List.of(PolarisEntity.toCore(catalogEntity)),
         new CatalogEntity.Builder(CatalogEntity.of(catalogEntity))
             .addProperty(
-                
PolarisConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "false")
+                
FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "false")
             .addProperty(
-                
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
+                
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
             .build());
     BasePolarisCatalog catalog = catalog();
     InMemoryFileIO fileIO = getInMemoryIo(catalog);
@@ -908,7 +908,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     PolarisCallContext polarisCallContext = 
callContext.getPolarisCallContext();
     if (!polarisCallContext
         .getConfigurationStore()
-        .getConfiguration(polarisCallContext, 
PolarisConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES)
+        .getConfiguration(polarisCallContext, 
FeatureConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES)
         .contains("FILE")) {
       Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, 
request))
           .isInstanceOf(ForbiddenException.class)
@@ -974,7 +974,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     PolarisCallContext polarisCallContext = 
callContext.getPolarisCallContext();
     if (!polarisCallContext
         .getConfigurationStore()
-        .getConfiguration(polarisCallContext, 
PolarisConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES)
+        .getConfiguration(polarisCallContext, 
FeatureConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES)
         .contains("FILE")) {
       Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, 
request))
           .isInstanceOf(ForbiddenException.class)
@@ -995,7 +995,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
 
     if (!polarisCallContext
         .getConfigurationStore()
-        .getConfiguration(polarisCallContext, 
PolarisConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES)
+        .getConfiguration(polarisCallContext, 
FeatureConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES)
         .contains("FILE")) {
       Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, 
newRequest))
           .isInstanceOf(ForbiddenException.class)
@@ -1471,10 +1471,10 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
             .setName(noPurgeCatalogName)
             .setDefaultBaseLocation(storageLocation)
             .setReplaceNewLocationPrefixWithCatalogDefault("file:")
-            
.addProperty(PolarisConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(),
 "true")
+            
.addProperty(FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(),
 "true")
             .addProperty(
-                
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
-            
.addProperty(PolarisConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), 
"false")
+                
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
+            
.addProperty(FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), 
"false")
             .setStorageConfigurationInfo(noPurgeStorageConfigModel, 
storageLocation)
             .build());
     PolarisPassthroughResolutionView passthroughView =
@@ -1511,7 +1511,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     // Attempt to drop the table:
     Assertions.assertThatThrownBy(() -> noPurgeCatalog.dropTable(TABLE, true))
         .isInstanceOf(ForbiddenException.class)
-        
.hasMessageContaining(PolarisConfiguration.DROP_WITH_PURGE_ENABLED.key);
+        
.hasMessageContaining(FeatureConfiguration.DROP_WITH_PURGE_ENABLED.key);
   }
 
   private TableMetadata createSampleTableMetadata(String tableLocation) {
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogViewTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogViewTest.java
index b85443faf..471afa2d9 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogViewTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogViewTest.java
@@ -39,13 +39,13 @@ import org.apache.iceberg.CatalogProperties;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.view.ViewCatalogTests;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.admin.model.FileStorageConfigInfo;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.CatalogEntity;
@@ -171,9 +171,9 @@ public class BasePolarisCatalogViewTest extends 
ViewCatalogTests<BasePolarisCata
     adminService.createCatalog(
         new CatalogEntity.Builder()
             .setName(CATALOG_NAME)
-            
.addProperty(PolarisConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(),
 "true")
+            
.addProperty(FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(),
 "true")
             .addProperty(
-                
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
+                
FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
             .setDefaultBaseLocation("file://tmp")
             .setStorageConfigurationInfo(
                 new FileStorageConfigInfo(
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolarisCatalogHandlerWrapperAuthzTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolarisCatalogHandlerWrapperAuthzTest.java
index 29d0bd90c..a17091cba 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolarisCatalogHandlerWrapperAuthzTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolarisCatalogHandlerWrapperAuthzTest.java
@@ -50,11 +50,11 @@ import 
org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
 import org.apache.iceberg.rest.requests.UpdateTableRequest;
 import org.apache.iceberg.view.ImmutableSQLViewRepresentation;
 import org.apache.iceberg.view.ImmutableViewVersion;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.admin.model.FileStorageConfigInfo;
 import org.apache.polaris.core.admin.model.PrincipalWithCredentialsCredentials;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.CatalogEntity;
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestHelper.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestHelper.java
index f7f5ed719..714929154 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestHelper.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestHelper.java
@@ -22,8 +22,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
 import java.time.Clock;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.service.context.RealmContextResolver;
 import org.junit.jupiter.api.TestInfo;
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
 
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
index 21d30c1d8..0eafc2765 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
@@ -45,7 +45,6 @@ import org.apache.iceberg.exceptions.NoSuchViewException;
 import org.apache.iceberg.exceptions.NotFoundException;
 import org.apache.iceberg.exceptions.ValidationException;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.admin.model.CatalogGrant;
 import org.apache.polaris.core.admin.model.CatalogPrivilege;
@@ -66,6 +65,7 @@ import 
org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.catalog.PolarisCatalogHelpers;
+import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.CatalogRoleEntity;
@@ -528,7 +528,7 @@ public class PolarisAdminService {
         getCurrentPolarisContext()
             .getConfigurationStore()
             .getConfiguration(
-                getCurrentPolarisContext(), 
PolarisConfiguration.ALLOW_OVERLAPPING_CATALOG_URLS);
+                getCurrentPolarisContext(), 
FeatureConfiguration.ALLOW_OVERLAPPING_CATALOG_URLS);
 
     if (allowOverlappingCatalogUrls) {
       return false;
@@ -598,7 +598,7 @@ public class PolarisAdminService {
     boolean cleanup =
         polarisCallContext
             .getConfigurationStore()
-            .getConfiguration(polarisCallContext, 
PolarisConfiguration.CLEANUP_ON_CATALOG_DROP);
+            .getConfiguration(polarisCallContext, 
FeatureConfiguration.CLEANUP_ON_CATALOG_DROP);
     DropEntityResult dropEntityResult =
         metaStoreManager.dropEntityIfExists(
             getCurrentPolarisContext(), null, entity, Map.of(), cleanup);
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
 
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
index 15fb3854d..8b92956f8 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
@@ -27,7 +27,6 @@ import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.exceptions.NotAuthorizedException;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
 import org.apache.polaris.core.admin.model.AddGrantRequest;
 import org.apache.polaris.core.admin.model.Catalog;
 import org.apache.polaris.core.admin.model.CatalogGrant;
@@ -58,6 +57,7 @@ import 
org.apache.polaris.core.admin.model.UpdatePrincipalRoleRequest;
 import org.apache.polaris.core.admin.model.ViewGrant;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.CatalogEntity;
@@ -137,7 +137,7 @@ public class PolarisServiceImpl
         polarisCallContext
             .getConfigurationStore()
             .getConfiguration(
-                polarisCallContext, 
PolarisConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES);
+                polarisCallContext, 
FeatureConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES);
     if 
(!allowedStorageTypes.contains(storageConfigInfo.getStorageType().name())) {
       LOGGER
           .atWarn()
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
index b49b73e9c..582394c37 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
@@ -77,9 +77,10 @@ import org.apache.iceberg.view.ViewMetadataParser;
 import org.apache.iceberg.view.ViewOperations;
 import org.apache.iceberg.view.ViewUtil;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.core.catalog.PolarisCatalogHelpers;
+import org.apache.polaris.core.config.BehaviorChangeConfiguration;
+import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.NamespaceEntity;
@@ -514,7 +515,7 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
         .getConfigurationStore()
         .getConfiguration(
             callContext.getPolarisCallContext(),
-            PolarisConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) {
+            FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) {
       LOGGER.debug("Validating no overlap for {} with sibling tables or 
namespaces", namespace);
       validateNoLocationOverlap(
           entity.getBaseLocation(), resolvedParent.getRawFullPath(), 
entity.getName());
@@ -650,7 +651,7 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
                 polarisCallContext
                     .getConfigurationStore()
                     .getConfiguration(
-                        polarisCallContext, 
PolarisConfiguration.CLEANUP_ON_NAMESPACE_DROP));
+                        polarisCallContext, 
FeatureConfiguration.CLEANUP_ON_NAMESPACE_DROP));
 
     if (!dropEntityResult.isSuccess() && 
dropEntityResult.failedBecauseNotEmpty()) {
       throw new NamespaceNotEmptyException("Namespace %s is not empty", 
namespace);
@@ -680,7 +681,7 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
         .getConfigurationStore()
         .getConfiguration(
             callContext.getPolarisCallContext(),
-            PolarisConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) {
+            FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) {
       LOGGER.debug("Validating no overlap with sibling tables or namespaces");
       validateNoLocationOverlap(
           NamespaceEntity.of(updatedEntity).getBaseLocation(),
@@ -976,7 +977,7 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
                   .getConfigurationStore()
                   .getConfiguration(
                       callContext.getPolarisCallContext(),
-                      PolarisConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES);
+                      FeatureConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES);
           if 
(!allowedStorageTypes.contains(StorageConfigInfo.StorageTypeEnum.FILE.name())) {
             List<String> invalidLocations =
                 locations.stream()
@@ -999,18 +1000,25 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
       CatalogEntity catalog,
       TableIdentifier identifier,
       List<PolarisEntity> resolvedNamespace,
-      String location) {
+      String location,
+      PolarisEntity entity) {
+    boolean validateViewOverlap =
+        callContext
+            .getPolarisCallContext()
+            .getConfigurationStore()
+            .getConfiguration(
+                callContext.getPolarisCallContext(),
+                BehaviorChangeConfiguration.VALIDATE_VIEW_LOCATION_OVERLAP);
+
     if (callContext
         .getPolarisCallContext()
         .getConfigurationStore()
         .getConfiguration(
             callContext.getPolarisCallContext(),
             catalog,
-            PolarisConfiguration.ALLOW_TABLE_LOCATION_OVERLAP)) {
+            FeatureConfiguration.ALLOW_TABLE_LOCATION_OVERLAP)) {
       LOGGER.debug("Skipping location overlap validation for identifier '{}'", 
identifier);
-    } else { // if (entity.getSubType().equals(PolarisEntitySubType.TABLE)) {
-      // TODO - is this necessary for views? overlapping views do not expose 
subdirectories via the
-      // credential vending so this feels like an unnecessary restriction
+    } else if (validateViewOverlap || 
entity.getSubType().equals(PolarisEntitySubType.TABLE)) {
       LOGGER.debug("Validating no overlap with sibling tables or namespaces");
       validateNoLocationOverlap(location, resolvedNamespace, 
identifier.name());
     }
@@ -1288,7 +1296,11 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
         dataLocations.forEach(
             location ->
                 validateNoLocationOverlap(
-                    catalogEntity, tableIdentifier, resolvedNamespace, 
location));
+                    catalogEntity,
+                    tableIdentifier,
+                    resolvedNamespace,
+                    location,
+                    resolvedStorageEntity.getRawLeafEntity()));
         // and that the metadata file points to a location within the table's 
directory structure
         if (metadata.metadataFileLocation() != null) {
           validateMetadataFileInTableDir(tableIdentifier, metadata, catalog);
@@ -1373,12 +1385,12 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
         polarisCallContext
             .getConfigurationStore()
             .getConfiguration(
-                polarisCallContext, 
PolarisConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION);
+                polarisCallContext, 
FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION);
     if (!allowEscape
         && !polarisCallContext
             .getConfigurationStore()
             .getConfiguration(
-                polarisCallContext, 
PolarisConfiguration.ALLOW_EXTERNAL_METADATA_FILE_LOCATION)) {
+                polarisCallContext, 
FeatureConfiguration.ALLOW_EXTERNAL_METADATA_FILE_LOCATION)) {
       LOGGER.debug(
           "Validating base location {} for table {} in metadata file {}",
           metadata.location(),
@@ -1487,7 +1499,11 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
         // for the storage configuration inherited under this entity's path.
         validateLocationForTableLike(identifier, metadata.location(), 
resolvedStorageEntity);
         validateNoLocationOverlap(
-            catalogEntity, identifier, resolvedNamespace, metadata.location());
+            catalogEntity,
+            identifier,
+            resolvedNamespace,
+            metadata.location(),
+            resolvedStorageEntity.getRawLeafEntity());
       }
 
       Map<String, String> tableProperties = new 
HashMap<>(metadata.properties());
@@ -1832,15 +1848,15 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
               .getConfiguration(
                   callContext.getPolarisCallContext(),
                   catalogEntity,
-                  PolarisConfiguration.DROP_WITH_PURGE_ENABLED);
+                  FeatureConfiguration.DROP_WITH_PURGE_ENABLED);
       if (!dropWithPurgeEnabled) {
         throw new ForbiddenException(
             String.format(
                 "Unable to purge entity: %s. To enable this feature, set the 
Polaris configuration %s "
                     + "or the catalog configuration %s",
                 identifier.name(),
-                PolarisConfiguration.DROP_WITH_PURGE_ENABLED.key,
-                PolarisConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig()));
+                FeatureConfiguration.DROP_WITH_PURGE_ENABLED.key,
+                FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig()));
       }
     }
 
@@ -2071,6 +2087,6 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
         .getPolarisCallContext()
         .getConfigurationStore()
         .getConfiguration(
-            callContext.getPolarisCallContext(), 
PolarisConfiguration.MAX_METADATA_REFRESH_RETRIES);
+            callContext.getPolarisCallContext(), 
FeatureConfiguration.MAX_METADATA_REFRESH_RETRIES);
   }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java
index 968d9c5fd..eb199f0e9 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java
@@ -55,10 +55,10 @@ import 
org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
 import org.apache.iceberg.rest.responses.ConfigResponse;
 import org.apache.iceberg.rest.responses.ImmutableLoadCredentialsResponse;
 import org.apache.iceberg.rest.responses.LoadTableResponse;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.PolarisEntity;
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java
index 489ccd488..837fb509f 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java
@@ -71,13 +71,13 @@ import org.apache.iceberg.rest.responses.ListTablesResponse;
 import org.apache.iceberg.rest.responses.LoadTableResponse;
 import org.apache.iceberg.rest.responses.LoadViewResponse;
 import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.catalog.PolarisCatalogHelpers;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PolarisEntitySubType;
@@ -850,11 +850,11 @@ public class PolarisCatalogHandlerWrapper implements 
AutoCloseable {
         && !configurationStore.getConfiguration(
             callContext.getPolarisCallContext(),
             catalogEntity,
-            PolarisConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING)) {
+            FeatureConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING)) {
       throw new ForbiddenException(
           "Access Delegation is not enabled for this catalog. Please consult 
applicable "
               + "documentation for the catalog config property '%s' to enable 
this feature",
-          
PolarisConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING.catalogConfig());
+          
FeatureConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING.catalogConfig());
     }
 
     // TODO: Find a way for the configuration or caller to better express 
whether to fail or omit
@@ -1070,7 +1070,7 @@ public class PolarisCatalogHandlerWrapper implements 
AutoCloseable {
                                   .getConfigurationStore()
                                   .getConfiguration(
                                       callContext.getPolarisCallContext(),
-                                      
PolarisConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) {
+                                      
FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) {
                             throw new BadRequestException(
                                 "Unsupported operation: commitTransaction 
containing SetLocation"
                                     + " for table '%s' and new location '%s'",
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/DefaultFileIOFactory.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/DefaultFileIOFactory.java
index 0a07a6097..31f2f9b03 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/DefaultFileIOFactory.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/DefaultFileIOFactory.java
@@ -31,7 +31,7 @@ import org.apache.hadoop.conf.Configuration;
 import org.apache.iceberg.CatalogUtil;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.io.FileIO;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.PolarisEntity;
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/FileIOUtil.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/FileIOUtil.java
index e0bed634f..f70c594dd 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/FileIOUtil.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/FileIOUtil.java
@@ -22,8 +22,8 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import org.apache.iceberg.catalog.TableIdentifier;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.entity.PolarisEntity;
 import org.apache.polaris.core.entity.PolarisEntityConstants;
@@ -88,8 +88,8 @@ public class FileIOUtil {
     boolean skipCredentialSubscopingIndirection =
         configurationStore.getConfiguration(
             callContext.getPolarisCallContext(),
-            PolarisConfiguration.SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION.key,
-            
PolarisConfiguration.SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION.defaultValue);
+            FeatureConfiguration.SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION.key,
+            
FeatureConfiguration.SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION.defaultValue);
     if (skipCredentialSubscopingIndirection) {
       LOGGER
           .atDebug()
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/WasbTranslatingFileIOFactory.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/WasbTranslatingFileIOFactory.java
index 3bb365368..a7074fdc0 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/WasbTranslatingFileIOFactory.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/WasbTranslatingFileIOFactory.java
@@ -26,7 +26,7 @@ import java.util.Map;
 import java.util.Set;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.io.FileIO;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/config/DefaultConfigurationStore.java
 
b/service/common/src/main/java/org/apache/polaris/service/config/DefaultConfigurationStore.java
index 3db2fd998..8d9fd0e43 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/config/DefaultConfigurationStore.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/config/DefaultConfigurationStore.java
@@ -25,7 +25,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import java.util.Map;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 
 @ApplicationScoped
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/context/DefaultCallContextResolver.java
 
b/service/common/src/main/java/org/apache/polaris/service/context/DefaultCallContextResolver.java
index 14246692e..90f381d75 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/context/DefaultCallContextResolver.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/context/DefaultCallContextResolver.java
@@ -24,8 +24,8 @@ import jakarta.inject.Inject;
 import java.time.Clock;
 import java.util.Map;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
diff --git 
a/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java
 
b/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index 7dd4f6804..0f63106ac 100644
--- 
a/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++ 
b/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -29,10 +29,10 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.PolarisEntity;
diff --git 
a/service/common/src/testFixtures/java/org/apache/polaris/service/catalog/io/MeasuredFileIOFactory.java
 
b/service/common/src/testFixtures/java/org/apache/polaris/service/catalog/io/MeasuredFileIOFactory.java
index 3e831361f..abfb6e8c3 100644
--- 
a/service/common/src/testFixtures/java/org/apache/polaris/service/catalog/io/MeasuredFileIOFactory.java
+++ 
b/service/common/src/testFixtures/java/org/apache/polaris/service/catalog/io/MeasuredFileIOFactory.java
@@ -29,7 +29,7 @@ import java.util.Set;
 import java.util.function.Supplier;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.io.FileIO;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;

Reply via email to