You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ib...@apache.org on 2021/12/06 09:24:54 UTC

[ignite-3] branch main updated: IGNITE-15897 ServiceLoader integration into configuration (#460)

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

ibessonov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new e984c7c  IGNITE-15897 ServiceLoader integration into configuration (#460)
e984c7c is described below

commit e984c7c83bd671d5be978ae586db547c3c4b50a0
Author: Roman Puchkovskiy <ro...@gmail.com>
AuthorDate: Mon Dec 6 13:24:46 2021 +0400

    IGNITE-15897 ServiceLoader integration into configuration (#460)
---
 .idea/codeStyles/codeStyleConfig.xml               |   2 +-
 modules/api/pom.xml                                |  24 ++++
 .../src/main/java/org/apache/ignite/Ignition.java  |  15 +++
 .../java/org/apache/ignite/IgnitionManager.java    |   2 +-
 .../CoreDistributedConfigurationModule.java        |  60 ++++++++++
 .../CoreLocalConfigurationModule.java              |  49 ++++++++
 ...nite.internal.configuration.ConfigurationModule |  18 +++
 .../CoreDistributedConfigurationModuleTest.java    | 100 ++++++++++++++++
 .../CoreLocalConfigurationModuleTest.java          |  93 +++++++++++++++
 .../ignite/configuration/annotation/Config.java    |  18 ++-
 .../annotation/ConfigurationRoot.java              |  30 ++++-
 .../configuration/ConfigurationModule.java         | 103 +++++++++++++++++
 modules/runner/pom.xml                             |   6 +
 .../org/apache/ignite/internal/app/IgniteImpl.java |  79 +++++++------
 .../apache/ignite/internal/app/IgnitionImpl.java   |  30 +++--
 .../internal/configuration/CompoundModule.java     |  89 +++++++++++++++
 .../configuration/ConfigurationModules.java        |  68 +++++++++++
 .../ServiceLoaderModulesProvider.java}             |  40 +++----
 .../internal/configuration/CompoundModuleTest.java | 127 +++++++++++++++++++++
 .../configuration/ConfigurationModulesTest.java    |  81 +++++++++++++
 .../ServiceLoaderModulesProviderTest.java          |  50 ++++++++
 .../configuration/TestConfigurationModule.java     |  31 +++++
 ...nite.internal.configuration.ConfigurationModule |  17 +++
 modules/schema/pom.xml                             |   7 +-
 .../SchemaDistributedConfigurationModule.java      |  47 ++++++++
 ...nite.internal.configuration.ConfigurationModule |  17 +++
 .../SchemaDistributedConfigurationModuleTest.java  |  82 +++++++++++++
 modules/table/pom.xml                              |  12 ++
 .../TableDistributedConfigurationModule.java       |  40 +++++++
 ...nite.internal.configuration.ConfigurationModule |  17 +++
 .../TableDistributedConfigurationModuleTest.java   |  73 ++++++++++++
 parent/pom.xml                                     |   7 ++
 32 files changed, 1356 insertions(+), 78 deletions(-)

diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index b9d18bf..ea0ad13 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -2,4 +2,4 @@
   <state>
     <option name="PREFERRED_PROJECT_CODE_STYLE" value="GoogleStyle" />
   </state>
-</component>
\ No newline at end of file
+</component>
diff --git a/modules/api/pom.xml b/modules/api/pom.xml
index f9f07e7..833b90f 100644
--- a/modules/api/pom.xml
+++ b/modules/api/pom.xml
@@ -38,11 +38,35 @@
             <artifactId>ignite-configuration-api</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-configuration</artifactId>
+        </dependency>
+
         <!-- 3rd party dependencies -->
         <dependency>
             <groupId>org.jetbrains</groupId>
             <artifactId>annotations</artifactId>
         </dependency>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.npathai</groupId>
+            <artifactId>hamcrest-optional</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/modules/api/src/main/java/org/apache/ignite/Ignition.java b/modules/api/src/main/java/org/apache/ignite/Ignition.java
index 2871273..4824fb6 100644
--- a/modules/api/src/main/java/org/apache/ignite/Ignition.java
+++ b/modules/api/src/main/java/org/apache/ignite/Ignition.java
@@ -39,6 +39,21 @@ public interface Ignition {
     public Ignite start(@NotNull String name, @Nullable Path configPath, @NotNull Path workDir);
 
     /**
+     * Starts an Ignite node with an optional bootstrap configuration from a HOCON file, with an optional
+     * class loader for further usage by {@link java.util.ServiceLoader}.
+     *
+     * @param name                     Name of the node. Must not be {@code null}.
+     * @param configPath               Path to the node configuration in the HOCON format. Can be {@code null}.
+     * @param workDir                  Work directory for the started node. Must not be {@code null}.
+     * @param serviceLoaderClassLoader The class loader to be used to load provider-configuration files and provider
+     *                                 classes, or {@code null} if the system
+     *                                 class loader (or, failing that, the bootstrap class loader) is to be used
+     * @return Started Ignite node.
+     */
+    public Ignite start(@NotNull String name, @Nullable Path configPath, @NotNull Path workDir,
+                        @Nullable ClassLoader serviceLoaderClassLoader);
+
+    /**
      * Starts an Ignite node with an optional bootstrap configuration from a URL linking to HOCON configs.
      *
      * @param name       Name of the node. Must not be {@code null}.
diff --git a/modules/api/src/main/java/org/apache/ignite/IgnitionManager.java b/modules/api/src/main/java/org/apache/ignite/IgnitionManager.java
index 400442b..84074f6 100644
--- a/modules/api/src/main/java/org/apache/ignite/IgnitionManager.java
+++ b/modules/api/src/main/java/org/apache/ignite/IgnitionManager.java
@@ -106,7 +106,7 @@ public class IgnitionManager {
             }
         }
 
-        return ignition.start(nodeName, cfgPath, workDir);
+        return ignition.start(nodeName, cfgPath, workDir, clsLdr);
     }
 
     /**
diff --git a/modules/api/src/main/java/org/apache/ignite/configuration/CoreDistributedConfigurationModule.java b/modules/api/src/main/java/org/apache/ignite/configuration/CoreDistributedConfigurationModule.java
new file mode 100644
index 0000000..e47b02f
--- /dev/null
+++ b/modules/api/src/main/java/org/apache/ignite/configuration/CoreDistributedConfigurationModule.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ignite.configuration;
+
+import java.util.Collection;
+import java.util.List;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.schemas.runner.ClusterConfiguration;
+import org.apache.ignite.configuration.schemas.store.DataStorageConfiguration;
+import org.apache.ignite.configuration.schemas.table.HashIndexConfigurationSchema;
+import org.apache.ignite.configuration.schemas.table.PartialIndexConfigurationSchema;
+import org.apache.ignite.configuration.schemas.table.SortedIndexConfigurationSchema;
+import org.apache.ignite.configuration.schemas.table.TablesConfiguration;
+import org.apache.ignite.internal.configuration.ConfigurationModule;
+
+/**
+ * {@link ConfigurationModule} for cluster-wide configuration provided by ignite-api.
+ */
+public class CoreDistributedConfigurationModule implements ConfigurationModule {
+    /** {@inheritDoc} */
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.DISTRIBUTED;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<RootKey<?, ?>> rootKeys() {
+        return List.of(
+                ClusterConfiguration.KEY,
+                TablesConfiguration.KEY,
+                DataStorageConfiguration.KEY
+        );
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<Class<?>> polymorphicSchemaExtensions() {
+        return List.of(
+                HashIndexConfigurationSchema.class,
+                SortedIndexConfigurationSchema.class,
+                PartialIndexConfigurationSchema.class
+        );
+    }
+}
diff --git a/modules/api/src/main/java/org/apache/ignite/configuration/CoreLocalConfigurationModule.java b/modules/api/src/main/java/org/apache/ignite/configuration/CoreLocalConfigurationModule.java
new file mode 100644
index 0000000..965bfca
--- /dev/null
+++ b/modules/api/src/main/java/org/apache/ignite/configuration/CoreLocalConfigurationModule.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration;
+
+import java.util.Collection;
+import java.util.List;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.schemas.clientconnector.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.schemas.network.NetworkConfiguration;
+import org.apache.ignite.configuration.schemas.rest.RestConfiguration;
+import org.apache.ignite.configuration.schemas.runner.NodeConfiguration;
+import org.apache.ignite.internal.configuration.ConfigurationModule;
+
+/**
+ * {@link ConfigurationModule} for node-local configuration provided by ignite-api.
+ */
+public class CoreLocalConfigurationModule implements ConfigurationModule {
+    /** {@inheritDoc} */
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.LOCAL;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<RootKey<?, ?>> rootKeys() {
+        return List.of(
+                NetworkConfiguration.KEY,
+                NodeConfiguration.KEY,
+                RestConfiguration.KEY,
+                ClientConnectorConfiguration.KEY
+        );
+    }
+}
diff --git a/modules/api/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule b/modules/api/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule
new file mode 100644
index 0000000..e16f06d
--- /dev/null
+++ b/modules/api/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+org.apache.ignite.configuration.CoreLocalConfigurationModule
+org.apache.ignite.configuration.CoreDistributedConfigurationModule
diff --git a/modules/api/src/test/java/org/apache/ignite/configuration/CoreDistributedConfigurationModuleTest.java b/modules/api/src/test/java/org/apache/ignite/configuration/CoreDistributedConfigurationModuleTest.java
new file mode 100644
index 0000000..84156b7
--- /dev/null
+++ b/modules/api/src/test/java/org/apache/ignite/configuration/CoreDistributedConfigurationModuleTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.ignite.configuration;
+
+import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent;
+import static org.apache.ignite.configuration.annotation.ConfigurationType.DISTRIBUTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.ServiceLoader.Provider;
+import org.apache.ignite.configuration.schemas.runner.ClusterConfiguration;
+import org.apache.ignite.configuration.schemas.store.DataStorageConfiguration;
+import org.apache.ignite.configuration.schemas.table.HashIndexConfigurationSchema;
+import org.apache.ignite.configuration.schemas.table.PartialIndexConfigurationSchema;
+import org.apache.ignite.configuration.schemas.table.SortedIndexConfigurationSchema;
+import org.apache.ignite.configuration.schemas.table.TablesConfiguration;
+import org.apache.ignite.internal.configuration.ConfigurationModule;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link CoreDistributedConfigurationModule}.
+ */
+class CoreDistributedConfigurationModuleTest {
+    private final CoreDistributedConfigurationModule module = new CoreDistributedConfigurationModule();
+
+    @Test
+    void typeIsDistributed() {
+        assertThat(module.type(), is(DISTRIBUTED));
+    }
+
+    @Test
+    void hasClusterConfigurationRoot() {
+        assertThat(module.rootKeys(), hasItem(ClusterConfiguration.KEY));
+    }
+
+    @Test
+    void hasTablesConfigurationRoot() {
+        assertThat(module.rootKeys(), hasItem(TablesConfiguration.KEY));
+    }
+
+    @Test
+    void hasDataStorageConfigurationRoot() {
+        assertThat(module.rootKeys(), hasItem(DataStorageConfiguration.KEY));
+    }
+
+    @Test
+    void providesNoValidators() {
+        assertThat(module.validators(), is(anEmptyMap()));
+    }
+
+    @Test
+    void providesNoInternalSchemaExtensions() {
+        assertThat(module.internalSchemaExtensions(), is(empty()));
+    }
+
+    @Test
+    void providesHashIndexConfigurationSchemaAsPolymorphicExtension() {
+        assertThat(module.polymorphicSchemaExtensions(), hasItem(HashIndexConfigurationSchema.class));
+    }
+
+    @Test
+    void providesSortedIndexConfigurationSchemaAsPolymorphicExtension() {
+        assertThat(module.polymorphicSchemaExtensions(), hasItem(SortedIndexConfigurationSchema.class));
+    }
+
+    @Test
+    void providesPartialIndexConfigurationSchemaAsPolymorphicExtension() {
+        assertThat(module.polymorphicSchemaExtensions(), hasItem(PartialIndexConfigurationSchema.class));
+    }
+
+    @Test
+    void isLoadedByServiceLoader() {
+        Optional<ConfigurationModule> maybeModule = ServiceLoader.load(ConfigurationModule.class).stream()
+                .map(Provider::get)
+                .filter(module -> module instanceof CoreDistributedConfigurationModule)
+                .findAny();
+
+        assertThat(maybeModule, isPresent());
+    }
+}
diff --git a/modules/api/src/test/java/org/apache/ignite/configuration/CoreLocalConfigurationModuleTest.java b/modules/api/src/test/java/org/apache/ignite/configuration/CoreLocalConfigurationModuleTest.java
new file mode 100644
index 0000000..46504ad
--- /dev/null
+++ b/modules/api/src/test/java/org/apache/ignite/configuration/CoreLocalConfigurationModuleTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.ignite.configuration;
+
+import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent;
+import static org.apache.ignite.configuration.annotation.ConfigurationType.LOCAL;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.ServiceLoader.Provider;
+import org.apache.ignite.configuration.schemas.clientconnector.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.schemas.network.NetworkConfiguration;
+import org.apache.ignite.configuration.schemas.rest.RestConfiguration;
+import org.apache.ignite.configuration.schemas.runner.NodeConfiguration;
+import org.apache.ignite.internal.configuration.ConfigurationModule;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link CoreLocalConfigurationModule}.
+ */
+class CoreLocalConfigurationModuleTest {
+    private final CoreLocalConfigurationModule module = new CoreLocalConfigurationModule();
+
+    @Test
+    void typeIsLocal() {
+        assertThat(module.type(), is(LOCAL));
+    }
+
+    @Test
+    void hasNetworkConfigurationRoot() {
+        assertThat(module.rootKeys(), hasItem(NetworkConfiguration.KEY));
+    }
+
+    @Test
+    void hasNodeConfigurationRoot() {
+        assertThat(module.rootKeys(), hasItem(NodeConfiguration.KEY));
+    }
+
+    @Test
+    void hasRestConfigurationRoot() {
+        assertThat(module.rootKeys(), hasItem(RestConfiguration.KEY));
+    }
+
+    @Test
+    void hasClientConnectorConfigurationRoot() {
+        assertThat(module.rootKeys(), hasItem(ClientConnectorConfiguration.KEY));
+    }
+
+    @Test
+    void providesNoValidators() {
+        assertThat(module.validators(), is(anEmptyMap()));
+    }
+
+    @Test
+    void providesNoInternalSchemaExtensions() {
+        assertThat(module.internalSchemaExtensions(), is(empty()));
+    }
+
+    @Test
+    void providesNoPolymorphicSchemaExtensions() {
+        assertThat(module.polymorphicSchemaExtensions(), is(empty()));
+    }
+
+    @Test
+    void isLoadedByServiceLoader() {
+        Optional<ConfigurationModule> maybeModule = ServiceLoader.load(ConfigurationModule.class).stream()
+                .map(Provider::get)
+                .filter(module -> module instanceof CoreLocalConfigurationModule)
+                .findAny();
+
+        assertThat(maybeModule, isPresent());
+    }
+}
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/Config.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/Config.java
index 18000bb..d17aacc 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/Config.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/Config.java
@@ -25,19 +25,20 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
 /**
- * This annotation, if applied to a class, marks it as a configuration schema. Annotation processor generates several classes for each
- * configuration schema:
+ * This annotation, if applied to a class, marks it as a configuration schema part. Annotation processor generates
+ * several classes for each configuration schema part:
  * <ul>
- * <li>Config - Represents configuration itself, provides API to init, change and view it. Extends {@code DynamicConfiguration}</li>
+ * <li>Config - Represents configuration itself, provides API to init, change and view it.
+ * Extends {@code DynamicConfiguration}</li>
  * <li>Change - changes config tree</li>
  * <li>View - immutable object to view config tree</li>
  * </ul>
  *
  * <h1 class="header">Example</h1>
- * Here is how to create a root configuration schema:
+ * Here is how to create a configuration schema part:
  * <pre><code>
- * {@literal @}Config(value = "local", root = true)
- * public class LocalConfigurationSchema {
+ * {@literal @}Config
+ * public class SubConfigurationSchema {
  *
  *      {@literal @}Value
  *      public String foo;
@@ -49,6 +50,11 @@ import java.lang.annotation.Target;
  *      public SomeOtherConfiguration someOther;
  * }
  * </code></pre>
+ *
+ * <p>The main difference between @{@link ConfigurationRoot} and @{@link Config} is that the former marks schema root,
+ * while the latter is for marking non-root parts of the schema.
+ *
+ * @see ConfigurationRoot
  */
 @Target({TYPE})
 @Retention(RUNTIME)
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/ConfigurationRoot.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/ConfigurationRoot.java
index f2c124e..e790bdb 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/ConfigurationRoot.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/ConfigurationRoot.java
@@ -25,8 +25,34 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
 /**
- * Annotation that marks underlying class as a root configuration schema. Has basically the same properties as {@link Config} + adds extra
- * properties.
+ * This annotation, if applied to a class, marks it as a configuration schema root. Annotation processor generates
+ * several classes for each configuration schema:
+ * <ul>
+ * <li>Config - Represents the configuration, provides API to init, change and view it.
+ * Extends {@code DynamicConfiguration}</li>
+ * <li>Change - changes the config tree</li>
+ * <li>View - an immutable object to view the config tree</li>
+ * </ul>
+ *
+ * <h1 class="header">Example</h1>
+ * Here is how to create a root configuration schema:
+ * <pre><code>
+ * {@literal @}ConfigurationRoot(rootName = "a.b", type = ConfigurationType.LOCAL)
+ * public class LocalConfigurationSchema {
+ *
+ *      {@literal @}Value
+ *      public String foo;
+ *
+ *      {@literal @}Value
+ *      public boolean bar;
+ *
+ *      {@literal @}ConfigValue
+ *      public SomeOtherConfiguration someOther;
+ * }
+ * </code></pre>
+ *
+ * <p>The main difference between @{@link ConfigurationRoot} and @{@link Config} is that the former marks schema root,
+ * while the latter is for marking non-root parts of the schema.
  *
  * @see Config
  */
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationModule.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationModule.java
new file mode 100644
index 0000000..94081c5
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationModule.java
@@ -0,0 +1,103 @@
+/*
+ * 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.ignite.internal.configuration;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.annotation.InternalConfiguration;
+import org.apache.ignite.configuration.annotation.PolymorphicConfig;
+import org.apache.ignite.configuration.validation.Validator;
+
+/**
+ * A module of configuration provided by a JAR file (or, in its source form, by a Maven/Gradle/... module).
+ *
+ * <p>Each configuration module only supplies configuration of only one {@link ConfigurationType}, so,
+ * if a library needs to provide both node-local and cluster-wide configuration, it needs to supply
+ * two ConfigurationModule instances.
+ *
+ * <p>Designed for integration with {@link java.util.ServiceLoader} mechanism, so ConfigurationModule instances
+ * provided by a library are to be defined as services either in
+ * {@code META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule}, or in a {@code module-info.java}.
+ *
+ * <p>Supplies the following configuration components:
+ * <ul>
+ *     <li><b>rootKeys</b> ({@link RootKey} instances)</li>
+ *     <li><b>validators</b> ({@link Validator} instances)</li>
+ *     <li><b>internalSchemaExtensions</b> (classes annotated with {@link InternalConfiguration})</li>
+ *     <li><b>polymorphicSchemaExtensions</b> (classes annotataed with {@link PolymorphicConfig})</li>
+ * </ul>
+ *
+ * @see ConfigurationType
+ * @see RootKey
+ * @see Validator
+ * @see InternalConfiguration
+ * @see PolymorphicConfig
+ */
+public interface ConfigurationModule {
+    /**
+     * Type of the configuration provided by this module.
+     *
+     * @return configuration type
+     */
+    ConfigurationType type();
+
+    /**
+     * Returns keys of configuration roots provided by this module.
+     *
+     * @return root keys
+     */
+    default Collection<RootKey<?, ?>> rootKeys() {
+        return emptySet();
+    }
+
+    /**
+     * Returns configuration validators provided by this module.
+     *
+     * @return configuration validators
+     */
+    default Map<Class<? extends Annotation>, Set<Validator<? extends Annotation, ?>>> validators() {
+        return emptyMap();
+    }
+
+    /**
+     * Returns classes of internal schema extensions (annotated with {@link InternalConfiguration})
+     * provided by this module.
+     *
+     * @return internal schema extensions' classes
+     */
+    default Collection<Class<?>> internalSchemaExtensions() {
+        return emptySet();
+    }
+
+    /**
+     * Returns classes of polymorphic schema extensions (annotated with {@link PolymorphicConfig})
+     * provided by this module.
+     *
+     * @return polymorphic schema extensions' classes
+     */
+    default Collection<Class<?>> polymorphicSchemaExtensions() {
+        return emptySet();
+    }
+}
diff --git a/modules/runner/pom.xml b/modules/runner/pom.xml
index 41eeeab..05e7c9e 100644
--- a/modules/runner/pom.xml
+++ b/modules/runner/pom.xml
@@ -113,6 +113,12 @@
         </dependency>
 
         <dependency>
+            <groupId>com.github.npathai</groupId>
+            <artifactId>hamcrest-optional</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
             <scope>test</scope>
diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index 5abb738..e375b3d 100644
--- a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -24,28 +24,20 @@ import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.ListIterator;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgnitionManager;
 import org.apache.ignite.client.handler.ClientHandlerModule;
-import org.apache.ignite.configuration.schemas.clientconnector.ClientConnectorConfiguration;
 import org.apache.ignite.configuration.schemas.network.NetworkConfiguration;
-import org.apache.ignite.configuration.schemas.rest.RestConfiguration;
-import org.apache.ignite.configuration.schemas.runner.ClusterConfiguration;
-import org.apache.ignite.configuration.schemas.runner.NodeConfiguration;
 import org.apache.ignite.configuration.schemas.store.DataStorageConfiguration;
-import org.apache.ignite.configuration.schemas.table.ColumnTypeValidator;
-import org.apache.ignite.configuration.schemas.table.HashIndexConfigurationSchema;
-import org.apache.ignite.configuration.schemas.table.PartialIndexConfigurationSchema;
-import org.apache.ignite.configuration.schemas.table.SortedIndexConfigurationSchema;
-import org.apache.ignite.configuration.schemas.table.TableValidator;
 import org.apache.ignite.configuration.schemas.table.TablesConfiguration;
 import org.apache.ignite.internal.baseline.BaselineManager;
 import org.apache.ignite.internal.configuration.ConfigurationManager;
+import org.apache.ignite.internal.configuration.ConfigurationModule;
+import org.apache.ignite.internal.configuration.ConfigurationModules;
 import org.apache.ignite.internal.configuration.ConfigurationRegistry;
-import org.apache.ignite.internal.configuration.schema.ExtendedTableConfigurationSchema;
+import org.apache.ignite.internal.configuration.ServiceLoaderModulesProvider;
 import org.apache.ignite.internal.configuration.storage.DistributedConfigurationStorage;
 import org.apache.ignite.internal.configuration.storage.LocalConfigurationStorage;
 import org.apache.ignite.internal.manager.IgniteComponent;
@@ -54,8 +46,6 @@ import org.apache.ignite.internal.metastorage.server.persistence.RocksDbKeyValue
 import org.apache.ignite.internal.processors.query.calcite.QueryProcessor;
 import org.apache.ignite.internal.processors.query.calcite.SqlQueryProcessor;
 import org.apache.ignite.internal.raft.Loza;
-import org.apache.ignite.internal.schema.configuration.ColumnTypeValidatorImpl;
-import org.apache.ignite.internal.schema.configuration.TableValidatorImpl;
 import org.apache.ignite.internal.table.distributed.TableManager;
 import org.apache.ignite.internal.table.distributed.TableTxManagerImpl;
 import org.apache.ignite.internal.tx.TxManager;
@@ -148,28 +138,29 @@ public class IgniteImpl implements Ignite {
     /**
      * The Constructor.
      *
-     * @param name    Ignite node name.
-     * @param workDir Work directory for the started node. Must not be {@code null}.
+     * @param name                       Ignite node name.
+     * @param workDir                    Work directory for the started node. Must not be {@code null}.
+     * @param serviceProviderClassLoader The class loader to be used to load provider-configuration files and provider
+     *                                   classes, or {@code null} if the system class loader (or, failing that
+     *                                   the bootstrap class loader) is to be used
      */
     IgniteImpl(
             String name,
-            Path workDir
+            Path workDir,
+            ClassLoader serviceProviderClassLoader
     ) {
         this.name = name;
 
         vaultMgr = createVault(workDir);
 
+        ConfigurationModules modules = loadConfigurationModules(serviceProviderClassLoader);
+
         nodeCfgMgr = new ConfigurationManager(
-                List.of(
-                        NetworkConfiguration.KEY,
-                        NodeConfiguration.KEY,
-                        RestConfiguration.KEY,
-                        ClientConnectorConfiguration.KEY
-                ),
-                Map.of(),
+                modules.local().rootKeys(),
+                modules.local().validators(),
                 new LocalConfigurationStorage(vaultMgr),
-                List.of(),
-                List.of()
+                modules.local().internalSchemaExtensions(),
+                modules.local().polymorphicSchemaExtensions()
         );
 
         NetworkConfiguration networkConfiguration = nodeCfgMgr.configurationRegistry().getConfiguration(NetworkConfiguration.KEY);
@@ -197,18 +188,11 @@ public class IgniteImpl implements Ignite {
         );
 
         clusterCfgMgr = new ConfigurationManager(
-                List.of(
-                        ClusterConfiguration.KEY,
-                        TablesConfiguration.KEY,
-                        DataStorageConfiguration.KEY
-                ),
-                Map.of(
-                        TableValidator.class, Set.of(TableValidatorImpl.INSTANCE),
-                        ColumnTypeValidator.class, Set.of(ColumnTypeValidatorImpl.INSTANCE)
-                ),
+                modules.distributed().rootKeys(),
+                modules.distributed().validators(),
                 new DistributedConfigurationStorage(metaStorageMgr, vaultMgr),
-                List.of(ExtendedTableConfigurationSchema.class),
-                List.of(HashIndexConfigurationSchema.class, SortedIndexConfigurationSchema.class, PartialIndexConfigurationSchema.class)
+                modules.distributed().internalSchemaExtensions(),
+                modules.distributed().polymorphicSchemaExtensions()
         );
 
         baselineMgr = new BaselineManager(
@@ -242,6 +226,29 @@ public class IgniteImpl implements Ignite {
         );
     }
 
+    private ConfigurationModules loadConfigurationModules(ClassLoader classLoader) {
+        var modulesProvider = new ServiceLoaderModulesProvider();
+        List<ConfigurationModule> modules = modulesProvider.modules(classLoader);
+
+        if (LOG.isInfoEnabled()) {
+            LOG.info("Configuration modules loaded: {}", modules);
+        }
+
+        if (modules.isEmpty()) {
+            throw new IllegalStateException("No configuration modules were loaded, this means Ignite cannot start. "
+                    + "Please make sure that the classloader for loading services is correct.");
+        }
+
+        var configModules = new ConfigurationModules(modules);
+
+        if (LOG.isInfoEnabled()) {
+            LOG.info("Local root keys: {}", configModules.local().rootKeys());
+            LOG.info("Distributed root keys: {}", configModules.distributed().rootKeys());
+        }
+
+        return configModules;
+    }
+
     /**
      * Starts ignite node.
      *
diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgnitionImpl.java b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgnitionImpl.java
index bb7f897..0d3a81b 100644
--- a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgnitionImpl.java
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgnitionImpl.java
@@ -62,16 +62,24 @@ public class IgnitionImpl implements Ignition {
     /**
      * Node name to node instance mapping. Please pay attention, that nodes in given map might be in any state: STARTING, STARTED, STOPPED.
      */
-    private static Map<String, IgniteImpl> nodes = new ConcurrentHashMap<>();
+    private static final Map<String, IgniteImpl> nodes = new ConcurrentHashMap<>();
 
     /** {@inheritDoc} */
     @Override
     public Ignite start(@NotNull String nodeName, @Nullable Path cfgPath, @NotNull Path workDir) {
+        return start(nodeName, cfgPath, workDir, defaultServiceClassLoader());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Ignite start(@NotNull String nodeName, @Nullable Path cfgPath, @NotNull Path workDir,
+                        @Nullable ClassLoader serviceLoaderClassLoader) {
         try {
             return doStart(
                     nodeName,
                     cfgPath == null ? null : Files.readString(cfgPath),
-                    workDir
+                    workDir,
+                    serviceLoaderClassLoader
             );
         } catch (IOException e) {
             throw new IgniteException("Unable to read user specific configuration.", e);
@@ -82,10 +90,10 @@ public class IgnitionImpl implements Ignition {
     @Override
     public Ignite start(@NotNull String name, @Nullable URL cfgUrl, @NotNull Path workDir) {
         if (cfgUrl == null) {
-            return doStart(name, null, workDir);
+            return doStart(name, null, workDir, defaultServiceClassLoader());
         } else {
             try (InputStream cfgStream = cfgUrl.openStream()) {
-                return doStart(name, new String(cfgStream.readAllBytes(), StandardCharsets.UTF_8), workDir);
+                return doStart(name, new String(cfgStream.readAllBytes(), StandardCharsets.UTF_8), workDir, defaultServiceClassLoader());
             } catch (IOException e) {
                 throw new IgniteException("Unable to read user specific configuration.", e);
             }
@@ -99,7 +107,8 @@ public class IgnitionImpl implements Ignition {
             return doStart(
                     name,
                     cfg == null ? null : new String(cfg.readAllBytes(), StandardCharsets.UTF_8),
-                    workDir
+                    workDir,
+                    defaultServiceClassLoader()
             );
         } catch (IOException e) {
             throw new IgniteException("Unable to read user specific configuration.", e);
@@ -109,7 +118,7 @@ public class IgnitionImpl implements Ignition {
     /** {@inheritDoc} */
     @Override
     public Ignite start(@NotNull String name, @NotNull Path workDir) {
-        return doStart(name, null, workDir);
+        return doStart(name, null, workDir, defaultServiceClassLoader());
     }
 
     /** {@inheritDoc} */
@@ -122,6 +131,10 @@ public class IgnitionImpl implements Ignition {
         });
     }
 
+    private ClassLoader defaultServiceClassLoader() {
+        return Thread.currentThread().getContextClassLoader();
+    }
+
     /**
      * Starts an Ignite node with an optional bootstrap configuration from a HOCON file.
      *
@@ -130,12 +143,13 @@ public class IgnitionImpl implements Ignition {
      * @param workDir    Work directory for the started node. Must not be {@code null}.
      * @return Started Ignite node.
      */
-    private static Ignite doStart(String nodeName, @Nullable String cfgContent, Path workDir) {
+    private static Ignite doStart(String nodeName, @Nullable String cfgContent, Path workDir,
+                                  @Nullable ClassLoader serviceLoaderClassLoader) {
         if (nodeName.isEmpty()) {
             throw new IllegalArgumentException("Node name must not be null or empty.");
         }
 
-        IgniteImpl nodeToStart = new IgniteImpl(nodeName, workDir);
+        IgniteImpl nodeToStart = new IgniteImpl(nodeName, workDir, serviceLoaderClassLoader);
 
         IgniteImpl prevNode = nodes.putIfAbsent(nodeName, nodeToStart);
 
diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/configuration/CompoundModule.java b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/CompoundModule.java
new file mode 100644
index 0000000..c6d9bec
--- /dev/null
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/CompoundModule.java
@@ -0,0 +1,89 @@
+/*
+ * 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.ignite.internal.configuration;
+
+import static java.util.Collections.unmodifiableMap;
+import static java.util.stream.Collectors.flatMapping;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toUnmodifiableList;
+import static java.util.stream.Collectors.toUnmodifiableSet;
+
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.validation.Validator;
+
+/**
+ * {@link ConfigurationModule} that merges a few {@code ConfigurationModule}s.
+ */
+public class CompoundModule implements ConfigurationModule {
+    private final ConfigurationType type;
+    private final List<ConfigurationModule> modules;
+
+    public CompoundModule(ConfigurationType type, Collection<ConfigurationModule> modules) {
+        this.type = type;
+        this.modules = List.copyOf(modules);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ConfigurationType type() {
+        return type;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<RootKey<?, ?>> rootKeys() {
+        return unionFromModulesExtractedWith(ConfigurationModule::rootKeys);
+    }
+
+    private <T> List<T> unionFromModulesExtractedWith(Function<? super ConfigurationModule, ? extends Collection<T>> extractor) {
+        return modules.stream()
+                .flatMap(module -> extractor.apply(module).stream())
+                .collect(toUnmodifiableList());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Map<Class<? extends Annotation>, Set<Validator<? extends Annotation, ?>>> validators() {
+        Map<Class<? extends Annotation>, Set<Validator<? extends Annotation, ?>>> result = modules.stream()
+                .flatMap(module -> module.validators().entrySet().stream())
+                .collect(groupingBy(
+                        Map.Entry::getKey,
+                        flatMapping(e -> e.getValue().stream(), toUnmodifiableSet())
+                ));
+        return unmodifiableMap(result);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<Class<?>> internalSchemaExtensions() {
+        return unionFromModulesExtractedWith(ConfigurationModule::internalSchemaExtensions);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<Class<?>> polymorphicSchemaExtensions() {
+        return unionFromModulesExtractedWith(ConfigurationModule::polymorphicSchemaExtensions);
+    }
+}
diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/configuration/ConfigurationModules.java b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/ConfigurationModules.java
new file mode 100644
index 0000000..06d12d6
--- /dev/null
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/ConfigurationModules.java
@@ -0,0 +1,68 @@
+/*
+ * 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.ignite.internal.configuration;
+
+import static java.util.stream.Collectors.toUnmodifiableList;
+
+import java.util.Collection;
+import java.util.List;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+
+/**
+ * An ensemble of {@link ConfigurationModule}s used to easily pick node-local and cluster-wide configuration
+ * and merge modules for convenience.
+ */
+public class ConfigurationModules {
+    private final CompoundModule localCompound;
+    private final CompoundModule distributedCompound;
+
+    /**
+     * Creates a new instance of {@link ConfigurationModules} wrapping modules passed to it.
+     *
+     * @param modules modules to wrap; they may be of different types
+     */
+    public ConfigurationModules(List<ConfigurationModule> modules) {
+        localCompound = compoundOfType(ConfigurationType.LOCAL, modules);
+        distributedCompound = compoundOfType(ConfigurationType.DISTRIBUTED, modules);
+    }
+
+    /**
+     * Return a module representing the result of merge of modules of type {@link ConfigurationType#LOCAL}.
+     *
+     * @return node-local configuration merge result
+     */
+    public ConfigurationModule local() {
+        return localCompound;
+    }
+
+    /**
+     * Return a module representing the result of merge of modules of type {@link ConfigurationType#DISTRIBUTED}.
+     *
+     * @return cluster-wide configuration merge result
+     */
+    public ConfigurationModule distributed() {
+        return distributedCompound;
+    }
+
+    private static CompoundModule compoundOfType(ConfigurationType type, Collection<ConfigurationModule> modules) {
+        List<ConfigurationModule> modulesOfGivenType = modules.stream()
+                .filter(module -> module.type() == type)
+                .collect(toUnmodifiableList());
+        return new CompoundModule(type, modulesOfGivenType);
+    }
+}
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/ConfigurationRoot.java b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/ServiceLoaderModulesProvider.java
similarity index 50%
copy from modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/ConfigurationRoot.java
copy to modules/runner/src/main/java/org/apache/ignite/internal/configuration/ServiceLoaderModulesProvider.java
index f2c124e..0b535e2 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/ConfigurationRoot.java
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/ServiceLoaderModulesProvider.java
@@ -15,36 +15,30 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.configuration.annotation;
+package org.apache.ignite.internal.configuration;
 
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.stream.Collectors.toUnmodifiableList;
 
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.ServiceLoader.Provider;
 
 /**
- * Annotation that marks underlying class as a root configuration schema. Has basically the same properties as {@link Config} + adds extra
- * properties.
+ * Provides {@link ConfigurationModule}s using {@link ServiceLoader} mechanism.
+ * Uses {@link ConfigurationModule} interface to query the ServiceLoader.
  *
- * @see Config
+ * @see ConfigurationModule
  */
-@Target(TYPE)
-@Retention(RUNTIME)
-@Documented
-public @interface ConfigurationRoot {
+public class ServiceLoaderModulesProvider {
     /**
-     * Returns the unique root name.
+     * Loads modules using {@link ServiceLoader} mechanism.
      *
-     * @return Unique root name.
+     * @param classLoader the class loader to use
+     * @return modules
      */
-    String rootName();
-
-    /**
-     * Returns the type of the configuration.
-     *
-     * @return Type of the configuration.
-     */
-    ConfigurationType type() default ConfigurationType.LOCAL;
+    public List<ConfigurationModule> modules(ClassLoader classLoader) {
+        return ServiceLoader.load(ConfigurationModule.class, classLoader).stream()
+                .map(Provider::get)
+                .collect(toUnmodifiableList());
+    }
 }
diff --git a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/CompoundModuleTest.java b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/CompoundModuleTest.java
new file mode 100644
index 0000000..6dca5d3
--- /dev/null
+++ b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/CompoundModuleTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.ignite.internal.configuration;
+
+import static java.util.Collections.emptyList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.when;
+
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.validation.Validator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class CompoundModuleTest {
+    @Mock
+    private RootKey<?, ?> rootKeyA;
+    @Mock
+    private RootKey<?, ?> rootKeyB;
+
+    @Mock
+    private Validator<AnnotationA, ?> validatorA;
+    @Mock
+    private Validator<AnnotationB, ?> validatorB;
+
+    @Mock
+    private ConfigurationModule moduleA;
+    @Mock
+    private ConfigurationModule moduleB;
+
+    private ConfigurationModule compound;
+
+    @BeforeEach
+    void createCompoundModule() {
+        compound = new CompoundModule(ConfigurationType.LOCAL, List.of(moduleA, moduleB));
+    }
+
+    @Test
+    void returnsTypePassedViaConstructor() {
+        var compound = new CompoundModule(ConfigurationType.LOCAL, emptyList());
+
+        assertThat(compound.type(), is(ConfigurationType.LOCAL));
+    }
+
+    @Test
+    void returnsUnionOfRootKeysOfItsModules() {
+        when(moduleA.rootKeys()).thenReturn(Set.of(rootKeyA));
+        when(moduleB.rootKeys()).thenReturn(Set.of(rootKeyB));
+
+        assertThat(compound.rootKeys(), containsInAnyOrder(rootKeyA, rootKeyB));
+    }
+
+    @Test
+    void providesUnionOfValidatorsMapsOfItsModulesPerKey() {
+        when(moduleA.validators()).thenReturn(Map.of(AnnotationA.class, Set.of(validatorA)));
+        when(moduleB.validators()).thenReturn(Map.of(AnnotationB.class, Set.of(validatorB)));
+
+        Map<Class<? extends Annotation>, Set<Validator<? extends Annotation, ?>>> validators = compound.validators();
+
+        assertThat(validators, hasEntry(AnnotationA.class, Set.of(validatorA)));
+        assertThat(validators, hasEntry(AnnotationB.class, Set.of(validatorB)));
+    }
+
+    @Test
+    void mergesValidatorsUnderTheSameKey() {
+        when(moduleA.validators()).thenReturn(Map.of(AnnotationA.class, Set.of(validatorA)));
+        when(moduleB.validators()).thenReturn(Map.of(AnnotationA.class, Set.of(validatorB)));
+
+        Map<Class<? extends Annotation>, Set<Validator<? extends Annotation, ?>>> validators = compound.validators();
+
+        assertThat(validators, hasEntry(AnnotationA.class, Set.of(validatorA, validatorB)));
+    }
+
+    @Test
+    void returnsUnionOfInternalSchemaExtensionsOfItsModules() {
+        when(moduleA.internalSchemaExtensions()).thenReturn(Set.of(ExtensionA.class));
+        when(moduleB.internalSchemaExtensions()).thenReturn(Set.of(ExtensionB.class));
+
+        assertThat(compound.internalSchemaExtensions(), containsInAnyOrder(ExtensionA.class, ExtensionB.class));
+    }
+
+    @Test
+    void returnsUnionOfPolymorphicSchemaExtensionsOfItsModules() {
+        when(moduleA.polymorphicSchemaExtensions()).thenReturn(Set.of(ExtensionA.class));
+        when(moduleB.polymorphicSchemaExtensions()).thenReturn(Set.of(ExtensionB.class));
+
+        assertThat(compound.polymorphicSchemaExtensions(), containsInAnyOrder(ExtensionA.class, ExtensionB.class));
+    }
+
+    private @interface AnnotationA {
+    }
+
+    private @interface AnnotationB {
+    }
+
+    private static class ExtensionA {
+    }
+
+    private static class ExtensionB {
+    }
+}
diff --git a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/ConfigurationModulesTest.java b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/ConfigurationModulesTest.java
new file mode 100644
index 0000000..aad5901
--- /dev/null
+++ b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/ConfigurationModulesTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.ignite.internal.configuration;
+
+import static org.apache.ignite.configuration.annotation.ConfigurationType.DISTRIBUTED;
+import static org.apache.ignite.configuration.annotation.ConfigurationType.LOCAL;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.ignite.configuration.RootKey;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class ConfigurationModulesTest {
+    @Mock
+    private RootKey<?, ?> rootKeyA;
+    @Mock
+    private RootKey<?, ?> rootKeyB;
+    @Mock
+    private RootKey<?, ?> rootKeyC;
+    @Mock
+    private RootKey<?, ?> rootKeyD;
+
+    @Mock
+    private ConfigurationModule moduleA;
+    @Mock
+    private ConfigurationModule moduleB;
+    @Mock
+    private ConfigurationModule moduleC;
+    @Mock
+    private ConfigurationModule moduleD;
+
+    @Test
+    void mergesLocalModules() {
+        when(moduleA.type()).thenReturn(LOCAL);
+        when(moduleB.type()).thenReturn(LOCAL);
+        when(moduleC.type()).thenReturn(DISTRIBUTED);
+        when(moduleD.type()).thenReturn(DISTRIBUTED);
+        when(moduleA.rootKeys()).thenReturn(Set.of(rootKeyA));
+        when(moduleB.rootKeys()).thenReturn(Set.of(rootKeyB));
+
+        var modules = new ConfigurationModules(List.of(moduleA, moduleB, moduleC, moduleD));
+
+        assertThat(modules.local().rootKeys(), containsInAnyOrder(rootKeyA, rootKeyB));
+    }
+
+    @Test
+    void mergesDistributedModules() {
+        when(moduleA.type()).thenReturn(LOCAL);
+        when(moduleB.type()).thenReturn(LOCAL);
+        when(moduleC.type()).thenReturn(DISTRIBUTED);
+        when(moduleD.type()).thenReturn(DISTRIBUTED);
+        when(moduleC.rootKeys()).thenReturn(Set.of(rootKeyC));
+        when(moduleD.rootKeys()).thenReturn(Set.of(rootKeyD));
+
+        var modules = new ConfigurationModules(List.of(moduleA, moduleB, moduleC, moduleD));
+
+        assertThat(modules.distributed().rootKeys(), containsInAnyOrder(rootKeyC, rootKeyD));
+    }
+}
diff --git a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/ServiceLoaderModulesProviderTest.java b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/ServiceLoaderModulesProviderTest.java
new file mode 100644
index 0000000..3ffc30f
--- /dev/null
+++ b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/ServiceLoaderModulesProviderTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.ignite.internal.configuration;
+
+import static java.util.stream.Collectors.toList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link ServiceLoaderModulesProvider}.
+ */
+class ServiceLoaderModulesProviderTest {
+    private final ServiceLoaderModulesProvider provider = new ServiceLoaderModulesProvider();
+
+    @Test
+    void loadsModulesDefinedAsServicesInMetaInf() {
+        List<ConfigurationModule> testModules = provider.modules(getClass().getClassLoader()).stream()
+                .filter(module -> module.getClass().equals(TestConfigurationModule.class))
+                .collect(toList());
+
+        assertThat(testModules, hasSize(1));
+    }
+
+    @Test
+    void loadsModulesWhenClassLoaderIsNull() {
+        List<ConfigurationModule> testModules = provider.modules(null).stream()
+                .filter(module -> module.getClass().equals(TestConfigurationModule.class))
+                .collect(toList());
+
+        assertThat(testModules, hasSize(1));
+    }
+}
diff --git a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/TestConfigurationModule.java b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/TestConfigurationModule.java
new file mode 100644
index 0000000..060b1ed
--- /dev/null
+++ b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/TestConfigurationModule.java
@@ -0,0 +1,31 @@
+/*
+ * 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.ignite.internal.configuration;
+
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+
+/**
+ * A test {@link ConfigurationModule} implementation used to test loading by {@link ServiceLoaderModulesProvider}.
+ */
+public class TestConfigurationModule implements ConfigurationModule {
+    /** {@inheritDoc} */
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.LOCAL;
+    }
+}
diff --git a/modules/runner/src/test/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule b/modules/runner/src/test/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule
new file mode 100644
index 0000000..685b7b2
--- /dev/null
+++ b/modules/runner/src/test/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+org.apache.ignite.internal.configuration.TestConfigurationModule
diff --git a/modules/schema/pom.xml b/modules/schema/pom.xml
index 40139cf..eea9904 100644
--- a/modules/schema/pom.xml
+++ b/modules/schema/pom.xml
@@ -51,7 +51,6 @@
         <dependency>
             <groupId>org.apache.ignite</groupId>
             <artifactId>ignite-configuration</artifactId>
-            <scope>test</scope>
         </dependency>
 
         <!-- 3rd party dependencies -->
@@ -94,6 +93,12 @@
         </dependency>
 
         <dependency>
+            <groupId>com.github.npathai</groupId>
+            <artifactId>hamcrest-optional</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
             <scope>test</scope>
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/configuration/SchemaDistributedConfigurationModule.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/configuration/SchemaDistributedConfigurationModule.java
new file mode 100644
index 0000000..32f0e9d
--- /dev/null
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/configuration/SchemaDistributedConfigurationModule.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.schema.configuration;
+
+import java.lang.annotation.Annotation;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.schemas.table.ColumnTypeValidator;
+import org.apache.ignite.configuration.schemas.table.TableValidator;
+import org.apache.ignite.configuration.validation.Validator;
+import org.apache.ignite.internal.configuration.ConfigurationModule;
+
+/**
+ * {@link ConfigurationModule} for cluster-wide configuration provided by ignite-schema.
+ */
+public class SchemaDistributedConfigurationModule implements ConfigurationModule {
+    /** {@inheritDoc} */
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.DISTRIBUTED;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Map<Class<? extends Annotation>, Set<Validator<? extends Annotation, ?>>> validators() {
+        return Map.of(
+                TableValidator.class, Set.of(TableValidatorImpl.INSTANCE),
+                ColumnTypeValidator.class, Set.of(ColumnTypeValidatorImpl.INSTANCE)
+        );
+    }
+}
diff --git a/modules/schema/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule b/modules/schema/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule
new file mode 100644
index 0000000..86abc55
--- /dev/null
+++ b/modules/schema/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+org.apache.ignite.internal.schema.configuration.SchemaDistributedConfigurationModule
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/configuration/SchemaDistributedConfigurationModuleTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/configuration/SchemaDistributedConfigurationModuleTest.java
new file mode 100644
index 0000000..1891eb2
--- /dev/null
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/configuration/SchemaDistributedConfigurationModuleTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ignite.internal.schema.configuration;
+
+import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent;
+import static org.apache.ignite.configuration.annotation.ConfigurationType.DISTRIBUTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import java.util.Optional;
+import java.util.ServiceLoader;
+import org.apache.ignite.configuration.schemas.table.ColumnTypeValidator;
+import org.apache.ignite.configuration.schemas.table.TableValidator;
+import org.apache.ignite.internal.configuration.ConfigurationModule;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link SchemaDistributedConfigurationModule}.
+ */
+class SchemaDistributedConfigurationModuleTest {
+    private final SchemaDistributedConfigurationModule module = new SchemaDistributedConfigurationModule();
+
+    @Test
+    void typeIsDistributed() {
+        assertThat(module.type(), is(DISTRIBUTED));
+    }
+
+    @Test
+    void hasNoConfigurationRoots() {
+        assertThat(module.rootKeys(), is(empty()));
+    }
+
+    @Test
+    void providesTableValidator() {
+        assertThat(module.validators(), hasEntry(is(TableValidator.class), hasItem(instanceOf(TableValidatorImpl.class))));
+    }
+
+    @Test
+    void providesColumnTypeValidator() {
+        assertThat(module.validators(),
+                hasEntry(is(ColumnTypeValidator.class), hasItem(instanceOf(ColumnTypeValidatorImpl.class))));
+    }
+
+    @Test
+    void providesNoInternalSchemaExtensions() {
+        assertThat(module.internalSchemaExtensions(), is(empty()));
+    }
+
+    @Test
+    void providesNoPolymorphicSchemaExtensions() {
+        assertThat(module.polymorphicSchemaExtensions(), is(empty()));
+    }
+
+    @Test
+    void isLoadedByServiceLoader() {
+        Optional<ConfigurationModule> maybeModule = ServiceLoader.load(ConfigurationModule.class).stream()
+                .map(ServiceLoader.Provider::get)
+                .filter(module -> module instanceof SchemaDistributedConfigurationModule)
+                .findAny();
+
+        assertThat(maybeModule, isPresent());
+    }
+}
diff --git a/modules/table/pom.xml b/modules/table/pom.xml
index 88a1f16..6007bcf 100644
--- a/modules/table/pom.xml
+++ b/modules/table/pom.xml
@@ -144,6 +144,18 @@
         </dependency>
 
         <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.npathai</groupId>
+            <artifactId>hamcrest-optional</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>org.apache.ignite</groupId>
             <artifactId>ignite-network</artifactId>
             <scope>test</scope>
diff --git a/modules/table/src/main/java/org/apache/ignite/internal/configuration/TableDistributedConfigurationModule.java b/modules/table/src/main/java/org/apache/ignite/internal/configuration/TableDistributedConfigurationModule.java
new file mode 100644
index 0000000..1eb4286
--- /dev/null
+++ b/modules/table/src/main/java/org/apache/ignite/internal/configuration/TableDistributedConfigurationModule.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration;
+
+import java.util.Collection;
+import java.util.List;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.schema.ExtendedTableConfigurationSchema;
+
+/**
+ * {@link ConfigurationModule} for cluster-wide configuration provided by ignite-table.
+ */
+public class TableDistributedConfigurationModule implements ConfigurationModule {
+    /** {@inheritDoc} */
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.DISTRIBUTED;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<Class<?>> internalSchemaExtensions() {
+        return List.of(ExtendedTableConfigurationSchema.class);
+    }
+}
diff --git a/modules/table/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule b/modules/table/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule
new file mode 100644
index 0000000..4e6012e
--- /dev/null
+++ b/modules/table/src/main/resources/META-INF/services/org.apache.ignite.internal.configuration.ConfigurationModule
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+org.apache.ignite.internal.configuration.TableDistributedConfigurationModule
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/configuration/TableDistributedConfigurationModuleTest.java b/modules/table/src/test/java/org/apache/ignite/internal/configuration/TableDistributedConfigurationModuleTest.java
new file mode 100644
index 0000000..f9c3273
--- /dev/null
+++ b/modules/table/src/test/java/org/apache/ignite/internal/configuration/TableDistributedConfigurationModuleTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.ignite.internal.configuration;
+
+import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent;
+import static org.apache.ignite.configuration.annotation.ConfigurationType.DISTRIBUTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+
+import java.util.Optional;
+import java.util.ServiceLoader;
+import org.apache.ignite.internal.configuration.schema.ExtendedTableConfigurationSchema;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link TableDistributedConfigurationModule}.
+ */
+class TableDistributedConfigurationModuleTest {
+    private final TableDistributedConfigurationModule module = new TableDistributedConfigurationModule();
+
+    @Test
+    void typeIsDistributed() {
+        assertThat(module.type(), is(DISTRIBUTED));
+    }
+
+    @Test
+    void hasNoConfigurationRoots() {
+        assertThat(module.rootKeys(), is(empty()));
+    }
+
+    @Test
+    void providesNoValidators() {
+        assertThat(module.validators(), is(anEmptyMap()));
+    }
+
+    @Test
+    void providesExternalTableConfigurationSchemaAsInternalExtension() {
+        assertThat(module.internalSchemaExtensions(), hasItem(ExtendedTableConfigurationSchema.class));
+    }
+
+    @Test
+    void providesNoPolymorphicSchemaExtensions() {
+        assertThat(module.polymorphicSchemaExtensions(), is(empty()));
+    }
+
+    @Test
+    void isLoadedByServiceLoader() {
+        Optional<ConfigurationModule> maybeModule = ServiceLoader.load(ConfigurationModule.class).stream()
+                .map(ServiceLoader.Provider::get)
+                .filter(module -> module instanceof TableDistributedConfigurationModule)
+                .findAny();
+
+        assertThat(maybeModule, isPresent());
+    }
+}
diff --git a/parent/pom.xml b/parent/pom.xml
index 77d226b..09286ef 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -68,6 +68,7 @@
         <spoon.framework.version>8.4.0-beta-18</spoon.framework.version>
         <typesafe.version>1.4.1</typesafe.version>
         <hamcrest.version>2.2</hamcrest.version>
+        <hamcrest.optional.version>2.0.0</hamcrest.optional.version>
         <scalecube.version>2.6.12</scalecube.version>
         <calcite.version>1.27.0</calcite.version>
         <janino.version>3.1.4</janino.version>
@@ -556,6 +557,12 @@
             </dependency>
 
             <dependency>
+                <groupId>com.github.npathai</groupId>
+                <artifactId>hamcrest-optional</artifactId>
+                <version>${hamcrest.optional.version}</version>
+            </dependency>
+
+            <dependency>
                 <groupId>io.scalecube</groupId>
                 <artifactId>scalecube-cluster</artifactId>
                 <version>${scalecube.version}</version>