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

adutra 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 579b271b8 Remove "giant" constructors from Handlers and Adapters 
(#3669)
579b271b8 is described below

commit 579b271b8c41cd26339bccfd771c1a591ad27039
Author: Alexandre Dutra <[email protected]>
AuthorDate: Fri Feb 6 13:35:11 2026 +0100

    Remove "giant" constructors from Handlers and Adapters (#3669)
    
    This PR attempts to reduce the "giant constructor" symptoms in catalog 
adapters and handlers.
    * The `CatalogHandler` subtypes are now `@PolarisImmutable` objects, and 
thus can leverage the generated builder to create instances without the need 
for big constructors.
    * The handlers are now produced by a new factory, which is a CDI 
request-scoped bean with direct field injection to avoid constructor injection.
---
 .../service/catalog/common/CatalogHandler.java     | 141 ++++++------
 .../generic/GenericTableCatalogAdapter.java        |  62 +-----
 .../generic/GenericTableCatalogHandler.java        |  50 ++---
 .../generic/GenericTableCatalogHandlerFactory.java |  55 +++++
 .../catalog/iceberg/IcebergCatalogAdapter.java     |  79 +------
 .../catalog/iceberg/IcebergCatalogHandler.java     | 246 ++++++++++-----------
 .../iceberg/IcebergCatalogHandlerFactory.java      |  78 +++++++
 .../catalog/policy/PolicyCatalogAdapter.java       |  68 ++----
 .../catalog/policy/PolicyCatalogHandler.java       |  80 +++----
 .../policy/PolicyCatalogHandlerFactory.java        |  47 ++++
 ...PolarisGenericTableCatalogHandlerAuthzTest.java |  31 ++-
 .../AbstractIcebergCatalogHandlerAuthzTest.java    | 138 ++++++------
 .../iceberg/IcebergAllowedLocationTest.java        |   6 +-
 .../catalog/iceberg/IcebergCatalogAdapterTest.java |   2 +-
 ...ebergCatalogHandlerFineGrainedDisabledTest.java |  35 +--
 .../policy/PolicyCatalogHandlerAuthzTest.java      |  18 +-
 .../org/apache/polaris/service/TestServices.java   |  43 +++-
 17 files changed, 583 insertions(+), 596 deletions(-)

diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
index 9d5778ac2..871de1ea9 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
@@ -20,7 +20,6 @@ package org.apache.polaris.service.catalog.common;
 
 import static 
org.apache.polaris.core.entity.PolarisEntitySubType.ICEBERG_TABLE;
 
-import jakarta.enterprise.inject.Instance;
 import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.List;
@@ -31,23 +30,23 @@ import org.apache.iceberg.exceptions.AlreadyExistsException;
 import org.apache.iceberg.exceptions.NoSuchNamespaceException;
 import org.apache.iceberg.exceptions.NoSuchTableException;
 import org.apache.iceberg.exceptions.NoSuchViewException;
-import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisPrincipal;
-import org.apache.polaris.core.catalog.ExternalCatalogFactory;
 import org.apache.polaris.core.catalog.PolarisCatalogHelpers;
 import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.context.CallContext;
-import org.apache.polaris.core.credentials.PolarisCredentialManager;
+import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
 import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.persistence.resolver.ResolverPath;
 import org.apache.polaris.core.persistence.resolver.ResolverStatus;
 import org.apache.polaris.service.types.PolicyIdentifier;
+import org.immutables.value.Value;
 
 /**
  * An ABC for catalog wrappers which provides authorize methods that should be 
called before a
@@ -56,48 +55,36 @@ import org.apache.polaris.service.types.PolicyIdentifier;
  */
 public abstract class CatalogHandler {
 
-  // Initialized in the authorize methods.
-  protected PolarisResolutionManifest resolutionManifest = null;
+  public abstract String catalogName();
+
+  public abstract PolarisPrincipal polarisPrincipal();
 
-  protected final ResolutionManifestFactory resolutionManifestFactory;
-  protected final String catalogName;
-  protected final PolarisAuthorizer authorizer;
-  protected final PolarisCredentialManager credentialManager;
-  protected final Instance<ExternalCatalogFactory> externalCatalogFactories;
-
-  protected final PolarisDiagnostics diagnostics;
-  protected final CallContext callContext;
-  protected final RealmConfig realmConfig;
-  protected final PolarisPrincipal polarisPrincipal;
-
-  public CatalogHandler(
-      PolarisDiagnostics diagnostics,
-      CallContext callContext,
-      ResolutionManifestFactory resolutionManifestFactory,
-      PolarisPrincipal principal,
-      String catalogName,
-      PolarisAuthorizer authorizer,
-      PolarisCredentialManager credentialManager,
-      Instance<ExternalCatalogFactory> externalCatalogFactories) {
-    this.diagnostics = diagnostics;
-    this.callContext = callContext;
-    this.realmConfig = callContext.getRealmConfig();
-    this.resolutionManifestFactory = resolutionManifestFactory;
-    this.catalogName = catalogName;
-    this.polarisPrincipal = principal;
-    this.authorizer = authorizer;
-    this.credentialManager = credentialManager;
-    this.externalCatalogFactories = externalCatalogFactories;
+  public abstract CallContext callContext();
+
+  @Value.Derived
+  public RealmConfig realmConfig() {
+    return callContext().getRealmConfig();
   }
 
-  protected PolarisCredentialManager getPolarisCredentialManager() {
-    return credentialManager;
+  @Value.Derived
+  public RealmContext realmContext() {
+    return callContext().getRealmContext();
   }
 
+  public abstract PolarisMetaStoreManager metaStoreManager();
+
+  public abstract ResolutionManifestFactory resolutionManifestFactory();
+
+  public abstract PolarisAuthorizer authorizer();
+
   protected PolarisResolutionManifest newResolutionManifest() {
-    return 
resolutionManifestFactory.createResolutionManifest(polarisPrincipal, 
catalogName);
+    return 
resolutionManifestFactory().createResolutionManifest(polarisPrincipal(), 
catalogName());
   }
 
+  // Initialized in the authorize methods.
+  @SuppressWarnings("immutables:incompat")
+  protected PolarisResolutionManifest resolutionManifest = null;
+
   /** Initialize the catalog once authorized. Called after all `authorize...` 
methods. */
   protected abstract void initializeCatalog();
 
@@ -152,12 +139,13 @@ public abstract class CatalogHandler {
     if (target == null) {
       throw new NoSuchNamespaceException("Namespace does not exist: %s", 
namespace);
     }
-    authorizer.authorizeOrThrow(
-        polarisPrincipal,
-        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-        op,
-        target,
-        null /* secondary */);
+    authorizer()
+        .authorizeOrThrow(
+            polarisPrincipal(),
+            resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+            op,
+            target,
+            null /* secondary */);
 
     initializeCatalog();
   }
@@ -184,12 +172,13 @@ public abstract class CatalogHandler {
     if (target == null) {
       throw new NoSuchNamespaceException("Namespace does not exist: %s", 
parentNamespace);
     }
-    authorizer.authorizeOrThrow(
-        polarisPrincipal,
-        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-        op,
-        target,
-        null /* secondary */);
+    authorizer()
+        .authorizeOrThrow(
+            polarisPrincipal(),
+            resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+            op,
+            target,
+            null /* secondary */);
 
     initializeCatalog();
   }
@@ -220,12 +209,13 @@ public abstract class CatalogHandler {
     if (target == null) {
       throw new NoSuchNamespaceException("Namespace does not exist: %s", 
namespace);
     }
-    authorizer.authorizeOrThrow(
-        polarisPrincipal,
-        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-        op,
-        target,
-        null /* secondary */);
+    authorizer()
+        .authorizeOrThrow(
+            polarisPrincipal(),
+            resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+            op,
+            target,
+            null /* secondary */);
 
     initializeCatalog();
   }
@@ -267,12 +257,13 @@ public abstract class CatalogHandler {
     }
 
     for (PolarisAuthorizableOperation op : ops) {
-      authorizer.authorizeOrThrow(
-          polarisPrincipal,
-          resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-          op,
-          target,
-          null /* secondary */);
+      authorizer()
+          .authorizeOrThrow(
+              polarisPrincipal(),
+              resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+              op,
+              target,
+              null /* secondary */);
     }
 
     initializeCatalog();
@@ -317,12 +308,13 @@ public abstract class CatalogHandler {
                                     : new NoSuchViewException(
                                         "View does not exist: %s", 
identifier)))
             .toList();
-    authorizer.authorizeOrThrow(
-        polarisPrincipal,
-        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-        op,
-        targets,
-        null /* secondaries */);
+    authorizer()
+        .authorizeOrThrow(
+            polarisPrincipal(),
+            resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+            op,
+            targets,
+            null /* secondaries */);
 
     initializeCatalog();
   }
@@ -384,12 +376,13 @@ public abstract class CatalogHandler {
         resolutionManifest.getResolvedPath(src, PolarisEntityType.TABLE_LIKE, 
subType, true);
     PolarisResolvedPathWrapper secondary =
         resolutionManifest.getResolvedPath(dst.namespace(), true);
-    authorizer.authorizeOrThrow(
-        polarisPrincipal,
-        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-        op,
-        target,
-        secondary);
+    authorizer()
+        .authorizeOrThrow(
+            polarisPrincipal(),
+            resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+            op,
+            target,
+            secondary);
 
     initializeCatalog();
   }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
index ba00e21a0..d467d52a5 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
@@ -19,23 +19,15 @@
 package org.apache.polaris.service.catalog.generic;
 
 import jakarta.enterprise.context.RequestScoped;
-import jakarta.enterprise.inject.Any;
-import jakarta.enterprise.inject.Instance;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.SecurityContext;
 import org.apache.iceberg.catalog.TableIdentifier;
-import org.apache.polaris.core.PolarisDiagnostics;
-import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisPrincipal;
-import org.apache.polaris.core.catalog.ExternalCatalogFactory;
 import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
-import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
-import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.service.catalog.CatalogPrefixParser;
 import 
org.apache.polaris.service.catalog.api.PolarisCatalogGenericTableApiService;
 import org.apache.polaris.service.catalog.common.CatalogAdapter;
@@ -43,68 +35,36 @@ import org.apache.polaris.service.config.ReservedProperties;
 import org.apache.polaris.service.types.CreateGenericTableRequest;
 import org.apache.polaris.service.types.ListGenericTablesResponse;
 import org.apache.polaris.service.types.LoadGenericTableResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RequestScoped
 public class GenericTableCatalogAdapter
     implements PolarisCatalogGenericTableApiService, CatalogAdapter {
 
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(GenericTableCatalogAdapter.class);
-
-  private final PolarisDiagnostics diagnostics;
   private final RealmContext realmContext;
   private final RealmConfig realmConfig;
-  private final CallContext callContext;
-  private final ResolutionManifestFactory resolutionManifestFactory;
-  private final PolarisMetaStoreManager metaStoreManager;
-  private final PolarisAuthorizer polarisAuthorizer;
   private final ReservedProperties reservedProperties;
   private final CatalogPrefixParser prefixParser;
-  private final PolarisCredentialManager polarisCredentialManager;
-  private final Instance<ExternalCatalogFactory> externalCatalogFactories;
+  private final GenericTableCatalogHandlerFactory handlerFactory;
 
   @Inject
   public GenericTableCatalogAdapter(
-      PolarisDiagnostics diagnostics,
-      RealmContext realmContext,
       CallContext callContext,
-      ResolutionManifestFactory resolutionManifestFactory,
-      PolarisMetaStoreManager metaStoreManager,
-      PolarisAuthorizer polarisAuthorizer,
       CatalogPrefixParser prefixParser,
       ReservedProperties reservedProperties,
-      PolarisCredentialManager polarisCredentialManager,
-      @Any Instance<ExternalCatalogFactory> externalCatalogFactories) {
-    this.diagnostics = diagnostics;
-    this.realmContext = realmContext;
-    this.callContext = callContext;
+      GenericTableCatalogHandlerFactory handlerFactory) {
+    this.realmContext = callContext.getRealmContext();
     this.realmConfig = callContext.getRealmConfig();
-    this.resolutionManifestFactory = resolutionManifestFactory;
-    this.metaStoreManager = metaStoreManager;
-    this.polarisAuthorizer = polarisAuthorizer;
     this.prefixParser = prefixParser;
     this.reservedProperties = reservedProperties;
-    this.polarisCredentialManager = polarisCredentialManager;
-    this.externalCatalogFactories = externalCatalogFactories;
+    this.handlerFactory = handlerFactory;
   }
 
-  private GenericTableCatalogHandler newHandlerWrapper(
-      SecurityContext securityContext, String prefix) {
+  private GenericTableCatalogHandler newHandler(SecurityContext 
securityContext, String prefix) {
     FeatureConfiguration.enforceFeatureEnabledOrThrow(
         realmConfig, FeatureConfiguration.ENABLE_GENERIC_TABLES);
     PolarisPrincipal principal = validatePrincipal(securityContext);
-
-    return new GenericTableCatalogHandler(
-        diagnostics,
-        callContext,
-        resolutionManifestFactory,
-        metaStoreManager,
-        principal,
-        prefixParser.prefixToCatalogName(prefix),
-        polarisAuthorizer,
-        polarisCredentialManager,
-        externalCatalogFactories);
+    String catalogName = prefixParser.prefixToCatalogName(prefix);
+    return handlerFactory.createHandler(catalogName, principal);
   }
 
   @Override
@@ -114,7 +74,7 @@ public class GenericTableCatalogAdapter
       CreateGenericTableRequest createGenericTableRequest,
       RealmContext realmContext,
       SecurityContext securityContext) {
-    GenericTableCatalogHandler handler = newHandlerWrapper(securityContext, 
prefix);
+    GenericTableCatalogHandler handler = newHandler(securityContext, prefix);
     LoadGenericTableResponse response =
         handler.createGenericTable(
             TableIdentifier.of(decodeNamespace(namespace), 
createGenericTableRequest.getName()),
@@ -133,7 +93,7 @@ public class GenericTableCatalogAdapter
       String genericTable,
       RealmContext realmContext,
       SecurityContext securityContext) {
-    GenericTableCatalogHandler handler = newHandlerWrapper(securityContext, 
prefix);
+    GenericTableCatalogHandler handler = newHandler(securityContext, prefix);
     handler.dropGenericTable(TableIdentifier.of(decodeNamespace(namespace), 
genericTable));
     return Response.noContent().build();
   }
@@ -146,7 +106,7 @@ public class GenericTableCatalogAdapter
       Integer pageSize,
       RealmContext realmContext,
       SecurityContext securityContext) {
-    GenericTableCatalogHandler handler = newHandlerWrapper(securityContext, 
prefix);
+    GenericTableCatalogHandler handler = newHandler(securityContext, prefix);
     ListGenericTablesResponse response = 
handler.listGenericTables(decodeNamespace(namespace));
     return Response.ok(response).build();
   }
@@ -158,7 +118,7 @@ public class GenericTableCatalogAdapter
       String genericTable,
       RealmContext realmContext,
       SecurityContext securityContext) {
-    GenericTableCatalogHandler handler = newHandlerWrapper(securityContext, 
prefix);
+    GenericTableCatalogHandler handler = newHandler(securityContext, prefix);
     LoadGenericTableResponse response =
         
handler.loadGenericTable(TableIdentifier.of(decodeNamespace(namespace), 
genericTable));
     return Response.ok(response).build();
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
index 7cd906f9a..0acae1d31 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
@@ -24,22 +24,17 @@ import java.util.LinkedHashSet;
 import java.util.Map;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
-import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
-import org.apache.polaris.core.auth.PolarisAuthorizer;
-import org.apache.polaris.core.auth.PolarisPrincipal;
 import org.apache.polaris.core.catalog.ExternalCatalogFactory;
 import org.apache.polaris.core.catalog.GenericTableCatalog;
 import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.connection.ConnectionType;
-import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.table.GenericTableEntity;
-import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
-import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
+import org.apache.polaris.immutables.PolarisImmutable;
 import org.apache.polaris.service.catalog.common.CatalogHandler;
 import org.apache.polaris.service.types.GenericTable;
 import org.apache.polaris.service.types.ListGenericTablesResponse;
@@ -47,34 +42,16 @@ import 
org.apache.polaris.service.types.LoadGenericTableResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class GenericTableCatalogHandler extends CatalogHandler {
+@PolarisImmutable
+@SuppressWarnings("immutables:incompat")
+public abstract class GenericTableCatalogHandler extends CatalogHandler {
   private static final Logger LOGGER = 
LoggerFactory.getLogger(GenericTableCatalogHandler.class);
 
-  private PolarisMetaStoreManager metaStoreManager;
+  protected abstract PolarisCredentialManager credentialManager();
 
-  private GenericTableCatalog genericTableCatalog;
+  protected abstract Instance<ExternalCatalogFactory> 
externalCatalogFactories();
 
-  public GenericTableCatalogHandler(
-      PolarisDiagnostics diagnostics,
-      CallContext callContext,
-      ResolutionManifestFactory resolutionManifestFactory,
-      PolarisMetaStoreManager metaStoreManager,
-      PolarisPrincipal principal,
-      String catalogName,
-      PolarisAuthorizer authorizer,
-      PolarisCredentialManager polarisCredentialManager,
-      Instance<ExternalCatalogFactory> externalCatalogFactories) {
-    super(
-        diagnostics,
-        callContext,
-        resolutionManifestFactory,
-        principal,
-        catalogName,
-        authorizer,
-        polarisCredentialManager,
-        externalCatalogFactories);
-    this.metaStoreManager = metaStoreManager;
-  }
+  private GenericTableCatalog genericTableCatalog;
 
   @Override
   protected void initializeCatalog() {
@@ -87,7 +64,7 @@ public class GenericTableCatalogHandler extends 
CatalogHandler {
           .addKeyValue("remoteUrl", connectionConfigInfoDpo.getUri())
           .log("Initializing federated catalog");
       FeatureConfiguration.enforceFeatureEnabledOrThrow(
-          realmConfig, FeatureConfiguration.ENABLE_CATALOG_FEDERATION);
+          realmConfig(), FeatureConfiguration.ENABLE_CATALOG_FEDERATION);
 
       GenericTableCatalog federatedCatalog;
       ConnectionType connectionType =
@@ -95,8 +72,8 @@ public class GenericTableCatalogHandler extends 
CatalogHandler {
 
       // Use the unified factory pattern for all external catalog types
       Instance<ExternalCatalogFactory> externalCatalogFactory =
-          externalCatalogFactories.select(
-              Identifier.Literal.of(connectionType.getFactoryIdentifier()));
+          externalCatalogFactories()
+              
.select(Identifier.Literal.of(connectionType.getFactoryIdentifier()));
       if (externalCatalogFactory.isResolvable()) {
         // Pass through catalog properties (e.g., rest.client.proxy.*, timeout 
settings)
         Map<String, String> catalogProperties = 
resolvedCatalogEntity.getPropertiesAsMap();
@@ -104,7 +81,7 @@ public class GenericTableCatalogHandler extends 
CatalogHandler {
             externalCatalogFactory
                 .get()
                 .createGenericCatalog(
-                    connectionConfigInfoDpo, getPolarisCredentialManager(), 
catalogProperties);
+                    connectionConfigInfoDpo, credentialManager(), 
catalogProperties);
       } else {
         throw new UnsupportedOperationException(
             "External catalog factory for type '" + connectionType + "' is 
unavailable.");
@@ -113,8 +90,9 @@ public class GenericTableCatalogHandler extends 
CatalogHandler {
     } else {
       LOGGER.atInfo().log("Initializing non-federated catalog");
       this.genericTableCatalog =
-          new PolarisGenericTableCatalog(metaStoreManager, callContext, 
this.resolutionManifest);
-      this.genericTableCatalog.initialize(catalogName, Map.of());
+          new PolarisGenericTableCatalog(
+              metaStoreManager(), callContext(), this.resolutionManifest);
+      this.genericTableCatalog.initialize(catalogName(), Map.of());
     }
   }
 
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandlerFactory.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandlerFactory.java
new file mode 100644
index 000000000..d0f42ca55
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandlerFactory.java
@@ -0,0 +1,55 @@
+/*
+ * 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.catalog.generic;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.Instance;
+import jakarta.inject.Inject;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.catalog.ExternalCatalogFactory;
+import org.apache.polaris.core.context.CallContext;
+import org.apache.polaris.core.credentials.PolarisCredentialManager;
+import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
+import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
+
+@RequestScoped
+public class GenericTableCatalogHandlerFactory {
+
+  @Inject CallContext callContext;
+  @Inject ResolutionManifestFactory resolutionManifestFactory;
+  @Inject PolarisMetaStoreManager metaStoreManager;
+  @Inject PolarisAuthorizer authorizer;
+  @Inject PolarisCredentialManager credentialManager;
+  @Inject @Any Instance<ExternalCatalogFactory> externalCatalogFactories;
+
+  public GenericTableCatalogHandler createHandler(String catalogName, 
PolarisPrincipal principal) {
+    return ImmutableGenericTableCatalogHandler.builder()
+        .catalogName(catalogName)
+        .polarisPrincipal(principal)
+        .callContext(callContext)
+        .resolutionManifestFactory(resolutionManifestFactory)
+        .metaStoreManager(metaStoreManager)
+        .authorizer(authorizer)
+        .credentialManager(credentialManager)
+        .externalCatalogFactories(externalCatalogFactories)
+        .build();
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
index e4582416b..a240186d0 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
@@ -24,8 +24,6 @@ import static 
org.apache.polaris.service.catalog.validation.IcebergPropertiesVal
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import jakarta.enterprise.context.RequestScoped;
-import jakarta.enterprise.inject.Any;
-import jakarta.enterprise.inject.Instance;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.Response;
@@ -51,27 +49,17 @@ import 
org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
 import org.apache.iceberg.rest.requests.UpdateTableRequest;
 import org.apache.iceberg.rest.responses.ImmutableLoadCredentialsResponse;
 import org.apache.iceberg.rest.responses.LoadTableResponse;
-import org.apache.polaris.core.PolarisDiagnostics;
-import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisPrincipal;
-import org.apache.polaris.core.catalog.ExternalCatalogFactory;
 import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
-import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
-import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
-import org.apache.polaris.core.persistence.resolver.ResolverFactory;
 import org.apache.polaris.core.rest.PolarisResourcePaths;
 import org.apache.polaris.service.catalog.AccessDelegationMode;
 import org.apache.polaris.service.catalog.CatalogPrefixParser;
 import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService;
 import 
org.apache.polaris.service.catalog.api.IcebergRestConfigurationApiService;
 import org.apache.polaris.service.catalog.common.CatalogAdapter;
-import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
 import org.apache.polaris.service.config.ReservedProperties;
-import org.apache.polaris.service.context.catalog.CallContextCatalogFactory;
-import org.apache.polaris.service.events.EventAttributeMap;
 import org.apache.polaris.service.http.IcebergHttpUtil;
 import org.apache.polaris.service.http.IfNoneMatch;
 import org.apache.polaris.service.reporting.PolarisMetricsReporter;
@@ -91,62 +79,29 @@ public class IcebergCatalogAdapter
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(IcebergCatalogAdapter.class);
 
-  private final PolarisDiagnostics diagnostics;
   private final RealmContext realmContext;
-  private final CallContext callContext;
   private final RealmConfig realmConfig;
-  private final CallContextCatalogFactory catalogFactory;
-  private final ResolutionManifestFactory resolutionManifestFactory;
-  private final ResolverFactory resolverFactory;
-  private final PolarisMetaStoreManager metaStoreManager;
-  private final PolarisCredentialManager credentialManager;
-  private final PolarisAuthorizer polarisAuthorizer;
   private final CatalogPrefixParser prefixParser;
   private final ReservedProperties reservedProperties;
-  private final CatalogHandlerUtils catalogHandlerUtils;
-  private final Instance<ExternalCatalogFactory> externalCatalogFactories;
-  private final StorageAccessConfigProvider storageAccessConfigProvider;
   private final PolarisMetricsReporter metricsReporter;
   private final Clock clock;
-  private final EventAttributeMap eventAttributeMap;
+  private final IcebergCatalogHandlerFactory handlerFactory;
 
   @Inject
   public IcebergCatalogAdapter(
-      PolarisDiagnostics diagnostics,
-      RealmContext realmContext,
       CallContext callContext,
-      CallContextCatalogFactory catalogFactory,
-      ResolverFactory resolverFactory,
-      ResolutionManifestFactory resolutionManifestFactory,
-      PolarisMetaStoreManager metaStoreManager,
-      PolarisCredentialManager credentialManager,
-      PolarisAuthorizer polarisAuthorizer,
       CatalogPrefixParser prefixParser,
       ReservedProperties reservedProperties,
-      CatalogHandlerUtils catalogHandlerUtils,
-      @Any Instance<ExternalCatalogFactory> externalCatalogFactories,
-      StorageAccessConfigProvider storageAccessConfigProvider,
       PolarisMetricsReporter metricsReporter,
       Clock clock,
-      EventAttributeMap eventAttributeMap) {
-    this.diagnostics = diagnostics;
-    this.realmContext = realmContext;
-    this.callContext = callContext;
+      IcebergCatalogHandlerFactory handlerFactory) {
+    this.realmContext = callContext.getRealmContext();
     this.realmConfig = callContext.getRealmConfig();
-    this.catalogFactory = catalogFactory;
-    this.resolutionManifestFactory = resolutionManifestFactory;
-    this.resolverFactory = resolverFactory;
-    this.metaStoreManager = metaStoreManager;
-    this.credentialManager = credentialManager;
-    this.polarisAuthorizer = polarisAuthorizer;
     this.prefixParser = prefixParser;
     this.reservedProperties = reservedProperties;
-    this.catalogHandlerUtils = catalogHandlerUtils;
-    this.externalCatalogFactories = externalCatalogFactories;
-    this.storageAccessConfigProvider = storageAccessConfigProvider;
     this.metricsReporter = metricsReporter;
     this.clock = clock;
-    this.eventAttributeMap = eventAttributeMap;
+    this.handlerFactory = handlerFactory;
   }
 
   /**
@@ -165,7 +120,7 @@ public class IcebergCatalogAdapter
       SecurityContext securityContext,
       String catalogName,
       Function<IcebergCatalogHandler, Response> action) {
-    try (IcebergCatalogHandler wrapper = newHandlerWrapper(securityContext, 
catalogName)) {
+    try (IcebergCatalogHandler wrapper = newHandler(securityContext, 
catalogName)) {
       return action.apply(wrapper);
     } catch (RuntimeException e) {
       LOGGER.debug("RuntimeException while operating on catalog. Propagating 
to caller.", e);
@@ -177,26 +132,9 @@ public class IcebergCatalogAdapter
   }
 
   @VisibleForTesting
-  IcebergCatalogHandler newHandlerWrapper(SecurityContext securityContext, 
String catalogName) {
+  IcebergCatalogHandler newHandler(SecurityContext securityContext, String 
catalogName) {
     PolarisPrincipal principal = validatePrincipal(securityContext);
-
-    return new IcebergCatalogHandler(
-        diagnostics,
-        callContext,
-        prefixParser,
-        resolverFactory,
-        resolutionManifestFactory,
-        metaStoreManager,
-        credentialManager,
-        principal,
-        catalogFactory,
-        catalogName,
-        polarisAuthorizer,
-        reservedProperties,
-        catalogHandlerUtils,
-        externalCatalogFactories,
-        storageAccessConfigProvider,
-        eventAttributeMap);
+    return handlerFactory.createHandler(catalogName, principal);
   }
 
   @Override
@@ -759,6 +697,9 @@ public class IcebergCatalogAdapter
   @Override
   public Response getConfig(
       String warehouse, RealmContext realmContext, SecurityContext 
securityContext) {
+    if (warehouse == null) {
+      throw new BadRequestException("Please specify a warehouse");
+    }
     return withCatalogByName(
         securityContext, warehouse, catalog -> 
Response.ok(catalog.getConfig()).build());
   }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
index 3a4cbf8d3..3f70f4a72 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
@@ -84,20 +84,16 @@ import org.apache.iceberg.rest.responses.LoadViewResponse;
 import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
-import org.apache.polaris.core.auth.PolarisAuthorizer;
-import org.apache.polaris.core.auth.PolarisPrincipal;
 import org.apache.polaris.core.catalog.ExternalCatalogFactory;
 import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.connection.ConnectionType;
-import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PolarisEntity;
 import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
-import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
 import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
 import 
org.apache.polaris.core.persistence.TransactionWorkspaceMetaStoreManager;
@@ -105,7 +101,6 @@ import 
org.apache.polaris.core.persistence.dao.entity.EntitiesResult;
 import org.apache.polaris.core.persistence.dao.entity.EntityWithPath;
 import org.apache.polaris.core.persistence.pagination.Page;
 import org.apache.polaris.core.persistence.pagination.PageToken;
-import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.persistence.resolver.Resolver;
 import org.apache.polaris.core.persistence.resolver.ResolverFactory;
 import org.apache.polaris.core.persistence.resolver.ResolverStatus;
@@ -113,6 +108,7 @@ import org.apache.polaris.core.rest.PolarisEndpoints;
 import org.apache.polaris.core.storage.PolarisStorageActions;
 import org.apache.polaris.core.storage.StorageAccessConfig;
 import org.apache.polaris.core.storage.StorageUtil;
+import org.apache.polaris.immutables.PolarisImmutable;
 import org.apache.polaris.service.catalog.AccessDelegationMode;
 import org.apache.polaris.service.catalog.CatalogPrefixParser;
 import org.apache.polaris.service.catalog.SupportsNotifications;
@@ -144,7 +140,9 @@ import org.slf4j.LoggerFactory;
  * model objects used in this layer to still benefit from the shared 
implementation of
  * authorization-aware catalog protocols.
  */
-public class IcebergCatalogHandler extends CatalogHandler implements 
AutoCloseable {
+@PolarisImmutable
+@SuppressWarnings("immutables:incompat")
+public abstract class IcebergCatalogHandler extends CatalogHandler implements 
AutoCloseable {
   private static final Logger LOGGER = 
LoggerFactory.getLogger(IcebergCatalogHandler.class);
 
   private static final Set<Endpoint> DEFAULT_ENDPOINTS =
@@ -178,63 +176,43 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
           .add(Endpoint.V1_RENAME_VIEW)
           .build();
 
-  private final CatalogPrefixParser prefixParser;
-  private final ResolverFactory resolverFactory;
-  private final PolarisMetaStoreManager metaStoreManager;
-  private final CallContextCatalogFactory catalogFactory;
-  private final ReservedProperties reservedProperties;
-  private final CatalogHandlerUtils catalogHandlerUtils;
-  private final StorageAccessConfigProvider storageAccessConfigProvider;
-  private final EventAttributeMap eventAttributeMap;
+  protected abstract PolarisDiagnostics diagnostics();
+
+  protected abstract PolarisCredentialManager credentialManager();
+
+  protected abstract Instance<ExternalCatalogFactory> 
externalCatalogFactories();
+
+  protected abstract CatalogPrefixParser prefixParser();
+
+  protected abstract ResolverFactory resolverFactory();
+
+  protected abstract CallContextCatalogFactory catalogFactory();
+
+  protected abstract ReservedProperties reservedProperties();
+
+  protected abstract CatalogHandlerUtils catalogHandlerUtils();
+
+  protected abstract StorageAccessConfigProvider storageAccessConfigProvider();
+
+  protected abstract EventAttributeMap eventAttributeMap();
 
   // Catalog instance will be initialized after authorizing resolver 
successfully resolves
   // the catalog entity.
-  protected Catalog baseCatalog = null;
-  protected SupportsNamespaces namespaceCatalog = null;
-  protected ViewCatalog viewCatalog = null;
-
-  public static final String SNAPSHOTS_ALL = "all";
-  public static final String SNAPSHOTS_REFS = "refs";
-
-  public IcebergCatalogHandler(
-      PolarisDiagnostics diagnostics,
-      CallContext callContext,
-      CatalogPrefixParser prefixParser,
-      ResolverFactory resolverFactory,
-      ResolutionManifestFactory resolutionManifestFactory,
-      PolarisMetaStoreManager metaStoreManager,
-      PolarisCredentialManager credentialManager,
-      PolarisPrincipal principal,
-      CallContextCatalogFactory catalogFactory,
-      String catalogName,
-      PolarisAuthorizer authorizer,
-      ReservedProperties reservedProperties,
-      CatalogHandlerUtils catalogHandlerUtils,
-      Instance<ExternalCatalogFactory> externalCatalogFactories,
-      StorageAccessConfigProvider storageAccessConfigProvider,
-      EventAttributeMap eventAttributeMap) {
-    super(
-        diagnostics,
-        callContext,
-        resolutionManifestFactory,
-        principal,
-        catalogName,
-        authorizer,
-        credentialManager,
-        externalCatalogFactories);
-    this.prefixParser = prefixParser;
-    this.resolverFactory = resolverFactory;
-    this.metaStoreManager = metaStoreManager;
-    this.catalogFactory = catalogFactory;
-    this.reservedProperties = reservedProperties;
-    this.catalogHandlerUtils = catalogHandlerUtils;
-    this.storageAccessConfigProvider = storageAccessConfigProvider;
-    this.eventAttributeMap = eventAttributeMap;
-  }
+  @SuppressWarnings("immutables:incompat")
+  private Catalog baseCatalog = null;
+
+  @SuppressWarnings("immutables:incompat")
+  private SupportsNamespaces namespaceCatalog = null;
+
+  @SuppressWarnings("immutables:incompat")
+  private ViewCatalog viewCatalog = null;
+
+  private static final String SNAPSHOTS_ALL = "all";
+  private static final String SNAPSHOTS_REFS = "refs";
 
   private CatalogEntity getResolvedCatalogEntity() {
     CatalogEntity catalogEntity = 
resolutionManifest.getResolvedCatalogEntity();
-    diagnostics.checkNotNull(catalogEntity, "No catalog available");
+    diagnostics().checkNotNull(catalogEntity, "No catalog available");
     return catalogEntity;
   }
 
@@ -260,7 +238,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
   }
 
   private boolean shouldDecodeToken() {
-    return realmConfig.getConfig(LIST_PAGINATION_ENABLED, 
getResolvedCatalogEntity());
+    return realmConfig().getConfig(LIST_PAGINATION_ENABLED, 
getResolvedCatalogEntity());
   }
 
   public ListNamespacesResponse listNamespaces(
@@ -276,7 +254,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
           .nextPageToken(results.encodedResponseToken())
           .build();
     } else {
-      return catalogHandlerUtils.listNamespaces(namespaceCatalog, parent, 
pageToken, pageSize);
+      return catalogHandlerUtils().listNamespaces(namespaceCatalog, parent, 
pageToken, pageSize);
     }
   }
 
@@ -291,7 +269,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
           .addKeyValue("remoteUrl", connectionConfigInfoDpo.getUri())
           .log("Initializing federated catalog");
       FeatureConfiguration.enforceFeatureEnabledOrThrow(
-          realmConfig, FeatureConfiguration.ENABLE_CATALOG_FEDERATION);
+          realmConfig(), FeatureConfiguration.ENABLE_CATALOG_FEDERATION);
 
       Catalog federatedCatalog;
       ConnectionType connectionType =
@@ -299,8 +277,8 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
 
       // Use the unified factory pattern for all external catalog types
       Instance<ExternalCatalogFactory> externalCatalogFactory =
-          externalCatalogFactories.select(
-              Identifier.Literal.of(connectionType.getFactoryIdentifier()));
+          externalCatalogFactories()
+              
.select(Identifier.Literal.of(connectionType.getFactoryIdentifier()));
       if (externalCatalogFactory.isResolvable()) {
         // Pass through catalog properties (e.g., rest.client.proxy.*, timeout 
settings)
         // to the external catalog factory for configuration of the underlying 
HTTP client
@@ -308,8 +286,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
         federatedCatalog =
             externalCatalogFactory
                 .get()
-                .createCatalog(
-                    connectionConfigInfoDpo, getPolarisCredentialManager(), 
catalogProperties);
+                .createCatalog(connectionConfigInfoDpo, credentialManager(), 
catalogProperties);
       } else {
         throw new UnsupportedOperationException(
             "External catalog factory for type '" + connectionType + "' is 
unavailable.");
@@ -321,7 +298,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
       this.baseCatalog = federatedCatalog;
     } else {
       LOGGER.atInfo().log("Initializing non-federated catalog");
-      this.baseCatalog = 
catalogFactory.createCallContextCatalog(resolutionManifest);
+      this.baseCatalog = 
catalogFactory().createCallContextCatalog(resolutionManifest);
     }
     this.namespaceCatalog =
         (baseCatalog instanceof SupportsNamespaces) ? (SupportsNamespaces) 
baseCatalog : null;
@@ -332,7 +309,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     PolarisAuthorizableOperation op = 
PolarisAuthorizableOperation.LIST_NAMESPACES;
     authorizeBasicNamespaceOperationOrThrow(op, parent);
 
-    return catalogHandlerUtils.listNamespaces(namespaceCatalog, parent);
+    return catalogHandlerUtils().listNamespaces(namespaceCatalog, parent);
   }
 
   public CreateNamespaceResponse createNamespace(CreateNamespaceRequest 
request) {
@@ -356,19 +333,20 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
       // retrieve the latest namespace metadata for the duration of the 
CreateNamespace
       // operation, even if the entityVersion and/or grantsVersion update in 
the interim.
       namespaceCatalog.createNamespace(
-          namespace, 
reservedProperties.removeReservedProperties(request.properties()));
+          namespace, 
reservedProperties().removeReservedProperties(request.properties()));
       Map<String, String> filteredProperties =
-          reservedProperties.removeReservedProperties(
-              resolutionManifest
-                  .getPassthroughResolvedPath(namespace)
-                  .getRawLeafEntity()
-                  .getPropertiesAsMap());
+          reservedProperties()
+              .removeReservedProperties(
+                  resolutionManifest
+                      .getPassthroughResolvedPath(namespace)
+                      .getRawLeafEntity()
+                      .getPropertiesAsMap());
       return CreateNamespaceResponse.builder()
           .withNamespace(namespace)
           .setProperties(filteredProperties)
           .build();
     } else {
-      return catalogHandlerUtils.createNamespace(namespaceCatalog, request);
+      return catalogHandlerUtils().createNamespace(namespaceCatalog, request);
     }
   }
 
@@ -376,7 +354,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     PolarisAuthorizableOperation op = 
PolarisAuthorizableOperation.LOAD_NAMESPACE_METADATA;
     authorizeBasicNamespaceOperationOrThrow(op, namespace);
 
-    return catalogHandlerUtils.loadNamespace(namespaceCatalog, namespace);
+    return catalogHandlerUtils().loadNamespace(namespaceCatalog, namespace);
   }
 
   public void namespaceExists(Namespace namespace) {
@@ -391,14 +369,14 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     authorizeBasicNamespaceOperationOrThrow(op, namespace);
 
     // TODO: Just skip CatalogHandlers for this one maybe
-    catalogHandlerUtils.loadNamespace(namespaceCatalog, namespace);
+    catalogHandlerUtils().loadNamespace(namespaceCatalog, namespace);
   }
 
   public void dropNamespace(Namespace namespace) {
     PolarisAuthorizableOperation op = 
PolarisAuthorizableOperation.DROP_NAMESPACE;
     authorizeBasicNamespaceOperationOrThrow(op, namespace);
 
-    catalogHandlerUtils.dropNamespace(namespaceCatalog, namespace);
+    catalogHandlerUtils().dropNamespace(namespaceCatalog, namespace);
   }
 
   public UpdateNamespacePropertiesResponse updateNamespaceProperties(
@@ -406,7 +384,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     PolarisAuthorizableOperation op = 
PolarisAuthorizableOperation.UPDATE_NAMESPACE_PROPERTIES;
     authorizeBasicNamespaceOperationOrThrow(op, namespace);
 
-    return catalogHandlerUtils.updateNamespaceProperties(namespaceCatalog, 
namespace, request);
+    return catalogHandlerUtils().updateNamespaceProperties(namespaceCatalog, 
namespace, request);
   }
 
   public ListTablesResponse listTables(Namespace namespace, String pageToken, 
Integer pageSize) {
@@ -421,7 +399,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
           .nextPageToken(results.encodedResponseToken())
           .build();
     } else {
-      return catalogHandlerUtils.listTables(baseCatalog, namespace, pageToken, 
pageSize);
+      return catalogHandlerUtils().listTables(baseCatalog, namespace, 
pageToken, pageSize);
     }
   }
 
@@ -429,7 +407,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_TABLES;
     authorizeBasicNamespaceOperationOrThrow(op, namespace);
 
-    return catalogHandlerUtils.listTables(baseCatalog, namespace);
+    return catalogHandlerUtils().listTables(baseCatalog, namespace);
   }
 
   /**
@@ -497,7 +475,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
 
     Map<String, String> properties = Maps.newHashMap();
     properties.put("created-at", 
OffsetDateTime.now(ZoneOffset.UTC).toString());
-    
properties.putAll(reservedProperties.removeReservedProperties(request.properties()));
+    
properties.putAll(reservedProperties().removeReservedProperties(request.properties()));
 
     Table table =
         baseCatalog
@@ -538,7 +516,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
 
     Map<String, String> properties = Maps.newHashMap();
     properties.put("created-at", 
OffsetDateTime.now(ZoneOffset.UTC).toString());
-    
properties.putAll(reservedProperties.removeReservedProperties(request.properties()));
+    
properties.putAll(reservedProperties().removeReservedProperties(request.properties()));
 
     String location;
     if (request.location() != null) {
@@ -638,7 +616,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     authorizeCreateTableLikeUnderNamespaceOperationOrThrow(
         op, TableIdentifier.of(namespace, request.name()));
 
-    return catalogHandlerUtils.registerTable(baseCatalog, namespace, request);
+    return catalogHandlerUtils().registerTable(baseCatalog, namespace, 
request);
   }
 
   public boolean sendNotification(TableIdentifier identifier, 
NotificationRequest request) {
@@ -854,8 +832,8 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     }
 
     if (baseCatalog instanceof IcebergCatalog
-        || realmConfig.getConfig(
-            ALLOW_FEDERATED_CATALOGS_CREDENTIAL_VENDING, 
getResolvedCatalogEntity())) {
+        || realmConfig()
+            .getConfig(ALLOW_FEDERATED_CATALOGS_CREDENTIAL_VENDING, 
getResolvedCatalogEntity())) {
 
       Set<String> tableLocations = 
StorageUtil.getLocationsUsedByTable(tableMetadata);
 
@@ -865,12 +843,13 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
       }
 
       StorageAccessConfig storageAccessConfig =
-          storageAccessConfigProvider.getStorageAccessConfig(
-              tableIdentifier,
-              tableLocations,
-              actions,
-              refreshCredentialsEndpoint,
-              resolvedStoragePath);
+          storageAccessConfigProvider()
+              .getStorageAccessConfig(
+                  tableIdentifier,
+                  tableLocations,
+                  actions,
+                  refreshCredentialsEndpoint,
+                  resolvedStoragePath);
       Map<String, String> credentialConfig = storageAccessConfig.credentials();
       if (delegationModes.contains(VENDED_CREDENTIALS)) {
         if (!credentialConfig.isEmpty()) {
@@ -882,7 +861,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
                   .build());
         } else {
           Boolean skipCredIndirection =
-              
realmConfig.getConfig(FeatureConfiguration.SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION);
+              
realmConfig().getConfig(FeatureConfiguration.SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION);
           Preconditions.checkArgument(
               !storageAccessConfig.supportsCredentialVending() || 
skipCredIndirection,
               "Credential vending was requested for table %s, but no 
credentials are available",
@@ -903,7 +882,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     try {
       // Delegate to common validation logic
       CatalogUtils.validateLocationsForTableLike(
-          realmConfig, tableIdentifier, tableLocations, resolvedStoragePath);
+          realmConfig(), tableIdentifier, tableLocations, resolvedStoragePath);
 
       LOGGER
           .atInfo()
@@ -963,8 +942,8 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     if (catalog.isStaticFacade()) {
       throw new BadRequestException("Cannot update table on static-facade 
external catalogs.");
     }
-    return catalogHandlerUtils.updateTable(
-        baseCatalog, tableIdentifier, applyUpdateFilters(request));
+    return catalogHandlerUtils()
+        .updateTable(baseCatalog, tableIdentifier, 
applyUpdateFilters(request));
   }
 
   public LoadTableResponse updateTableForStagedCreate(
@@ -976,8 +955,8 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     if (catalog.isStaticFacade()) {
       throw new BadRequestException("Cannot update table on static-facade 
external catalogs.");
     }
-    return catalogHandlerUtils.updateTable(
-        baseCatalog, tableIdentifier, applyUpdateFilters(request));
+    return catalogHandlerUtils()
+        .updateTable(baseCatalog, tableIdentifier, 
applyUpdateFilters(request));
   }
 
   public void dropTableWithoutPurge(TableIdentifier tableIdentifier) {
@@ -985,7 +964,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     authorizeBasicTableLikeOperationOrThrow(
         op, PolarisEntitySubType.ICEBERG_TABLE, tableIdentifier);
 
-    catalogHandlerUtils.dropTable(baseCatalog, tableIdentifier);
+    catalogHandlerUtils().dropTable(baseCatalog, tableIdentifier);
   }
 
   public void dropTableWithPurge(TableIdentifier tableIdentifier) {
@@ -997,7 +976,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     if (catalog.isStaticFacade()) {
       throw new BadRequestException("Cannot drop table on static-facade 
external catalogs.");
     }
-    catalogHandlerUtils.purgeTable(baseCatalog, tableIdentifier);
+    catalogHandlerUtils().purgeTable(baseCatalog, tableIdentifier);
   }
 
   public void tableExists(TableIdentifier tableIdentifier) {
@@ -1006,7 +985,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
         op, PolarisEntitySubType.ICEBERG_TABLE, tableIdentifier);
 
     // TODO: Just skip CatalogHandlers for this one maybe
-    catalogHandlerUtils.loadTable(baseCatalog, tableIdentifier);
+    catalogHandlerUtils().loadTable(baseCatalog, tableIdentifier);
   }
 
   public void renameTable(RenameTableRequest request) {
@@ -1018,7 +997,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     if (catalog.isStaticFacade()) {
       throw new BadRequestException("Cannot rename table on static-facade 
external catalogs.");
     }
-    catalogHandlerUtils.renameTable(baseCatalog, request);
+    catalogHandlerUtils().renameTable(baseCatalog, request);
   }
 
   public void commitTransaction(CommitTransactionRequest 
commitTransactionRequest) {
@@ -1048,7 +1027,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     // only go into an in-memory collection that we can commit as a single 
atomic unit after all
     // validations.
     TransactionWorkspaceMetaStoreManager transactionMetaStoreManager =
-        new TransactionWorkspaceMetaStoreManager(diagnostics, 
metaStoreManager);
+        new TransactionWorkspaceMetaStoreManager(diagnostics(), 
metaStoreManager());
     ((IcebergCatalog) 
baseCatalog).setMetaStoreManager(transactionMetaStoreManager);
 
     List<TableMetadata> tableMetadataObjs = new ArrayList<>();
@@ -1083,8 +1062,9 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
                         // expose the concept of being able to read 
uncommitted updates.
                         if (singleUpdate instanceof MetadataUpdate.SetLocation 
setLocation) {
                           if 
(!currentMetadata.location().equals(setLocation.location())
-                              && !realmConfig.getConfig(
-                                  
FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) {
+                              && !realmConfig()
+                                  .getConfig(
+                                      
FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) {
                             throw new BadRequestException(
                                 "Unsupported operation: commitTransaction 
containing SetLocation"
                                     + " for table '%s' and new location '%s'",
@@ -1109,8 +1089,9 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     // Commit the collected updates in a single atomic operation
     List<EntityWithPath> pendingUpdates = 
transactionMetaStoreManager.getPendingUpdates();
     EntitiesResult result =
-        metaStoreManager.updateEntitiesPropertiesIfNotChanged(
-            callContext.getPolarisCallContext(), pendingUpdates);
+        metaStoreManager()
+            .updateEntitiesPropertiesIfNotChanged(
+                callContext().getPolarisCallContext(), pendingUpdates);
     if (!result.isSuccess()) {
       // TODO: Retries and server-side cleanup on failure, review possible 
exceptions
       throw new CommitFailedException(
@@ -1118,7 +1099,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
           result.getReturnStatus(), result.getExtraInformation());
     }
 
-    eventAttributeMap.put(EventAttributes.TABLE_METADATAS, tableMetadataObjs);
+    eventAttributeMap().put(EventAttributes.TABLE_METADATAS, 
tableMetadataObjs);
   }
 
   public ListTablesResponse listViews(Namespace namespace, String pageToken, 
Integer pageSize) {
@@ -1133,7 +1114,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
           .nextPageToken(results.encodedResponseToken())
           .build();
     } else if (baseCatalog instanceof ViewCatalog viewCatalog) {
-      return catalogHandlerUtils.listViews(viewCatalog, namespace, pageToken, 
pageSize);
+      return catalogHandlerUtils().listViews(viewCatalog, namespace, 
pageToken, pageSize);
     } else {
       throw new BadRequestException(
           "Unsupported operation: listViews with baseCatalog type: %s",
@@ -1145,7 +1126,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_VIEWS;
     authorizeBasicNamespaceOperationOrThrow(op, namespace);
 
-    return catalogHandlerUtils.listViews(viewCatalog, namespace);
+    return catalogHandlerUtils().listViews(viewCatalog, namespace);
   }
 
   public LoadViewResponse createView(Namespace namespace, CreateViewRequest 
request) {
@@ -1157,14 +1138,14 @@ public class IcebergCatalogHandler extends 
CatalogHandler implements AutoCloseab
     if (catalog.isStaticFacade()) {
       throw new BadRequestException("Cannot create view on static-facade 
external catalogs.");
     }
-    return catalogHandlerUtils.createView(viewCatalog, namespace, request);
+    return catalogHandlerUtils().createView(viewCatalog, namespace, request);
   }
 
   public LoadViewResponse loadView(TableIdentifier viewIdentifier) {
     PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LOAD_VIEW;
     authorizeBasicTableLikeOperationOrThrow(op, 
PolarisEntitySubType.ICEBERG_VIEW, viewIdentifier);
 
-    return catalogHandlerUtils.loadView(viewCatalog, viewIdentifier);
+    return catalogHandlerUtils().loadView(viewCatalog, viewIdentifier);
   }
 
   public LoadViewResponse replaceView(TableIdentifier viewIdentifier, 
UpdateTableRequest request) {
@@ -1175,14 +1156,15 @@ public class IcebergCatalogHandler extends 
CatalogHandler implements AutoCloseab
     if (catalog.isStaticFacade()) {
       throw new BadRequestException("Cannot replace view on static-facade 
external catalogs.");
     }
-    return catalogHandlerUtils.updateView(viewCatalog, viewIdentifier, 
applyUpdateFilters(request));
+    return catalogHandlerUtils()
+        .updateView(viewCatalog, viewIdentifier, applyUpdateFilters(request));
   }
 
   public void dropView(TableIdentifier viewIdentifier) {
     PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DROP_VIEW;
     authorizeBasicTableLikeOperationOrThrow(op, 
PolarisEntitySubType.ICEBERG_VIEW, viewIdentifier);
 
-    catalogHandlerUtils.dropView(viewCatalog, viewIdentifier);
+    catalogHandlerUtils().dropView(viewCatalog, viewIdentifier);
   }
 
   public void viewExists(TableIdentifier viewIdentifier) {
@@ -1190,7 +1172,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     authorizeBasicTableLikeOperationOrThrow(op, 
PolarisEntitySubType.ICEBERG_VIEW, viewIdentifier);
 
     // TODO: Just skip CatalogHandlers for this one maybe
-    catalogHandlerUtils.loadView(viewCatalog, viewIdentifier);
+    catalogHandlerUtils().loadView(viewCatalog, viewIdentifier);
   }
 
   public void renameView(RenameTableRequest request) {
@@ -1202,7 +1184,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
     if (catalog.isStaticFacade()) {
       throw new BadRequestException("Cannot rename view on static-facade 
external catalogs.");
     }
-    catalogHandlerUtils.renameView(viewCatalog, request);
+    catalogHandlerUtils().renameView(viewCatalog, request);
   }
 
   private @Nonnull LoadTableResponse filterResponseToSnapshots(
@@ -1233,9 +1215,10 @@ public class IcebergCatalogHandler extends 
CatalogHandler implements AutoCloseab
   private EnumSet<PolarisAuthorizableOperation> 
getUpdateTableAuthorizableOperations(
       UpdateTableRequest request) {
     boolean useFineGrainedOperations =
-        realmConfig.getConfig(
-            FeatureConfiguration.ENABLE_FINE_GRAINED_UPDATE_TABLE_PRIVILEGES,
-            getResolvedCatalogEntity());
+        realmConfig()
+            .getConfig(
+                
FeatureConfiguration.ENABLE_FINE_GRAINED_UPDATE_TABLE_PRIVILEGES,
+                getResolvedCatalogEntity());
 
     if (useFineGrainedOperations) {
       EnumSet<PolarisAuthorizableOperation> actions =
@@ -1308,13 +1291,15 @@ public class IcebergCatalogHandler extends 
CatalogHandler implements AutoCloseab
     LOGGER.info("Catalog type: {}", catalogEntity.getCatalogType());
     LOGGER.info(
         "allow external catalog credential vending: {}",
-        realmConfig.getConfig(
-            FeatureConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING, 
catalogEntity));
+        realmConfig()
+            .getConfig(
+                
FeatureConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING, catalogEntity));
     if (catalogEntity
             .getCatalogType()
             
.equals(org.apache.polaris.core.admin.model.Catalog.TypeEnum.EXTERNAL)
-        && !realmConfig.getConfig(
-            FeatureConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING, 
catalogEntity)) {
+        && !realmConfig()
+            .getConfig(
+                
FeatureConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING, catalogEntity)) 
{
       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",
@@ -1330,25 +1315,16 @@ public class IcebergCatalogHandler extends 
CatalogHandler implements AutoCloseab
   }
 
   public ConfigResponse getConfig() {
-    // 'catalogName' is taken from the REST request's 'warehouse' query 
parameter.
-    // 'warehouse' as an output will be treated by the client as a default 
catalog
-    //   storage base location.
-    // 'prefix' as an output is the REST subpath that routes to the catalog
-    //   resource, which may be URL-escaped catalogName or potentially a 
different
-    //   unique identifier for the catalog being accessed.
-    if (catalogName == null) {
-      throw new BadRequestException("Please specify a warehouse");
-    }
-    Resolver resolver = resolverFactory.createResolver(polarisPrincipal, 
catalogName);
+    Resolver resolver = resolverFactory().createResolver(polarisPrincipal(), 
catalogName());
     ResolverStatus resolverStatus = resolver.resolveAll();
     if (!resolverStatus.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) 
{
-      throw new NotFoundException("Unable to find warehouse %s", catalogName);
+      throw new NotFoundException("Unable to find warehouse %s", 
catalogName());
     }
     ResolvedPolarisEntity resolvedReferenceCatalog = 
resolver.getResolvedReferenceCatalog();
     Map<String, String> properties =
         
PolarisEntity.of(resolvedReferenceCatalog.getEntity()).getPropertiesAsMap();
 
-    String prefix = prefixParser.catalogNameToPrefix(catalogName);
+    String prefix = prefixParser().catalogNameToPrefix(catalogName());
     return ConfigResponse.builder()
         .withDefaults(properties) // catalog properties are defaults
         .withOverrides(ImmutableMap.of("prefix", prefix))
@@ -1356,8 +1332,8 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
             ImmutableList.<Endpoint>builder()
                 .addAll(DEFAULT_ENDPOINTS)
                 .addAll(VIEW_ENDPOINTS)
-                
.addAll(PolarisEndpoints.getSupportedGenericTableEndpoints(realmConfig))
-                
.addAll(PolarisEndpoints.getSupportedPolicyEndpoints(realmConfig))
+                
.addAll(PolarisEndpoints.getSupportedGenericTableEndpoints(realmConfig()))
+                
.addAll(PolarisEndpoints.getSupportedPolicyEndpoints(realmConfig()))
                 .build())
         .build();
   }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFactory.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFactory.java
new file mode 100644
index 000000000..40f69bc4c
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFactory.java
@@ -0,0 +1,78 @@
+/*
+ * 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.catalog.iceberg;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.Instance;
+import jakarta.inject.Inject;
+import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.catalog.ExternalCatalogFactory;
+import org.apache.polaris.core.context.CallContext;
+import org.apache.polaris.core.credentials.PolarisCredentialManager;
+import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
+import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
+import org.apache.polaris.core.persistence.resolver.ResolverFactory;
+import org.apache.polaris.service.catalog.CatalogPrefixParser;
+import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
+import org.apache.polaris.service.config.ReservedProperties;
+import org.apache.polaris.service.context.catalog.CallContextCatalogFactory;
+import org.apache.polaris.service.events.EventAttributeMap;
+
+@RequestScoped
+public class IcebergCatalogHandlerFactory {
+
+  @Inject PolarisDiagnostics diagnostics;
+  @Inject CallContext callContext;
+  @Inject CatalogPrefixParser prefixParser;
+  @Inject ResolverFactory resolverFactory;
+  @Inject ResolutionManifestFactory resolutionManifestFactory;
+  @Inject PolarisMetaStoreManager metaStoreManager;
+  @Inject PolarisCredentialManager credentialManager;
+  @Inject CallContextCatalogFactory catalogFactory;
+  @Inject PolarisAuthorizer authorizer;
+  @Inject ReservedProperties reservedProperties;
+  @Inject CatalogHandlerUtils catalogHandlerUtils;
+  @Inject @Any Instance<ExternalCatalogFactory> externalCatalogFactories;
+  @Inject StorageAccessConfigProvider storageAccessConfigProvider;
+  @Inject EventAttributeMap eventAttributeMap;
+
+  public IcebergCatalogHandler createHandler(String catalogName, 
PolarisPrincipal principal) {
+    return ImmutableIcebergCatalogHandler.builder()
+        .catalogName(catalogName)
+        .polarisPrincipal(principal)
+        .diagnostics(diagnostics)
+        .callContext(callContext)
+        .prefixParser(prefixParser)
+        .resolverFactory(resolverFactory)
+        .resolutionManifestFactory(resolutionManifestFactory)
+        .metaStoreManager(metaStoreManager)
+        .credentialManager(credentialManager)
+        .catalogFactory(catalogFactory)
+        .authorizer(authorizer)
+        .reservedProperties(reservedProperties)
+        .catalogHandlerUtils(catalogHandlerUtils)
+        .externalCatalogFactories(externalCatalogFactories)
+        .storageAccessConfigProvider(storageAccessConfigProvider)
+        .eventAttributeMap(eventAttributeMap)
+        .build();
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java
index 9b6ebd4e7..523107dc4 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java
@@ -19,24 +19,16 @@
 package org.apache.polaris.service.catalog.policy;
 
 import jakarta.enterprise.context.RequestScoped;
-import jakarta.enterprise.inject.Any;
-import jakarta.enterprise.inject.Instance;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.SecurityContext;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.rest.RESTUtil;
-import org.apache.polaris.core.PolarisDiagnostics;
-import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisPrincipal;
-import org.apache.polaris.core.catalog.ExternalCatalogFactory;
 import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
-import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
-import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.service.catalog.CatalogPrefixParser;
 import org.apache.polaris.service.catalog.api.PolarisCatalogPolicyApiService;
@@ -49,62 +41,32 @@ import 
org.apache.polaris.service.types.ListPoliciesResponse;
 import org.apache.polaris.service.types.LoadPolicyResponse;
 import org.apache.polaris.service.types.PolicyIdentifier;
 import org.apache.polaris.service.types.UpdatePolicyRequest;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RequestScoped
 public class PolicyCatalogAdapter implements PolarisCatalogPolicyApiService, 
CatalogAdapter {
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(PolicyCatalogAdapter.class);
 
-  private final PolarisDiagnostics diagnostics;
   private final RealmContext realmContext;
   private final RealmConfig realmConfig;
-  private final CallContext callContext;
-  private final ResolutionManifestFactory resolutionManifestFactory;
-  private final PolarisMetaStoreManager metaStoreManager;
-  private final PolarisAuthorizer polarisAuthorizer;
   private final CatalogPrefixParser prefixParser;
-  private final PolarisCredentialManager polarisCredentialManager;
-  private final Instance<ExternalCatalogFactory> externalCatalogFactories;
+  private final PolicyCatalogHandlerFactory handlerFactory;
 
   @Inject
   public PolicyCatalogAdapter(
-      PolarisDiagnostics diagnostics,
-      RealmContext realmContext,
       CallContext callContext,
-      ResolutionManifestFactory resolutionManifestFactory,
-      PolarisMetaStoreManager metaStoreManager,
-      PolarisAuthorizer polarisAuthorizer,
       CatalogPrefixParser prefixParser,
-      PolarisCredentialManager polarisCredentialManager,
-      @Any Instance<ExternalCatalogFactory> externalCatalogFactories) {
-    this.diagnostics = diagnostics;
-    this.realmContext = realmContext;
-    this.callContext = callContext;
+      PolicyCatalogHandlerFactory handlerFactory) {
+    this.realmContext = callContext.getRealmContext();
     this.realmConfig = callContext.getRealmConfig();
-    this.resolutionManifestFactory = resolutionManifestFactory;
-    this.metaStoreManager = metaStoreManager;
-    this.polarisAuthorizer = polarisAuthorizer;
     this.prefixParser = prefixParser;
-    this.polarisCredentialManager = polarisCredentialManager;
-    this.externalCatalogFactories = externalCatalogFactories;
+    this.handlerFactory = handlerFactory;
   }
 
-  private PolicyCatalogHandler newHandlerWrapper(SecurityContext 
securityContext, String prefix) {
+  private PolicyCatalogHandler newHandler(SecurityContext securityContext, 
String prefix) {
     FeatureConfiguration.enforceFeatureEnabledOrThrow(
         realmConfig, FeatureConfiguration.ENABLE_POLICY_STORE);
     PolarisPrincipal principal = validatePrincipal(securityContext);
-
-    return new PolicyCatalogHandler(
-        diagnostics,
-        callContext,
-        resolutionManifestFactory,
-        metaStoreManager,
-        principal,
-        prefixParser.prefixToCatalogName(prefix),
-        polarisAuthorizer,
-        polarisCredentialManager,
-        externalCatalogFactories);
+    String catalogName = prefixParser.prefixToCatalogName(prefix);
+    return handlerFactory.createHandler(catalogName, principal);
   }
 
   @Override
@@ -115,7 +77,7 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
       RealmContext realmContext,
       SecurityContext securityContext) {
     Namespace ns = decodeNamespace(namespace);
-    PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix);
+    PolicyCatalogHandler handler = newHandler(securityContext, prefix);
     LoadPolicyResponse response = handler.createPolicy(ns, 
createPolicyRequest);
     return Response.ok(response).build();
   }
@@ -132,7 +94,7 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
     Namespace ns = decodeNamespace(namespace);
     PolicyType type =
         policyType != null ? 
PolicyType.fromName(RESTUtil.decodeString(policyType)) : null;
-    PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix);
+    PolicyCatalogHandler handler = newHandler(securityContext, prefix);
     ListPoliciesResponse response = handler.listPolicies(ns, type);
     return Response.ok(response).build();
   }
@@ -146,7 +108,7 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
       SecurityContext securityContext) {
     Namespace ns = decodeNamespace(namespace);
     PolicyIdentifier identifier = new PolicyIdentifier(ns, 
RESTUtil.decodeString(policyName));
-    PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix);
+    PolicyCatalogHandler handler = newHandler(securityContext, prefix);
     LoadPolicyResponse response = handler.loadPolicy(identifier);
     return Response.ok(response).build();
   }
@@ -161,7 +123,7 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
       SecurityContext securityContext) {
     Namespace ns = decodeNamespace(namespace);
     PolicyIdentifier identifier = new PolicyIdentifier(ns, 
RESTUtil.decodeString(policyName));
-    PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix);
+    PolicyCatalogHandler handler = newHandler(securityContext, prefix);
     LoadPolicyResponse response = handler.updatePolicy(identifier, 
updatePolicyRequest);
     return Response.ok(response).build();
   }
@@ -176,7 +138,7 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
       SecurityContext securityContext) {
     Namespace ns = decodeNamespace(namespace);
     PolicyIdentifier identifier = new PolicyIdentifier(ns, 
RESTUtil.decodeString(policyName));
-    PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix);
+    PolicyCatalogHandler handler = newHandler(securityContext, prefix);
     handler.dropPolicy(identifier, detachAll != null && detachAll);
     return Response.noContent().build();
   }
@@ -191,7 +153,7 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
       SecurityContext securityContext) {
     Namespace ns = decodeNamespace(namespace);
     PolicyIdentifier identifier = new PolicyIdentifier(ns, 
RESTUtil.decodeString(policyName));
-    PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix);
+    PolicyCatalogHandler handler = newHandler(securityContext, prefix);
     handler.attachPolicy(identifier, attachPolicyRequest);
     return Response.noContent().build();
   }
@@ -206,7 +168,7 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
       SecurityContext securityContext) {
     Namespace ns = decodeNamespace(namespace);
     PolicyIdentifier identifier = new PolicyIdentifier(ns, 
RESTUtil.decodeString(policyName));
-    PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix);
+    PolicyCatalogHandler handler = newHandler(securityContext, prefix);
     handler.detachPolicy(identifier, detachPolicyRequest);
     return Response.noContent().build();
   }
@@ -225,7 +187,7 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
     String target = targetName != null ? RESTUtil.decodeString(targetName) : 
null;
     PolicyType type =
         policyType != null ? 
PolicyType.fromName(RESTUtil.decodeString(policyType)) : null;
-    PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix);
+    PolicyCatalogHandler handler = newHandler(securityContext, prefix);
     GetApplicablePoliciesResponse response = handler.getApplicablePolicies(ns, 
target, type);
     return Response.ok(response).build();
   }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java
index 712193e40..85b0816d1 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java
@@ -20,7 +20,6 @@ package org.apache.polaris.service.catalog.policy;
 
 import com.google.common.base.Strings;
 import jakarta.annotation.Nullable;
-import jakarta.enterprise.inject.Instance;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -29,23 +28,16 @@ import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.exceptions.NoSuchNamespaceException;
 import org.apache.iceberg.exceptions.NoSuchTableException;
 import org.apache.iceberg.exceptions.NotFoundException;
-import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
-import org.apache.polaris.core.auth.PolarisAuthorizer;
-import org.apache.polaris.core.auth.PolarisPrincipal;
-import org.apache.polaris.core.catalog.ExternalCatalogFactory;
 import org.apache.polaris.core.catalog.PolarisCatalogHelpers;
-import org.apache.polaris.core.context.CallContext;
-import org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.PolarisEntityType;
-import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
-import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.persistence.resolver.ResolverPath;
 import org.apache.polaris.core.persistence.resolver.ResolverStatus;
 import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException;
+import org.apache.polaris.immutables.PolarisImmutable;
 import org.apache.polaris.service.catalog.common.CatalogHandler;
 import org.apache.polaris.service.types.AttachPolicyRequest;
 import org.apache.polaris.service.types.CreatePolicyRequest;
@@ -57,37 +49,16 @@ import 
org.apache.polaris.service.types.PolicyAttachmentTarget;
 import org.apache.polaris.service.types.PolicyIdentifier;
 import org.apache.polaris.service.types.UpdatePolicyRequest;
 
-public class PolicyCatalogHandler extends CatalogHandler {
-
-  private PolarisMetaStoreManager metaStoreManager;
+@PolarisImmutable
+@SuppressWarnings("immutables:incompat")
+public abstract class PolicyCatalogHandler extends CatalogHandler {
 
   private PolicyCatalog policyCatalog;
 
-  public PolicyCatalogHandler(
-      PolarisDiagnostics diagnostics,
-      CallContext callContext,
-      ResolutionManifestFactory resolutionManifestFactory,
-      PolarisMetaStoreManager metaStoreManager,
-      PolarisPrincipal principal,
-      String catalogName,
-      PolarisAuthorizer authorizer,
-      PolarisCredentialManager polarisCredentialManager,
-      Instance<ExternalCatalogFactory> externalCatalogFactories) {
-    super(
-        diagnostics,
-        callContext,
-        resolutionManifestFactory,
-        principal,
-        catalogName,
-        authorizer,
-        polarisCredentialManager,
-        externalCatalogFactories);
-    this.metaStoreManager = metaStoreManager;
-  }
-
   @Override
   protected void initializeCatalog() {
-    this.policyCatalog = new PolicyCatalog(metaStoreManager, callContext, 
this.resolutionManifest);
+    this.policyCatalog =
+        new PolicyCatalog(metaStoreManager(), callContext(), 
this.resolutionManifest);
   }
 
   public ListPoliciesResponse listPolicies(Namespace parent, @Nullable 
PolicyType policyType) {
@@ -179,12 +150,13 @@ public class PolicyCatalogHandler extends CatalogHandler {
       throw new NoSuchPolicyException(String.format("Policy does not exist: 
%s", identifier));
     }
 
-    authorizer.authorizeOrThrow(
-        polarisPrincipal,
-        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-        op,
-        target,
-        null /* secondary */);
+    authorizer()
+        .authorizeOrThrow(
+            polarisPrincipal(),
+            resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+            op,
+            target,
+            null /* secondary */);
 
     initializeCatalog();
   }
