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

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


The following commit(s) were added to refs/heads/main by this push:
     new ab732667b5 [#9546] feat(optimizer):  code skeleton for recommender  
(#9544)
ab732667b5 is described below

commit ab732667b59649627dcd5083661abc22601d2d2a
Author: FANNG <[email protected]>
AuthorDate: Wed Dec 24 17:59:11 2025 +0900

    [#9546] feat(optimizer):  code skeleton for recommender  (#9544)
    
    ### What changes were proposed in this pull request?
    
    Add recommender to drive the job submission by leveraging stats, policy
    systems
    * API
    * code skeleton
    
    the entrypoint is Recommender#recommendForStrategyType. to select the
    table based on the scores generated by the strategy.
    
    ### Why are the changes needed?
    
    Fix: #9546
    
    ### Does this PR introduce _any_ user-facing change?
    no
    
    ### How was this patch tested?
    
    testing by the whole workflow
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 build.gradle.kts                                   |   3 +
 .../apache/gravitino/config/ConfigConstants.java   |   3 +
 maintenance/build.gradle.kts                       |  22 ++
 maintenance/optimizer/build.gradle.kts             |  83 ++++++
 .../optimizer/api/common/PartitionEntry.java       |  47 ++++
 .../optimizer/api/common/PartitionPath.java        |  78 ++++++
 .../maintenance/optimizer/api/common/Provider.java |  49 ++++
 .../optimizer/api/common/StatisticEntry.java       |  46 ++++
 .../maintenance/optimizer/api/common/Strategy.java |  84 ++++++
 .../api/recommender/JobExecutionContext.java       |  56 ++++
 .../optimizer/api/recommender/JobSubmitter.java    |  40 +++
 .../api/recommender/StatisticsProvider.java        |  35 +++
 .../api/recommender/StrategyEvaluation.java        |  38 +++
 .../optimizer/api/recommender/StrategyHandler.java |  83 ++++++
 .../api/recommender/StrategyHandlerContext.java    | 168 ++++++++++++
 .../api/recommender/StrategyProvider.java          |  54 ++++
 .../api/recommender/SupportTableStatistics.java    |  58 +++++
 .../api/recommender/TableMetadataProvider.java     |  44 ++++
 .../optimizer/common/CloseableGroup.java           |  69 +++++
 .../maintenance/optimizer/common/OptimizerEnv.java |  40 +++
 .../optimizer/common/conf/OptimizerConfig.java     | 123 +++++++++
 .../optimizer/common/util/ProviderUtils.java       |  92 +++++++
 .../optimizer/recommender/Recommender.java         | 286 +++++++++++++++++++++
 settings.gradle.kts                                |   1 +
 24 files changed, 1602 insertions(+)

diff --git a/build.gradle.kts b/build.gradle.kts
index e2b75f13f2..aaa743b7ac 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -716,6 +716,7 @@ tasks {
       ":authorizations:copyLibAndConfig",
       ":iceberg:iceberg-rest-server:copyLibAndConfigs",
       ":lance:lance-rest-server:copyLibAndConfigs",
+      ":maintenance:optimizer:copyLibAndConfigs",
       ":web:web:build"
     )
 
@@ -997,6 +998,7 @@ tasks {
         it.name != "integration-test" &&
         it.name != "trino-connector" &&
         it.parent?.name != "bundles" &&
+        it.parent?.name != "maintenance" &&
         it.name != "mcp-server"
       ) {
         from(it.configurations.runtimeClasspath)
@@ -1032,6 +1034,7 @@ tasks {
         it.name != "docs" &&
         it.name != "hadoop-common" &&
         it.parent?.name != "bundles" &&
+        it.parent?.name != "maintenance" &&
         it.name != "mcp-server"
       ) {
         dependsOn("${it.name}:build")
diff --git 
a/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java 
b/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
index 0ef761a6ca..0546965867 100644
--- a/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
+++ b/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
@@ -83,6 +83,9 @@ public final class ConfigConstants {
   /** The version number for the 1.1.0 release. */
   public static final String VERSION_1_1_0 = "1.1.0";
 
+  /** The version number for the 1.2.0 release. */
+  public static final String VERSION_1_2_0 = "1.2.0";
+
   /** The current version of backend storage initialization script. */
   public static final String CURRENT_SCRIPT_VERSION = VERSION_1_1_0;
 }
diff --git a/maintenance/build.gradle.kts b/maintenance/build.gradle.kts
new file mode 100644
index 0000000000..fa6eb7d5ef
--- /dev/null
+++ b/maintenance/build.gradle.kts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+tasks.all {
+    enabled = false
+}
diff --git a/maintenance/optimizer/build.gradle.kts 
b/maintenance/optimizer/build.gradle.kts
new file mode 100644
index 0000000000..66dfe8eb18
--- /dev/null
+++ b/maintenance/optimizer/build.gradle.kts
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+description = "Gravitino Optimizer"
+
+plugins {
+  `maven-publish`
+  id("java")
+  id("idea")
+}
+
+dependencies {
+  implementation(project(":api"))
+  implementation(project(":common"))
+  implementation(libs.commons.lang3)
+  implementation(libs.guava)
+  implementation(libs.slf4j.api)
+}
+
+tasks {
+  val copyDepends by registering(Copy::class) {
+    from(configurations.runtimeClasspath)
+    into("build/libs")
+  }
+  jar {
+    finalizedBy(copyDepends)
+  }
+
+  register("copyLibs", Copy::class) {
+    dependsOn(copyDepends, "build")
+    from("build/libs")
+    into("$rootDir/distribution/package/optimizer/libs")
+  }
+
+  register("copyConfigs", Copy::class) {
+    from("src/main/resources")
+    into("$rootDir/distribution/package/optimizer/conf")
+
+    rename { original ->
+      if (original.endsWith(".template")) {
+        original.replace(".template", "")
+      } else {
+        original
+      }
+    }
+
+    fileMode = 0b111101101
+  }
+
+  register("copyLibAndConfigs", Copy::class) {
+    dependsOn("copyLibs", "copyConfigs")
+  }
+}
+
+tasks.test {
+  val skipITs = project.hasProperty("skipITs")
+  if (skipITs) {
+    // Exclude integration tests
+    exclude("**/integration/test/**")
+  } else {
+    dependsOn(tasks.jar)
+    dependsOn(":server:jar")
+  }
+}
+
+tasks.getByName("generateMetadataFileForMavenJavaPublication") {
+  dependsOn("copyDepends")
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionEntry.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionEntry.java
new file mode 100644
index 0000000000..aa284933bd
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionEntry.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.gravitino.maintenance.optimizer.api.common;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/**
+ * A single partition key/value pair.
+ *
+ * <p>For multi-level partitions, each level is represented by a separate 
entry (for example, {@code
+ * p1=a} and {@code p2=b} for a {@code p1=a/p2=b} partition path). Combine 
entries with {@link
+ * PartitionPath#of(java.util.List)} when returning the whole partition path.
+ */
+@DeveloperApi
+public interface PartitionEntry {
+
+  /**
+   * Partition name.
+   *
+   * @return name of the partition field (for example, {@code ds} or {@code 
bucket_id})
+   */
+  String partitionName();
+
+  /**
+   * Partition value as a string.
+   *
+   * @return string representation of the partition value (e.g., {@code 
YYYY-MM-DD})
+   */
+  String partitionValue();
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionPath.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionPath.java
new file mode 100644
index 0000000000..11f4e80ad6
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/PartitionPath.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.gravitino.maintenance.optimizer.api.common;
+
+import com.google.common.base.Preconditions;
+import java.util.List;
+
+/**
+ * Immutable, ordered path of partition entries from outer to inner level. 
Often used as the map key
+ * when returning partition-level statistics.
+ */
+public final class PartitionPath {
+  private final List<PartitionEntry> entries;
+
+  private PartitionPath(List<PartitionEntry> entries) {
+    this.entries = entries;
+  }
+
+  /**
+   * Create a partition path from a list of entries (outer to inner).
+   *
+   * @param entries partition entries ordered from outer to inner level
+   * @return a partition path
+   */
+  public static PartitionPath of(List<PartitionEntry> entries) {
+    Preconditions.checkArgument(
+        entries != null && !entries.isEmpty(), "partition entries must not be 
empty");
+    return new PartitionPath(List.copyOf(entries));
+  }
+
+  /**
+   * Partition entries ordered from outer to inner level.
+   *
+   * @return immutable list of partition entries
+   */
+  public List<PartitionEntry> entries() {
+    return entries;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof PartitionPath)) {
+      return false;
+    }
+    PartitionPath that = (PartitionPath) o;
+    return entries.equals(that.entries);
+  }
+
+  @Override
+  public int hashCode() {
+    return entries.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return "PartitionPath " + entries;
+  }
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Provider.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Provider.java
new file mode 100644
index 0000000000..8a2b33adab
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Provider.java
@@ -0,0 +1,49 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.common;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.maintenance.optimizer.common.OptimizerEnv;
+
+/**
+ * Base SPI for optimizer plug-ins (statistics, strategies, submitters). 
Implementations are
+ * discovered through {@link java.util.ServiceLoader} and follow a simple 
lifecycle: instantiated
+ * via default constructor, {@link #initialize(OptimizerEnv)} is called once 
before use, then {@link
+ * #close()} is invoked when the optimizer shuts down.
+ */
+@DeveloperApi
+public interface Provider extends AutoCloseable {
+
+  /**
+   * Stable provider name used for discovery, configuration lookup, and 
logging.
+   *
+   * @return provider name
+   */
+  String name();
+
+  /**
+   * Initialize the provider with the optimizer environment. This method is 
called once before the
+   * provider is used. Implementations should use this method to wire external 
resources (clients,
+   * credentials, caches) before first use.
+   *
+   * @param optimizerEnv shared optimizer environment/configuration
+   */
+  void initialize(OptimizerEnv optimizerEnv);
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/StatisticEntry.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/StatisticEntry.java
new file mode 100644
index 0000000000..9c2443db50
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/StatisticEntry.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.maintenance.optimizer.api.common;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.stats.StatisticValue;
+
+/**
+ * Named statistic value produced or consumed by optimizer components.
+ *
+ * @param <T> underlying Java type wrapped by the {@link StatisticValue}
+ */
+@DeveloperApi
+public interface StatisticEntry<T> {
+
+  /**
+   * Stable metric key used for lookup and reporting.
+   *
+   * @return non-null metric name
+   */
+  String name();
+
+  /**
+   * Typed statistic value wrapper.
+   *
+   * @return statistic value container
+   */
+  StatisticValue<T> value();
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Strategy.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Strategy.java
new file mode 100644
index 0000000000..0030e7d738
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/common/Strategy.java
@@ -0,0 +1,84 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.common;
+
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler;
+
+/**
+ * Strategy definition supplied by the control plane. The recommender pulls 
strategies from a {@link
+ * 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyProvider}, 
routes them to a
+ * {@link StrategyHandler} by {@link #strategyType()}, and lets the handler 
interpret the remaining
+ * fields as needed.
+ */
+@DeveloperApi
+public interface Strategy {
+
+  /**
+   * Strategy name.
+   *
+   * <p>This name is used to identify the strategy in the optimizer, for 
example, {@code
+   * compaction-strategy-1}.
+   *
+   * @return strategy name
+   */
+  String name();
+
+  /**
+   * Strategy type used to route to a {@link StrategyHandler}. Built-in 
handlers should document
+   * their expected type string.
+   *
+   * <p>There may be multiple strategy names for the same strategy type.
+   *
+   * @return strategy type identifier
+   */
+  String strategyType();
+
+  /**
+   * Arbitrary attributes for the strategy.
+   *
+   * @return strategy-level options
+   */
+  Map<String, String> properties();
+
+  /**
+   * Structured rule payload that the handler can interpret, for example, 
trigger or score
+   * expressions.
+   *
+   * @return map of rule identifiers to rule objects
+   */
+  Map<String, Object> rules();
+
+  /**
+   * Job-level options that should be applied when the handler builds a {@code 
JobExecutionContext}
+   * (for example, target file size for the compaction job).
+   *
+   * @return job-specific options
+   */
+  Map<String, String> jobOptions();
+
+  /**
+   * Optional job template name to resolve the job in the job submitter.
+   *
+   * @return job template name
+   */
+  String jobTemplateName();
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobExecutionContext.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobExecutionContext.java
new file mode 100644
index 0000000000..ec95fcce66
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobExecutionContext.java
@@ -0,0 +1,56 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/**
+ * Immutable description of a recommended optimization job. Produced by {@link 
StrategyHandler} and
+ * consumed by {@link JobSubmitter}.
+ */
+@DeveloperApi
+public interface JobExecutionContext {
+
+  /**
+   * Target table identifier for the job.
+   *
+   * @return fully qualified table identifier (catalog/schema/table)
+   */
+  NameIdentifier nameIdentifier();
+
+  /**
+   * Free-form job configuration, such as engine parameters (e.g., target file 
size bytes).
+   *
+   * <p>The {@code StrategyHandler} is free to add additional job 
configuration besides the job
+   * options specified in the strategy.
+   *
+   * @return immutable map of configuration entries
+   */
+  Map<String, String> jobConfig();
+
+  /**
+   * Job template name to resolve the job in the job submitter.
+   *
+   * @return job template name
+   */
+  String jobTemplateName();
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobSubmitter.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobSubmitter.java
new file mode 100644
index 0000000000..7c3309def2
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/JobSubmitter.java
@@ -0,0 +1,40 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.exceptions.NoSuchJobTemplateException;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+
+/** Submits recommended jobs to an execution backend. */
+@DeveloperApi
+public interface JobSubmitter extends Provider {
+
+  /**
+   * Submit a job for execution.
+   *
+   * @param jobTemplateName the job template name (routes to the appropriate 
job template)
+   * @param jobExecutionContext execution context built by the strategy handler
+   * @return provider-specific job id
+   * @throws NoSuchJobTemplateException if the job template name does not exist
+   */
+  String submitJob(String jobTemplateName, JobExecutionContext 
jobExecutionContext)
+      throws NoSuchJobTemplateException;
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StatisticsProvider.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StatisticsProvider.java
new file mode 100644
index 0000000000..cac61e3524
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StatisticsProvider.java
@@ -0,0 +1,35 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+
+/**
+ * Marker interface for providers that can supply statistics to {@link 
StrategyHandler} instances.
+ * Implementations are invoked by the {@link
+ * org.apache.gravitino.maintenance.optimizer.recommender.Recommender} only 
when a handler declares
+ * the matching {@link
+ * 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler.DataRequirement}.
 Most
+ * providers should also implement a narrower contract such as {@link 
SupportTableStatistics} to
+ * expose concrete capabilities.
+ */
+@DeveloperApi
+public interface StatisticsProvider extends Provider {}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyEvaluation.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyEvaluation.java
new file mode 100644
index 0000000000..d9965978ed
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyEvaluation.java
@@ -0,0 +1,38 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+/**
+ * Encapsulates the scored result and job execution context for a single 
strategy evaluation. The
+ * recommender ranks evaluations by {@link #score()} before asking the {@link
+ * org.apache.gravitino.maintenance.optimizer.api.recommender.JobSubmitter 
JobSubmitter} to launch
+ * work.
+ */
+public interface StrategyEvaluation {
+
+  /**
+   * Score used to rank multiple recommendations of the same strategy. Higher 
wins; equal scores
+   * preserve the evaluation order returned by the {@code StrategyHandler}.
+   */
+  long score();
+
+  /** Job execution context for this evaluation. */
+  JobExecutionContext jobExecutionContext();
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandler.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandler.java
new file mode 100644
index 0000000000..879b2ecb73
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandler.java
@@ -0,0 +1,83 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.Set;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/**
+ * Executes a single recommender strategy: declares required inputs, 
initializes with those inputs,
+ * scores a potential action, and optionally submits a job description for 
execution.
+ */
+@DeveloperApi
+public interface StrategyHandler {
+
+  /**
+   * Declares the optional data the handler needs before it can be 
initialized. The {@link
+   * StrategyHandlerContext} handed to {@link 
#initialize(StrategyHandlerContext)} will include only
+   * the requested items.
+   */
+  enum DataRequirement {
+    TABLE_METADATA,
+    TABLE_STATISTICS,
+    PARTITION_STATISTICS
+  }
+
+  /**
+   * Declares which pieces of data this handler needs before it can be 
initialized.
+   *
+   * @return set of requested data items; empty means no additional data needed
+   */
+  default Set<DataRequirement> dataRequirements() {
+    return Set.of();
+  }
+
+  /**
+   * Initialize the handler with the supplied context.
+   *
+   * @param context immutable view of the target identifier, strategy, and any 
requested metadata or
+   *     statistics
+   */
+  void initialize(StrategyHandlerContext context);
+
+  /**
+   * Stable identifier for the strategy type this handler supports (for 
example, {@code
+   * COMPACTION}).
+   *
+   * @return strategy type string
+   */
+  String strategyType();
+
+  /**
+   * Cheap pre-check to determine whether this handler should be evaluated for 
the current context.
+   * Implementations should avoid heavy work here.
+   *
+   * @return {@code true} if the strategy should be evaluated, {@code false} 
otherwise
+   */
+  boolean shouldTrigger();
+
+  /**
+   * Evaluate the strategy and return a scored recommendation. Only invoked if 
{@link
+   * #shouldTrigger} returns {@code true}.
+   *
+   * @return scored evaluation result with job execution context
+   */
+  StrategyEvaluation evaluate();
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandlerContext.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandlerContext.java
new file mode 100644
index 0000000000..82f6c9307b
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyHandlerContext.java
@@ -0,0 +1,168 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.maintenance.optimizer.api.common.PartitionPath;
+import org.apache.gravitino.maintenance.optimizer.api.common.StatisticEntry;
+import org.apache.gravitino.maintenance.optimizer.api.common.Strategy;
+import org.apache.gravitino.rel.Table;
+
+/**
+ * Immutable context handed to a {@link StrategyHandler}. Contains only the 
data requested through
+ * {@link StrategyHandler#dataRequirements()} and safely copies any mutable 
collections.
+ */
+@DeveloperApi
+public final class StrategyHandlerContext {
+  private final NameIdentifier identifier;
+  private final Strategy strategy;
+  private final Optional<Table> tableMetadata;
+  private final List<StatisticEntry<?>> tableStatistics;
+  private final Map<PartitionPath, List<StatisticEntry<?>>> 
partitionStatistics;
+
+  private StrategyHandlerContext(Builder builder) {
+    this.identifier = builder.identifier;
+    this.strategy = builder.strategy;
+    this.tableMetadata = builder.tableMetadata;
+    this.tableStatistics = builder.tableStatistics;
+    this.partitionStatistics = builder.partitionStatistics;
+  }
+
+  /**
+   * Fully qualified table identifier the strategy is evaluated against.
+   *
+   * @return target table identifier (catalog/schema/table)
+   */
+  public NameIdentifier nameIdentifier() {
+    return identifier;
+  }
+
+  /**
+   * Strategy definition that triggered the handler.
+   *
+   * @return recommender strategy
+   */
+  public Strategy strategy() {
+    return strategy;
+  }
+
+  /**
+   * Returns the table metadata if {@code DataRequirement.TABLE_METADATA} was 
requested in {@link
+   * StrategyHandler#dataRequirements()}, otherwise returns {@link 
Optional#empty()}.
+   *
+   * @return the table metadata if requested, otherwise {@link 
Optional#empty()}
+   */
+  public Optional<Table> tableMetadata() {
+    return tableMetadata;
+  }
+
+  /**
+   * Returns table-level statistics if {@code 
DataRequirement.TABLE_STATISTICS} was requested,
+   * otherwise returns an empty list.
+   */
+  public List<StatisticEntry<?>> tableStatistics() {
+    return tableStatistics;
+  }
+
+  /**
+   * Returns partition-level statistics if {@code 
DataRequirement.PARTITION_STATISTICS} was
+   * requested, otherwise returns an empty map.
+   *
+   * @return map of partition path to statistics; empty when not requested or 
unavailable
+   */
+  public Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics() {
+    return partitionStatistics;
+  }
+
+  /**
+   * Create a new builder for a handler context with the required identifier 
and strategy.
+   *
+   * @param identifier target table identifier for the strategy handler 
(catalog/schema/table)
+   * @param strategy recommender strategy definition
+   * @return a builder to populate optional metadata and statistics
+   */
+  public static Builder builder(NameIdentifier identifier, Strategy strategy) {
+    return new Builder(identifier, strategy);
+  }
+
+  public static final class Builder {
+    private final NameIdentifier identifier;
+    private final Strategy strategy;
+    private Optional<Table> tableMetadata = Optional.empty();
+    private List<StatisticEntry<?>> tableStatistics = Collections.emptyList();
+    private Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics =
+        Collections.emptyMap();
+
+    private Builder(NameIdentifier identifier, Strategy strategy) {
+      this.identifier = Objects.requireNonNull(identifier, "identifier must 
not be null");
+      this.strategy = Objects.requireNonNull(strategy, "strategy must not be 
null");
+    }
+
+    /**
+     * Attach table metadata when the handler requests {@code TABLE_METADATA}.
+     *
+     * @param metadata table definition; null clears metadata
+     * @return builder for chaining
+     */
+    public Builder withTableMetadata(Table metadata) {
+      this.tableMetadata = Optional.ofNullable(metadata);
+      return this;
+    }
+
+    /**
+     * Attach table-level statistics when the handler requests {@code 
TABLE_STATISTICS}.
+     *
+     * @param statistics statistics list; null treated as empty
+     * @return builder for chaining
+     */
+    public Builder withTableStatistics(List<StatisticEntry<?>> statistics) {
+      this.tableStatistics = statistics == null ? Collections.emptyList() : 
List.copyOf(statistics);
+      return this;
+    }
+
+    /**
+     * Attach partition-level statistics when the handler requests {@code 
PARTITION_STATISTICS}.
+     *
+     * @param partitionStatistics statistics map; null treated as empty
+     * @return builder for chaining
+     */
+    public Builder withPartitionStatistics(
+        Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics) {
+      this.partitionStatistics =
+          partitionStatistics == null ? Collections.emptyMap() : 
Map.copyOf(partitionStatistics);
+      return this;
+    }
+
+    /**
+     * Build an immutable {@link StrategyHandlerContext} instance.
+     *
+     * @return constructed handler context
+     */
+    public StrategyHandlerContext build() {
+      return new StrategyHandlerContext(this);
+    }
+  }
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyProvider.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyProvider.java
new file mode 100644
index 0000000000..2410afbe58
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/StrategyProvider.java
@@ -0,0 +1,54 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.List;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.exceptions.NotFoundException;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+import org.apache.gravitino.maintenance.optimizer.api.common.Strategy;
+
+/**
+ * Supplies strategies attached to identifiers (for example, tables). The 
recommender asks this
+ * provider for strategies before instantiating {@link
+ * org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler 
strategy handlers}.
+ */
+@DeveloperApi
+public interface StrategyProvider extends Provider {
+
+  /**
+   * List all strategies attached to the given identifier.
+   *
+   * @param nameIdentifier fully qualified table identifier 
(catalog/schema/table)
+   * @return list of strategies for the table (empty if none)
+   */
+  List<Strategy> strategies(NameIdentifier nameIdentifier);
+
+  /**
+   * Fetch a single strategy by name.
+   *
+   * @param strategyName unique strategy name
+   * @return strategy definition; implementations may throw if the strategy is 
missing or
+   *     inaccessible
+   * @throws NotFoundException if the strategy is not found
+   */
+  Strategy strategy(String strategyName) throws NotFoundException;
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/SupportTableStatistics.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/SupportTableStatistics.java
new file mode 100644
index 0000000000..f232e1bf70
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/SupportTableStatistics.java
@@ -0,0 +1,58 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.maintenance.optimizer.api.common.PartitionPath;
+import org.apache.gravitino.maintenance.optimizer.api.common.StatisticEntry;
+
+/**
+ * Provides table and partition-level statistics to {@link StrategyHandler} 
implementations. The
+ * recommender will only call these methods when the handler declares {@code 
TABLE_STATISTICS} or
+ * {@code PARTITION_STATISTICS} in {@link StrategyHandler#dataRequirements()}.
+ */
+@DeveloperApi
+public interface SupportTableStatistics extends StatisticsProvider {
+
+  /**
+   * Retrieve table-level statistics.
+   *
+   * @param tableIdentifier catalog/schema/table identifier (must be three 
levels)
+   * @return list of statistics; empty when none are available
+   * @throws NoSuchTableException if the table does not exist
+   */
+  List<StatisticEntry<?>> tableStatistics(NameIdentifier tableIdentifier)
+      throws NoSuchTableException;
+
+  /**
+   * Retrieve partition-level statistics.
+   *
+   * @param tableIdentifier catalog/schema/table identifier (must be three 
levels)
+   * @return map keyed by partition path with partition-level statistics; 
empty when none are
+   *     available or the table is unpartitioned
+   * @throws NoSuchTableException if the table does not exist
+   */
+  Map<PartitionPath, List<StatisticEntry<?>>> 
partitionStatistics(NameIdentifier tableIdentifier)
+      throws NoSuchTableException;
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/TableMetadataProvider.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/TableMetadataProvider.java
new file mode 100644
index 0000000000..d95a5585d8
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/api/recommender/TableMetadataProvider.java
@@ -0,0 +1,44 @@
+/*
+ * 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.gravitino.maintenance.optimizer.api.recommender;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+import org.apache.gravitino.rel.Table;
+
+/**
+ * Supplies table definitions to {@link StrategyHandler} implementations. The 
recommender calls this
+ * provider only when a handler declares {@code TABLE_METADATA} in {@link
+ * StrategyHandler#dataRequirements()}.
+ */
+@DeveloperApi
+public interface TableMetadataProvider extends Provider {
+
+  /**
+   * Fetch table metadata for a fully-qualified table identifier.
+   *
+   * @param tableIdentifier catalog/schema/table identifier (must be three 
levels)
+   * @return table definition
+   * @throws NoSuchTableException if the table does not exist
+   */
+  Table tableMetadata(NameIdentifier tableIdentifier) throws 
NoSuchTableException;
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/CloseableGroup.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/CloseableGroup.java
new file mode 100644
index 0000000000..872e777d89
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/CloseableGroup.java
@@ -0,0 +1,69 @@
+/*
+ * 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.gravitino.maintenance.optimizer.common;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility to close a collection of resources while preserving the first 
failure and suppressing the
+ * rest. Resources are closed in reverse registration order to mimic typical 
stack lifecycles.
+ */
+public final class CloseableGroup implements AutoCloseable {
+  private static final Logger LOG = 
LoggerFactory.getLogger(CloseableGroup.class);
+  private final Deque<CloseableEntry> entries = new ArrayDeque<>(4);
+
+  public void register(AutoCloseable closeable, String name) {
+    entries.addFirst(new CloseableEntry(closeable, name));
+  }
+
+  @Override
+  public void close() throws Exception {
+    Exception rootCause = null;
+    while (!entries.isEmpty()) {
+      CloseableEntry entry = entries.removeFirst();
+      try {
+        entry.closeable.close();
+      } catch (Exception e) {
+        LOG.warn("Failed to close {}", entry.name, e);
+        if (rootCause == null) {
+          rootCause = e;
+        } else if (rootCause != e) {
+          rootCause.addSuppressed(e);
+        }
+      }
+    }
+    if (rootCause != null) {
+      throw rootCause;
+    }
+  }
+
+  private static final class CloseableEntry {
+    private final AutoCloseable closeable;
+    private final String name;
+
+    private CloseableEntry(AutoCloseable closeable, String name) {
+      this.closeable = closeable;
+      this.name = name;
+    }
+  }
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/OptimizerEnv.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/OptimizerEnv.java
new file mode 100644
index 0000000000..5444b4c4fc
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/OptimizerEnv.java
@@ -0,0 +1,40 @@
+/*
+ * 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.gravitino.maintenance.optimizer.common;
+
+import org.apache.gravitino.maintenance.optimizer.common.conf.OptimizerConfig;
+
+/**
+ * Immutable container for runtime optimizer context passed into pluggable 
components. Today it only
+ * exposes {@link OptimizerConfig}, but it is intended to carry additional 
shared resources (client
+ * handles, metrics) as the optimizer matures.
+ */
+public class OptimizerEnv {
+  // The config items from the config file
+  private final OptimizerConfig config;
+
+  public OptimizerEnv(OptimizerConfig config) {
+    this.config = config;
+  }
+
+  public OptimizerConfig config() {
+    return config;
+  }
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/conf/OptimizerConfig.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/conf/OptimizerConfig.java
new file mode 100644
index 0000000000..bf96e359dd
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/conf/OptimizerConfig.java
@@ -0,0 +1,123 @@
+/*
+ * 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.gravitino.maintenance.optimizer.common.conf;
+
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.config.ConfigBuilder;
+import org.apache.gravitino.config.ConfigConstants;
+import org.apache.gravitino.config.ConfigEntry;
+
+/**
+ * Central configuration holder for the optimizer/recommender runtime. Keys 
are grouped under the
+ * {@code gravitino.optimizer.*} prefix and capture both core connectivity 
(URI, metalake, default
+ * catalog) and pluggable implementation wiring (statistics provider, strategy 
provider, table
+ * metadata provider, job submitter).
+ */
+public class OptimizerConfig extends Config {
+
+  public static final String OPTIMIZER_PREFIX = "gravitino.optimizer.";
+
+  public static final String GRAVITINO_URI = OPTIMIZER_PREFIX + "gravitinoUri";
+  public static final String GRAVITINO_METALAKE = OPTIMIZER_PREFIX + 
"gravitinoMetalake";
+  public static final String GRAVITINO_DEFAULT_CATALOG =
+      OPTIMIZER_PREFIX + "gravitinoDefaultCatalog";
+
+  private static final String RECOMMENDER_PREFIX = OPTIMIZER_PREFIX + 
"recommender.";
+  private static final String STATISTICS_PROVIDER = RECOMMENDER_PREFIX + 
"statisticsProvider";
+  private static final String STRATEGY_PROVIDER = RECOMMENDER_PREFIX + 
"strategyProvider";
+  private static final String TABLE_META_PROVIDER = RECOMMENDER_PREFIX + 
"tableMetaProvider";
+  private static final String JOB_SUBMITTER = RECOMMENDER_PREFIX + 
"jobSubmitter";
+
+  public static final ConfigEntry<String> STATISTICS_PROVIDER_CONFIG =
+      new ConfigBuilder(STATISTICS_PROVIDER)
+          .doc(
+              "Statistics provider implementation name (matches 
Provider.name()) discoverable via "
+                  + "ServiceLoader. Example: 'gravitino-statistics-provider'.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .create();
+
+  public static final ConfigEntry<String> STRATEGY_PROVIDER_CONFIG =
+      new ConfigBuilder(STRATEGY_PROVIDER)
+          .doc(
+              "Strategy provider implementation name (matches Provider.name()) 
discoverable via "
+                  + "ServiceLoader. Example: 'gravitino-strategy-provider'.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .create();
+
+  public static final ConfigEntry<String> TABLE_META_PROVIDER_CONFIG =
+      new ConfigBuilder(TABLE_META_PROVIDER)
+          .doc(
+              "Table metadata provider implementation name (matches 
Provider.name()) discoverable "
+                  + "via ServiceLoader. Example: 
'gravitino-table-metadata-provider'.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .create();
+
+  public static final ConfigEntry<String> JOB_SUBMITTER_CONFIG =
+      new ConfigBuilder(JOB_SUBMITTER)
+          .doc(
+              "Job submitter implementation name (matches Provider.name()) 
discoverable via "
+                  + "ServiceLoader. Example: 'gravitino-job-submitter'.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .create();
+
+  public static final ConfigEntry<String> GRAVITINO_URI_CONFIG =
+      new ConfigBuilder(GRAVITINO_URI)
+          .doc("The URI of the Gravitino server.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .createWithDefault("http://localhost:8090";);
+
+  public static final ConfigEntry<String> GRAVITINO_METALAKE_CONFIG =
+      new ConfigBuilder(GRAVITINO_METALAKE)
+          .doc("The metalake name in Gravitino.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .checkValue(StringUtils::isNotBlank, 
ConfigConstants.NOT_BLANK_ERROR_MSG)
+          .create();
+
+  public static final ConfigEntry<String> GRAVITINO_DEFAULT_CATALOG_CONFIG =
+      new ConfigBuilder(GRAVITINO_DEFAULT_CATALOG)
+          .doc("The default catalog name in Gravitino.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .create();
+
+  /** Create an empty optimizer config to populate programmatically. */
+  public OptimizerConfig() {
+    super(false);
+  }
+
+  /**
+   * Create an optimizer config from a pre-parsed map, typically from a 
configuration file or
+   * command overrides.
+   *
+   * @param properties key/value pairs respecting the {@code 
gravitino.optimizer.*} namespace
+   */
+  public OptimizerConfig(Map<String, String> properties) {
+    super(false);
+    loadFromMap(properties, k -> true);
+  }
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/util/ProviderUtils.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/util/ProviderUtils.java
new file mode 100644
index 0000000000..48b422c21e
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/common/util/ProviderUtils.java
@@ -0,0 +1,92 @@
+/*
+ * 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.gravitino.maintenance.optimizer.common.util;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.stream.Collectors;
+import org.apache.gravitino.maintenance.optimizer.api.common.Provider;
+import org.apache.gravitino.maintenance.optimizer.api.recommender.JobSubmitter;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StatisticsProvider;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyProvider;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.TableMetadataProvider;
+
+/** Helper for loading optimizer providers via {@link ServiceLoader}. */
+public class ProviderUtils {
+
+  public static <T extends Provider> T createProviderInstance(
+      Class<T> providerClass, String provider) {
+    ServiceLoader<Provider> loader = ServiceLoader.load(Provider.class);
+    List<Class<? extends T>> providers =
+        Streams.stream(loader.iterator())
+            .filter(p -> provider.equalsIgnoreCase(p.name()))
+            .filter(providerClass::isInstance)
+            .map(p -> (Class<? extends T>) p.getClass())
+            .collect(Collectors.toList());
+
+    String providerClassName = Provider.class.getName();
+
+    if (providers.isEmpty()) {
+      throw new IllegalArgumentException(
+          "No "
+              + providerClass.getSimpleName()
+              + " found for provider name '"
+              + provider
+              + "'. Ensure the provider is registered in META-INF/services/"
+              + providerClassName
+              + " and the implementation is on the classpath, and that the 
provider's name()"
+              + " method returns the expected value.");
+    } else if (providers.size() > 1) {
+      throw new IllegalArgumentException(
+          "Multiple "
+              + providerClass.getSimpleName()
+              + " implementations found for provider name '"
+              + provider
+              + "'. Ensure only one implementation with this name exists in 
META-INF/services/"
+              + providerClassName
+              + ".");
+    } else {
+      Class<? extends T> providerClz = Iterables.getOnlyElement(providers);
+      try {
+        return providerClz.getDeclaredConstructor().newInstance();
+      } catch (Exception e) {
+        throw new RuntimeException("Failed to instantiate provider: " + 
providerClz.getName(), e);
+      }
+    }
+  }
+
+  public static StrategyProvider createStrategyProviderInstance(String 
provider) {
+    return createProviderInstance(StrategyProvider.class, provider);
+  }
+
+  public static StatisticsProvider createStatisticsProviderInstance(String 
provider) {
+    return createProviderInstance(StatisticsProvider.class, provider);
+  }
+
+  public static TableMetadataProvider 
createTableMetadataProviderInstance(String provider) {
+    return createProviderInstance(TableMetadataProvider.class, provider);
+  }
+
+  public static JobSubmitter createJobSubmitterInstance(String provider) {
+    return createProviderInstance(JobSubmitter.class, provider);
+  }
+}
diff --git 
a/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/Recommender.java
 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/Recommender.java
new file mode 100644
index 0000000000..8cb960fd52
--- /dev/null
+++ 
b/maintenance/optimizer/src/main/java/org/apache/gravitino/maintenance/optimizer/recommender/Recommender.java
@@ -0,0 +1,286 @@
+/*
+ * 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.gravitino.maintenance.optimizer.recommender;
+
+import com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.maintenance.optimizer.api.common.PartitionPath;
+import org.apache.gravitino.maintenance.optimizer.api.common.StatisticEntry;
+import org.apache.gravitino.maintenance.optimizer.api.common.Strategy;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.JobExecutionContext;
+import org.apache.gravitino.maintenance.optimizer.api.recommender.JobSubmitter;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StatisticsProvider;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyEvaluation;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandler.DataRequirement;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyHandlerContext;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.StrategyProvider;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.SupportTableStatistics;
+import 
org.apache.gravitino.maintenance.optimizer.api.recommender.TableMetadataProvider;
+import org.apache.gravitino.maintenance.optimizer.common.CloseableGroup;
+import org.apache.gravitino.maintenance.optimizer.common.OptimizerEnv;
+import org.apache.gravitino.maintenance.optimizer.common.conf.OptimizerConfig;
+import org.apache.gravitino.maintenance.optimizer.common.util.ProviderUtils;
+import org.apache.gravitino.rel.Table;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Entry point that wires together strategy, statistics, metadata providers, 
and the job submitter
+ * to generate optimization recommendations for tables.
+ *
+ * <p>Workflow:
+ *
+ * <ol>
+ *   <li>Load {@link StrategyProvider}, {@link StatisticsProvider}, {@link 
TableMetadataProvider},
+ *       and {@link JobSubmitter} from {@link java.util.ServiceLoader} using 
names declared in
+ *       {@link OptimizerConfig}.
+ *   <li>For each target identifier, fetch its strategies, filter by strategy 
type, and create a
+ *       matching {@link StrategyHandler}.
+ *   <li>Collect only the metadata and statistics the handler asked for via 
{@link
+ *       StrategyHandler#dataRequirements()} and build a {@link 
StrategyHandlerContext}.
+ *   <li>Invoke {@link StrategyHandler#shouldTrigger()} followed by {@link
+ *       StrategyHandler#evaluate()} and hand the resulting {@link 
JobExecutionContext} to the
+ *       {@link JobSubmitter}.
+ * </ol>
+ */
+public class Recommender implements AutoCloseable {
+  private static final Logger LOG = LoggerFactory.getLogger(Recommender.class);
+  private final StrategyProvider strategyProvider;
+  private final StatisticsProvider statisticsProvider;
+  private final TableMetadataProvider tableMetadataProvider;
+  private final JobSubmitter jobSubmitter;
+  private final CloseableGroup closeableGroup = new CloseableGroup();
+
+  /**
+   * Create a recommender whose providers and submitter are resolved from the 
optimizer
+   * configuration. All components are initialized eagerly and closed together 
via {@link #close()}.
+   *
+   * @param optimizerEnv shared optimizer environment supplying configuration
+   */
+  public Recommender(OptimizerEnv optimizerEnv) {
+    OptimizerConfig config = optimizerEnv.config();
+    StrategyProvider strategyProvider = loadStrategyProvider(config);
+    StatisticsProvider statisticsProvider = loadStatisticsProvider(config);
+    TableMetadataProvider tableMetadataProvider = 
loadTableMetadataProvider(config);
+    JobSubmitter jobSubmitter = loadJobSubmitter(config);
+
+    this.strategyProvider = strategyProvider;
+    this.statisticsProvider = statisticsProvider;
+    this.tableMetadataProvider = tableMetadataProvider;
+    this.jobSubmitter = jobSubmitter;
+
+    this.strategyProvider.initialize(optimizerEnv);
+    this.statisticsProvider.initialize(optimizerEnv);
+    this.tableMetadataProvider.initialize(optimizerEnv);
+    this.jobSubmitter.initialize(optimizerEnv);
+
+    closeableGroup.register(strategyProvider, "strategy provider");
+    closeableGroup.register(statisticsProvider, "statistics provider");
+    closeableGroup.register(tableMetadataProvider, "table metadata provider");
+    closeableGroup.register(jobSubmitter, "job submitter");
+  }
+
+  /**
+   * Generate and submit recommendations for all identifiers that have a 
strategy of the specified
+   * type. Each matching strategy instance is evaluated independently and 
submitted through the
+   * configured job submitter.
+   *
+   * @param nameIdentifiers fully qualified table identifiers to evaluate 
(catalog/schema/table)
+   * @param strategyType strategy type to filter and evaluate
+   */
+  public void recommendForStrategyType(List<NameIdentifier> nameIdentifiers, 
String strategyType) {
+    Preconditions.checkArgument(
+        nameIdentifiers != null && !nameIdentifiers.isEmpty(), 
"nameIdentifiers must not be empty");
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(strategyType), "strategyType must not be 
blank");
+
+    Map<String, List<NameIdentifier>> identifiersByStrategyName =
+        getIdentifiersByStrategyName(nameIdentifiers, strategyType);
+
+    for (Map.Entry<String, List<NameIdentifier>> entry : 
identifiersByStrategyName.entrySet()) {
+      String strategyName = entry.getKey();
+      List<JobExecutionContext> jobConfigs =
+          recommendForOneStrategy(entry.getValue(), strategyName);
+      for (JobExecutionContext jobConfig : jobConfigs) {
+        String templateName = jobConfig.jobTemplateName();
+        String jobId = jobSubmitter.submitJob(templateName, jobConfig);
+        LOG.info("Submit job {} for strategy {} with context {}", jobId, 
strategyName, jobConfig);
+      }
+    }
+  }
+
+  /** Close all registered providers and job submitter, suppressing secondary 
failures. */
+  @Override
+  public void close() throws Exception {
+    closeableGroup.close();
+  }
+
+  private List<JobExecutionContext> recommendForOneStrategy(
+      List<NameIdentifier> identifiers, String strategyName) {
+    LOG.info("Recommend strategy {} for identifiers {}", strategyName, 
identifiers);
+    Strategy strategy = strategyProvider.strategy(strategyName);
+
+    PriorityQueue<StrategyEvaluation> scoreQueue =
+        new PriorityQueue<>((a, b) -> Long.compare(b.score(), a.score()));
+    for (NameIdentifier identifier : identifiers) {
+      StrategyHandler strategyHandler = loadStrategyHandler(strategy, 
identifier);
+      if (!strategyHandler.shouldTrigger()) {
+        continue;
+      }
+      StrategyEvaluation evaluation = strategyHandler.evaluate();
+      LOG.info(
+          "Recommend strategy {} for identifier {} score: {}",
+          strategyName,
+          identifier,
+          evaluation.score());
+      scoreQueue.add(evaluation);
+    }
+
+    return scoreQueue.stream()
+        .map(StrategyEvaluation::jobExecutionContext)
+        .collect(Collectors.toList());
+  }
+
+  private StrategyHandler loadStrategyHandler(Strategy strategy, 
NameIdentifier nameIdentifier) {
+    StrategyHandler strategyHandler = 
createStrategyHandler(strategy.strategyType());
+
+    Set<DataRequirement> declaredRequirements = 
strategyHandler.dataRequirements();
+    EnumSet<DataRequirement> requirements =
+        declaredRequirements.isEmpty()
+            ? EnumSet.noneOf(DataRequirement.class)
+            : EnumSet.copyOf(declaredRequirements);
+    Table tableMetadata = null;
+    List<StatisticEntry<?>> statistics = Collections.emptyList();
+    Map<PartitionPath, List<StatisticEntry<?>>> partitionStatistics = 
Collections.emptyMap();
+
+    if (requirements.contains(DataRequirement.TABLE_METADATA)) {
+      tableMetadata = tableMetadataProvider.tableMetadata(nameIdentifier);
+    }
+
+    if (requirements.contains(DataRequirement.TABLE_STATISTICS)
+        || requirements.contains(DataRequirement.PARTITION_STATISTICS)) {
+      SupportTableStatistics supportTableStatistics = 
requireTableStatisticsProvider();
+      if (requirements.contains(DataRequirement.TABLE_STATISTICS)) {
+        statistics = supportTableStatistics.tableStatistics(nameIdentifier);
+      }
+      if (requirements.contains(DataRequirement.PARTITION_STATISTICS)) {
+        partitionStatistics = 
supportTableStatistics.partitionStatistics(nameIdentifier);
+      }
+    }
+
+    StrategyHandlerContext context =
+        StrategyHandlerContext.builder(nameIdentifier, strategy)
+            .withTableMetadata(tableMetadata)
+            .withTableStatistics(statistics)
+            .withPartitionStatistics(partitionStatistics)
+            .build();
+    strategyHandler.initialize(context);
+
+    return strategyHandler;
+  }
+
+  private StrategyHandler createStrategyHandler(String strategyType) {
+    String strategyHandlerClassName = 
getStrategyHandlerClassName(strategyType);
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(strategyHandlerClassName),
+        "No StrategyHandler class configured for strategy type: %s",
+        strategyType);
+    try {
+      return (StrategyHandler)
+          
Class.forName(strategyHandlerClassName).getDeclaredConstructor().newInstance();
+    } catch (Exception e) {
+      String message =
+          "Failed to instantiate StrategyHandler '"
+              + strategyHandlerClassName
+              + "' for strategy type: "
+              + strategyType;
+      LOG.error(message, e);
+      throw new RuntimeException(message, e);
+    }
+  }
+
+  /**
+   * Resolve the implementation class for a given strategy type. In production 
this should be backed
+   * by configuration or an explicit registry that maps stable strategy type 
strings (for example,
+   * {@code COMPACTION}) to {@link StrategyHandler} implementations.
+   */
+  @SuppressWarnings("UnusedVariable")
+  private String getStrategyHandlerClassName(String strategyType) {
+    return "";
+  }
+
+  private Map<String, List<NameIdentifier>> getIdentifiersByStrategyName(
+      List<NameIdentifier> nameIdentifiers, String strategyType) {
+    Map<String, List<NameIdentifier>> identifiersByStrategyName = new 
LinkedHashMap<>();
+    for (NameIdentifier nameIdentifier : nameIdentifiers) {
+      strategyProvider.strategies(nameIdentifier).stream()
+          .filter(strategy -> strategy.strategyType().equals(strategyType))
+          .forEach(
+              strategy ->
+                  identifiersByStrategyName
+                      .computeIfAbsent(strategy.name(), key -> new 
ArrayList<>())
+                      .add(nameIdentifier));
+    }
+    return identifiersByStrategyName;
+  }
+
+  private StrategyProvider loadStrategyProvider(OptimizerConfig config) {
+    String strategyProviderName = 
config.get(OptimizerConfig.STRATEGY_PROVIDER_CONFIG);
+    return ProviderUtils.createStrategyProviderInstance(strategyProviderName);
+  }
+
+  private StatisticsProvider loadStatisticsProvider(OptimizerConfig config) {
+    String statisticsProviderName = 
config.get(OptimizerConfig.STATISTICS_PROVIDER_CONFIG);
+    return 
ProviderUtils.createStatisticsProviderInstance(statisticsProviderName);
+  }
+
+  private TableMetadataProvider loadTableMetadataProvider(OptimizerConfig 
config) {
+    String tableMetadataProviderName = 
config.get(OptimizerConfig.TABLE_META_PROVIDER_CONFIG);
+    return 
ProviderUtils.createTableMetadataProviderInstance(tableMetadataProviderName);
+  }
+
+  private JobSubmitter loadJobSubmitter(OptimizerConfig config) {
+    String jobSubmitterName = config.get(OptimizerConfig.JOB_SUBMITTER_CONFIG);
+    return ProviderUtils.createJobSubmitterInstance(jobSubmitterName);
+  }
+
+  private SupportTableStatistics requireTableStatisticsProvider() {
+    if (statisticsProvider instanceof SupportTableStatistics) {
+      return (SupportTableStatistics) statisticsProvider;
+    }
+    throw new IllegalStateException(
+        String.format(
+            "Statistics provider '%s' does not implement 
SupportTableStatistics interface. "
+                + "To use TABLE_STATISTICS or PARTITION_STATISTICS data 
requirements, "
+                + "configure a statistics provider that implements 
SupportTableStatistics.",
+            statisticsProvider.name()));
+  }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d3f622dbab..1be14c8e1d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -88,3 +88,4 @@ include(":bundles:azure", ":bundles:azure-bundle", 
":bundles:iceberg-azure-bundl
 include(":catalogs:hadoop-common")
 include(":lineage")
 include(":mcp-server")
+include(":maintenance:optimizer")

Reply via email to