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

roryqi 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 6b818355b1 [#12488] feat(kms): Identify keys by provider and keyId 
(#12489)
6b818355b1 is described below

commit 6b818355b14be4ea67889a79a4062f0c70f36ea9
Author: Nevin Zheng <[email protected]>
AuthorDate: Wed Aug 19 11:48:20 2026 +0800

    [#12488] feat(kms): Identify keys by provider and keyId (#12489)
    
    ### What changes were proposed in this pull request?
    
    `KmsReference` now identifies a key by configured provider name and
    `keyId` only. The KMS protocol (`api`) stays in server config, and the
    previous `source` name is replaced by `provider`.
    
    Callers resolve a client from `KmsClientRegistry` by provider. The
    registry binds each named provider to its API at startup from
    `gravitino.kms.provider.<name>.api`. The public reference no longer
    carries `api()` or `source()`.
    
    Server config becomes provider-named:
    
    ```
    gravitino.kms.providers=aws-prod,aws-dr,azure-eu
    gravitino.kms.provider.aws-prod.api=aws-kms
    gravitino.kms.provider.aws-dr.api=aws-kms
    gravitino.kms.provider.azure-eu.api=azure-key-vault
    ```
    
    JSON DTO: `{"provider":"aws-prod","keyId":"..."}`.
    
    This implements #12488 on the unreleased Developer API from #12132 and
    #12133. Enterprise stack, design-pack adapter prose, demo conf, and
    `gravitino-server-config.md` remain follow-up.
    
    ### Why are the changes needed?
    
    Putting `api` and `source` on the public key identity forced callers to
    know the protocol and invented a second name for a configured instance.
    Reviewers asked to change upstream `KmsReference` instead of adding a
    parallel identity type, and to use `provider` rather than `source`.
    
    Keeping the protocol in server config lets many named providers share
    one API without leaking that binding onto stored or exchanged key
    identity.
    
    Fix: #12488
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, for the unreleased `@DeveloperApi` KMS contracts:
    
    - `KmsReference` is `{provider, keyId}` instead of `{api, source,
    keyId}`
    - DTO JSON uses `provider` instead of `api` and `source`
    - Server config keys are `gravitino.kms.providers` and
    `gravitino.kms.provider.<name>.*` instead of `sources` /
    `source.<name>.*`
    - `KmsClientFactory.create` and registry lookup take a provider name;
    registry lookup no longer checks `reference.api()`
    
    There are no REST API changes. Docs for `gravitino-server-config.md` are
    out of scope here.
    
    ### How was this patch tested?
    
    - `./gradlew :api:test :common:test :core:test` for KMS tests passed
    
    ### Related work
    
    Closes: #12488
    Related to: #12131, #12132, #12133
    
    Nevin
    Sent from my 🤖 (Cursor)
    
    Made with [Cursor](https://cursor.com)
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 .../encryption/kms/KmsApiIdentifiers.java          |  57 ----
 .../gravitino/encryption/kms/KmsReference.java     |  50 +--
 .../encryption/kms/TestKmsApiIdentifiers.java      |  62 ----
 .../gravitino/encryption/kms/TestKmsReference.java |  55 +---
 common/build.gradle.kts                            |   1 +
 .../dto/encryption/kms/KmsReferenceDTO.java        |  14 +-
 .../gravitino/encryption/kms/KmsClientFactory.java |  25 +-
 .../dto/encryption/kms/TestKmsReferenceDTO.java    |  48 +--
 .../encryption/kms/TestFakeKmsClient.java          |  55 +---
 .../gravitino/encryption/kms/TestKmsClient.java    |   2 +-
 .../gravitino/encryption/kms/FakeKmsClient.java    |  26 +-
 .../encryption/kms/TestKmsClientContract.java      |  15 +-
 .../kms/TestKmsClientFactoryContract.java          |  45 ---
 .../encryption/kms/KmsClientRegistry.java          | 132 ++++----
 .../apache/gravitino/encryption/kms/KmsConfig.java | 108 +++----
 .../TestGravitinoEnvKmsClientRegistry.java         |   5 +-
 .../encryption/kms/TestKmsClientRegistry.java      | 355 +++++++++------------
 .../gravitino/encryption/kms/TestKmsConfig.java    | 110 ++++---
 docs/gravitino-server-config.md                    |  39 +++
 19 files changed, 440 insertions(+), 764 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/encryption/kms/KmsApiIdentifiers.java 
b/api/src/main/java/org/apache/gravitino/encryption/kms/KmsApiIdentifiers.java
deleted file mode 100644
index 7b2f9b74b6..0000000000
--- 
a/api/src/main/java/org/apache/gravitino/encryption/kms/KmsApiIdentifiers.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.encryption.kms;
-
-import com.google.common.base.Preconditions;
-import java.util.regex.Pattern;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.gravitino.annotation.DeveloperApi;
-
-/**
- * Validation helpers for KMS API identifiers.
- *
- * <p>Identifiers are matched exactly. They must be lowercase kebab-case with 
no surrounding
- * whitespace (for example {@code aws-kms}). Values are never normalized.
- */
-@DeveloperApi
-public final class KmsApiIdentifiers {
-
-  private static final Pattern LOWERCASE_KEBAB_CASE = 
Pattern.compile("^[a-z0-9]+(-[a-z0-9]+)*$");
-
-  private KmsApiIdentifiers() {}
-
-  /**
-   * Validates a KMS API identifier.
-   *
-   * @param api the identifier to validate
-   * @return the same identifier when valid
-   * @throws IllegalArgumentException if {@code api} is null, blank, padded, 
or not lowercase
-   *     kebab-case
-   */
-  public static String requireValid(String api) {
-    Preconditions.checkArgument(StringUtils.isNotBlank(api), "KMS API cannot 
be blank");
-    Preconditions.checkArgument(
-        api.equals(api.trim()), "KMS API cannot have leading or trailing 
whitespace");
-    Preconditions.checkArgument(
-        LOWERCASE_KEBAB_CASE.matcher(api).matches(),
-        "KMS API must be lowercase kebab-case: '%s'",
-        api);
-    return api;
-  }
-}
diff --git 
a/api/src/main/java/org/apache/gravitino/encryption/kms/KmsReference.java 
b/api/src/main/java/org/apache/gravitino/encryption/kms/KmsReference.java
index e68c0c7e00..8adae1d1ad 100644
--- a/api/src/main/java/org/apache/gravitino/encryption/kms/KmsReference.java
+++ b/api/src/main/java/org/apache/gravitino/encryption/kms/KmsReference.java
@@ -25,56 +25,40 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.annotation.DeveloperApi;
 
 /**
- * Identifies a key owned by a configured KMS source.
+ * Identifies a key owned by a named KMS provider.
  *
- * <p>Contains no credentials or key material.
- *
- * <p>KMS API identifiers use exact string matching. Callers must supply 
lowercase kebab-case values
- * with no leading or trailing whitespace (for example {@code aws-kms}). This 
type does not
- * normalize the identifier.
+ * <p>Contains no credentials or key material. The provider name is the 
configured instance handle
+ * ({@code gravitino.kms.providers}). The server loads that name's factory 
from {@code
+ * gravitino.kms.provider.<name>.className} and resolves a client from {@code 
KmsClientRegistry}.
  */
 @DeveloperApi
 public final class KmsReference {
 
-  private final String api;
-  private final String source;
+  private final String provider;
   private final String keyId;
 
   /**
    * Creates a structurally valid key reference without contacting the 
provider.
    *
-   * @param api explicitly selected KMS API identifier; must be lowercase 
kebab-case with no
-   *     surrounding whitespace
-   * @param source configured KMS client-instance name
+   * @param provider configured KMS provider name
    * @param keyId provider-native key identifier
-   * @throws IllegalArgumentException if any argument is null or blank, or if 
{@code api} is not a
-   *     valid KMS API identifier
+   * @throws IllegalArgumentException if either argument is null or blank
    */
-  public KmsReference(String api, String source, String keyId) {
-    this.api = KmsApiIdentifiers.requireValid(api);
-    Preconditions.checkArgument(StringUtils.isNotBlank(source), "KMS source 
cannot be blank");
+  public KmsReference(String provider, String keyId) {
+    Preconditions.checkArgument(StringUtils.isNotBlank(provider), "KMS 
provider cannot be blank");
     Preconditions.checkArgument(StringUtils.isNotBlank(keyId), "KMS key ID 
cannot be blank");
 
-    this.source = source.trim();
+    this.provider = provider.trim();
     this.keyId = keyId;
   }
 
   /**
-   * Returns the explicitly selected KMS API identifier.
-   *
-   * @return the exact KMS API identifier
-   */
-  public String api() {
-    return api;
-  }
-
-  /**
-   * Returns the configured KMS client-instance name.
+   * Returns the configured KMS provider name.
    *
-   * @return the source name
+   * @return the provider name
    */
-  public String source() {
-    return source;
+  public String provider() {
+    return provider;
   }
 
   /**
@@ -95,16 +79,16 @@ public final class KmsReference {
       return false;
     }
     KmsReference that = (KmsReference) other;
-    return api.equals(that.api) && source.equals(that.source) && 
keyId.equals(that.keyId);
+    return provider.equals(that.provider) && keyId.equals(that.keyId);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(api, source, keyId);
+    return Objects.hash(provider, keyId);
   }
 
   @Override
   public String toString() {
-    return String.format("KmsReference{api='%s', source='%s', keyId='%s'}", 
api, source, keyId);
+    return String.format("KmsReference{provider='%s', keyId='%s'}", provider, 
keyId);
   }
 }
diff --git 
a/api/src/test/java/org/apache/gravitino/encryption/kms/TestKmsApiIdentifiers.java
 
b/api/src/test/java/org/apache/gravitino/encryption/kms/TestKmsApiIdentifiers.java
deleted file mode 100644
index 2743451393..0000000000
--- 
a/api/src/test/java/org/apache/gravitino/encryption/kms/TestKmsApiIdentifiers.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.encryption.kms;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-public class TestKmsApiIdentifiers {
-
-  @Test
-  void testAcceptsLowercaseKebabCase() {
-    Assertions.assertEquals("aws-kms", 
KmsApiIdentifiers.requireValid("aws-kms"));
-    Assertions.assertEquals("test", KmsApiIdentifiers.requireValid("test"));
-    Assertions.assertEquals("acme-kms-v2", 
KmsApiIdentifiers.requireValid("acme-kms-v2"));
-  }
-
-  @Test
-  void testRejectsBlankAndPaddedValues() {
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid(null));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid(""));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> KmsApiIdentifiers.requireValid(" 
"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> KmsApiIdentifiers.requireValid(" 
aws-kms"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid("aws-kms "));
-  }
-
-  @Test
-  void testRejectsNonKebabCaseValues() {
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid("AWS-KMS"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid("aws_kms"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid("aws kms"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid("aws--kms"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid("-aws-kms"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
KmsApiIdentifiers.requireValid("aws-kms-"));
-  }
-}
diff --git 
a/api/src/test/java/org/apache/gravitino/encryption/kms/TestKmsReference.java 
b/api/src/test/java/org/apache/gravitino/encryption/kms/TestKmsReference.java
index 9c658b9c9b..071144fce1 100644
--- 
a/api/src/test/java/org/apache/gravitino/encryption/kms/TestKmsReference.java
+++ 
b/api/src/test/java/org/apache/gravitino/encryption/kms/TestKmsReference.java
@@ -24,64 +24,39 @@ import org.junit.jupiter.api.Test;
 public class TestKmsReference {
 
   @Test
-  void testStoresExactApiAndPreservesProviderKey() {
-    KmsReference reference = new KmsReference("aws-kms", " production ", " 
alias/Customer-Key ");
+  void testTrimsProviderAndPreservesKeyId() {
+    KmsReference reference = new KmsReference(" production ", " 
alias/Customer-Key ");
 
-    Assertions.assertEquals("aws-kms", reference.api());
-    Assertions.assertEquals("production", reference.source());
+    Assertions.assertEquals("production", reference.provider());
     Assertions.assertEquals(" alias/Customer-Key ", reference.keyId());
   }
 
   @Test
   void testRejectsMissingFields() {
+    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
KmsReference(null, "key"));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
KmsReference("", "key"));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
KmsReference(" ", "key"));
     Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference(null, 
"production", "key"));
+        IllegalArgumentException.class, () -> new KmsReference("production", 
null));
     Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("", 
"production", "key"));
+        IllegalArgumentException.class, () -> new KmsReference("production", 
""));
     Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference(" ", 
"production", "key"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("aws-kms", 
null, "key"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("aws-kms", 
"production", null));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("aws-kms", "", 
"key"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("aws-kms", " ", 
"key"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("aws-kms", 
"production", ""));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("aws-kms", 
"production", " "));
-  }
-
-  @Test
-  void testRejectsInvalidApiIdentifiers() {
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference(" aws-kms", 
"production", "key"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("aws-kms ", 
"production", "key"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("AWS-KMS", 
"production", "key"));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsReference("aws_kms", 
"production", "key"));
+        IllegalArgumentException.class, () -> new KmsReference("production", " 
"));
   }
 
   @Test
   void testValueSemantics() {
-    KmsReference first = new KmsReference("aws-kms", "production", "key");
-    KmsReference same = new KmsReference("aws-kms", "production", "key");
-    KmsReference differentApi = new KmsReference("google-cloud-kms", 
"production", "key");
-    KmsReference differentSource = new KmsReference("aws-kms", "recovery", 
"key");
-    KmsReference differentKey = new KmsReference("aws-kms", "production", 
"another-key");
+    KmsReference first = new KmsReference("production", "key");
+    KmsReference same = new KmsReference("production", "key");
+    KmsReference differentProvider = new KmsReference("recovery", "key");
+    KmsReference differentKey = new KmsReference("production", "another-key");
 
     Assertions.assertEquals(first, same);
     Assertions.assertEquals(first.hashCode(), same.hashCode());
-    Assertions.assertNotEquals(first, differentApi);
-    Assertions.assertNotEquals(first, differentSource);
+    Assertions.assertNotEquals(first, differentProvider);
     Assertions.assertNotEquals(first, differentKey);
     Assertions.assertNotEquals(first, null);
     Assertions.assertNotEquals(first, "key");
-    Assertions.assertEquals(
-        "KmsReference{api='aws-kms', source='production', keyId='key'}", 
first.toString());
+    Assertions.assertEquals("KmsReference{provider='production', 
keyId='key'}", first.toString());
   }
 }
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index 3f20efd17a..09d549183b 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -54,6 +54,7 @@ dependencies {
 
   testFixturesApi(project(":api"))
   testFixturesApi(libs.junit.jupiter.api)
+  testFixturesImplementation(libs.commons.lang3)
 }
 
 fun getGitCommitId(): String {
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/encryption/kms/KmsReferenceDTO.java
 
b/common/src/main/java/org/apache/gravitino/dto/encryption/kms/KmsReferenceDTO.java
index 3e9f501a23..ac4fb88e6d 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/encryption/kms/KmsReferenceDTO.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/encryption/kms/KmsReferenceDTO.java
@@ -35,11 +35,8 @@ import org.apache.gravitino.encryption.kms.KmsReference;
 @Builder(setterPrefix = "with")
 public class KmsReferenceDTO {
 
-  @JsonProperty("api")
-  private String api;
-
-  @JsonProperty("source")
-  private String source;
+  @JsonProperty("provider")
+  private String provider;
 
   @JsonProperty("keyId")
   private String keyId;
@@ -47,13 +44,10 @@ public class KmsReferenceDTO {
   /**
    * Converts this DTO to a {@link KmsReference}.
    *
-   * <p>{@code api} is validated by {@link KmsReference}: it must already be 
lowercase kebab-case
-   * with no surrounding whitespace and is matched exactly.
-   *
    * @return the KMS key reference
    */
   public KmsReference toKmsReference() {
-    return new KmsReference(api, source, keyId);
+    return new KmsReference(provider, keyId);
   }
 
   /**
@@ -63,6 +57,6 @@ public class KmsReferenceDTO {
    * @return the KMS key reference DTO
    */
   public static KmsReferenceDTO fromKmsReference(KmsReference reference) {
-    return new KmsReferenceDTO(reference.api(), reference.source(), 
reference.keyId());
+    return new KmsReferenceDTO(reference.provider(), reference.keyId());
   }
 }
diff --git 
a/common/src/main/java/org/apache/gravitino/encryption/kms/KmsClientFactory.java
 
b/common/src/main/java/org/apache/gravitino/encryption/kms/KmsClientFactory.java
index c72ea2a89a..c290edfe87 100644
--- 
a/common/src/main/java/org/apache/gravitino/encryption/kms/KmsClientFactory.java
+++ 
b/common/src/main/java/org/apache/gravitino/encryption/kms/KmsClientFactory.java
@@ -21,22 +21,17 @@ package org.apache.gravitino.encryption.kms;
 import java.util.Map;
 import org.apache.gravitino.annotation.DeveloperApi;
 
-/** Creates server-side KMS clients for one KMS API. */
+/**
+ * Creates a server-side KMS client for one configured provider.
+ *
+ * <p>The server loads an implementation from {@code 
gravitino.kms.provider.<name>.className}. The
+ * class must have a public no-arg constructor.
+ */
 @DeveloperApi
 public interface KmsClientFactory {
 
   /**
-   * Returns the exact KMS API identifier implemented by this factory.
-   *
-   * <p>Identifiers use lowercase kebab-case with no surrounding whitespace 
({@link
-   * KmsApiIdentifiers}) and are matched exactly against {@link 
KmsReference#api()}.
-   *
-   * @return the KMS API identifier
-   */
-  String api();
-
-  /**
-   * Creates a client bound to a configured KMS source.
+   * Creates a client bound to a configured KMS provider.
    *
    * <p>Provider credentials are private implementation details of the 
returned client. They must
    * not be exposed as Gravitino credentials or key properties. The caller 
owns the returned client
@@ -44,10 +39,10 @@ public interface KmsClientFactory {
    * contacting the configured KMS; network and authentication failures are 
reported by client
    * operations.
    *
-   * @param source logical name of the configured KMS instance
+   * @param provider logical name of the configured KMS instance
    * @param properties provider-specific configuration
    * @return the configured client
-   * @throws IllegalArgumentException if the source or configuration is invalid
+   * @throws IllegalArgumentException if the provider or configuration is 
invalid
    */
-  KmsClient create(String source, Map<String, String> properties);
+  KmsClient create(String provider, Map<String, String> properties);
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/encryption/kms/TestKmsReferenceDTO.java
 
b/common/src/test/java/org/apache/gravitino/dto/encryption/kms/TestKmsReferenceDTO.java
index a7353f45ed..1b0f10eeee 100644
--- 
a/common/src/test/java/org/apache/gravitino/dto/encryption/kms/TestKmsReferenceDTO.java
+++ 
b/common/src/test/java/org/apache/gravitino/dto/encryption/kms/TestKmsReferenceDTO.java
@@ -30,9 +30,8 @@ public class TestKmsReferenceDTO {
   private final ObjectMapper objectMapper = JsonUtils.objectMapper();
 
   @Test
-  void testRoundTripPreservesExactApi() throws JsonProcessingException {
-    KmsReference reference =
-        new KmsReference("google-cloud-kms", "analytics-prod", 
"projects/p/keys/k");
+  void testRoundTripPreservesProviderAndKeyId() throws JsonProcessingException 
{
+    KmsReference reference = new KmsReference("analytics-prod", 
"projects/p/keys/k");
 
     KmsReferenceDTO dto = KmsReferenceDTO.fromKmsReference(reference);
     String json = objectMapper.writeValueAsString(dto);
@@ -40,52 +39,23 @@ public class TestKmsReferenceDTO {
 
     Assertions.assertEquals(reference, restored);
     Assertions.assertEquals(
-        objectMapper.readTree(
-            "{\"api\":\"google-cloud-kms\",\"source\":\"analytics-prod\","
-                + "\"keyId\":\"projects/p/keys/k\"}"),
+        
objectMapper.readTree("{\"provider\":\"analytics-prod\",\"keyId\":\"projects/p/keys/k\"}"),
         objectMapper.readTree(json));
   }
 
   @Test
-  void testAcceptsCustomApiIdentifier() throws JsonProcessingException {
-    KmsReference reference = new KmsReference("acme-kms-v2", "team-a", 
"keys/primary");
-    KmsReferenceDTO dto = KmsReferenceDTO.fromKmsReference(reference);
-
-    String json = objectMapper.writeValueAsString(dto);
-    KmsReference restored = objectMapper.readValue(json, 
KmsReferenceDTO.class).toKmsReference();
-
-    Assertions.assertEquals("acme-kms-v2", restored.api());
-    Assertions.assertEquals(reference, restored);
-  }
-
-  @Test
-  void testToKmsReferenceRejectsPaddedApi() {
+  void testToKmsReferenceRejectsBlankProvider() {
     KmsReferenceDTO dto =
-        KmsReferenceDTO.builder()
-            .withApi(" google-cloud-kms ")
-            .withSource("analytics-prod")
-            .withKeyId("projects/p/keys/k")
-            .build();
+        KmsReferenceDTO.builder().withProvider(" 
").withKeyId("projects/p/keys/k").build();
 
     Assertions.assertThrows(IllegalArgumentException.class, 
dto::toKmsReference);
   }
 
   @Test
-  void testToKmsReferenceRejectsInvalidApiFormat() {
-    KmsReferenceDTO uppercase =
-        KmsReferenceDTO.builder()
-            .withApi("AWS-KMS")
-            .withSource("production")
-            .withKeyId("key")
-            .build();
-    KmsReferenceDTO snakeCase =
-        KmsReferenceDTO.builder()
-            .withApi("aws_kms")
-            .withSource("production")
-            .withKeyId("key")
-            .build();
+  void testToKmsReferenceRejectsBlankKeyId() {
+    KmsReferenceDTO dto =
+        KmsReferenceDTO.builder().withProvider("analytics-prod").withKeyId(" 
").build();
 
-    Assertions.assertThrows(IllegalArgumentException.class, 
uppercase::toKmsReference);
-    Assertions.assertThrows(IllegalArgumentException.class, 
snakeCase::toKmsReference);
+    Assertions.assertThrows(IllegalArgumentException.class, 
dto::toKmsReference);
   }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/encryption/kms/TestFakeKmsClient.java
 
b/common/src/test/java/org/apache/gravitino/encryption/kms/TestFakeKmsClient.java
index 99683410ac..224e4e7bf4 100644
--- 
a/common/src/test/java/org/apache/gravitino/encryption/kms/TestFakeKmsClient.java
+++ 
b/common/src/test/java/org/apache/gravitino/encryption/kms/TestFakeKmsClient.java
@@ -23,66 +23,35 @@ import org.junit.jupiter.api.Test;
 
 public class TestFakeKmsClient extends TestKmsClientContract {
 
-  private static final String API = "test-kms";
-  private static final String SOURCE = "test";
+  private static final String PROVIDER = "test";
   private static final String USABLE_KEY = "usable";
   private static final String DISABLED_KEY = "disabled";
   private static final String MISSING_KEY = "missing";
 
   private final FakeKmsClient client =
-      new FakeKmsClient(API, SOURCE)
+      new FakeKmsClient(PROVIDER)
           .putKey(USABLE_KEY, true, true, true)
           .putKey(DISABLED_KEY, false, true, true);
 
   @Test
-  void testRejectsBlankApi() {
-    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient(null, SOURCE));
-    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient("", SOURCE));
-    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient(" ", SOURCE));
+  void testRejectsBlankProvider() {
+    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient(null));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient(""));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient(" "));
   }
 
   @Test
-  void testRejectsPaddedApi() {
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new FakeKmsClient(" " + API, 
SOURCE));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new FakeKmsClient(API + " ", 
SOURCE));
-  }
-
-  @Test
-  void testRejectsInvalidApiFormat() {
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new FakeKmsClient("TEST-KMS", 
SOURCE));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new FakeKmsClient("test_kms", 
SOURCE));
-  }
-
-  @Test
-  void testMatchesApiExactly() {
-    Assertions.assertThrows(
-        IllegalArgumentException.class,
-        () -> client.getKeyProperties(new KmsReference("other-kms", SOURCE, 
USABLE_KEY)));
-  }
-
-  @Test
-  void testRejectsBlankSource() {
-    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient(API, null));
-    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient(API, ""));
-    Assertions.assertThrows(IllegalArgumentException.class, () -> new 
FakeKmsClient(API, " "));
-  }
-
-  @Test
-  void testNormalizesSource() {
-    FakeKmsClient paddedSourceClient = new FakeKmsClient(API, " " + SOURCE + " 
");
+  void testNormalizesProvider() {
+    FakeKmsClient paddedProviderClient = new FakeKmsClient(" " + PROVIDER + " 
");
 
     Assertions.assertDoesNotThrow(
-        () -> paddedSourceClient.getKeyProperties(new KmsReference(API, 
SOURCE, MISSING_KEY)));
+        () -> paddedProviderClient.getKeyProperties(new KmsReference(PROVIDER, 
MISSING_KEY)));
   }
 
   @Test
   void testReportsDisabledKeyAsPresent() {
     KmsKeyProperties properties =
-        client.getKeyProperties(new KmsReference(API, SOURCE, 
DISABLED_KEY)).orElseThrow();
+        client.getKeyProperties(new KmsReference(PROVIDER, 
DISABLED_KEY)).orElseThrow();
 
     Assertions.assertFalse(properties.enabled());
   }
@@ -94,11 +63,11 @@ public class TestFakeKmsClient extends 
TestKmsClientContract {
 
   @Override
   protected KmsReference usableKey() {
-    return new KmsReference(API, SOURCE, USABLE_KEY);
+    return new KmsReference(PROVIDER, USABLE_KEY);
   }
 
   @Override
   protected KmsReference missingKey() {
-    return new KmsReference(API, SOURCE, MISSING_KEY);
+    return new KmsReference(PROVIDER, MISSING_KEY);
   }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/encryption/kms/TestKmsClient.java 
b/common/src/test/java/org/apache/gravitino/encryption/kms/TestKmsClient.java
index 9c2b6b4c08..65f1c6cefa 100644
--- 
a/common/src/test/java/org/apache/gravitino/encryption/kms/TestKmsClient.java
+++ 
b/common/src/test/java/org/apache/gravitino/encryption/kms/TestKmsClient.java
@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
 
 public class TestKmsClient {
 
-  private static final KmsReference REFERENCE = new KmsReference("test-kms", 
"production", "key");
+  private static final KmsReference REFERENCE = new KmsReference("production", 
"key");
 
   @Test
   void testReturnsProviderProperties() {
diff --git 
a/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/FakeKmsClient.java
 
b/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/FakeKmsClient.java
index 591c13f203..eb1f31ff6d 100644
--- 
a/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/FakeKmsClient.java
+++ 
b/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/FakeKmsClient.java
@@ -21,27 +21,24 @@ package org.apache.gravitino.encryption.kms;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
 
 /** In-memory KMS client for contract and consumer tests. */
 public final class FakeKmsClient implements KmsClient {
 
-  private final String api;
-  private final String source;
+  private final String provider;
   private final Map<String, KeyState> keys = new HashMap<>();
 
   /**
    * Creates an empty fake client.
    *
-   * @param api exact KMS API identifier accepted by the client; lowercase 
kebab-case with no
-   *     surrounding whitespace
-   * @param source configured source accepted by the client
+   * @param provider configured provider accepted by the client
    */
-  public FakeKmsClient(String api, String source) {
-    this.api = KmsApiIdentifiers.requireValid(api);
-    if (source == null || source.trim().isEmpty()) {
-      throw new IllegalArgumentException("KMS source cannot be blank");
+  public FakeKmsClient(String provider) {
+    if (StringUtils.isBlank(provider)) {
+      throw new IllegalArgumentException("KMS provider cannot be blank");
     }
-    this.source = source.trim();
+    this.provider = provider.trim();
   }
 
   /**
@@ -75,14 +72,11 @@ public final class FakeKmsClient implements KmsClient {
     if (reference == null) {
       throw new IllegalArgumentException("KMS reference cannot be null");
     }
-    if (!reference.api().equals(api)) {
-      throw new IllegalArgumentException(
-          String.format("Expected KMS API '%s' but received '%s'", api, 
reference.api()));
-    }
-    if (!source.equals(reference.source())) {
+    if (!provider.equals(reference.provider())) {
       throw new IllegalArgumentException(
           String.format(
-              "KMS source %s does not match configured source %s", 
reference.source(), source));
+              "KMS provider %s does not match configured provider %s",
+              reference.provider(), provider));
     }
   }
 
diff --git 
a/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/TestKmsClientContract.java
 
b/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/TestKmsClientContract.java
index 868d64bd62..9b4b55ec67 100644
--- 
a/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/TestKmsClientContract.java
+++ 
b/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/TestKmsClientContract.java
@@ -74,20 +74,9 @@ public abstract class TestKmsClientContract {
   }
 
   @Test
-  void testRejectsMismatchedSource() {
+  void testRejectsMismatchedProvider() {
     KmsReference reference = usableKey();
-    KmsReference mismatched =
-        new KmsReference(reference.api(), reference.source() + "-other", 
reference.keyId());
-
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> 
client().getKeyProperties(mismatched));
-  }
-
-  @Test
-  void testRejectsMismatchedApi() {
-    KmsReference reference = usableKey();
-    KmsReference mismatched =
-        new KmsReference(reference.api() + "-other", reference.source(), 
reference.keyId());
+    KmsReference mismatched = new KmsReference(reference.provider() + 
"-other", reference.keyId());
 
     Assertions.assertThrows(
         IllegalArgumentException.class, () -> 
client().getKeyProperties(mismatched));
diff --git 
a/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/TestKmsClientFactoryContract.java
 
b/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/TestKmsClientFactoryContract.java
deleted file mode 100644
index 52eec8f5dc..0000000000
--- 
a/common/src/testFixtures/java/org/apache/gravitino/encryption/kms/TestKmsClientFactoryContract.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.encryption.kms;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-/** Common contract for KMS client factories. */
-public abstract class TestKmsClientFactoryContract {
-
-  /**
-   * Returns the factory under test.
-   *
-   * @return the factory
-   */
-  protected abstract KmsClientFactory factory();
-
-  /**
-   * Returns the API expected from the factory.
-   *
-   * @return the expected API identifier
-   */
-  protected abstract String expectedApi();
-
-  @Test
-  void testReportsExpectedApi() {
-    Assertions.assertEquals(expectedApi(), factory().api());
-  }
-}
diff --git 
a/core/src/main/java/org/apache/gravitino/encryption/kms/KmsClientRegistry.java 
b/core/src/main/java/org/apache/gravitino/encryption/kms/KmsClientRegistry.java
index c7a687fe53..c8f9a585c8 100644
--- 
a/core/src/main/java/org/apache/gravitino/encryption/kms/KmsClientRegistry.java
+++ 
b/core/src/main/java/org/apache/gravitino/encryption/kms/KmsClientRegistry.java
@@ -23,55 +23,54 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.ServiceLoader;
 import org.apache.gravitino.Config;
 
-/** Creates, resolves, and owns server-private KMS clients by configured 
source. */
+/** Creates, resolves, and owns server-private KMS clients by configured 
provider. */
 public final class KmsClientRegistry implements AutoCloseable {
 
-  private final Map<String, ConfiguredClient> clients;
+  private final Map<String, KmsClient> clients;
   private volatile boolean closed;
 
   /**
-   * Loads configuration and available {@link KmsClientFactory} 
implementations, then creates one
-   * client for each configured source.
+   * Loads configuration and creates one client for each configured provider 
by instantiating {@code
+   * gravitino.kms.provider.<name>.className}.
    *
    * @param config Gravitino server configuration
-   * @throws IllegalArgumentException if configuration or factory discovery is 
invalid
+   * @throws IllegalArgumentException if configuration or factory construction 
is invalid
    */
   public KmsClientRegistry(Config config) {
-    this(config, loadFactories());
+    this(config, KmsClientRegistry::loadFactory);
   }
 
-  KmsClientRegistry(Config config, Iterable<KmsClientFactory> factories) {
+  KmsClientRegistry(Config config, FactoryLoader loader) {
     KmsConfig kmsConfig = new KmsConfig(config);
-    if (kmsConfig.sources().isEmpty()) {
+    if (kmsConfig.providers().isEmpty()) {
       this.clients = Collections.emptyMap();
       return;
     }
 
-    if (factories == null) {
-      throw new IllegalArgumentException("KMS client factories cannot be 
null");
+    if (loader == null) {
+      throw new IllegalArgumentException("KMS client factory loader cannot be 
null");
     }
 
-    Map<String, KmsClientFactory> factoriesByApi = indexFactories(factories);
-    this.clients = createClients(kmsConfig.sources(), factoriesByApi);
+    this.clients = createClients(kmsConfig.providers(), loader);
   }
 
   /**
    * Resolves the client configured for a key reference.
    *
    * <p>The registry owns the returned client. Callers must not close it or 
use it after the
-   * registry is closed.
+   * registry is closed. Lookup is by {@link KmsReference#provider()} only; 
the provider's factory
+   * was loaded at startup from {@code 
gravitino.kms.provider.<name>.className}.
    *
-   * @param reference key whose source and API select the client
+   * @param reference key whose provider name selects the client
    * @return client configured for the reference
-   * @throws IllegalArgumentException if the source is unknown or configured 
for another API
+   * @throws IllegalArgumentException if the provider is unknown
    * @throws IllegalStateException if the registry is closed
    */
   public KmsClient getClient(KmsReference reference) {
     checkOpen();
-    return resolveClient(reference).client;
+    return resolveClient(reference);
   }
 
   /** Closes all configured clients. This operation is idempotent. */
@@ -87,52 +86,55 @@ public final class KmsClientRegistry implements 
AutoCloseable {
     }
   }
 
-  private static Map<String, KmsClientFactory> indexFactories(
-      Iterable<KmsClientFactory> factories) {
-    Map<String, KmsClientFactory> factoriesByApi = new LinkedHashMap<>();
-    for (KmsClientFactory factory : factories) {
-      if (factory == null) {
-        throw new IllegalArgumentException("KMS client factory cannot be 
null");
-      }
-      String api = KmsApiIdentifiers.requireValid(factory.api());
-      KmsClientFactory existing = factoriesByApi.putIfAbsent(api, factory);
-      if (existing != null) {
-        throw new IllegalArgumentException(
-            String.format("Multiple KMS client factories support API '%s'", 
api));
-      }
-    }
-    return factoriesByApi;
+  /** Loads a {@link KmsClientFactory} from a configured class name. */
+  @FunctionalInterface
+  interface FactoryLoader {
+    /**
+     * Instantiates the factory named by {@code className}.
+     *
+     * @param className factory class name
+     * @return the factory
+     */
+    KmsClientFactory load(String className);
   }
 
-  private static Iterable<KmsClientFactory> loadFactories() {
-    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
-    if (classLoader == null) {
-      classLoader = KmsClientRegistry.class.getClassLoader();
+  private static KmsClientFactory loadFactory(String className) {
+    try {
+      Object instance = 
Class.forName(className).getDeclaredConstructor().newInstance();
+      if (!(instance instanceof KmsClientFactory)) {
+        throw new IllegalArgumentException(
+            String.format("KMS factory class '%s' does not implement 
KmsClientFactory", className));
+      }
+      return (KmsClientFactory) instance;
+    } catch (ClassNotFoundException e) {
+      throw new IllegalArgumentException(
+          String.format("No KMS client factory class '%s'", className), e);
+    } catch (ReflectiveOperationException e) {
+      throw new IllegalArgumentException(
+          String.format("Failed to create KMS client factory '%s'", 
className), e);
     }
-    return ServiceLoader.load(KmsClientFactory.class, classLoader);
   }
 
-  private static Map<String, ConfiguredClient> createClients(
-      Map<String, KmsConfig.SourceConfig> sourceConfigs,
-      Map<String, KmsClientFactory> factoriesByApi) {
-    Map<String, ConfiguredClient> clients = new LinkedHashMap<>();
+  private static Map<String, KmsClient> createClients(
+      Map<String, KmsConfig.ProviderConfig> providerConfigs, FactoryLoader 
loader) {
+    Map<String, KmsClient> clients = new LinkedHashMap<>();
     try {
-      sourceConfigs.forEach(
-          (source, sourceConfig) -> {
-            KmsClientFactory factory = factoriesByApi.get(sourceConfig.api());
+      providerConfigs.forEach(
+          (provider, providerConfig) -> {
+            KmsClientFactory factory = loader.load(providerConfig.className());
             if (factory == null) {
-              throw new IllegalArgumentException(
+              throw new IllegalStateException(
                   String.format(
-                      "No KMS client factory supports API '%s' for source 
'%s'",
-                      sourceConfig.api(), source));
+                      "KMS client factory '%s' returned null", 
providerConfig.className()));
             }
-            KmsClient client = factory.create(source, 
sourceConfig.properties());
+            KmsClient client = factory.create(provider, 
providerConfig.properties());
             if (client == null) {
               throw new IllegalStateException(
                   String.format(
-                      "KMS client factory for API '%s' returned null", 
sourceConfig.api()));
+                      "KMS client factory '%s' returned a null client",
+                      providerConfig.className()));
             }
-            clients.put(source, new ConfiguredClient(sourceConfig.api(), 
client));
+            clients.put(provider, client);
           });
       return Collections.unmodifiableMap(clients);
     } catch (RuntimeException | Error e) {
@@ -144,11 +146,11 @@ public final class KmsClientRegistry implements 
AutoCloseable {
     }
   }
 
-  private static RuntimeException closeClients(List<ConfiguredClient> clients) 
{
+  private static RuntimeException closeClients(List<KmsClient> clients) {
     RuntimeException failure = null;
     for (int index = clients.size() - 1; index >= 0; index--) {
       try {
-        clients.get(index).client.close();
+        clients.get(index).close();
       } catch (RuntimeException e) {
         if (failure == null) {
           failure = e;
@@ -160,23 +162,17 @@ public final class KmsClientRegistry implements 
AutoCloseable {
     return failure;
   }
 
-  private ConfiguredClient resolveClient(KmsReference reference) {
+  private KmsClient resolveClient(KmsReference reference) {
     if (reference == null) {
       throw new IllegalArgumentException("KMS reference cannot be null");
     }
 
-    ConfiguredClient configuredClient = clients.get(reference.source());
-    if (configuredClient == null) {
-      throw new IllegalArgumentException(
-          String.format("No KMS client is configured for source '%s'", 
reference.source()));
-    }
-    if (!configuredClient.api.equals(reference.api())) {
+    KmsClient client = clients.get(reference.provider());
+    if (client == null) {
       throw new IllegalArgumentException(
-          String.format(
-              "KMS source '%s' uses API '%s', not '%s'",
-              reference.source(), configuredClient.api, reference.api()));
+          String.format("No KMS client is configured for provider '%s'", 
reference.provider()));
     }
-    return configuredClient;
+    return client;
   }
 
   private void checkOpen() {
@@ -184,14 +180,4 @@ public final class KmsClientRegistry implements 
AutoCloseable {
       throw new IllegalStateException("KMS client registry is closed");
     }
   }
-
-  private static final class ConfiguredClient {
-    private final String api;
-    private final KmsClient client;
-
-    private ConfiguredClient(String api, KmsClient client) {
-      this.api = api;
-      this.client = client;
-    }
-  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/encryption/kms/KmsConfig.java 
b/core/src/main/java/org/apache/gravitino/encryption/kms/KmsConfig.java
index 24b46f26d7..63e65da4fb 100644
--- a/core/src/main/java/org/apache/gravitino/encryption/kms/KmsConfig.java
+++ b/core/src/main/java/org/apache/gravitino/encryption/kms/KmsConfig.java
@@ -31,14 +31,14 @@ import org.apache.gravitino.Config;
 final class KmsConfig {
 
   static final String KMS_CONFIG_PREFIX = "gravitino.kms.";
-  static final String KMS_SOURCES = KMS_CONFIG_PREFIX + "sources";
+  static final String KMS_PROVIDERS = KMS_CONFIG_PREFIX + "providers";
 
-  private static final String SOURCES = "sources";
-  private static final String SOURCE_PREFIX = "source.";
-  private static final String API = "api";
-  private static final Pattern SOURCE_NAME_PATTERN = 
Pattern.compile("[A-Za-z0-9][A-Za-z0-9_-]*");
+  private static final String PROVIDERS = "providers";
+  private static final String PROVIDER_PREFIX = "provider.";
+  private static final String CLASS_NAME = "className";
+  private static final Pattern PROVIDER_NAME_PATTERN = 
Pattern.compile("[A-Za-z0-9][A-Za-z0-9_-]*");
 
-  private final Map<String, SourceConfig> sources;
+  private final Map<String, ProviderConfig> providers;
 
   KmsConfig(Config config) {
     if (config == null) {
@@ -46,94 +46,88 @@ final class KmsConfig {
     }
 
     Map<String, String> values = 
config.getConfigsWithPrefix(KMS_CONFIG_PREFIX);
-    List<String> configuredSources = parseSources(values.get(SOURCES));
-    this.sources = parseSourceConfigs(values, configuredSources);
+    List<String> configuredProviders = parseProviders(values.get(PROVIDERS));
+    this.providers = parseProviderConfigs(values, configuredProviders);
   }
 
-  Map<String, SourceConfig> sources() {
-    return sources;
+  Map<String, ProviderConfig> providers() {
+    return providers;
   }
 
-  private static List<String> parseSources(String value) {
+  private static List<String> parseProviders(String value) {
     if (value == null || value.trim().isEmpty()) {
       return Collections.emptyList();
     }
 
-    List<String> sources = new ArrayList<>();
-    Set<String> uniqueSources = new LinkedHashSet<>();
+    List<String> providers = new ArrayList<>();
+    Set<String> uniqueProviders = new LinkedHashSet<>();
     for (String item : value.split(",", -1)) {
-      String source = item.trim();
-      if (!SOURCE_NAME_PATTERN.matcher(source).matches()) {
+      String provider = item.trim();
+      if (!PROVIDER_NAME_PATTERN.matcher(provider).matches()) {
         throw new KmsConfigurationException(
-            "Invalid KMS source name '%s' in %s", source, KMS_SOURCES);
+            "Invalid KMS provider name '%s' in %s", provider, KMS_PROVIDERS);
       }
-      if (!uniqueSources.add(source)) {
-        throw new KmsConfigurationException("Duplicate KMS source '%s' in %s", 
source, KMS_SOURCES);
+      if (!uniqueProviders.add(provider)) {
+        throw new KmsConfigurationException(
+            "Duplicate KMS provider '%s' in %s", provider, KMS_PROVIDERS);
       }
-      sources.add(source);
+      providers.add(provider);
     }
-    return Collections.unmodifiableList(sources);
+    return Collections.unmodifiableList(providers);
   }
 
-  private static Map<String, SourceConfig> parseSourceConfigs(
-      Map<String, String> values, List<String> configuredSources) {
-    Map<String, Map<String, String>> propertiesBySource = new 
LinkedHashMap<>();
-    for (String source : configuredSources) {
-      propertiesBySource.put(source, new LinkedHashMap<>());
+  private static Map<String, ProviderConfig> parseProviderConfigs(
+      Map<String, String> values, List<String> configuredProviders) {
+    Map<String, Map<String, String>> propertiesByProvider = new 
LinkedHashMap<>();
+    for (String provider : configuredProviders) {
+      propertiesByProvider.put(provider, new LinkedHashMap<>());
     }
 
     for (Map.Entry<String, String> entry : values.entrySet()) {
       String key = entry.getKey();
-      if (SOURCES.equals(key)) {
+      if (PROVIDERS.equals(key)) {
         continue;
       }
-      if (!key.startsWith(SOURCE_PREFIX)) {
+      if (!key.startsWith(PROVIDER_PREFIX)) {
         throw invalidConfigurationKey(key);
       }
 
-      String sourceAndProperty = key.substring(SOURCE_PREFIX.length());
-      int separator = sourceAndProperty.indexOf('.');
-      if (separator <= 0 || separator == sourceAndProperty.length() - 1) {
+      String providerAndProperty = key.substring(PROVIDER_PREFIX.length());
+      int separator = providerAndProperty.indexOf('.');
+      if (separator <= 0 || separator == providerAndProperty.length() - 1) {
         throw invalidConfigurationKey(key);
       }
 
-      String source = sourceAndProperty.substring(0, separator);
-      if (!SOURCE_NAME_PATTERN.matcher(source).matches()) {
+      String provider = providerAndProperty.substring(0, separator);
+      if (!PROVIDER_NAME_PATTERN.matcher(provider).matches()) {
         throw invalidConfigurationKey(key);
       }
 
-      Map<String, String> properties = propertiesBySource.get(source);
+      Map<String, String> properties = propertiesByProvider.get(provider);
       if (properties == null) {
         throw new KmsConfigurationException(
-            "KMS configuration references unlisted source '%s'", source);
+            "KMS configuration references unlisted provider '%s'", provider);
       }
 
-      String property = sourceAndProperty.substring(separator + 1);
+      String property = providerAndProperty.substring(separator + 1);
       properties.put(property, entry.getValue());
     }
 
-    Map<String, SourceConfig> sourceConfigs = new LinkedHashMap<>();
+    Map<String, ProviderConfig> providerConfigs = new LinkedHashMap<>();
 
-    for (String source : configuredSources) {
-      String apiKey = SOURCE_PREFIX + source + "." + API;
-      Map<String, String> properties = propertiesBySource.get(source);
-      String apiValue = properties.remove(API);
-      if (apiValue == null || apiValue.trim().isEmpty()) {
-        throw new KmsConfigurationException(
-            "KMS API property '%s%s' cannot be blank", KMS_CONFIG_PREFIX, 
apiKey);
-      }
-      String api;
-      try {
-        api = KmsApiIdentifiers.requireValid(apiValue);
-      } catch (IllegalArgumentException e) {
+    for (String provider : configuredProviders) {
+      String classNameKey = PROVIDER_PREFIX + provider + "." + CLASS_NAME;
+      Map<String, String> properties = propertiesByProvider.get(provider);
+      String className = properties.remove(CLASS_NAME);
+      if (className == null || className.trim().isEmpty()) {
         throw new KmsConfigurationException(
-            e, "Invalid KMS API property '%s%s': %s", KMS_CONFIG_PREFIX, 
apiKey, e.getMessage());
+            "KMS className property '%s%s' cannot be blank", 
KMS_CONFIG_PREFIX, classNameKey);
       }
 
-      sourceConfigs.put(source, new SourceConfig(api, properties));
+      providerConfigs.put(provider, new ProviderConfig(className.trim(), 
properties));
     }
 
-    return Collections.unmodifiableMap(sourceConfigs);
+    return Collections.unmodifiableMap(providerConfigs);
   }
 
   private static KmsConfigurationException invalidConfigurationKey(String key) 
{
@@ -141,17 +135,17 @@ final class KmsConfig {
         "Invalid KMS configuration key '%s%s'", KMS_CONFIG_PREFIX, key);
   }
 
-  static final class SourceConfig {
-    private final String api;
+  static final class ProviderConfig {
+    private final String className;
     private final Map<String, String> properties;
 
-    private SourceConfig(String api, Map<String, String> properties) {
-      this.api = api;
+    private ProviderConfig(String className, Map<String, String> properties) {
+      this.className = className;
       this.properties = Collections.unmodifiableMap(new 
LinkedHashMap<>(properties));
     }
 
-    String api() {
-      return api;
+    String className() {
+      return className;
     }
 
     Map<String, String> properties() {
diff --git 
a/core/src/test/java/org/apache/gravitino/TestGravitinoEnvKmsClientRegistry.java
 
b/core/src/test/java/org/apache/gravitino/TestGravitinoEnvKmsClientRegistry.java
index 5f08061a78..142aaa500a 100644
--- 
a/core/src/test/java/org/apache/gravitino/TestGravitinoEnvKmsClientRegistry.java
+++ 
b/core/src/test/java/org/apache/gravitino/TestGravitinoEnvKmsClientRegistry.java
@@ -35,7 +35,7 @@ public class TestGravitinoEnvKmsClientRegistry {
     FieldUtils.writeField(env, "kmsClientRegistry", registry, true);
 
     Assertions.assertSame(registry, env.kmsClientRegistry());
-    KmsReference reference = new KmsReference("aws-kms", "missing", "key");
+    KmsReference reference = new KmsReference("missing", "key");
     Assertions.assertThrows(IllegalArgumentException.class, () -> 
registry.getClient(reference));
 
     env.shutdown();
@@ -54,8 +54,7 @@ public class TestGravitinoEnvKmsClientRegistry {
 
     Assertions.assertSame(registry, env.kmsClientRegistry());
     Assertions.assertThrows(
-        IllegalStateException.class,
-        () -> registry.getClient(new KmsReference("aws-kms", "missing", 
"key")));
+        IllegalStateException.class, () -> registry.getClient(new 
KmsReference("missing", "key")));
   }
 
   private static final class TestGravitinoEnv extends GravitinoEnv {}
diff --git 
a/core/src/test/java/org/apache/gravitino/encryption/kms/TestKmsClientRegistry.java
 
b/core/src/test/java/org/apache/gravitino/encryption/kms/TestKmsClientRegistry.java
index aebdcfcfb5..ea9d559a6a 100644
--- 
a/core/src/test/java/org/apache/gravitino/encryption/kms/TestKmsClientRegistry.java
+++ 
b/core/src/test/java/org/apache/gravitino/encryption/kms/TestKmsClientRegistry.java
@@ -18,11 +18,6 @@
  */
 package org.apache.gravitino.encryption.kms;
 
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -32,47 +27,51 @@ import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.gravitino.Config;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
 
 public class TestKmsClientRegistry {
 
-  private static final String AWS_API = "aws-kms";
-  private static final String GCP_API = "google-cloud-kms";
-  private static final String AZURE_API = "azure-key-vault";
+  private static final String AWS_FACTORY = "test.AwsKmsClientFactory";
+  private static final String GCP_FACTORY = "test.GcpKmsClientFactory";
+  private static final String AZURE_FACTORY = "test.AzureKmsClientFactory";
 
   @Test
-  void testEmptyRegistryDoesNotEnumerateFactories() {
-    Iterable<KmsClientFactory> factories =
-        () -> {
-          throw new AssertionError("Factories must not be enumerated without 
configured sources");
+  void testEmptyRegistryDoesNotLoadFactories() {
+    KmsClientRegistry.FactoryLoader loader =
+        className -> {
+          throw new AssertionError("Factories must not be loaded without 
configured providers");
         };
-    KmsClientRegistry registry = new KmsClientRegistry(config(), factories);
-    KmsReference reference = new KmsReference(AWS_API, "primary", 
"alias/orders");
+    KmsClientRegistry registry = new KmsClientRegistry(config(), loader);
+    KmsReference reference = new KmsReference("primary", "alias/orders");
 
     IllegalArgumentException exception =
         Assertions.assertThrows(
             IllegalArgumentException.class, () -> 
registry.getClient(reference));
     Assertions.assertEquals(
-        "No KMS client is configured for source 'primary'", 
exception.getMessage());
+        "No KMS client is configured for provider 'primary'", 
exception.getMessage());
   }
 
   @Test
   void testCreatesAndDispatchesConfiguredClients() {
-    RecordingFactory awsFactory = new RecordingFactory(AWS_API);
-    RecordingFactory gcpFactory = new RecordingFactory(GCP_API);
+    RecordingFactory awsFactory = new RecordingFactory();
+    RecordingFactory gcpFactory = new RecordingFactory();
     KmsClientRegistry registry =
         new KmsClientRegistry(
             config(
-                "gravitino.kms.sources", "primary,analytics",
-                "gravitino.kms.source.primary.api", AWS_API,
-                "gravitino.kms.source.primary.endpoint.region", "us-west-2",
-                "gravitino.kms.source.analytics.api", "google-cloud-kms",
-                "gravitino.kms.source.analytics.endpoint.project", 
"data-project"),
-            List.of(awsFactory, gcpFactory));
-
-    KmsReference awsReference = new KmsReference(AWS_API, "primary", 
"alias/orders");
+                "gravitino.kms.providers",
+                "primary,analytics",
+                "gravitino.kms.provider.primary.className",
+                AWS_FACTORY,
+                "gravitino.kms.provider.primary.endpoint.region",
+                "us-west-2",
+                "gravitino.kms.provider.analytics.className",
+                GCP_FACTORY,
+                "gravitino.kms.provider.analytics.endpoint.project",
+                "data-project"),
+            loader(Map.of(AWS_FACTORY, awsFactory, GCP_FACTORY, gcpFactory)));
+
+    KmsReference awsReference = new KmsReference("primary", "alias/orders");
     KmsReference gcpReference =
-        new KmsReference(GCP_API, "analytics", 
"projects/p/locations/l/keyRings/r/cryptoKeys/k");
+        new KmsReference("analytics", 
"projects/p/locations/l/keyRings/r/cryptoKeys/k");
 
     KmsClient awsClient = registry.getClient(awsReference);
     KmsClient gcpClient = registry.getClient(gcpReference);
@@ -81,43 +80,45 @@ public class TestKmsClientRegistry {
     Assertions.assertSame(gcpClient, registry.getClient(gcpReference));
     Assertions.assertEquals(Map.of("endpoint.region", "us-west-2"), 
awsFactory.properties);
     Assertions.assertEquals(Map.of("endpoint.project", "data-project"), 
gcpFactory.properties);
-    Assertions.assertEquals("primary", awsFactory.createdSource);
-    Assertions.assertEquals("analytics", gcpFactory.createdSource);
+    Assertions.assertEquals("primary", awsFactory.createdProvider);
+    Assertions.assertEquals("analytics", gcpFactory.createdProvider);
     Assertions.assertEquals(1, awsFactory.createCount.get());
     Assertions.assertEquals(1, gcpFactory.createCount.get());
   }
 
   @Test
-  void testRejectsUnknownSourceAndApiMismatch() {
+  void testRejectsUnknownProvider() {
     KmsClientRegistry registry =
         new KmsClientRegistry(
             config(
-                "gravitino.kms.sources", "primary",
-                "gravitino.kms.source.primary.api", "aws-kms"),
-            List.of(new RecordingFactory(AWS_API)));
+                "gravitino.kms.providers",
+                "primary",
+                "gravitino.kms.provider.primary.className",
+                AWS_FACTORY),
+            loader(Map.of(AWS_FACTORY, new RecordingFactory())));
 
     Assertions.assertThrows(
-        IllegalArgumentException.class,
-        () -> registry.getClient(new KmsReference(AWS_API, "other", "key")));
-    Assertions.assertThrows(
-        IllegalArgumentException.class,
-        () -> registry.getClient(new KmsReference(GCP_API, "primary", "key")));
+        IllegalArgumentException.class, () -> registry.getClient(new 
KmsReference("other", "key")));
+    Assertions.assertDoesNotThrow(() -> registry.getClient(new 
KmsReference("primary", "key")));
     Assertions.assertThrows(IllegalArgumentException.class, () -> 
registry.getClient(null));
   }
 
   @Test
-  void testCreatesMultipleSourcesForSameApi() {
-    RecordingFactory factory = new RecordingFactory(AZURE_API);
+  void testCreatesMultipleProvidersForSameClass() {
+    RecordingFactory factory = new RecordingFactory();
     KmsClientRegistry registry =
         new KmsClientRegistry(
             config(
-                "gravitino.kms.sources", "azure-eu,azure-us",
-                "gravitino.kms.source.azure-eu.api", "azure-key-vault",
-                "gravitino.kms.source.azure-us.api", "azure-key-vault"),
-            List.of(factory));
+                "gravitino.kms.providers",
+                "azure-eu,azure-us",
+                "gravitino.kms.provider.azure-eu.className",
+                AZURE_FACTORY,
+                "gravitino.kms.provider.azure-us.className",
+                AZURE_FACTORY),
+            loader(Map.of(AZURE_FACTORY, factory)));
 
-    KmsReference euReference = new KmsReference(AZURE_API, "azure-eu", 
"primary");
-    KmsReference usReference = new KmsReference(AZURE_API, "azure-us", 
"primary");
+    KmsReference euReference = new KmsReference("azure-eu", "primary");
+    KmsReference usReference = new KmsReference("azure-us", "primary");
 
     KmsClient euClient = registry.getClient(euReference);
     KmsClient usClient = registry.getClient(usReference);
@@ -128,136 +129,89 @@ public class TestKmsClientRegistry {
   }
 
   @Test
-  void testRoutesCustomApi() {
-    String customApi = "custom-kms";
-    KmsClientRegistry registry =
-        new KmsClientRegistry(
-            config("gravitino.kms.sources", "custom", 
"gravitino.kms.source.custom.api", customApi),
-            List.of(new RecordingFactory(customApi)));
-    KmsReference reference = new KmsReference(customApi, "custom", "key");
-
-    Assertions.assertNotNull(registry.getClient(reference));
-  }
-
-  @Test
-  void testMatchesApiIdentifiersByValue() {
-    KmsClientRegistry registry =
-        new KmsClientRegistry(
-            config(
-                "gravitino.kms.sources",
-                "primary",
-                "gravitino.kms.source.primary.api",
-                new String(AWS_API)),
-            List.of(new RecordingFactory(new String(AWS_API))));
-    KmsReference reference = new KmsReference(new String(AWS_API), "primary", 
"key");
-
-    Assertions.assertNotNull(registry.getClient(reference));
-  }
-
-  @Test
-  void testRejectsMissingDuplicateAndInvalidFactories() {
+  void testRejectsMissingFactoryClass() {
     Config awsConfig =
         config(
-            "gravitino.kms.sources", "primary",
-            "gravitino.kms.source.primary.api", "aws-kms");
+            "gravitino.kms.providers",
+            "primary",
+            "gravitino.kms.provider.primary.className",
+            AWS_FACTORY);
 
     Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsClientRegistry(awsConfig, 
List.of()));
-    Assertions.assertThrows(
-        IllegalArgumentException.class,
-        () ->
-            new KmsClientRegistry(
-                awsConfig, List.of(new RecordingFactory(AWS_API), new 
RecordingFactory(AWS_API))));
-    Assertions.assertThrows(
-        IllegalArgumentException.class,
-        () -> new KmsClientRegistry(awsConfig, List.of(new 
RecordingFactory(null))));
-    Assertions.assertThrows(
-        IllegalArgumentException.class,
-        () -> new KmsClientRegistry(awsConfig, List.of(new RecordingFactory(" 
"))));
-    Assertions.assertThrows(
-        IllegalArgumentException.class,
-        () -> new KmsClientRegistry(awsConfig, List.of(new RecordingFactory(" 
aws-kms"))));
-    Assertions.assertThrows(
-        IllegalArgumentException.class,
-        () -> new KmsClientRegistry(awsConfig, List.of(new 
RecordingFactory("AWS-KMS"))));
+        IllegalArgumentException.class, () -> new KmsClientRegistry(awsConfig, 
loader(Map.of())));
     Assertions.assertThrows(
         IllegalArgumentException.class,
-        () -> new KmsClientRegistry(awsConfig, 
java.util.Arrays.asList((KmsClientFactory) null)));
-    Assertions.assertThrows(
-        IllegalArgumentException.class, () -> new KmsClientRegistry(awsConfig, 
null));
+        () -> new KmsClientRegistry(awsConfig, 
(KmsClientRegistry.FactoryLoader) null));
   }
 
   @Test
-  void testRejectsConfiguredApiWithoutFactory() {
-    Config customConfig =
-        config(
-            "gravitino.kms.sources", "primary",
-            "gravitino.kms.source.primary.api", "custom-kms");
+  void testPublicConstructorLoadsFactoryByClassName() {
+    try (KmsClientRegistry registry =
+        new KmsClientRegistry(
+            config(
+                "gravitino.kms.providers",
+                "primary",
+                "gravitino.kms.provider.primary.className",
+                ClassLoadedFactory.class.getName()))) {
+      KmsReference reference = new KmsReference("primary", "key");
+      Assertions.assertNotNull(registry.getClient(reference));
+    }
+  }
 
+  @Test
+  void testPublicConstructorRejectsUnknownClass() {
     IllegalArgumentException exception =
         Assertions.assertThrows(
             IllegalArgumentException.class,
-            () -> new KmsClientRegistry(customConfig, List.of(new 
RecordingFactory(AWS_API))));
-    Assertions.assertTrue(
-        exception.getMessage().contains("No KMS client factory supports API 
'custom-kms'"));
-  }
-
-  @Test
-  void testPublicConstructorUsesContextClassLoader(@TempDir Path 
tempDirectory) throws Exception {
-    Path serviceFile =
-        tempDirectory.resolve(
-            
"META-INF/services/org.apache.gravitino.encryption.kms.KmsClientFactory");
-    Files.createDirectories(serviceFile.getParent());
-    Files.write(serviceFile, 
ServiceLoadedFactory.class.getName().getBytes(StandardCharsets.UTF_8));
-
-    ClassLoader originalClassLoader = 
Thread.currentThread().getContextClassLoader();
-    try (URLClassLoader serviceClassLoader =
-        new URLClassLoader(new URL[] {tempDirectory.toUri().toURL()}, 
originalClassLoader)) {
-      Thread.currentThread().setContextClassLoader(serviceClassLoader);
-      try (KmsClientRegistry registry =
-          new KmsClientRegistry(
-              config(
-                  "gravitino.kms.sources", "primary",
-                  "gravitino.kms.source.primary.api", "aws-kms"))) {
-        KmsReference reference = new KmsReference(AWS_API, "primary", "key");
-        Assertions.assertNotNull(registry.getClient(reference));
-      }
-    } finally {
-      Thread.currentThread().setContextClassLoader(originalClassLoader);
-    }
+            () ->
+                new KmsClientRegistry(
+                    config(
+                        "gravitino.kms.providers",
+                        "primary",
+                        "gravitino.kms.provider.primary.className",
+                        "test.MissingKmsClientFactory")));
+    Assertions.assertTrue(exception.getMessage().contains("No KMS client 
factory class"));
   }
 
   @Test
   void testRejectsNullClientAndClosesPreviouslyCreatedClient() {
-    CloseTrackingFactory awsFactory =
-        new CloseTrackingFactory(AWS_API, "aws", new ArrayList<>(), null);
+    CloseTrackingFactory awsFactory = new CloseTrackingFactory("aws", new 
ArrayList<>(), null);
     Config awsConfig =
         config(
-            "gravitino.kms.sources", "primary,analytics",
-            "gravitino.kms.source.primary.api", "aws-kms",
-            "gravitino.kms.source.analytics.api", "google-cloud-kms");
+            "gravitino.kms.providers",
+            "primary,analytics",
+            "gravitino.kms.provider.primary.className",
+            AWS_FACTORY,
+            "gravitino.kms.provider.analytics.className",
+            GCP_FACTORY);
 
-    KmsClientFactory nullClientFactory = factory(GCP_API, (source, properties) 
-> null);
+    KmsClientFactory nullClientFactory = factory((provider, properties) -> 
null);
 
     Assertions.assertThrows(
         IllegalStateException.class,
-        () -> new KmsClientRegistry(awsConfig, List.of(awsFactory, 
nullClientFactory)));
+        () ->
+            new KmsClientRegistry(
+                awsConfig,
+                loader(Map.of(AWS_FACTORY, awsFactory, GCP_FACTORY, 
nullClientFactory))));
     Assertions.assertEquals(1, awsFactory.closeCount.get());
   }
 
   @Test
   void testClosesClientsInReverseOrderAndIsIdempotent() {
     List<String> closeOrder = new ArrayList<>();
-    CloseTrackingFactory awsFactory = new CloseTrackingFactory(AWS_API, "aws", 
closeOrder, null);
-    CloseTrackingFactory gcpFactory = new CloseTrackingFactory(GCP_API, "gcp", 
closeOrder, null);
+    CloseTrackingFactory awsFactory = new CloseTrackingFactory("aws", 
closeOrder, null);
+    CloseTrackingFactory gcpFactory = new CloseTrackingFactory("gcp", 
closeOrder, null);
     KmsClientRegistry registry =
         new KmsClientRegistry(
             config(
-                "gravitino.kms.sources", "primary,analytics",
-                "gravitino.kms.source.primary.api", "aws-kms",
-                "gravitino.kms.source.analytics.api", "google-cloud-kms"),
-            List.of(awsFactory, gcpFactory));
-    KmsReference awsReference = new KmsReference(AWS_API, "primary", "key");
+                "gravitino.kms.providers",
+                "primary,analytics",
+                "gravitino.kms.provider.primary.className",
+                AWS_FACTORY,
+                "gravitino.kms.provider.analytics.className",
+                GCP_FACTORY),
+            loader(Map.of(AWS_FACTORY, awsFactory, GCP_FACTORY, gcpFactory)));
+    KmsReference awsReference = new KmsReference("primary", "key");
 
     registry.close();
     registry.close();
@@ -271,11 +225,10 @@ public class TestKmsClientRegistry {
   @Test
   void testClosesCreatedClientsAfterPartialInitializationFailure() {
     List<String> closeOrder = new ArrayList<>();
-    CloseTrackingFactory awsFactory = new CloseTrackingFactory(AWS_API, "aws", 
closeOrder, null);
+    CloseTrackingFactory awsFactory = new CloseTrackingFactory("aws", 
closeOrder, null);
     KmsClientFactory failingFactory =
         factory(
-            GCP_API,
-            (source, properties) -> {
+            (provider, properties) -> {
               throw new IllegalArgumentException("invalid GCP configuration");
             });
 
@@ -284,10 +237,13 @@ public class TestKmsClientRegistry {
         () ->
             new KmsClientRegistry(
                 config(
-                    "gravitino.kms.sources", "primary,analytics",
-                    "gravitino.kms.source.primary.api", "aws-kms",
-                    "gravitino.kms.source.analytics.api", "google-cloud-kms"),
-                List.of(awsFactory, failingFactory)));
+                    "gravitino.kms.providers",
+                    "primary,analytics",
+                    "gravitino.kms.provider.primary.className",
+                    AWS_FACTORY,
+                    "gravitino.kms.provider.analytics.className",
+                    GCP_FACTORY),
+                loader(Map.of(AWS_FACTORY, awsFactory, GCP_FACTORY, 
failingFactory))));
 
     Assertions.assertEquals(1, awsFactory.closeCount.get());
     Assertions.assertEquals(List.of("aws"), closeOrder);
@@ -297,13 +253,12 @@ public class TestKmsClientRegistry {
   void testPreservesInitializationFailureWhenCleanupFails() {
     RuntimeException closeFailure = new IllegalStateException("close failed");
     CloseTrackingFactory awsFactory =
-        new CloseTrackingFactory(AWS_API, "aws", new ArrayList<>(), 
closeFailure);
+        new CloseTrackingFactory("aws", new ArrayList<>(), closeFailure);
     IllegalArgumentException creationFailure =
         new IllegalArgumentException("invalid GCP configuration");
     KmsClientFactory failingFactory =
         factory(
-            GCP_API,
-            (source, properties) -> {
+            (provider, properties) -> {
               throw creationFailure;
             });
 
@@ -313,10 +268,13 @@ public class TestKmsClientRegistry {
             () ->
                 new KmsClientRegistry(
                     config(
-                        "gravitino.kms.sources", "primary,analytics",
-                        "gravitino.kms.source.primary.api", "aws-kms",
-                        "gravitino.kms.source.analytics.api", 
"google-cloud-kms"),
-                    List.of(awsFactory, failingFactory)));
+                        "gravitino.kms.providers",
+                        "primary,analytics",
+                        "gravitino.kms.provider.primary.className",
+                        AWS_FACTORY,
+                        "gravitino.kms.provider.analytics.className",
+                        GCP_FACTORY),
+                    loader(Map.of(AWS_FACTORY, awsFactory, GCP_FACTORY, 
failingFactory))));
 
     Assertions.assertSame(creationFailure, exception);
     Assertions.assertArrayEquals(new Throwable[] {closeFailure}, 
exception.getSuppressed());
@@ -327,16 +285,19 @@ public class TestKmsClientRegistry {
     RuntimeException awsFailure = new IllegalStateException("aws close 
failed");
     RuntimeException gcpFailure = new IllegalStateException("gcp close 
failed");
     CloseTrackingFactory awsFactory =
-        new CloseTrackingFactory(AWS_API, "aws", new ArrayList<>(), 
awsFailure);
+        new CloseTrackingFactory("aws", new ArrayList<>(), awsFailure);
     CloseTrackingFactory gcpFactory =
-        new CloseTrackingFactory(GCP_API, "gcp", new ArrayList<>(), 
gcpFailure);
+        new CloseTrackingFactory("gcp", new ArrayList<>(), gcpFailure);
     KmsClientRegistry registry =
         new KmsClientRegistry(
             config(
-                "gravitino.kms.sources", "primary,analytics",
-                "gravitino.kms.source.primary.api", "aws-kms",
-                "gravitino.kms.source.analytics.api", "google-cloud-kms"),
-            List.of(awsFactory, gcpFactory));
+                "gravitino.kms.providers",
+                "primary,analytics",
+                "gravitino.kms.provider.primary.className",
+                AWS_FACTORY,
+                "gravitino.kms.provider.analytics.className",
+                GCP_FACTORY),
+            loader(Map.of(AWS_FACTORY, awsFactory, GCP_FACTORY, gcpFactory)));
 
     RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, registry::close);
     Assertions.assertSame(gcpFailure, exception);
@@ -351,70 +312,54 @@ public class TestKmsClientRegistry {
     return new MapConfig(properties);
   }
 
-  private static KmsClientFactory factory(String api, ClientCreator creator) {
-    return new KmsClientFactory() {
-      @Override
-      public String api() {
-        return api;
-      }
-
-      @Override
-      public KmsClient create(String source, Map<String, String> properties) {
-        return creator.create(source, properties);
+  private static KmsClientRegistry.FactoryLoader loader(Map<String, 
KmsClientFactory> factories) {
+    return className -> {
+      KmsClientFactory factory = factories.get(className);
+      if (factory == null) {
+        throw new IllegalArgumentException(
+            String.format("No KMS client factory class '%s'", className));
       }
+      return factory;
     };
   }
 
+  private static KmsClientFactory factory(ClientCreator creator) {
+    return creator::create;
+  }
+
   private interface ClientCreator {
-    KmsClient create(String source, Map<String, String> properties);
+    KmsClient create(String provider, Map<String, String> properties);
   }
 
   private static final class RecordingFactory implements KmsClientFactory {
-    private final String api;
-    private String createdSource;
+    private String createdProvider;
     private Map<String, String> properties;
     private final AtomicInteger createCount = new AtomicInteger();
 
-    private RecordingFactory(String api) {
-      this.api = api;
-    }
-
-    @Override
-    public String api() {
-      return api;
-    }
-
     @Override
-    public KmsClient create(String source, Map<String, String> properties) {
+    public KmsClient create(String provider, Map<String, String> properties) {
       createCount.incrementAndGet();
-      this.createdSource = source;
+      this.createdProvider = provider;
       this.properties = properties;
       return reference -> Optional.of(new Properties(reference));
     }
   }
 
   private static final class CloseTrackingFactory implements KmsClientFactory {
-    private final String api;
     private final String name;
     private final List<String> closeOrder;
     private final RuntimeException closeFailure;
     private final AtomicInteger closeCount = new AtomicInteger();
 
     private CloseTrackingFactory(
-        String api, String name, List<String> closeOrder, RuntimeException 
closeFailure) {
-      this.api = api;
+        String name, List<String> closeOrder, RuntimeException closeFailure) {
       this.name = name;
       this.closeOrder = closeOrder;
       this.closeFailure = closeFailure;
     }
 
     @Override
-    public String api() {
-      return api;
-    }
-
-    @Override
-    public KmsClient create(String source, Map<String, String> properties) {
+    public KmsClient create(String provider, Map<String, String> properties) {
       return new KmsClient() {
         @Override
         public Optional<KmsKeyProperties> getKeyProperties(KmsReference 
reference) {
@@ -468,21 +413,15 @@ public class TestKmsClientRegistry {
     }
   }
 
-  /** Factory exposed for the context-classloader ServiceLoader test. */
-  public static final class ServiceLoadedFactory implements KmsClientFactory {
+  /** Factory loaded by {@link Class#forName(String)} in the 
public-constructor test. */
+  public static final class ClassLoadedFactory implements KmsClientFactory {
 
-    /** Creates a test service-loaded factory. */
-    public ServiceLoadedFactory() {}
-
-    /** {@inheritDoc} */
-    @Override
-    public String api() {
-      return AWS_API;
-    }
+    /** Creates a test factory. */
+    public ClassLoadedFactory() {}
 
     /** {@inheritDoc} */
     @Override
-    public KmsClient create(String source, Map<String, String> properties) {
+    public KmsClient create(String provider, Map<String, String> properties) {
       return reference -> Optional.of(new Properties(reference));
     }
   }
diff --git 
a/core/src/test/java/org/apache/gravitino/encryption/kms/TestKmsConfig.java 
b/core/src/test/java/org/apache/gravitino/encryption/kms/TestKmsConfig.java
index 5a47052cb2..8cdbde87c4 100644
--- a/core/src/test/java/org/apache/gravitino/encryption/kms/TestKmsConfig.java
+++ b/core/src/test/java/org/apache/gravitino/encryption/kms/TestKmsConfig.java
@@ -26,97 +26,109 @@ import org.junit.jupiter.api.Test;
 
 public class TestKmsConfig {
 
-  private static final String AWS_API = "aws-kms";
-  private static final String GCP_API = "google-cloud-kms";
+  private static final String AWS_FACTORY =
+      "org.apache.gravitino.encryption.kms.aws.AwsKmsClientFactory";
+  private static final String GCP_FACTORY =
+      "org.apache.gravitino.encryption.kms.gcp.GcpKmsClientFactory";
 
   @Test
-  void testParsesSourcesAndProviderProperties() {
+  void testParsesProvidersAndProperties() {
     KmsConfig config =
         parse(
             Map.of(
-                "gravitino.kms.sources", "primary, disaster-recovery",
-                "gravitino.kms.source.primary.api", AWS_API,
-                "gravitino.kms.source.primary.endpoint.region", "us-west-2",
-                "gravitino.kms.source.primary.credential.method", "default",
-                "gravitino.kms.source.disaster-recovery.api", 
"google-cloud-kms",
-                "gravitino.kms.source.disaster-recovery.endpoint.projectId", 
"backup-project",
-                "gravitino.kms.source.disaster-recovery.credential.method", 
"default"));
+                "gravitino.kms.providers",
+                "primary,disaster-recovery",
+                "gravitino.kms.provider.primary.className",
+                AWS_FACTORY,
+                "gravitino.kms.provider.primary.endpoint.region",
+                "us-west-2",
+                "gravitino.kms.provider.primary.credential.method",
+                "default",
+                "gravitino.kms.provider.disaster-recovery.className",
+                GCP_FACTORY,
+                "gravitino.kms.provider.disaster-recovery.endpoint.projectId",
+                "backup-project",
+                "gravitino.kms.provider.disaster-recovery.credential.method",
+                "default"));
 
-    Assertions.assertEquals(2, config.sources().size());
-    Assertions.assertEquals(AWS_API, config.sources().get("primary").api());
+    Assertions.assertEquals(2, config.providers().size());
+    Assertions.assertEquals(AWS_FACTORY, 
config.providers().get("primary").className());
     Assertions.assertEquals(
         Map.of("endpoint.region", "us-west-2", "credential.method", "default"),
-        config.sources().get("primary").properties());
-    Assertions.assertEquals(GCP_API, 
config.sources().get("disaster-recovery").api());
+        config.providers().get("primary").properties());
+    Assertions.assertEquals(GCP_FACTORY, 
config.providers().get("disaster-recovery").className());
     Assertions.assertEquals(
         Map.of("endpoint.projectId", "backup-project", "credential.method", 
"default"),
-        config.sources().get("disaster-recovery").properties());
+        config.providers().get("disaster-recovery").properties());
     Assertions.assertThrows(
         UnsupportedOperationException.class,
-        () -> 
config.sources().get("primary").properties().put("endpoint.region", "other"));
+        () -> 
config.providers().get("primary").properties().put("endpoint.region", "other"));
   }
 
   @Test
-  void testAllowsNoConfiguredSources() {
-    Assertions.assertTrue(parse(Map.of()).sources().isEmpty());
-    Assertions.assertTrue(parse(Map.of("gravitino.kms.sources", "  
")).sources().isEmpty());
+  void testAllowsNoConfiguredProviders() {
+    Assertions.assertTrue(parse(Map.of()).providers().isEmpty());
+    Assertions.assertTrue(parse(Map.of("gravitino.kms.providers", "  
")).providers().isEmpty());
   }
 
   @Test
-  void testRejectsInvalidOrDuplicateSourceNames() {
-    assertInvalid(Map.of("gravitino.kms.sources", "primary,"), "Invalid KMS 
source name");
-    assertInvalid(Map.of("gravitino.kms.sources", "bad.name"), "Invalid KMS 
source name");
+  void testRejectsInvalidOrDuplicateProviderNames() {
+    assertInvalid(Map.of("gravitino.kms.providers", "primary,"), "Invalid KMS 
provider name");
+    assertInvalid(Map.of("gravitino.kms.providers", "bad.name"), "Invalid KMS 
provider name");
     assertInvalid(
-        Map.of("gravitino.kms.sources", "primary,primary"), "Duplicate KMS 
source 'primary'");
+        Map.of("gravitino.kms.providers", "primary,primary"), "Duplicate KMS 
provider 'primary'");
   }
 
   @Test
-  void testRejectsMalformedOrUnlistedSourceProperties() {
+  void testRejectsMalformedOrUnlistedProviderProperties() {
     assertInvalid(Map.of("gravitino.kms.unexpected", "value"), "Invalid KMS 
configuration key");
-    assertInvalid(Map.of("gravitino.kms.source.primary", "value"), "Invalid 
KMS configuration key");
-    assertInvalid(Map.of("gravitino.kms.source..api", AWS_API), "Invalid KMS 
configuration key");
     assertInvalid(
-        Map.of("gravitino.kms.source.bad$name.api", AWS_API), "Invalid KMS 
configuration key");
+        Map.of("gravitino.kms.provider.primary", "value"), "Invalid KMS 
configuration key");
+    assertInvalid(
+        Map.of("gravitino.kms.provider..className", AWS_FACTORY), "Invalid KMS 
configuration key");
+    assertInvalid(
+        Map.of("gravitino.kms.provider.bad$name.className", AWS_FACTORY),
+        "Invalid KMS configuration key");
     assertInvalid(
         Map.of(
-            "gravitino.kms.sources", "primary",
-            "gravitino.kms.source.other.api", "aws-kms"),
-        "unlisted source 'other'");
+            "gravitino.kms.providers",
+            "primary",
+            "gravitino.kms.provider.other.className",
+            AWS_FACTORY),
+        "unlisted provider 'other'");
     assertInvalid(
         Map.of(
-            "gravitino.kms.sources", "primary",
-            "gravitino.kms.source.primary.", "value"),
+            "gravitino.kms.providers", "primary",
+            "gravitino.kms.provider.primary.", "value"),
         "Invalid KMS configuration key");
   }
 
   @Test
-  void testRequiresApi() {
+  void testRequiresClassName() {
     assertInvalid(
-        Map.of("gravitino.kms.sources", "primary"),
-        "gravitino.kms.source.primary.api' cannot be blank");
-    assertInvalid(
-        Map.of(
-            "gravitino.kms.sources", "primary",
-            "gravitino.kms.source.primary.api", " "),
-        "gravitino.kms.source.primary.api' cannot be blank");
+        Map.of("gravitino.kms.providers", "primary"),
+        "gravitino.kms.provider.primary.className' cannot be blank");
     assertInvalid(
         Map.of(
-            "gravitino.kms.sources", "primary",
-            "gravitino.kms.source.primary.api", "Custom-KMS"),
-        "must be lowercase kebab-case");
+            "gravitino.kms.providers", "primary",
+            "gravitino.kms.provider.primary.className", " "),
+        "gravitino.kms.provider.primary.className' cannot be blank");
   }
 
   @Test
-  void testAllowsMoreThanOneSourceForAnApi() {
+  void testAllowsMoreThanOneProviderForAClass() {
     KmsConfig config =
         parse(
             Map.of(
-                "gravitino.kms.sources", "primary,secondary",
-                "gravitino.kms.source.primary.api", "aws-kms",
-                "gravitino.kms.source.secondary.api", "aws-kms"));
+                "gravitino.kms.providers",
+                "primary,secondary",
+                "gravitino.kms.provider.primary.className",
+                AWS_FACTORY,
+                "gravitino.kms.provider.secondary.className",
+                AWS_FACTORY));
 
-    Assertions.assertEquals(AWS_API, config.sources().get("primary").api());
-    Assertions.assertEquals(AWS_API, config.sources().get("secondary").api());
+    Assertions.assertEquals(AWS_FACTORY, 
config.providers().get("primary").className());
+    Assertions.assertEquals(AWS_FACTORY, 
config.providers().get("secondary").className());
   }
 
   @Test
diff --git a/docs/gravitino-server-config.md b/docs/gravitino-server-config.md
index c2ad57ba33..963774a51b 100644
--- a/docs/gravitino-server-config.md
+++ b/docs/gravitino-server-config.md
@@ -486,6 +486,45 @@ server, are documented with those services. See
 | `gravitino.job.stagingDirKeepTimeInMs` | How long in milliseconds a finished 
job's staging files are kept. Use at least 10 minutes outside testing. | 
`604800000` (7 days)          |
 | `gravitino.job.statusPullIntervalInMs` | Interval in milliseconds between 
job status polls. Use at least 1 minute outside testing.                  | 
`300000` (5 minutes)          |
 
+### Key Management
+
+The server talks to KMS instances you name in `gravitino.conf`. Each name is 
one configured
+instance. To add one, implement `KmsClientFactory` with a public no-arg 
constructor, put the jar on
+the server classpath, and set `gravitino.kms.provider.<name>.className` to 
that class.
+`create(provider, properties)` builds the `KmsClient` for that name. Gravitino 
does not ship AWS or
+Azure factories. Two names may share one class, which is how you run more than 
one vault of the
+same kind.
+
+The list is empty by default, and then the server has no KMS clients. Naming a 
provider without a
+`className`, or with a class the server cannot construct as a 
`KmsClientFactory`, fails startup.
+Client construction validates local configuration only; the first call to the 
provider is a later
+key inspection, not startup.
+
+| Configuration Item                         | Description                     
                                                                                
                                                     | Default Value |
+|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
+| `gravitino.kms.providers`                  | Comma-separated KMS instance 
names, with no spaces after commas. Each name must match 
`[A-Za-z0-9][A-Za-z0-9_-]*` and cannot contain `.`. Duplicates fail startup. | 
(empty)       |
+| `gravitino.kms.provider.<name>.className`  | Required factory class name for 
that instance. The class must have a no-arg constructor and implement 
`KmsClientFactory`.                                          | (none)        |
+| `gravitino.kms.provider.<name>.<key>`      | Any other property under that 
name is passed to that factory. Nested dots in `<key>` are allowed, as in 
`endpoint.region`.                                           | (none)        |
+
+Every name in `providers` needs a matching `.className`. A 
`gravitino.kms.provider.<name>.*` key
+for a name that is not in the list, or any other `gravitino.kms.*` key, fails 
startup.
+
+Callers name the instance and the key. They do not send `className`. The 
server already constructed
+the factory for `aws-prod` at startup.
+
+```text
+# conf/gravitino.conf
+gravitino.kms.providers = aws-prod,aws-dr,azure-eu
+
+gravitino.kms.provider.aws-prod.className = 
com.example.kms.AwsCustomKmsClientFactory
+gravitino.kms.provider.aws-dr.className = 
com.example.kms.AwsCustomKmsClientFactory
+gravitino.kms.provider.azure-eu.className = 
com.example.kms.AzureCustomKmsClientFactory
+```
+
+That configuration builds three clients: two instances of one custom AWS 
factory and one custom
+Azure factory. Further `gravitino.kms.provider.<name>.*` keys are factory 
properties, not a closed
+schema; each factory documents the keys it accepts.
+
 ## Catalog Properties
 
 Catalog properties configure one catalog rather than the server. They come 
from two places: a

Reply via email to