@@ -221,12 +193,13 @@ public class PolicyCatalogHandler extends CatalogHandler {
     if (targetCatalog == null) {
       throw new NotFoundException("Catalog not found");
     }
-    authorizer.authorizeOrThrow(
-        polarisPrincipal,
-        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-        op,
-        targetCatalog,
-        null);
+    authorizer()
+        .authorizeOrThrow(
+            polarisPrincipal(),
+            resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+            op,
+            targetCatalog,
+            null);
 
     initializeCatalog();
   }
@@ -278,12 +251,13 @@ public class PolicyCatalogHandler extends CatalogHandler {
     PolarisAuthorizableOperation op =
         determinePolicyMappingOperation(target, targetWrapper, isAttach);
 
-    authorizer.authorizeOrThrow(
-        polarisPrincipal,
-        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-        op,
-        policyWrapper,
-        targetWrapper);
+    authorizer()
+        .authorizeOrThrow(
+            polarisPrincipal(),
+            resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+            op,
+            policyWrapper,
+            targetWrapper);
 
     initializeCatalog();
   }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerFactory.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerFactory.java
new file mode 100644
index 000000000..5d8b305b4
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.catalog.policy;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import org.apache.polaris.core.auth.PolarisAuthorizer;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.context.CallContext;
+import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
+import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
+
+@RequestScoped
+public class PolicyCatalogHandlerFactory {
+
+  @Inject CallContext callContext;
+  @Inject ResolutionManifestFactory resolutionManifestFactory;
+  @Inject PolarisMetaStoreManager metaStoreManager;
+  @Inject PolarisAuthorizer authorizer;
+
+  public PolicyCatalogHandler createHandler(String catalogName, 
PolarisPrincipal principal) {
+    return ImmutablePolicyCatalogHandler.builder()
+        .catalogName(catalogName)
+        .polarisPrincipal(principal)
+        .callContext(callContext)
+        .resolutionManifestFactory(resolutionManifestFactory)
+        .metaStoreManager(metaStoreManager)
+        .authorizer(authorizer)
+        .build();
+  }
+}
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java
index 75067a48e..a6fc272c2 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java
@@ -20,14 +20,17 @@ package org.apache.polaris.service.catalog.generic;
 
 import io.quarkus.test.junit.QuarkusTest;
 import io.quarkus.test.junit.TestProfile;
+import jakarta.enterprise.inject.Instance;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.catalog.ExternalCatalogFactory;
 import org.apache.polaris.core.entity.PolarisPrivilege;
 import org.apache.polaris.service.admin.PolarisAuthzTestBase;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 @QuarkusTest
 @TestProfile(PolarisAuthzTestBase.Profile.class)
@@ -41,20 +44,28 @@ public class PolarisGenericTableCatalogHandlerAuthzTest 
extends PolarisAuthzTest
     return newWrapper(activatedPrincipalRoles, CATALOG_NAME);
   }
 
+  @SuppressWarnings("unchecked")
+  private static Instance<ExternalCatalogFactory> 
emptyExternalCatalogFactory() {
+    Instance<ExternalCatalogFactory> mock = Mockito.mock(Instance.class);
+    Mockito.when(mock.select(Mockito.any())).thenReturn(mock);
+    Mockito.when(mock.isUnsatisfied()).thenReturn(true);
+    return mock;
+  }
+
   private GenericTableCatalogHandler newWrapper(
       Set<String> activatedPrincipalRoles, String catalogName) {
     PolarisPrincipal authenticatedPrincipal =
         PolarisPrincipal.of(principalEntity, activatedPrincipalRoles);
-    return new GenericTableCatalogHandler(
-        diagServices,
-        callContext,
-        resolutionManifestFactory,
-        metaStoreManager,
-        authenticatedPrincipal,
-        catalogName,
-        polarisAuthorizer,
-        null,
-        null);
+    return ImmutableGenericTableCatalogHandler.builder()
+        .catalogName(catalogName)
+        .polarisPrincipal(authenticatedPrincipal)
+        .callContext(callContext)
+        .resolutionManifestFactory(resolutionManifestFactory)
+        .metaStoreManager(metaStoreManager)
+        .credentialManager(credentialManager)
+        .authorizer(polarisAuthorizer)
+        .externalCatalogFactories(emptyExternalCatalogFactory())
+        .build();
   }
 
   /**
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
index 76aa2e4e1..91af34e5b 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
@@ -117,23 +117,24 @@ public abstract class 
AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
       Set<String> activatedPrincipalRoles, String catalogName, 
CallContextCatalogFactory factory) {
     PolarisPrincipal authenticatedPrincipal =
         PolarisPrincipal.of(principalEntity, activatedPrincipalRoles);
-    return new IcebergCatalogHandler(
-        diagServices,
-        callContext,
-        prefixParser,
-        resolverFactory,
-        resolutionManifestFactory,
-        metaStoreManager,
-        credentialManager,
-        authenticatedPrincipal,
-        factory,
-        catalogName,
-        polarisAuthorizer,
-        reservedProperties,
-        catalogHandlerUtils,
-        emptyExternalCatalogFactory(),
-        storageAccessConfigProvider,
-        eventAttributeMap);
+    return ImmutableIcebergCatalogHandler.builder()
+        .catalogName(catalogName)
+        .polarisPrincipal(authenticatedPrincipal)
+        .diagnostics(diagServices)
+        .callContext(callContext)
+        .prefixParser(prefixParser)
+        .resolverFactory(resolverFactory)
+        .resolutionManifestFactory(resolutionManifestFactory)
+        .metaStoreManager(metaStoreManager)
+        .credentialManager(credentialManager)
+        .catalogFactory(factory)
+        .authorizer(polarisAuthorizer)
+        .reservedProperties(reservedProperties)
+        .catalogHandlerUtils(catalogHandlerUtils)
+        .externalCatalogFactories(emptyExternalCatalogFactory())
+        .storageAccessConfigProvider(storageAccessConfigProvider)
+        .eventAttributeMap(eventAttributeMap)
+        .build();
   }
 
   protected void doTestInsufficientPrivileges(
@@ -258,37 +259,39 @@ public abstract class 
AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
 
     PolarisPrincipal authenticatedPrincipal =
         PolarisPrincipal.of(newPrincipal.getPrincipal(), 
Set.of(PRINCIPAL_ROLE1, PRINCIPAL_ROLE2));
-    IcebergCatalogHandler wrapper =
-        new IcebergCatalogHandler(
-            diagServices,
-            callContext,
-            prefixParser,
-            resolverFactory,
-            resolutionManifestFactory,
-            metaStoreManager,
-            credentialManager,
-            authenticatedPrincipal,
-            callContextCatalogFactory,
-            CATALOG_NAME,
-            polarisAuthorizer,
-            reservedProperties,
-            catalogHandlerUtils,
-            emptyExternalCatalogFactory(),
-            storageAccessConfigProvider,
-            eventAttributeMap);
+
+    IcebergCatalogHandler handler =
+        ImmutableIcebergCatalogHandler.builder()
+            .catalogName(CATALOG_NAME)
+            .polarisPrincipal(authenticatedPrincipal)
+            .diagnostics(diagServices)
+            .callContext(callContext)
+            .prefixParser(prefixParser)
+            .resolverFactory(resolverFactory)
+            .resolutionManifestFactory(resolutionManifestFactory)
+            .metaStoreManager(metaStoreManager)
+            .credentialManager(credentialManager)
+            .catalogFactory(callContextCatalogFactory)
+            .authorizer(polarisAuthorizer)
+            .reservedProperties(reservedProperties)
+            .catalogHandlerUtils(catalogHandlerUtils)
+            .externalCatalogFactories(emptyExternalCatalogFactory())
+            .storageAccessConfigProvider(storageAccessConfigProvider)
+            .eventAttributeMap(eventAttributeMap)
+            .build();
 
     // a variety of actions are all disallowed because the principal's 
credentials must be rotated
     doTestInsufficientPrivileges(
         List.of(PolarisPrivilege.values()),
         principalName,
-        () -> wrapper.listNamespaces(Namespace.of()));
+        () -> handler.listNamespaces(Namespace.of()));
     Namespace ns3 = Namespace.of("ns3");
     doTestInsufficientPrivileges(
         List.of(PolarisPrivilege.values()),
         principalName,
-        () -> 
wrapper.createNamespace(CreateNamespaceRequest.builder().withNamespace(ns3).build()));
+        () -> 
handler.createNamespace(CreateNamespaceRequest.builder().withNamespace(ns3).build()));
     doTestInsufficientPrivileges(
-        List.of(PolarisPrivilege.values()), principalName, () -> 
wrapper.listTables(NS1));
+        List.of(PolarisPrivilege.values()), principalName, () -> 
handler.listTables(NS1));
     PrincipalWithCredentialsCredentials credentials =
         new PrincipalWithCredentialsCredentials(
             newPrincipal.getPrincipalSecrets().getPrincipalClientId(),
@@ -298,24 +301,13 @@ public abstract class 
AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
             metaStoreManager, principalName, credentials, 
callContext.getPolarisCallContext());
     PolarisPrincipal authenticatedPrincipal1 =
         PolarisPrincipal.of(refreshPrincipal, Set.of(PRINCIPAL_ROLE1, 
PRINCIPAL_ROLE2));
+
+    @SuppressWarnings("resource")
     IcebergCatalogHandler refreshedWrapper =
-        new IcebergCatalogHandler(
-            diagServices,
-            callContext,
-            prefixParser,
-            resolverFactory,
-            resolutionManifestFactory,
-            metaStoreManager,
-            credentialManager,
-            authenticatedPrincipal1,
-            callContextCatalogFactory,
-            CATALOG_NAME,
-            polarisAuthorizer,
-            reservedProperties,
-            catalogHandlerUtils,
-            emptyExternalCatalogFactory(),
-            storageAccessConfigProvider,
-            eventAttributeMap);
+        ImmutableIcebergCatalogHandler.builder()
+            .from(handler)
+            .polarisPrincipal(authenticatedPrincipal1)
+            .build();
 
     doTestSufficientPrivilegeSets(
         List.of(Set.of(PolarisPrivilege.NAMESPACE_LIST)),
@@ -1185,27 +1177,29 @@ public abstract class 
AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
         };
 
     // Mock the regular CallContext calls
+    Mockito.when(mockCallContext.getRealmContext()).thenReturn(() -> "test");
     
Mockito.when(mockCallContext.getRealmConfig()).thenReturn(customRealmConfig);
     Mockito.when(mockCallContext.getPolarisCallContext())
         .thenReturn(callContext.getPolarisCallContext());
 
-    return new IcebergCatalogHandler(
-        diagServices,
-        mockCallContext,
-        prefixParser,
-        resolverFactory,
-        resolutionManifestFactory,
-        metaStoreManager,
-        credentialManager,
-        authenticatedPrincipal,
-        factory,
-        catalogName,
-        polarisAuthorizer,
-        reservedProperties,
-        catalogHandlerUtils,
-        emptyExternalCatalogFactory(),
-        storageAccessConfigProvider,
-        eventAttributeMap);
+    return ImmutableIcebergCatalogHandler.builder()
+        .catalogName(catalogName)
+        .polarisPrincipal(authenticatedPrincipal)
+        .diagnostics(diagServices)
+        .callContext(mockCallContext)
+        .prefixParser(prefixParser)
+        .resolverFactory(resolverFactory)
+        .resolutionManifestFactory(resolutionManifestFactory)
+        .metaStoreManager(metaStoreManager)
+        .credentialManager(credentialManager)
+        .catalogFactory(factory)
+        .authorizer(polarisAuthorizer)
+        .reservedProperties(reservedProperties)
+        .catalogHandlerUtils(catalogHandlerUtils)
+        .externalCatalogFactories(emptyExternalCatalogFactory())
+        .storageAccessConfigProvider(storageAccessConfigProvider)
+        .eventAttributeMap(eventAttributeMap)
+        .build();
   }
 
   @Test
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java
index 825131494..d7a48e481 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java
@@ -195,7 +195,7 @@ public class IcebergAllowedLocationTest {
     var updateResponse =
         services
             .catalogAdapter()
-            .newHandlerWrapper(services.securityContext(), catalog)
+            .newHandler(services.securityContext(), catalog)
             .replaceView(viewId, updateRequest);
     assertEquals(
         
updateResponse.metadata().properties().get(USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY),
@@ -253,7 +253,7 @@ public class IcebergAllowedLocationTest {
         () ->
             services
                 .catalogAdapter()
-                .newHandlerWrapper(services.securityContext(), catalog)
+                .newHandler(services.securityContext(), catalog)
                 .replaceView(viewId, updateRequest));
 
     // Test 2: Try to create a view with location not allowed
@@ -344,7 +344,7 @@ public class IcebergAllowedLocationTest {
     var updateResponse =
         services
             .catalogAdapter()
-            .newHandlerWrapper(services.securityContext(), catalog)
+            .newHandler(services.securityContext(), catalog)
             .updateTable(tableId, updateRequest);
 
     assertThat(updateResponse.tableMetadata().properties())
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapterTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapterTest.java
index f5e6a81c3..5254f85a7 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapterTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapterTest.java
@@ -219,7 +219,7 @@ public class IcebergCatalogAdapterTest {
               return wrappedHandler;
             })
         .when(catalogAdapter)
-        .newHandlerWrapper(Mockito.any(), Mockito.any());
+        .newHandler(Mockito.any(), Mockito.any());
   }
 
   private static Stream<Arguments> paginationTestCases() {
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
index 16cb0974f..5da024ba1 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
@@ -60,23 +60,24 @@ public class IcebergCatalogHandlerFineGrainedDisabledTest 
extends PolarisAuthzTe
 
   private IcebergCatalogHandler newWrapper() {
     PolarisPrincipal authenticatedPrincipal = 
PolarisPrincipal.of(principalEntity, Set.of());
-    return new IcebergCatalogHandler(
-        diagServices,
-        callContext,
-        prefixParser,
-        resolverFactory,
-        resolutionManifestFactory,
-        metaStoreManager,
-        credentialManager,
-        authenticatedPrincipal,
-        callContextCatalogFactory,
-        CATALOG_NAME,
-        polarisAuthorizer,
-        reservedProperties,
-        catalogHandlerUtils,
-        emptyExternalCatalogFactory(),
-        storageAccessConfigProvider,
-        eventAttributeMap);
+    return ImmutableIcebergCatalogHandler.builder()
+        .catalogName(CATALOG_NAME)
+        .polarisPrincipal(authenticatedPrincipal)
+        .diagnostics(diagServices)
+        .callContext(callContext)
+        .prefixParser(prefixParser)
+        .resolverFactory(resolverFactory)
+        .resolutionManifestFactory(resolutionManifestFactory)
+        .metaStoreManager(metaStoreManager)
+        .credentialManager(credentialManager)
+        .catalogFactory(callContextCatalogFactory)
+        .authorizer(polarisAuthorizer)
+        .reservedProperties(reservedProperties)
+        .catalogHandlerUtils(catalogHandlerUtils)
+        .externalCatalogFactories(emptyExternalCatalogFactory())
+        .storageAccessConfigProvider(storageAccessConfigProvider)
+        .eventAttributeMap(eventAttributeMap)
+        .build();
   }
 
   public static class Profile extends PolarisAuthzTestBase.Profile {
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java
index 4bd6dc5c8..ea73cbe20 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java
@@ -50,16 +50,14 @@ public class PolicyCatalogHandlerAuthzTest extends 
PolarisAuthzTestBase {
   private PolicyCatalogHandler newWrapper(Set<String> activatedPrincipalRoles, 
String catalogName) {
     PolarisPrincipal authenticatedPrincipal =
         PolarisPrincipal.of(principalEntity, activatedPrincipalRoles);
-    return new PolicyCatalogHandler(
-        diagServices,
-        callContext,
-        resolutionManifestFactory,
-        metaStoreManager,
-        authenticatedPrincipal,
-        catalogName,
-        polarisAuthorizer,
-        null,
-        null);
+    return ImmutablePolicyCatalogHandler.builder()
+        .catalogName(catalogName)
+        .polarisPrincipal(authenticatedPrincipal)
+        .callContext(callContext)
+        .resolutionManifestFactory(resolutionManifestFactory)
+        .authorizer(polarisAuthorizer)
+        .metaStoreManager(metaStoreManager)
+        .build();
   }
 
   /**
diff --git 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index 7b4e5c2dd..b6572b2d6 100644
--- 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++ 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -73,8 +73,11 @@ import 
org.apache.polaris.service.catalog.api.IcebergRestConfigurationApi;
 import 
org.apache.polaris.service.catalog.api.IcebergRestConfigurationApiService;
 import org.apache.polaris.service.catalog.iceberg.CatalogHandlerUtils;
 import org.apache.polaris.service.catalog.iceberg.IcebergCatalogAdapter;
+import org.apache.polaris.service.catalog.iceberg.IcebergCatalogHandler;
+import org.apache.polaris.service.catalog.iceberg.IcebergCatalogHandlerFactory;
 import 
org.apache.polaris.service.catalog.iceberg.IcebergRestCatalogEventServiceDelegator;
 import 
org.apache.polaris.service.catalog.iceberg.IcebergRestConfigurationEventServiceDelegator;
+import 
org.apache.polaris.service.catalog.iceberg.ImmutableIcebergCatalogHandler;
 import org.apache.polaris.service.catalog.io.FileIOFactory;
 import org.apache.polaris.service.catalog.io.MeasuredFileIOFactory;
 import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
@@ -336,25 +339,41 @@ public record TestServices(
       Mockito.when(externalCatalogFactory.isUnsatisfied()).thenReturn(true);
 
       EventAttributeMap eventAttributeMap = new EventAttributeMap();
+
+      IcebergCatalogHandlerFactory handlerFactory =
+          new IcebergCatalogHandlerFactory() {
+            @Override
+            public IcebergCatalogHandler createHandler(
+                String catalogName, PolarisPrincipal principal) {
+              return ImmutableIcebergCatalogHandler.builder()
+                  .catalogName(catalogName)
+                  .polarisPrincipal(principal)
+                  .diagnostics(diagnostics)
+                  .callContext(callContext)
+                  .prefixParser(new DefaultCatalogPrefixParser())
+                  .resolverFactory(resolverFactory)
+                  .resolutionManifestFactory(resolutionManifestFactory)
+                  .metaStoreManager(metaStoreManager)
+                  .credentialManager(credentialManager)
+                  .catalogFactory(callContextFactory)
+                  .authorizer(authorizer)
+                  .reservedProperties(reservedProperties)
+                  .catalogHandlerUtils(catalogHandlerUtils)
+                  .externalCatalogFactories(externalCatalogFactory)
+                  .storageAccessConfigProvider(storageAccessConfigProvider)
+                  .eventAttributeMap(eventAttributeMap)
+                  .build();
+            }
+          };
+
       IcebergCatalogAdapter catalogService =
           new IcebergCatalogAdapter(
-              diagnostics,
-              realmContext,
               callContext,
-              callContextFactory,
-              resolverFactory,
-              resolutionManifestFactory,
-              metaStoreManager,
-              credentialManager,
-              authorizer,
               new DefaultCatalogPrefixParser(),
               reservedProperties,
-              catalogHandlerUtils,
-              externalCatalogFactory,
-              storageAccessConfigProvider,
               new DefaultMetricsReporter(),
               Clock.systemUTC(),
-              eventAttributeMap);
+              handlerFactory);
 
       // Optionally wrap with event delegator
       IcebergRestCatalogApiService finalRestCatalogService = catalogService;

Reply via email to