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/04/02 11:19:36 UTC

[ignite-3] branch main updated: IGNITE-14180 Implemented the ability to subscribe to configuration updates. (#76)

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 297468b  IGNITE-14180 Implemented the ability to subscribe to configuration updates. (#76)
297468b is described below

commit 297468b6b5a819e48b7b9dacfeb5c06d5f6dd910
Author: ibessonov <be...@gmail.com>
AuthorDate: Fri Apr 2 14:19:30 2021 +0300

    IGNITE-14180 Implemented the ability to subscribe to configuration updates. (#76)
---
 modules/configuration-annotation-processor/pom.xml |  12 -
 .../processor/internal/Processor.java              |  31 +--
 .../configuration/processor/internal/Utils.java    | 109 +--------
 .../storage => }/ConfigurationChangerTest.java     |  28 ++-
 .../internal/util/ConfigurationUtilTest.java       |  82 ++++++-
 .../internal/validation/ValidationUtilTest.java    |   2 +-
 .../notifications/ConfigurationListenerTest.java   | 245 +++++++++++++++++++++
 .../sample/LocalConfigurationSchema.java           |   2 +-
 .../sample/NetworkConfigurationSchema.java         |   2 +-
 .../ignite/configuration/sample/UsageTest.java     |  16 +-
 .../storage/TestConfigurationStorage.java          |  25 +--
 modules/configuration/pom.xml                      |  13 --
 .../ignite/configuration/ConfigurationChanger.java | 195 +++++++++-------
 .../configuration/ConfigurationProperty.java       |  14 +-
 .../configuration/ConfigurationRegistry.java       |  55 ++++-
 .../ignite/configuration/ConfigurationTree.java    |   4 +-
 .../ignite/configuration/ConfigurationValue.java   |   6 +-
 .../configuration/NamedConfigurationTree.java      |   9 +-
 .../ignite/configuration/PropertyListener.java     |  61 -----
 .../configuration/internal/ConfigurationNode.java  |  19 +-
 .../internal/DynamicConfiguration.java             |   9 +-
 .../configuration/internal/DynamicProperty.java    |  16 +-
 .../internal/NamedListConfiguration.java           |  25 ++-
 .../ConfigurationNotificationEventImpl.java        |  50 +++++
 .../util/ConfigurationNotificationsUtil.java       | 184 ++++++++++++++++
 .../internal/util/ConfigurationUtil.java           |  42 ++++
 .../ConfigurationListener.java}                    |  28 +--
 .../ConfigurationNamedListListener.java            |  44 ++++
 .../ConfigurationNotificationEvent.java}           |  45 ++--
 .../storage/ConfigurationStorage.java              |  16 +-
 .../storage/ConfigurationStorageListener.java      |   1 +
 .../apache/ignite/configuration/storage/Data.java  |  22 +-
 .../ignite/configuration/tree/NamedListNode.java   |   5 +
 .../ignite/configuration/tree/NamedListView.java   |   5 +
 modules/network/pom.xml                            |  12 -
 .../InMemoryConfigurationStorage.java              |  25 +--
 .../rest/presentation/json/JsonConverterTest.java  |   7 +
 .../json/TestConfigurationStorage.java             |  14 +-
 parent/pom.xml                                     |   7 -
 39 files changed, 1036 insertions(+), 451 deletions(-)

diff --git a/modules/configuration-annotation-processor/pom.xml b/modules/configuration-annotation-processor/pom.xml
index be4a44b..3e880b4 100644
--- a/modules/configuration-annotation-processor/pom.xml
+++ b/modules/configuration-annotation-processor/pom.xml
@@ -49,18 +49,6 @@
 
         <!-- Test dependencies. -->
         <dependency>
-            <groupId>log4j</groupId>
-            <artifactId>log4j</artifactId>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
             <groupId>com.google.testing.compile</groupId>
             <artifactId>compile-testing</artifactId>
             <scope>test</scope>
diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
index 81395d6..9b4f2e0 100644
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
+++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
@@ -50,19 +50,12 @@ import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.MirroredTypesException;
 import javax.lang.model.type.TypeMirror;
 import javax.lang.model.util.Elements;
-import org.apache.ignite.configuration.ConfigurationChanger;
-import org.apache.ignite.configuration.ConfigurationRegistry;
-import org.apache.ignite.configuration.ConfigurationTree;
-import org.apache.ignite.configuration.ConfigurationValue;
 import org.apache.ignite.configuration.NamedConfigurationTree;
-import org.apache.ignite.configuration.RootKey;
 import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.internal.DynamicConfiguration;
-import org.apache.ignite.configuration.internal.DynamicProperty;
 import org.apache.ignite.configuration.internal.NamedListConfiguration;
 import org.apache.ignite.configuration.tree.ConfigurationSource;
 import org.apache.ignite.configuration.tree.ConfigurationVisitor;
@@ -92,6 +85,9 @@ public class Processor extends AbstractProcessor {
     /** Inherit doc javadoc. */
     private static final String INHERIT_DOC = "{@inheritDoc}";
 
+    /** */
+    private static final ClassName ROOT_KEY_CLASSNAME = ClassName.get("org.apache.ignite.configuration", "RootKey");
+
     /**
      * Constructor.
      */
@@ -302,14 +298,17 @@ public class Processor extends AbstractProcessor {
         ClassName schemaClassName
     ) {
         ClassName viewClassName = Utils.getViewName(schemaClassName);
-        ParameterizedTypeName fieldTypeName = ParameterizedTypeName.get(ClassName.get(RootKey.class), configInterface, viewClassName);
+
+        ParameterizedTypeName fieldTypeName = ParameterizedTypeName.get(ROOT_KEY_CLASSNAME, configInterface, viewClassName);
 
         ClassName nodeClassName = Utils.getNodeName(schemaClassName);
 
+        ClassName cfgRegistryClassName = ClassName.get("org.apache.ignite.configuration", "ConfigurationRegistry");
+
         FieldSpec keyField = FieldSpec.builder(fieldTypeName, "KEY", PUBLIC, STATIC, FINAL)
             .initializer(
                 "$T.newRootKey($S, $T.class, $T::new, $T::new)",
-                ConfigurationRegistry.class, configDesc.getName(), storageType, nodeClassName,
+                cfgRegistryClassName, configDesc.getName(), storageType, nodeClassName,
                 Utils.getConfigurationName(schemaClassName)
             )
             .build();
@@ -394,8 +393,10 @@ public class Processor extends AbstractProcessor {
 
         final Value valueAnnotation = field.getAnnotation(Value.class);
         if (valueAnnotation != null) {
-            ClassName dynPropClass = ClassName.get(DynamicProperty.class);
-            ClassName confValueClass = ClassName.get(ConfigurationValue.class);
+            // It is necessary to use class names without loading classes so that we won't
+            // accidentally get NoClassDefFoundError
+            ClassName dynPropClass = ClassName.get("org.apache.ignite.configuration.internal", "DynamicProperty");
+            ClassName confValueClass = ClassName.get("org.apache.ignite.configuration", "ConfigurationValue");
 
             TypeName genericType = baseType;
 
@@ -495,8 +496,8 @@ public class Processor extends AbstractProcessor {
         }
 
         builder
-            .addParameter(ParameterizedTypeName.get(ClassName.get(RootKey.class), WILDCARD, WILDCARD), "rootKey")
-            .addParameter(ConfigurationChanger.class, "changer");
+            .addParameter(ParameterizedTypeName.get(ROOT_KEY_CLASSNAME, WILDCARD, WILDCARD), "rootKey")
+            .addParameter(ClassName.get("org.apache.ignite.configuration", "ConfigurationChanger"), "changer");
 
         if (isRoot)
             builder.addStatement("super($T.emptyList(), $S, rootKey, changer)", Collections.class, configName);
@@ -528,12 +529,12 @@ public class Processor extends AbstractProcessor {
         final ClassName initClassName = Utils.getInitName(schemaClassName);
         final ClassName changeClassName = Utils.getChangeName(schemaClassName);
 
-        ClassName dynConfClass = ClassName.get(DynamicConfiguration.class);
+        ClassName dynConfClass = ClassName.get("org.apache.ignite.configuration.internal", "DynamicConfiguration");
         TypeName dynConfViewClassType = ParameterizedTypeName.get(dynConfClass, viewClassTypeName, initClassName, changeClassName);
 
         configurationClassBuilder.superclass(dynConfViewClassType);
 
-        ClassName confTreeInterface = ClassName.get(ConfigurationTree.class);
+        ClassName confTreeInterface = ClassName.get("org.apache.ignite.configuration", "ConfigurationTree");
         TypeName confTreeParameterized = ParameterizedTypeName.get(confTreeInterface, viewClassTypeName, changeClassName);
 
         configurationInterfaceBuilder.addSuperinterface(confTreeParameterized);
diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
index b63d1b9..1606a21 100644
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
+++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
@@ -18,106 +18,22 @@ package org.apache.ignite.configuration.processor.internal;
 
 import com.squareup.javapoet.AnnotationSpec;
 import com.squareup.javapoet.ClassName;
-import com.squareup.javapoet.CodeBlock;
-import com.squareup.javapoet.FieldSpec;
-import com.squareup.javapoet.MethodSpec;
 import com.squareup.javapoet.ParameterizedTypeName;
 import com.squareup.javapoet.TypeName;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-import javax.lang.model.element.Modifier;
-import javax.lang.model.element.VariableElement;
-import org.apache.ignite.configuration.internal.DynamicConfiguration;
 import org.apache.ignite.configuration.internal.NamedListConfiguration;
 
 /**
  * Annotation processing utilities.
  */
 public class Utils {
+    /** */
+    private static final ClassName NAMED_LIST_CFG_CLASSNAME = ClassName.get("org.apache.ignite.configuration.internal", "NamedListConfiguration");
+
     /** Private constructor. */
     private Utils() {
     }
 
     /**
-     * Create constructor for
-     *
-     * @param fieldSpecs List of fields.
-     * @return Constructor method.
-     */
-    public static MethodSpec createConstructor(List<FieldSpec> fieldSpecs) {
-        final MethodSpec.Builder builder = MethodSpec.constructorBuilder();
-
-        for (FieldSpec field : fieldSpecs) {
-            builder.addParameter(field.type, field.name);
-            builder.addStatement("this.$L = $L", field.name, field.name);
-        }
-
-        return builder.build();
-    }
-
-    /**
-     * Create getters for fields.
-     *
-     * @param fieldSpecs List of fields.
-     * @return List of getter methods.
-     */
-    public static List<MethodSpec> createGetters(List<FieldSpec> fieldSpecs) {
-        return fieldSpecs.stream().map(field ->
-            MethodSpec.methodBuilder(field.name)
-                .returns(field.type)
-                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
-                .addStatement("return $L", field.name)
-                .build()).collect(Collectors.toList()
-        );
-    }
-
-    /**
-     * Create builder-style setters.
-     *
-     * @param fieldSpecs List of fields.
-     * @return List of setter methods.
-     */
-    public static List<MethodSpec> createBuildSetters(List<FieldSpec> fieldSpecs) {
-        return fieldSpecs.stream().map(field -> {
-            return MethodSpec.methodBuilder("with" + field.name)
-                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
-                .addStatement("this.$L = $L", field.name, field.name)
-                .build();
-        }).collect(Collectors.toList());
-    }
-
-    /**
-     * Create '{@code new SomeObject(arg1, arg2, ..., argN)}' code block.
-     *
-     * @param type Type of the new object.
-     * @param fieldSpecs List of arguments.
-     * @return New object code block.
-     */
-    public static CodeBlock newObject(TypeName type, List<VariableElement> fieldSpecs) {
-        String args = fieldSpecs.stream().map(f -> f.getSimpleName().toString()).collect(Collectors.joining(", "));
-        return CodeBlock.builder()
-            .add("new $T($L)", type, args)
-            .build();
-    }
-
-    /**
-     * Get class with parameters, boxing them if necessary.
-     *
-     * @param clz Generic class.
-     * @param types Generic parameters.
-     * @return Parameterized type.
-     */
-    public static ParameterizedTypeName getParameterized(ClassName clz, TypeName... types) {
-        types = Arrays.stream(types).map(t -> {
-            if (t.isPrimitive())
-                t = t.box();
-            return t;
-        }).toArray(TypeName[]::new);
-        return ParameterizedTypeName.get(clz, types);
-    }
-
-    /**
      * Get {@link ClassName} for configuration class.
      *
      * @param schemaClassName Configuration schema ClassName.
@@ -200,30 +116,13 @@ public class Utils {
         if (type instanceof ParameterizedTypeName) {
             ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName) type;
 
-            if (parameterizedTypeName.rawType.equals(ClassName.get(NamedListConfiguration.class)))
+            if (parameterizedTypeName.rawType.equals(NAMED_LIST_CFG_CLASSNAME))
                 return true;
         }
         return false;
     }
 
     /**
-     * Get {@code DynamicConfiguration} inside of the named configuration.
-     *
-     * @param type Type name.
-     * @return {@link DynamicConfiguration} class name.
-     */
-    public static TypeName unwrapNamedListConfigurationClass(TypeName type) {
-        if (type instanceof ParameterizedTypeName) {
-            ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName) type;
-
-            if (parameterizedTypeName.rawType.equals(ClassName.get(NamedListConfiguration.class)))
-                return parameterizedTypeName.typeArguments.get(1);
-        }
-
-        throw new ProcessorException(type + " is not a NamedListConfiguration class");
-    }
-
-    /**
      * @return {@code @SuppressWarnings("unchecked")} annotation spec object.
      */
     public static AnnotationSpec suppressWarningsUnchecked() {
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/ConfigurationChangerTest.java
similarity index 87%
rename from modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java
rename to modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/ConfigurationChangerTest.java
index 57d54a2..e621c7a 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/ConfigurationChangerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.ignite.configuration.sample.storage;
+package org.apache.ignite.configuration;
 
 import java.io.Serializable;
 import java.lang.annotation.Retention;
@@ -24,14 +24,13 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
-import org.apache.ignite.configuration.ConfigurationChangeException;
-import org.apache.ignite.configuration.ConfigurationChanger;
 import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
 import org.apache.ignite.configuration.storage.Data;
+import org.apache.ignite.configuration.storage.TestConfigurationStorage;
 import org.apache.ignite.configuration.validation.ValidationContext;
 import org.apache.ignite.configuration.validation.ValidationIssue;
 import org.apache.ignite.configuration.validation.Validator;
@@ -39,8 +38,9 @@ import org.junit.jupiter.api.Test;
 
 import static java.lang.annotation.ElementType.FIELD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.concurrent.CompletableFuture.completedFuture;
 import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.apache.ignite.configuration.sample.storage.AConfiguration.KEY;
+import static org.apache.ignite.configuration.AConfiguration.KEY;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -99,7 +99,8 @@ public class ConfigurationChangerTest {
             .initChild(init -> init.initIntCfg(1).initStrCfg("1"))
             .initElements(change -> change.create("a", init -> init.initStrCfg("1")));
 
-        final ConfigurationChanger changer = new ConfigurationChanger(KEY);
+        ConfigurationChanger changer = new ConfigurationChanger((oldRoot, newRoot, revision) -> completedFuture(null));
+        changer.addRootKey(KEY);
         changer.register(storage);
 
         changer.change(Collections.singletonMap(KEY, data)).get(1, SECONDS);
@@ -129,10 +130,12 @@ public class ConfigurationChangerTest {
                 .create("b", init -> init.initStrCfg("2"))
             );
 
-        final ConfigurationChanger changer1 = new ConfigurationChanger(KEY);
+        ConfigurationChanger changer1 = new ConfigurationChanger((oldRoot, newRoot, revision) -> completedFuture(null));
+        changer1.addRootKey(KEY);
         changer1.register(storage);
 
-        final ConfigurationChanger changer2 = new ConfigurationChanger(KEY);
+        ConfigurationChanger changer2 = new ConfigurationChanger((oldRoot, newRoot, revision) -> completedFuture(null));
+        changer2.addRootKey(KEY);
         changer2.register(storage);
 
         changer1.change(Collections.singletonMap(KEY, data1)).get(1, SECONDS);
@@ -171,10 +174,12 @@ public class ConfigurationChangerTest {
                 .create("b", init -> init.initStrCfg("2"))
             );
 
-        final ConfigurationChanger changer1 = new ConfigurationChanger(KEY);
+        ConfigurationChanger changer1 = new ConfigurationChanger((oldRoot, newRoot, revision) -> completedFuture(null));
+        changer1.addRootKey(KEY);
         changer1.register(storage);
 
-        final ConfigurationChanger changer2 = new ConfigurationChanger(KEY);
+        ConfigurationChanger changer2 = new ConfigurationChanger((oldRoot, newRoot, revision) -> completedFuture(null));
+        changer2.addRootKey(KEY);
         changer2.register(storage);
 
         changer1.change(Collections.singletonMap(KEY, data1)).get(1, SECONDS);
@@ -203,7 +208,8 @@ public class ConfigurationChangerTest {
 
         ANode data = new ANode().initChild(child -> child.initIntCfg(1));
 
-        final ConfigurationChanger changer = new ConfigurationChanger(KEY);
+        ConfigurationChanger changer = new ConfigurationChanger((oldRoot, newRoot, revision) -> completedFuture(null));
+        changer.addRootKey(KEY);
 
         storage.fail(true);
 
@@ -258,7 +264,7 @@ public class ConfigurationChangerTest {
 
     @Test
     public void defaultsOnInit() throws Exception {
-        var changer = new ConfigurationChanger();
+        var changer = new ConfigurationChanger((oldRoot, newRoot, revision) -> completedFuture(null));
 
         changer.addRootKey(DefaultsConfiguration.KEY);
 
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/util/ConfigurationUtilTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/util/ConfigurationUtilTest.java
index 34f7f68..6e8061d 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/util/ConfigurationUtilTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/util/ConfigurationUtilTest.java
@@ -21,10 +21,14 @@ import java.util.List;
 import java.util.Map;
 import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
+import org.apache.ignite.configuration.annotation.ConfigurationRoot;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
+import org.apache.ignite.configuration.internal.SuperRoot;
+import org.apache.ignite.configuration.storage.TestConfigurationStorage;
 import org.junit.jupiter.api.Test;
 
+import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonMap;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -72,7 +76,7 @@ public class ConfigurationUtilTest {
     }
 
     /** */
-    @Config
+    @ConfigurationRoot(rootName = "root", storage = TestConfigurationStorage.class)
     public static class ParentConfigurationSchema {
         /** */
         @NamedConfigValue
@@ -235,6 +239,56 @@ public class ConfigurationUtilTest {
 
     /** */
     @Test
+    public void nodeToFlatMap() {
+        var parentNode = new ParentNode();
+
+        var parentSuperRoot = new SuperRoot(emptyMap(), Map.of(
+            ParentConfiguration.KEY,
+            parentNode
+        ));
+
+        assertEquals(
+            emptyMap(),
+            ConfigurationUtil.nodeToFlatMap(null, parentSuperRoot)
+        );
+
+        // No defaults in this test so everything must be initialized explicitly.
+        parentNode.changeElements(elements ->
+            elements.create("name", element ->
+                element.initChild(child ->
+                    child.initStr("foo")
+                )
+            )
+        );
+
+        assertEquals(
+            singletonMap("root.elements.name.child.str", "foo"),
+            ConfigurationUtil.nodeToFlatMap(null, parentSuperRoot)
+        );
+
+        assertEquals(
+            emptyMap(),
+            ConfigurationUtil.nodeToFlatMap(parentSuperRoot, new SuperRoot(emptyMap(), singletonMap(
+                ParentConfiguration.KEY,
+                new ParentNode().changeElements(elements ->
+                    elements.delete("void")
+                )
+            )))
+        );
+
+        assertEquals(
+            singletonMap("root.elements.name.child.str", null),
+            ConfigurationUtil.nodeToFlatMap(parentSuperRoot, new SuperRoot(emptyMap(), singletonMap(
+                ParentConfiguration.KEY,
+                new ParentNode().changeElements(elements ->
+                    elements.delete("name")
+                )
+            )))
+        );
+    }
+
+    /** */
+    @Test
     public void patch() {
         var originalRoot = new ParentNode().initElements(elements ->
             elements.create("name1", element ->
@@ -285,4 +339,30 @@ public class ConfigurationUtilTest {
         assertNull(shrinkedRoot.elements().get("name1"));
         assertNotNull(shrinkedRoot.elements().get("name2"));
     }
+
+    /** */
+    @Test
+    public void cleanupMatchingValues() {
+        var curParent = new ParentNode().initElements(elements -> elements
+            .create("missing", element -> {})
+            .create("match", element -> element.initChild(child -> child.initStr("match")))
+            .create("mismatch", element -> element.initChild(child -> child.initStr("foo")))
+        );
+
+        var newParent = new ParentNode().initElements(elements -> elements
+            .create("extra", element -> {})
+            .create("match", element -> element.initChild(child -> child.initStr("match")))
+            .create("mismatch", element -> element.initChild(child -> child.initStr("bar")))
+        );
+
+        ConfigurationUtil.cleanupMatchingValues(curParent, newParent);
+
+        // Old node stayed intact.
+        assertEquals("match", curParent.elements().get("match").child().str());
+        assertEquals("foo", curParent.elements().get("mismatch").child().str());
+
+        // New node was modified.
+        assertNull(newParent.elements().get("match").child().str());
+        assertEquals("bar", newParent.elements().get("mismatch").child().str());
+    }
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/validation/ValidationUtilTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/validation/ValidationUtilTest.java
index bf00949..a289bde 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/validation/ValidationUtilTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/validation/ValidationUtilTest.java
@@ -31,7 +31,7 @@ import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
 import org.apache.ignite.configuration.internal.SuperRoot;
 import org.apache.ignite.configuration.internal.util.ConfigurationUtil;
-import org.apache.ignite.configuration.sample.storage.TestConfigurationStorage;
+import org.apache.ignite.configuration.storage.TestConfigurationStorage;
 import org.apache.ignite.configuration.tree.NamedListView;
 import org.apache.ignite.configuration.validation.ValidationContext;
 import org.apache.ignite.configuration.validation.ValidationIssue;
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/notifications/ConfigurationListenerTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/notifications/ConfigurationListenerTest.java
new file mode 100644
index 0000000..8fa9320
--- /dev/null
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/notifications/ConfigurationListenerTest.java
@@ -0,0 +1,245 @@
+/*
+ * 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.notifications;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.configuration.ConfigurationRegistry;
+import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.ConfigValue;
+import org.apache.ignite.configuration.annotation.ConfigurationRoot;
+import org.apache.ignite.configuration.annotation.NamedConfigValue;
+import org.apache.ignite.configuration.annotation.Value;
+import org.apache.ignite.configuration.storage.TestConfigurationStorage;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/** */
+public class ConfigurationListenerTest {
+    /** */
+    @ConfigurationRoot(rootName = "parent", storage = TestConfigurationStorage.class)
+    public static class ParentConfigurationSchema {
+        /** */
+        @ConfigValue
+        private ChildConfigurationSchema child;
+
+        /** */
+        @NamedConfigValue
+        private ChildConfigurationSchema elements;
+    }
+
+    /** */
+    @Config
+    public static class ChildConfigurationSchema {
+        /** */
+        @Value(hasDefault = true)
+        public String str = "default";
+    }
+
+    /** */
+    private final ConfigurationRegistry registry = new ConfigurationRegistry();
+
+    /** */
+    private ParentConfiguration configuration;
+
+    /** */
+    @BeforeEach
+    public void before() {
+        registry.registerRootKey(ParentConfiguration.KEY);
+
+        registry.registerStorage(new TestConfigurationStorage());
+
+        registry.startStorageConfigurations(TestConfigurationStorage.class);
+
+        configuration = registry.getConfiguration(ParentConfiguration.KEY);
+    }
+
+    /** */
+    @AfterEach
+    public void after() {
+        registry.stop();
+    }
+
+    /** */
+    @Test
+    public void childNode() throws Exception {
+        List<String> log = new ArrayList<>();
+
+        configuration.listen(ctx -> {
+            assertEquals(ctx.oldValue().child().str(), "default");
+            assertEquals(ctx.newValue().child().str(), "foo");
+
+            log.add("parent");
+
+            return completedFuture(null);
+        });
+
+        configuration.child().listen(ctx -> {
+            assertEquals(ctx.oldValue().str(), "default");
+            assertEquals(ctx.newValue().str(), "foo");
+
+            log.add("child");
+
+            return completedFuture(null);
+        });
+
+        configuration.child().str().listen(ctx -> {
+            assertEquals(ctx.oldValue(), "default");
+            assertEquals(ctx.newValue(), "foo");
+
+            log.add("str");
+
+            return completedFuture(null);
+        });
+
+        configuration.elements().listen(ctx -> {
+            log.add("elements");
+
+            return completedFuture(null);
+        });
+
+        configuration.change(parent -> parent.changeChild(child -> child.changeStr("foo"))).get(1, SECONDS);
+
+        assertEquals(List.of("parent", "child", "str"), log);
+    }
+
+    /** */
+    @Test
+    public void namedListNode() throws Exception {
+        List<String> log = new ArrayList<>();
+
+        configuration.listen(ctx -> {
+            log.add("parent");
+
+            return completedFuture(null);
+        });
+
+        configuration.child().listen(ctx -> {
+            log.add("child");
+
+            return completedFuture(null);
+        });
+
+        configuration.elements().listen(ctx -> {
+            if (ctx.oldValue().size() == 0) {
+                ChildView newValue = ctx.newValue().get("name");
+
+                assertNotNull(newValue);
+                assertEquals("default", newValue.str());
+            }
+            else if (ctx.newValue().size() == 0) {
+                ChildView oldValue = ctx.oldValue().get("name");
+
+                assertNotNull(oldValue);
+                assertEquals("foo", oldValue.str());
+            }
+            else {
+                ChildView oldValue = ctx.oldValue().get("name");
+
+                assertNotNull(oldValue);
+                assertEquals("default", oldValue.str());
+
+                ChildView newValue = ctx.newValue().get("name");
+
+                assertNotNull(newValue);
+                assertEquals("foo", newValue.str());
+            }
+
+            log.add("elements");
+
+            return completedFuture(null);
+        });
+
+        configuration.elements().listen(new ConfigurationNamedListListener<ChildView>() {
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onCreate(ConfigurationNotificationEvent<ChildView> ctx) {
+                assertNull(ctx.oldValue());
+
+                ChildView newValue = ctx.newValue();
+
+                assertNotNull(newValue);
+                assertEquals("default", newValue.str());
+
+                log.add("create");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onUpdate(ConfigurationNotificationEvent<ChildView> ctx) {
+                ChildView oldValue = ctx.oldValue();
+
+                assertNotNull(oldValue);
+                assertEquals("default", oldValue.str());
+
+                ChildView newValue = ctx.newValue();
+
+                assertNotNull(newValue);
+                assertEquals("foo", newValue.str());
+
+                log.add("update");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onDelete(ConfigurationNotificationEvent<ChildView> ctx) {
+                assertNull(ctx.newValue());
+
+                ChildView oldValue = ctx.oldValue();
+
+                assertNotNull(oldValue);
+                assertEquals("foo", oldValue.str());
+
+                log.add("delete");
+
+                return completedFuture(null);
+            }
+        });
+
+        configuration.change(parent ->
+            parent.changeElements(elements -> elements.create("name", element -> {}))
+        ).get(1, SECONDS);
+
+        assertEquals(List.of("parent", "elements", "create"), log);
+
+        log.clear();
+
+        configuration.change(parent ->
+            parent.changeElements(elements -> elements.update("name", element -> element.changeStr("foo")))
+        ).get(1, SECONDS);
+
+        assertEquals(List.of("parent", "elements", "update"), log);
+
+        log.clear();
+
+        configuration.change(parent ->
+            parent.changeElements(elements -> elements.delete("name"))
+        ).get(1, SECONDS);
+
+        assertEquals(List.of("parent", "elements", "delete"), log);
+    }
+}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/LocalConfigurationSchema.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/LocalConfigurationSchema.java
index 8f091fb..fe7c2d7 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/LocalConfigurationSchema.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/LocalConfigurationSchema.java
@@ -19,7 +19,7 @@ package org.apache.ignite.configuration.sample;
 
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
-import org.apache.ignite.configuration.sample.storage.TestConfigurationStorage;
+import org.apache.ignite.configuration.storage.TestConfigurationStorage;
 
 /**
  * Test local configuration schema.
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/NetworkConfigurationSchema.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/NetworkConfigurationSchema.java
index bcb104d..d9a8101 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/NetworkConfigurationSchema.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/NetworkConfigurationSchema.java
@@ -19,7 +19,7 @@ package org.apache.ignite.configuration.sample;
 
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
-import org.apache.ignite.configuration.sample.storage.TestConfigurationStorage;
+import org.apache.ignite.configuration.storage.TestConfigurationStorage;
 
 /**
  * Test network configuration schema.
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/UsageTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/UsageTest.java
index 4eb513c..41f14d9 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/UsageTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/UsageTest.java
@@ -18,7 +18,8 @@
 package org.apache.ignite.configuration.sample;
 
 import org.apache.ignite.configuration.ConfigurationRegistry;
-import org.apache.ignite.configuration.sample.storage.TestConfigurationStorage;
+import org.apache.ignite.configuration.storage.TestConfigurationStorage;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -31,13 +32,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  * Simple usage test of generated configuration schema.
  */
 public class UsageTest {
+    /** */
+    private final ConfigurationRegistry registry = new ConfigurationRegistry();
+
+    /** */
+    @AfterEach
+    public void after() {
+        registry.stop();
+    }
+
     /**
      * Test creation of configuration and calling configuration API methods.
      */
     @Test
     public void test() throws Exception {
-        var registry = new ConfigurationRegistry();
-
         registry.registerRootKey(LocalConfiguration.KEY);
 
         registry.registerStorage(new TestConfigurationStorage());
@@ -92,8 +100,6 @@ public class UsageTest {
 
         long autoAdjustTimeout = 30_000L;
 
-        ConfigurationRegistry registry = new ConfigurationRegistry();
-
         registry.registerRootKey(NetworkConfiguration.KEY);
         registry.registerRootKey(LocalConfiguration.KEY);
 
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/TestConfigurationStorage.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/storage/TestConfigurationStorage.java
similarity index 80%
rename from modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/TestConfigurationStorage.java
rename to modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/storage/TestConfigurationStorage.java
index 5c2d405..f2c2982 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/TestConfigurationStorage.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/storage/TestConfigurationStorage.java
@@ -14,21 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.ignite.configuration.sample.storage;
+package org.apache.ignite.configuration.storage;
 
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicLong;
-import org.apache.ignite.configuration.storage.ConfigurationStorage;
-import org.apache.ignite.configuration.storage.ConfigurationStorageListener;
-import org.apache.ignite.configuration.storage.Data;
-import org.apache.ignite.configuration.storage.StorageException;
 
 /**
  * Test configuration storage.
@@ -59,11 +54,11 @@ public class TestConfigurationStorage implements ConfigurationStorage {
         if (fail)
             throw new StorageException("Failed to read data");
 
-        return new Data(new HashMap<>(map), version.get());
+        return new Data(new HashMap<>(map), version.get(), 0);
     }
 
     /** {@inheritDoc} */
-    @Override public synchronized CompletableFuture<Boolean> write(Map<String, Serializable> newValues, long sentVersion) throws StorageException {
+    @Override public synchronized CompletableFuture<Boolean> write(Map<String, Serializable> newValues, long sentVersion) {
         if (fail)
             return CompletableFuture.failedFuture(new StorageException("Failed to write data"));
 
@@ -79,20 +74,12 @@ public class TestConfigurationStorage implements ConfigurationStorage {
 
         version.incrementAndGet();
 
-        listeners.forEach(listener -> listener.onEntriesChanged(new Data(newValues, version.get())));
+        listeners.forEach(listener -> listener.onEntriesChanged(new Data(newValues, version.get(), 0)));
 
         return CompletableFuture.completedFuture(true);
     }
 
     /** {@inheritDoc} */
-    @Override public synchronized Set<String> keys() throws StorageException {
-        if (fail)
-            throw new StorageException("Failed to get keys");
-
-        return map.keySet();
-    }
-
-    /** {@inheritDoc} */
     @Override public void addListener(ConfigurationStorageListener listener) {
         listeners.add(listener);
     }
@@ -101,4 +88,8 @@ public class TestConfigurationStorage implements ConfigurationStorage {
     @Override public void removeListener(ConfigurationStorageListener listener) {
         listeners.remove(listener);
     }
+
+    /** {@inheritDoc} */
+    @Override public void notifyApplied(long storageRevision) {
+    }
 }
diff --git a/modules/configuration/pom.xml b/modules/configuration/pom.xml
index 0da06b4..0b5484c 100644
--- a/modules/configuration/pom.xml
+++ b/modules/configuration/pom.xml
@@ -44,18 +44,5 @@
             <groupId>org.jetbrains</groupId>
             <artifactId>annotations</artifactId>
         </dependency>
-
-        <!-- Test dependencies. -->
-        <dependency>
-            <groupId>log4j</groupId>
-            <artifactId>log4j</artifactId>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <scope>test</scope>
-        </dependency>
     </dependencies>
 </project>
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
index 0773fe1..8d92cc4 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
@@ -40,8 +40,11 @@ import org.apache.ignite.configuration.tree.InnerNode;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
 import org.apache.ignite.configuration.validation.ValidationIssue;
 import org.apache.ignite.configuration.validation.Validator;
+import org.jetbrains.annotations.NotNull;
 
+import static java.util.concurrent.CompletableFuture.completedFuture;
 import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.addDefaults;
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.cleanupMatchingValues;
 import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.fillFromPrefixMap;
 import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.nodeToFlatMap;
 import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.patch;
@@ -50,7 +53,7 @@ import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.to
 /**
  * Class that handles configuration changes, by validating them, passing to storage and listening to storage updates.
  */
-public class ConfigurationChanger {
+public final class ConfigurationChanger {
     /** */
     private final ForkJoinPool pool = new ForkJoinPool(2);
 
@@ -64,6 +67,26 @@ public class ConfigurationChanger {
     private Map<Class<? extends Annotation>, Set<Validator<?, ?>>> validators = new HashMap<>();
 
     /**
+     * Closure interface to be used by the configuration changer. An instance of this closure is passed into the constructor and
+     * invoked every time when there's an update from any of the storages.
+     */
+    @FunctionalInterface
+    public interface Notificator {
+        /**
+         * Invoked every time when the configuration is updated.
+         * @param oldRoot Old roots values. All these roots always belong to a single storage.
+         * @param newRoot New values for the same roots as in {@code oldRoot}.
+         * @param storageRevision Revision of the storage.
+         * @return Not-null future that must signify when processing is completed. Exceptional completion is not
+         *      expected.
+         */
+        @NotNull CompletableFuture<Void> notify(SuperRoot oldRoot, SuperRoot newRoot, long storageRevision);
+    }
+
+    /** Closure to execute when an update from the storage is received. */
+    private final Notificator notificator;
+
+    /**
      * Immutable data container to store version and all roots associated with the specific storage.
      */
     private static class StorageRoots {
@@ -86,10 +109,11 @@ public class ConfigurationChanger {
     /** Storage instances by their classes. Comes in handy when all you have is {@link RootKey}. */
     private final Map<Class<? extends ConfigurationStorage>, ConfigurationStorage> storageInstances = new HashMap<>();
 
-    /** Constructor. */
-    public ConfigurationChanger(RootKey<?, ?>... rootKeys) {
-        for (RootKey<?, ?> rootKey : rootKeys)
-            this.rootKeys.put(rootKey.key(), rootKey);
+    /**
+     * @param notificator Closure to execute when update from the storage is received.
+     */
+    public ConfigurationChanger(Notificator notificator) {
+        this.notificator = notificator;
     }
 
     /** */
@@ -141,7 +165,7 @@ public class ConfigurationChanger {
             superRoot.addRoot(rootKey, rootNode);
         }
 
-        StorageRoots storageRoots = new StorageRoots(superRoot, data.version());
+        StorageRoots storageRoots = new StorageRoots(superRoot, data.cfgVersion());
 
         storagesRootsMap.put(configurationStorage.getClass(), storageRoots);
 
@@ -176,7 +200,7 @@ public class ConfigurationChanger {
             throw new ConfigurationValidationException(validationIssues);
 
         try {
-            change(defaultsNode, storageType).get();
+            changeInternally(defaultsNode, storageInstances.get(storageType)).get();
         }
         catch (InterruptedException | ExecutionException e) {
             throw new ConfigurationChangeException(
@@ -197,7 +221,12 @@ public class ConfigurationChanger {
 
         source.descend(superRoot);
 
-        return change(superRoot, storage.getClass());
+        return changeInternally(superRoot, storage);
+    }
+
+    /** Stop component. */
+    public void stop() {
+        pool.shutdownNow();
     }
 
     /**
@@ -215,7 +244,7 @@ public class ConfigurationChanger {
      */
     public CompletableFuture<Void> change(Map<RootKey<?, ?>, InnerNode> changes) {
         if (changes.isEmpty())
-            return CompletableFuture.completedFuture(null);
+            return completedFuture(null);
 
         Set<Class<? extends ConfigurationStorage>> storagesTypes = changes.keySet().stream()
             .map(RootKey::getStorageType)
@@ -229,7 +258,7 @@ public class ConfigurationChanger {
             );
         }
 
-        return change(new SuperRoot(rootKeys, changes), storagesTypes.iterator().next());
+        return changeInternally(new SuperRoot(rootKeys, changes), storageInstances.get(storagesTypes.iterator().next()));
     }
 
     /** */
@@ -242,80 +271,78 @@ public class ConfigurationChanger {
         return mergedSuperRoot;
     }
 
-    /** */
-    private CompletableFuture<Void> change(SuperRoot changes, Class<? extends ConfigurationStorage> storageType) {
-        ConfigurationStorage storage = storageInstances.get(storageType);
-
-        CompletableFuture<Void> fut = new CompletableFuture<>();
-
-        pool.execute(() -> change0(changes, storage, fut));
-
-        return fut;
-    }
-
     /**
      * Internal configuration change method that completes provided future.
      * @param changes Map of changes by root key.
      * @param storage Storage instance.
-     * @param fut Future, that must be completed after changes are written to the storage.
+     * @return fut Future that will be completed after changes are written to the storage.
      */
-    private void change0(
+    private CompletableFuture<Void> changeInternally(
         SuperRoot changes,
-        ConfigurationStorage storage,
-        CompletableFuture<?> fut
+        ConfigurationStorage storage
     ) {
         StorageRoots storageRoots = storagesRootsMap.get(storage.getClass());
-        SuperRoot curRoots = storageRoots.roots;
-
-        Map<String, Serializable> allChanges = new HashMap<>();
-
-        //TODO IGNITE-14180 single putAll + remove matching value, this way "allChanges" will be fair.
-        // These are changes explicitly provided by the client.
-        allChanges.putAll(nodeToFlatMap(curRoots, changes));
-
-        // It is necessary to reinitialize default values every time.
-        // Possible use case that explicitly requires it: creation of the same named list entry with slightly
-        // different set of values and different dynamic defaults at the same time.
-        SuperRoot patchedSuperRoot = patch(curRoots, changes);
 
-        SuperRoot defaultsNode = new SuperRoot(rootKeys);
-
-        addDefaults(patchedSuperRoot, defaultsNode);
-
-        // These are default values for non-initialized values, required to complete the configuration.
-        allChanges.putAll(nodeToFlatMap(patchedSuperRoot, defaultsNode));
-
-        // Unlikely but still possible.
-        if (allChanges.isEmpty()) {
-            fut.complete(null);
-
-            return;
-        }
-
-        List<ValidationIssue> validationIssues = ValidationUtil.validate(
-            storageRoots.roots,
-            patch(patchedSuperRoot, defaultsNode),
-            this::getRootNode,
-            cachedAnnotations,
-            validators
-        );
-
-        if (!validationIssues.isEmpty()) {
-            fut.completeExceptionally(new ConfigurationValidationException(validationIssues));
-
-            return;
-        }
-
-        CompletableFuture<Boolean> writeFut = storage.write(allChanges, storageRoots.version);
-
-        writeFut.whenCompleteAsync((casResult, throwable) -> {
-            if (throwable != null)
-                fut.completeExceptionally(new ConfigurationChangeException("Failed to change configuration", throwable));
-            else if (casResult)
-                fut.complete(null);
-            else
-                change0(changes, storage, fut);
-        }, pool);
+        return CompletableFuture
+            .supplyAsync(() -> {
+                SuperRoot curRoots = storageRoots.roots;
+
+                // It is necessary to reinitialize default values every time.
+                // Possible use case that explicitly requires it: creation of the same named list entry with slightly
+                // different set of values and different dynamic defaults at the same time.
+                SuperRoot patchedSuperRoot = patch(curRoots, changes);
+
+                SuperRoot defaultsNode = new SuperRoot(rootKeys);
+
+                addDefaults(patchedSuperRoot, defaultsNode);
+
+                SuperRoot patchedChanges = patch(changes, defaultsNode);
+
+                cleanupMatchingValues(curRoots, changes);
+
+                Map<String, Serializable> allChanges = nodeToFlatMap(curRoots, patchedChanges);
+
+                // Unlikely but still possible.
+                if (allChanges.isEmpty())
+                    return null;
+
+                List<ValidationIssue> validationIssues = ValidationUtil.validate(
+                    storageRoots.roots,
+                    patch(patchedSuperRoot, defaultsNode),
+                    this::getRootNode,
+                    cachedAnnotations,
+                    validators
+                );
+
+                if (!validationIssues.isEmpty())
+                    throw new ConfigurationValidationException(validationIssues);
+
+                return allChanges;
+            }, pool)
+            .thenCompose(allChanges -> {
+                if (allChanges == null)
+                    return CompletableFuture.completedFuture(true);
+                return storage.write(allChanges, storageRoots.version)
+                    .exceptionally(throwable -> {
+                        throw new ConfigurationChangeException("Failed to change configuration", throwable);
+                    });
+            })
+            .thenCompose(casResult -> {
+                if (casResult)
+                    return CompletableFuture.completedFuture(null);
+                else {
+                    try {
+                        // Is this ok to have a busy wait on concurrent configuration updates?
+                        // Maybe we'll fix it while implementing metastorage storage implementation.
+                        Thread.sleep(10);
+                    }
+                    catch (InterruptedException e) {
+                        return CompletableFuture.failedFuture(e);
+                    }
+
+                    return changeInternally(changes, storage);
+                }
+            });
     }
 
     /**
@@ -333,15 +360,25 @@ public class ConfigurationChanger {
 
         compressDeletedEntries(dataValuesPrefixMap);
 
-        SuperRoot newSuperRoot = oldStorageRoots.roots.copy();
+        SuperRoot oldSuperRoot = oldStorageRoots.roots;
+        SuperRoot newSuperRoot = oldSuperRoot.copy();
 
         fillFromPrefixMap(newSuperRoot, dataValuesPrefixMap);
 
-        StorageRoots storageRoots = new StorageRoots(newSuperRoot, changedEntries.version());
+        StorageRoots newStorageRoots = new StorageRoots(newSuperRoot, changedEntries.cfgVersion());
+
+        storagesRootsMap.put(storageType, newStorageRoots);
+
+        ConfigurationStorage storage = storageInstances.get(storageType);
 
-        storagesRootsMap.put(storageType, storageRoots);
+        long storageRevision = changedEntries.storageRevision();
 
-        //TODO IGNITE-14180 Notify listeners.
+        // This will also be updated during the metastorage integration.
+        notificator.notify(
+            oldSuperRoot,
+            newSuperRoot,
+            storageRevision
+        ).whenCompleteAsync((res, throwable) -> storage.notifyApplied(storageRevision), pool);
     }
 
     /**
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationProperty.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationProperty.java
index e61b720..f1a6729 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationProperty.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationProperty.java
@@ -17,12 +17,14 @@
 
 package org.apache.ignite.configuration;
 
+import org.apache.ignite.configuration.notifications.ConfigurationListener;
+
 /**
  * Base interface for configuration.
- * @param <VALUE> Type of the value.
+ * @param <VIEW> Type of the value.
  * @param <CHANGE> Type of the object that changes the value of configuration.
  */
-public interface ConfigurationProperty<VALUE, CHANGE> {
+public interface ConfigurationProperty<VIEW, CHANGE> {
     /**
      * Get key of this node.
      * @return Key.
@@ -33,5 +35,11 @@ public interface ConfigurationProperty<VALUE, CHANGE> {
      * Get value of this property.
      * @return Value of this property.
      */
-    VALUE value();
+    VIEW value();
+
+    /**
+     * Add configuration values listener.
+     * @param listener Listener.
+     */
+    void listen(ConfigurationListener<VIEW> listener);
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationRegistry.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationRegistry.java
index b7931de..4087013 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationRegistry.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationRegistry.java
@@ -19,14 +19,17 @@ package org.apache.ignite.configuration;
 
 import java.io.Serializable;
 import java.lang.annotation.Annotation;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.BiFunction;
+import java.util.function.Function;
 import java.util.function.Supplier;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
 import org.apache.ignite.configuration.internal.DynamicConfiguration;
 import org.apache.ignite.configuration.internal.RootKeyImpl;
@@ -42,13 +45,19 @@ import org.apache.ignite.configuration.tree.InnerNode;
 import org.apache.ignite.configuration.tree.TraversableTreeNode;
 import org.apache.ignite.configuration.validation.Validator;
 
+import static org.apache.ignite.configuration.internal.util.ConfigurationNotificationsUtil.notifyListeners;
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.innerNodeVisitor;
+
 /** */
 public class ConfigurationRegistry {
     /** */
+    private static final System.Logger logger = System.getLogger(ConfigurationRegistry.class.getName());
+
+    /** */
     private final Map<String, DynamicConfiguration<?, ?, ?>> configs = new HashMap<>();
 
     /** */
-    private final ConfigurationChanger changer = new ConfigurationChanger();
+    private final ConfigurationChanger changer = new ConfigurationChanger(this::notificator);
 
     {
         // Default vaildators implemented in current module.
@@ -61,8 +70,6 @@ public class ConfigurationRegistry {
         changer.addRootKey(rootKey);
 
         configs.put(rootKey.key(), (DynamicConfiguration<?, ?, ?>)rootKey.createPublicRoot(changer));
-
-        //TODO IGNITE-14180 link these two entities.
     }
 
     /** */
@@ -76,6 +83,11 @@ public class ConfigurationRegistry {
     }
 
     /** */
+    public void startStorageConfigurations(Class<? extends ConfigurationStorage> storageType) {
+        changer.initialize(storageType);
+    }
+
+    /** */
     public <V, C, T extends ConfigurationTree<V, C>> T getConfiguration(RootKey<T, V> rootKey) {
         return (T)configs.get(rootKey.key());
     }
@@ -113,6 +125,43 @@ public class ConfigurationRegistry {
         return changer.changeX(path, changesSource, storage);
     }
 
+    /** */
+    public void stop() {
+        changer.stop();
+    }
+
+    /** */
+    private @NotNull CompletableFuture<Void> notificator(SuperRoot oldSuperRoot, SuperRoot newSuperRoot, long storageRevision) {
+        List<CompletableFuture<?>> futures = new ArrayList<>();
+
+        newSuperRoot.traverseChildren(new ConfigurationVisitor<Void>() {
+            @Override public Void visitInnerNode(String key, InnerNode newRoot) {
+                InnerNode oldRoot = oldSuperRoot.traverseChild(key, innerNodeVisitor());
+
+                var cfg = (DynamicConfiguration<InnerNode, ?, ?>)configs.get(key);
+
+                assert oldRoot != null && cfg != null : key;
+
+                if (oldRoot != newRoot)
+                    notifyListeners(oldRoot, newRoot, cfg, storageRevision, futures);
+
+                return null;
+            }
+        });
+
+        // Map futures into a "suppressed" future that won't throw any exceptions on completion.
+        Function<CompletableFuture<?>, CompletableFuture<?>> mapping = fut -> fut.handle((res, throwable) -> {
+            if (throwable != null)
+                logger.log(System.Logger.Level.ERROR, "Failed to notify configuration listener.", throwable);
+
+            return res;
+        });
+
+        CompletableFuture[] resultFutures = futures.stream().map(mapping).toArray(CompletableFuture[]::new);
+
+        return CompletableFuture.allOf(resultFutures);
+    }
+
     /**
      * Method to instantiate a new {@link RootKey} for your configuration root. Invoked in generated code only.
      * Does not register this root anywhere, used for static object initialization only.
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationTree.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationTree.java
index 28e84de..bdab694 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationTree.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationTree.java
@@ -24,10 +24,10 @@ import org.apache.ignite.configuration.validation.ConfigurationValidationExcepti
 
 /**
  * Configuration tree with configuration values and other configuration trees as child nodes.
- * @param <VALUE> Value type of the node.
+ * @param <VIEW> Value type of the node.
  * @param <CHANGE> Type of the object that changes this node's value.
  */
-public interface ConfigurationTree<VALUE, CHANGE> extends ConfigurationProperty<VALUE, CHANGE> {
+public interface ConfigurationTree<VIEW, CHANGE> extends ConfigurationProperty<VIEW, CHANGE> {
     /** Children of the tree. */
     Map<String, ConfigurationProperty<?, ?>> members();
 
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationValue.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationValue.java
index 4739521..a9e0eec 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationValue.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationValue.java
@@ -22,9 +22,9 @@ import org.apache.ignite.configuration.validation.ConfigurationValidationExcepti
 
 /**
  * Configuration value.
- * @param <VALUE> Type of the value.
+ * @param <VIEW> Type of the value.
  */
-public interface ConfigurationValue<VALUE> extends ConfigurationProperty<VALUE, VALUE> {
+public interface ConfigurationValue<VIEW> extends ConfigurationProperty<VIEW, VIEW> {
 
     /**
      * Update this configuration node value.
@@ -33,5 +33,5 @@ public interface ConfigurationValue<VALUE> extends ConfigurationProperty<VALUE,
      * @returns Future that signifies end of the update operation. Can also be completed with
      *      {@link ConfigurationValidationException} and {@link ConfigurationChangeException}.
      */
-    Future<Void> update(VALUE change) throws ConfigurationValidationException;
+    Future<Void> update(VIEW change) throws ConfigurationValidationException;
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/NamedConfigurationTree.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/NamedConfigurationTree.java
index e4f096c..eeac54b 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/NamedConfigurationTree.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/NamedConfigurationTree.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.configuration;
 
+import org.apache.ignite.configuration.notifications.ConfigurationNamedListListener;
 import org.apache.ignite.configuration.tree.NamedListChange;
 import org.apache.ignite.configuration.tree.NamedListView;
 
@@ -24,7 +25,7 @@ import org.apache.ignite.configuration.tree.NamedListView;
  * Configuration tree representing arbitrary set of named underlying configuration tree of the same type.
  *
  * @param <T> Type of the underlying configuration tree.
- * @param <VALUE> Value type of the underlying node.
+ * @param <VIEW> Value type of the underlying node.
  * @param <CHANGE> Type of the object that changes underlying nodes values.
  */
 public interface NamedConfigurationTree<T extends ConfigurationProperty<VIEW, CHANGE>, VIEW, CHANGE, INIT>
@@ -36,4 +37,10 @@ public interface NamedConfigurationTree<T extends ConfigurationProperty<VIEW, CH
      * @return Configuration.
      */
     T get(String name);
+
+    /**
+     * Add named-list-specific configuration values listener.
+     * @param listener Listener.
+     */
+    void listen(ConfigurationNamedListListener<VIEW> listener);
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/PropertyListener.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/PropertyListener.java
deleted file mode 100644
index 52f3522..0000000
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/PropertyListener.java
+++ /dev/null
@@ -1,61 +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.ignite.configuration;
-
-import java.io.Serializable;
-
-/**
- * Configuration property change listener.
- *
- * @param <VIEW> VIEW type of property.
- * @param <CHANGE> CHANGE type of property.
- */
-public interface PropertyListener<VIEW extends Serializable, CHANGE extends Serializable> {
-    /**
-     * Called before property value is updated.
-     *
-     * @param oldValue Previous value.
-     * @param newValue New value.
-     * @param modifier Property itself.
-     * @return {@code true} if changes are approved and {@code false} then property update must be aborted.
-     */
-    default boolean beforeUpdate(VIEW oldValue, VIEW newValue, ConfigurationProperty<VIEW, CHANGE> modifier) {
-        return true;
-    }
-
-    /**
-     * Called on property value update.
-     *
-     * @param newValue New value of the property.
-     * @param modifier Property itself.
-     */
-    default void update(VIEW newValue, ConfigurationProperty<VIEW, CHANGE> modifier) {
-        /* No-op */
-    }
-
-    /**
-     * Called after property value update.
-     *
-     * @param newValue New value of the property.
-     * @param modifier Property itself.
-     */
-    default void afterUpdate(VIEW newValue, ConfigurationProperty<VIEW, CHANGE> modifier) {
-        /* No-op */
-    }
-
-}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
index 67f875c..328ca10 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
@@ -17,19 +17,26 @@
 
 package org.apache.ignite.configuration.internal;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
 import org.apache.ignite.configuration.ConfigurationChanger;
+import org.apache.ignite.configuration.ConfigurationProperty;
 import org.apache.ignite.configuration.RootKey;
 import org.apache.ignite.configuration.internal.util.ConfigurationUtil;
 import org.apache.ignite.configuration.internal.util.KeyNotFoundException;
+import org.apache.ignite.configuration.notifications.ConfigurationListener;
 import org.apache.ignite.configuration.tree.TraversableTreeNode;
 
 /**
  * Super class for dynamic configuration tree nodes. Has all common data and value retrieving algorithm in it.
  */
-public abstract class ConfigurationNode<VIEW> {
+abstract class ConfigurationNode<VIEW, CHANGE> implements ConfigurationProperty<VIEW, CHANGE> {
+    /** Listeners of property update. */
+    protected final List<ConfigurationListener<VIEW>> updateListeners = new CopyOnWriteArrayList<>();
+
     /** Full path to the current node. */
     protected final List<String> keys;
 
@@ -73,6 +80,16 @@ public abstract class ConfigurationNode<VIEW> {
         assert Objects.equals(rootKey.key(), keys.get(0));
     }
 
+    /** {@inheritDoc} */
+    @Override public void listen(ConfigurationListener<VIEW> listener) {
+        updateListeners.add(listener);
+    }
+
+    /** @return List of update listeners. */
+    public List<ConfigurationListener<VIEW>> listeners() {
+        return Collections.unmodifiableList(updateListeners);
+    }
+
     /**
      * Returns latest value of the configuration or throws exception.
      *
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
index e2ca703..b2eb63f 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
@@ -37,10 +37,11 @@ import org.apache.ignite.configuration.validation.ConfigurationValidationExcepti
 /**
  * This class represents configuration root or node.
  */
-public abstract class DynamicConfiguration<VIEW, INIT, CHANGE> extends ConfigurationNode<VIEW> implements ConfigurationProperty<VIEW, CHANGE>, ConfigurationTree<VIEW, CHANGE> {
-
+public abstract class DynamicConfiguration<VIEW, INIT, CHANGE> extends ConfigurationNode<VIEW, CHANGE>
+    implements ConfigurationTree<VIEW, CHANGE>
+{
     /** Configuration members (leaves and nodes). */
-    protected final Map<String, ConfigurationProperty<?, ?>> members = new HashMap<>();
+    private final Map<String, ConfigurationProperty<?, ?>> members = new HashMap<>();
 
     /**
      * Constructor.
@@ -118,7 +119,7 @@ public abstract class DynamicConfiguration<VIEW, INIT, CHANGE> extends Configura
     }
 
     /** {@inheritDoc} */
-    @Override public final Map<String, ConfigurationProperty<?, ?>> members() {
+    @Override public Map<String, ConfigurationProperty<?, ?>> members() {
         return Collections.unmodifiableMap(members);
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
index d74913a..b163501 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
@@ -18,16 +18,13 @@
 package org.apache.ignite.configuration.internal;
 
 import java.io.Serializable;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.RandomAccess;
 import java.util.concurrent.Future;
 import org.apache.ignite.configuration.ConfigurationChanger;
-import org.apache.ignite.configuration.ConfigurationProperty;
 import org.apache.ignite.configuration.ConfigurationValue;
-import org.apache.ignite.configuration.PropertyListener;
 import org.apache.ignite.configuration.RootKey;
 import org.apache.ignite.configuration.tree.ConfigurationSource;
 import org.apache.ignite.configuration.tree.ConstructableTreeNode;
@@ -37,10 +34,7 @@ import org.apache.ignite.configuration.validation.ConfigurationValidationExcepti
 /**
  * Holder for property value. Expected to be used with numbers, strings and other immutable objects, e.g. IP addresses.
  */
-public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T> implements ConfigurationProperty<T, T>, ConfigurationValue<T> {
-    /** Listeners of property update. */
-    private final List<PropertyListener<T, T>> updateListeners = new ArrayList<>();
-
+public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T, T> implements ConfigurationValue<T> {
     /**
      * Constructor.
      * @param prefix Property prefix.
@@ -57,14 +51,6 @@ public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T
         super(prefix, key, rootKey, changer);
     }
 
-    /**
-     * Add change listener to this property.
-     * @param listener Property change listener.
-     */
-    public void addListener(PropertyListener<T, T> listener) {
-        updateListeners.add(listener);
-    }
-
     /** {@inheritDoc} */
     @Override public T value() {
         return refreshValue();
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
index 9c9e53a..8a51b73 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
@@ -17,14 +17,17 @@
 
 package org.apache.ignite.configuration.internal;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.BiFunction;
 import org.apache.ignite.configuration.ConfigurationChanger;
 import org.apache.ignite.configuration.ConfigurationProperty;
 import org.apache.ignite.configuration.NamedConfigurationTree;
 import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.notifications.ConfigurationNamedListListener;
 import org.apache.ignite.configuration.tree.NamedListChange;
 import org.apache.ignite.configuration.tree.NamedListInit;
 import org.apache.ignite.configuration.tree.NamedListView;
@@ -32,10 +35,13 @@ import org.apache.ignite.configuration.tree.NamedListView;
 /**
  * Named configuration wrapper.
  */
-public class NamedListConfiguration<T extends ConfigurationProperty<VIEW, CHANGE>, VIEW, CHANGE, INIT>
+public final class NamedListConfiguration<T extends ConfigurationProperty<VIEW, CHANGE>, VIEW, CHANGE, INIT>
     extends DynamicConfiguration<NamedListView<VIEW>, NamedListInit<INIT>, NamedListChange<CHANGE, INIT>>
     implements NamedConfigurationTree<T, VIEW, CHANGE, INIT>
 {
+    /** Listeners of property update. */
+    private final List<ConfigurationNamedListListener<VIEW>> extendedListeners = new CopyOnWriteArrayList<>();
+
     /** Creator of named configuration. */
     private final BiFunction<List<String>, String, T> creator;
 
@@ -84,4 +90,21 @@ public class NamedListConfiguration<T extends ConfigurationProperty<VIEW, CHANGE
 
         this.values = newValues;
     }
+
+    /** {@inheritDoc} */
+    @Override public Map<String, ConfigurationProperty<?, ?>> members() {
+        refreshValue();
+
+        return Collections.unmodifiableMap(values);
+    }
+
+    /** */
+    public List<ConfigurationNamedListListener<VIEW>> extendedListeners() {
+        return Collections.unmodifiableList(extendedListeners);
+    }
+
+    /** {@inheritDoc} */
+    @Override public final void listen(ConfigurationNamedListListener<VIEW> listener) {
+        extendedListeners.add(listener);
+    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/notifications/ConfigurationNotificationEventImpl.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/notifications/ConfigurationNotificationEventImpl.java
new file mode 100644
index 0000000..89a5776
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/notifications/ConfigurationNotificationEventImpl.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.configuration.internal.notifications;
+
+import org.apache.ignite.configuration.notifications.ConfigurationNotificationEvent;
+import org.jetbrains.annotations.Nullable;
+
+public class ConfigurationNotificationEventImpl<VIEW> implements ConfigurationNotificationEvent<VIEW> {
+    private final VIEW oldValue;
+
+    private final VIEW newValue;
+
+    private final long storageRevision;
+
+    public ConfigurationNotificationEventImpl(VIEW oldValue, VIEW newValue, long storageRevision) {
+        this.oldValue = oldValue;
+        this.newValue = newValue;
+        this.storageRevision = storageRevision;
+    }
+
+    /** {@inheritDoc} */
+    @Override public @Nullable VIEW oldValue() {
+        return oldValue;
+    }
+
+    /** {@inheritDoc} */
+    @Override public @Nullable VIEW newValue() {
+        return newValue;
+    }
+
+    /** {@inheritDoc} */
+    @Override public long storageRevision() {
+        return storageRevision;
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationNotificationsUtil.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationNotificationsUtil.java
new file mode 100644
index 0000000..ebf9772
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationNotificationsUtil.java
@@ -0,0 +1,184 @@
+/*
+ * 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.internal.util;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.ConfigurationProperty;
+import org.apache.ignite.configuration.internal.DynamicConfiguration;
+import org.apache.ignite.configuration.internal.DynamicProperty;
+import org.apache.ignite.configuration.internal.NamedListConfiguration;
+import org.apache.ignite.configuration.internal.notifications.ConfigurationNotificationEventImpl;
+import org.apache.ignite.configuration.notifications.ConfigurationListener;
+import org.apache.ignite.configuration.notifications.ConfigurationNotificationEvent;
+import org.apache.ignite.configuration.tree.ConfigurationVisitor;
+import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.tree.NamedListNode;
+import org.apache.ignite.configuration.tree.NamedListView;
+
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.innerNodeVisitor;
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.leafNodeVisitor;
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.namedListNodeVisitor;
+
+/** */
+public class ConfigurationNotificationsUtil {
+    /**
+     * Recursively notifies all public configuration listeners, accumulating resulting futures in {@code futures} list.
+     * @param oldInnerNode Old configuration values root.
+     * @param newInnerNode New configuration values root.
+     * @param cfgNode Public configuration tree node corresponding to the current inner nodes.
+     * @param storageRevision Storage revision.
+     * @param futures Write-only list of futures to accumulate results.
+     */
+    public static void notifyListeners(
+        InnerNode oldInnerNode,
+        InnerNode newInnerNode,
+        DynamicConfiguration<InnerNode, ?, ?> cfgNode,
+        long storageRevision,
+        List<CompletableFuture<?>> futures
+    ) {
+        if (oldInnerNode == null || oldInnerNode == newInnerNode)
+            return;
+
+        notifyPublicListeners(cfgNode.listeners(), oldInnerNode, newInnerNode, storageRevision, futures);
+
+        oldInnerNode.traverseChildren(new ConfigurationVisitor<Void>() {
+            /** {@inheritDoc} */
+            @Override public Void visitLeafNode(String key, Serializable oldLeaf) {
+                Serializable newLeaf = newInnerNode.traverseChild(key, leafNodeVisitor());
+
+                if (newLeaf != oldLeaf) {
+                    var dynProperty = (DynamicProperty<Serializable>)cfgNode.members().get(key);
+
+                    notifyPublicListeners(dynProperty.listeners(), oldLeaf, newLeaf, storageRevision, futures);
+                }
+
+                return null;
+            }
+
+            /** {@inheritDoc} */
+            @Override public Void visitInnerNode(String key, InnerNode oldNode) {
+                InnerNode newNode = newInnerNode.traverseChild(key, innerNodeVisitor());
+
+                var dynCfg = (DynamicConfiguration<InnerNode, ?, ?>)cfgNode.members().get(key);
+
+                notifyListeners(oldNode, newNode, dynCfg, storageRevision, futures);
+
+                return null;
+            }
+
+            /** {@inheritDoc} */
+            @Override public <N extends InnerNode> Void visitNamedListNode(String key, NamedListNode<N> oldNamedList) {
+                var newNamedList = (NamedListNode<InnerNode>)newInnerNode.traverseChild(key, namedListNodeVisitor());
+
+                if (newNamedList != oldNamedList) {
+                    var namedListCfg = (NamedListConfiguration<?, InnerNode, ?, ?>)cfgNode.members().get(key);
+
+                    notifyPublicListeners(namedListCfg.listeners(), (NamedListView<InnerNode>)oldNamedList, newNamedList, storageRevision, futures);
+
+                    // This is optimization, we could use "NamedListConfiguration#get" directly, but we don't want to.
+
+                    Set<String> oldNames = oldNamedList.namedListKeys();
+                    Set<String> newNames = newNamedList.namedListKeys();
+
+                    Map<String, ConfigurationProperty<?, ?>> namedListCfgMembers = namedListCfg.members();
+
+                    Set<String> created = new HashSet<>(newNames);
+                    created.removeAll(oldNames);
+
+                    if (!created.isEmpty()) {
+                        List<ConfigurationListener<InnerNode>> list = namedListCfg.extendedListeners()
+                            .stream()
+                            .map(l -> (ConfigurationListener<InnerNode>)l::onCreate)
+                            .collect(Collectors.toList());
+
+                        for (String name : created)
+                            notifyPublicListeners(list, null, newNamedList.get(name), storageRevision, futures);
+                    }
+
+                    Set<String> deleted = new HashSet<>(oldNames);
+                    deleted.removeAll(newNames);
+
+                    if (!deleted.isEmpty()) {
+                        List<ConfigurationListener<InnerNode>> list = namedListCfg.extendedListeners()
+                            .stream()
+                            .map(l -> (ConfigurationListener<InnerNode>)l::onDelete)
+                            .collect(Collectors.toList());
+
+                        for (String name : deleted)
+                            notifyPublicListeners(list, oldNamedList.get(name), null, storageRevision, futures);
+                    }
+
+                    for (String name : newNames) {
+                        if (!oldNames.contains(name))
+                            continue;
+
+                        notifyPublicListeners(namedListCfg.extendedListeners(), oldNamedList.get(name), newNamedList.get(name), storageRevision, futures);
+
+                        var dynCfg = (DynamicConfiguration<InnerNode, ?, ?>)namedListCfgMembers.get(name);
+
+                        notifyListeners(oldNamedList.get(name), newNamedList.get(name), dynCfg, storageRevision, futures);
+                    }
+                }
+
+                return null;
+            }
+        });
+    }
+
+    /**
+     * Invoke {@link ConfigurationListener#onUpdate(ConfigurationNotificationEvent)} on all passed listeners and put
+     * results in {@code futures}. Not recursively.
+     * @param listeners List o flisteners.
+     * @param oldVal Old configuration value.
+     * @param newVal New configuration value.
+     * @param storageRevision Storage revision.
+     * @param futures Write-only list of futures.
+     * @param <V> Type of the node.
+     */
+    private static <V> void notifyPublicListeners(
+        List<? extends ConfigurationListener<V>> listeners,
+        V oldVal,
+        V newVal,
+        long storageRevision,
+        List<CompletableFuture<?>> futures
+    ) {
+        ConfigurationNotificationEvent<V> evt = new ConfigurationNotificationEventImpl<>(
+            oldVal,
+            newVal,
+            storageRevision
+        );
+
+        for (ConfigurationListener<V> listener : listeners) {
+            try {
+                CompletableFuture<?> future = listener.onUpdate(evt);
+
+                if (future != null && (future.isCompletedExceptionally() || future.isCancelled() || !future.isDone()))
+                    futures.add(future);
+            }
+            catch (Throwable t) {
+                futures.add(CompletableFuture.failedFuture(t));
+            }
+        }
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
index e92659c..c1708b7 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
@@ -24,6 +24,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Objects;
 import java.util.RandomAccess;
 import java.util.stream.Collectors;
 import org.apache.ignite.configuration.internal.SuperRoot;
@@ -508,6 +509,47 @@ public class ConfigurationUtil {
         };
     }
 
+    /**
+     * Nullifies leaves in {@code newNode} node that are equal to the corresponding leaf values in {@code curNode}.
+     * In this context we view {@code curNode} as a full configuration node with all the data, while {@code newNode}
+     * contains only updates that we plan to apply to the {@code curNode} in the future.
+     * @param curNode Node to look for definitive values.
+     * @param newNode Node to nullify matching values.
+     */
+    public static void cleanupMatchingValues(InnerNode curNode, InnerNode newNode) {
+        if (curNode == null || newNode == null)
+            return;
+
+        assert curNode.getClass() == newNode.getClass();
+
+        newNode.traverseChildren(new ConfigurationVisitor<Void>() {
+            /** {@inheritDoc} */
+            @Override public Void visitLeafNode(String key, Serializable newVal) {
+                if (Objects.deepEquals(curNode.traverseChild(key, leafNodeVisitor()), newVal))
+                    newNode.construct(key, null);
+
+                return null;
+            }
+
+            /** {@inheritDoc} */
+            @Override public Void visitInnerNode(String key, InnerNode newInnerNode) {
+                cleanupMatchingValues(curNode.traverseChild(key, innerNodeVisitor()), newInnerNode);
+
+                return null;
+            }
+
+            /** {@inheritDoc} */
+            @Override public <N extends InnerNode> Void visitNamedListNode(String key, NamedListNode<N> newNamedList) {
+                NamedListNode<?> curNamedList = curNode.traverseChild(key, namedListNodeVisitor());
+
+                for (String name : newNamedList.namedListKeys())
+                    cleanupMatchingValues(curNamedList.get(name), newNamedList.get(name));
+
+                return null;
+            }
+        });
+    }
+
     /** @see #patch(ConstructableTreeNode, TraversableTreeNode) */
     private static class PatchLeafConfigurationSource implements ConfigurationSource {
         /** */
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListView.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationListener.java
similarity index 60%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListView.java
copy to modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationListener.java
index 6914df8..252a462 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListView.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationListener.java
@@ -15,22 +15,24 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.configuration.tree;
+package org.apache.ignite.configuration.notifications;
 
-import java.util.Set;
-
-/** */
-public interface NamedListView<T> {
-    /**
-     * @return Immutable collection of keys contained within this list.
-     */
-    Set<String> namedListKeys();
+import java.util.concurrent.CompletableFuture;
+import org.jetbrains.annotations.NotNull;
 
+/**
+ * Configuration property change listener.
+ *
+ * @param <VIEW> VIEW type configuration.
+ */
+@FunctionalInterface
+public interface ConfigurationListener<VIEW> {
     /**
-     * Returns value associated with the passed key.
+     * Called on property value update.
      *
-     * @param key Key string.
-     * @return Requested value or {@code null} if it's not found.
+     * @param ctx Notification context.
+     * @return Future that signifies end of listener execution.
      */
-    T get(String key);
+    @NotNull CompletableFuture<?> onUpdate(ConfigurationNotificationEvent<VIEW> ctx);
 }
+
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNamedListListener.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNamedListListener.java
new file mode 100644
index 0000000..028a8d8
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNamedListListener.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.notifications;
+
+import java.util.concurrent.CompletableFuture;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Configuration property change listener for named list configurations.
+ *
+ * @param <VIEW> VIEW type configuration.
+ */
+public interface ConfigurationNamedListListener<VIEW> extends ConfigurationListener<VIEW> {
+    /**
+     * Called when new named list element is created.
+     *
+     * @param ctx Notification context.
+     * @return Future that signifies end of listener execution.
+     */
+    @NotNull CompletableFuture<?> onCreate(ConfigurationNotificationEvent<VIEW> ctx);
+
+    /**
+     * Called when named list element is deleted.
+     *
+     * @param ctx Notification context.
+     * @return Future that signifies end of listener execution.
+     */
+    @NotNull CompletableFuture<?> onDelete(ConfigurationNotificationEvent<VIEW> ctx);
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNotificationEvent.java
similarity index 50%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java
copy to modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNotificationEvent.java
index 8c41d20..393ea83 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNotificationEvent.java
@@ -14,44 +14,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.ignite.configuration.storage;
 
-import java.io.Serializable;
-import java.util.Map;
+package org.apache.ignite.configuration.notifications;
+
+import org.apache.ignite.configuration.ConfigurationProperty;
+import org.jetbrains.annotations.Nullable;
 
 /**
- * Represents data in configuration storage.
+ * Event object propogated on configuration change. Passed to listeners after configuration changes are applied.
+ *
+ * @see ConfigurationProperty#listen(ConfigurationListener)
+ * @see ConfigurationListener
+ * @see ConfigurationNotificationEvent
  */
-public class Data {
-    /** Values. */
-    private final Map<String, Serializable> values;
-
-    /** Configuration storage version. */
-    private final long version;
-
+public interface ConfigurationNotificationEvent<VIEW> {
     /**
-     * Constructor.
-     * @param values Values.
-     * @param version Version.
+     * Previous value of the updated configuration.
      */
-    public Data(Map<String, Serializable> values, long version) {
-        this.values = values;
-        this.version = version;
-    }
+    @Nullable VIEW oldValue();
 
     /**
-     * Get values.
-     * @return Values.
+     * Updated value of the configuration.
      */
-    public Map<String, Serializable> values() {
-        return values;
-    }
+    @Nullable VIEW newValue();
 
     /**
-     * Get version.
-     * @return version.
+     * Monotonously increasing counter, linked to the specific storage for current configuration values. Gives you a
+     * unique change identifier inside a specific configuration storage.
      */
-    public long version() {
-        return version;
-    }
+    long storageRevision();
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorage.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorage.java
index b0cc047..edd0edf 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorage.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorage.java
@@ -18,7 +18,6 @@ package org.apache.ignite.configuration.storage;
 
 import java.io.Serializable;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 
 /**
@@ -42,14 +41,7 @@ public interface ConfigurationStorage {
     CompletableFuture<Boolean> write(Map<String, Serializable> newValues, long version);
 
     /**
-     * Get all the keys of the configuration storage.
-     * @return Set of keys.
-     * @throws StorageException If failed to retrieve keys.
-     */
-    Set<String> keys() throws StorageException;
-
-    /**
-     * Add listener to the storage that notifies of data changes..
+     * Add listener to the storage that notifies of data changes.
      * @param listener Listener.
      */
     void addListener(ConfigurationStorageListener listener);
@@ -59,4 +51,10 @@ public interface ConfigurationStorage {
      * @param listener Listener.
      */
     void removeListener(ConfigurationStorageListener listener);
+
+    /**
+     * Notify storage that this specific revision was successfully handled and it is not necessary to repeat the same
+     * notification on node restart.
+     */
+    void notifyApplied(long storageRevision);
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorageListener.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorageListener.java
index f8cb7b4..ac0f5b7 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorageListener.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorageListener.java
@@ -19,6 +19,7 @@ package org.apache.ignite.configuration.storage;
 /**
  * Configuration storage listener for changes.
  */
+@FunctionalInterface
 public interface ConfigurationStorageListener {
     /**
      * Method called when entries in storage change.
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java
index 8c41d20..0178520 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java
@@ -27,16 +27,21 @@ public class Data {
     private final Map<String, Serializable> values;
 
     /** Configuration storage version. */
-    private final long version;
+    private final long cfgVersion;
+
+    /** */
+    private final long storageRevision;
 
     /**
      * Constructor.
      * @param values Values.
-     * @param version Version.
+     * @param cfgVersion Version.
+     * @param storageRevision Storage revision.
      */
-    public Data(Map<String, Serializable> values, long version) {
+    public Data(Map<String, Serializable> values, long cfgVersion, long storageRevision) {
         this.values = values;
-        this.version = version;
+        this.cfgVersion = cfgVersion;
+        this.storageRevision = storageRevision;
     }
 
     /**
@@ -51,7 +56,12 @@ public class Data {
      * Get version.
      * @return version.
      */
-    public long version() {
-        return version;
+    public long cfgVersion() {
+        return cfgVersion;
+    }
+
+    /** */
+    public long storageRevision() {
+        return storageRevision;
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
index 718e0b0..c9d3836 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
@@ -69,6 +69,11 @@ public final class NamedListNode<N extends InnerNode> implements NamedListView<N
     }
 
     /** {@inheritDoc} */
+    @Override public int size() {
+        return map.size();
+    }
+
+    /** {@inheritDoc} */
     @Override public final NamedListChange<N, N> update(String key, Consumer<N> valConsumer) {
         Objects.requireNonNull(valConsumer, "valConsumer");
 
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListView.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListView.java
index 6914df8..f6e8ffc 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListView.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListView.java
@@ -33,4 +33,9 @@ public interface NamedListView<T> {
      * @return Requested value or {@code null} if it's not found.
      */
     T get(String key);
+
+    /**
+     * @return Number of elements.
+     */
+    int size();
 }
diff --git a/modules/network/pom.xml b/modules/network/pom.xml
index a5bbf95..314a169 100644
--- a/modules/network/pom.xml
+++ b/modules/network/pom.xml
@@ -50,18 +50,6 @@
 
         <!-- Test dependencies. -->
         <dependency>
-            <groupId>log4j</groupId>
-            <artifactId>log4j</artifactId>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
             <groupId>org.hamcrest</groupId>
             <artifactId>hamcrest-library</artifactId>
             <scope>test</scope>
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/configuration/InMemoryConfigurationStorage.java b/modules/rest/src/main/java/org/apache/ignite/rest/configuration/InMemoryConfigurationStorage.java
index a74ff59..00e06c5 100644
--- a/modules/rest/src/main/java/org/apache/ignite/rest/configuration/InMemoryConfigurationStorage.java
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/configuration/InMemoryConfigurationStorage.java
@@ -17,13 +17,12 @@
 package org.apache.ignite.rest.configuration;
 
 import java.io.Serializable;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicLong;
 import org.apache.ignite.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.configuration.storage.ConfigurationStorageListener;
@@ -38,19 +37,19 @@ public class InMemoryConfigurationStorage implements ConfigurationStorage {
     private Map<String, Serializable> map = new ConcurrentHashMap<>();
 
     /** Change listeners. */
-    private List<ConfigurationStorageListener> listeners = new ArrayList<>();
+    private List<ConfigurationStorageListener> listeners = new CopyOnWriteArrayList<>();
 
     /** Storage version. */
     private AtomicLong version = new AtomicLong(0);
 
     /** {@inheritDoc} */
     @Override public synchronized Data readAll() throws StorageException {
-        return new Data(new HashMap<>(map), version.get());
+        return new Data(new HashMap<>(map), version.get(), 0);
     }
 
     /** {@inheritDoc} */
-    @Override public synchronized CompletableFuture<Boolean> write(Map<String, Serializable> newValues, long sentVersion) throws StorageException {
-        if (sentVersion != version.get())
+    @Override public synchronized CompletableFuture<Boolean> write(Map<String, Serializable> newValues, long version) {
+        if (version != this.version.get())
             return CompletableFuture.completedFuture(false);
 
         for (Map.Entry<String, Serializable> entry : newValues.entrySet()) {
@@ -60,19 +59,14 @@ public class InMemoryConfigurationStorage implements ConfigurationStorage {
                 map.remove(entry.getKey());
         }
 
-        version.incrementAndGet();
+        this.version.incrementAndGet();
 
-        listeners.forEach(listener -> listener.onEntriesChanged(new Data(newValues, version.get())));
+        listeners.forEach(listener -> listener.onEntriesChanged(new Data(newValues, this.version.get(), 0)));
 
         return CompletableFuture.completedFuture(true);
     }
 
     /** {@inheritDoc} */
-    @Override public synchronized Set<String> keys() throws StorageException {
-        return map.keySet();
-    }
-
-    /** {@inheritDoc} */
     @Override public void addListener(ConfigurationStorageListener listener) {
         listeners.add(listener);
     }
@@ -81,4 +75,9 @@ public class InMemoryConfigurationStorage implements ConfigurationStorage {
     @Override public void removeListener(ConfigurationStorageListener listener) {
         listeners.remove(listener);
     }
+
+    /** {@inheritDoc} */
+    @Override public void notifyApplied(long storageRevision) {
+        // No-op.
+    }
 }
diff --git a/modules/rest/src/test/java/org/apache/ignite/rest/presentation/json/JsonConverterTest.java b/modules/rest/src/test/java/org/apache/ignite/rest/presentation/json/JsonConverterTest.java
index f855ec8..8326a07 100644
--- a/modules/rest/src/test/java/org/apache/ignite/rest/presentation/json/JsonConverterTest.java
+++ b/modules/rest/src/test/java/org/apache/ignite/rest/presentation/json/JsonConverterTest.java
@@ -24,6 +24,7 @@ import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -113,6 +114,12 @@ public class JsonConverterTest {
     }
 
     /** */
+    @AfterEach
+    public void after() {
+        registry.stop();
+    }
+
+    /** */
     @Test
     public void toJson() throws Exception {
         assertEquals(
diff --git a/modules/rest/src/test/java/org/apache/ignite/rest/presentation/json/TestConfigurationStorage.java b/modules/rest/src/test/java/org/apache/ignite/rest/presentation/json/TestConfigurationStorage.java
index e175d54..3a6834e 100644
--- a/modules/rest/src/test/java/org/apache/ignite/rest/presentation/json/TestConfigurationStorage.java
+++ b/modules/rest/src/test/java/org/apache/ignite/rest/presentation/json/TestConfigurationStorage.java
@@ -29,29 +29,24 @@ import org.apache.ignite.configuration.storage.Data;
 import org.apache.ignite.configuration.storage.StorageException;
 
 /** */
-public class TestConfigurationStorage implements ConfigurationStorage {
+class TestConfigurationStorage implements ConfigurationStorage {
     /** */
     private final Set<ConfigurationStorageListener> listeners = new HashSet<>();
 
     /** {@inheritDoc} */
     @Override public Data readAll() throws StorageException {
-        return new Data(Collections.emptyMap(), 0);
+        return new Data(Collections.emptyMap(), 0, 0);
     }
 
     /** {@inheritDoc} */
     @Override public CompletableFuture<Boolean> write(Map<String, Serializable> newValues, long version) {
         for (ConfigurationStorageListener listener : listeners)
-            listener.onEntriesChanged(new Data(newValues, version + 1));
+            listener.onEntriesChanged(new Data(newValues, version + 1, 0));
 
         return CompletableFuture.completedFuture(true);
     }
 
     /** {@inheritDoc} */
-    @Override public Set<String> keys() throws StorageException {
-        return null;
-    }
-
-    /** {@inheritDoc} */
     @Override public void addListener(ConfigurationStorageListener listener) {
         listeners.add(listener);
     }
@@ -60,4 +55,7 @@ public class TestConfigurationStorage implements ConfigurationStorage {
     @Override public void removeListener(ConfigurationStorageListener listener) {
         listeners.remove(listener);
     }
+
+    @Override public void notifyApplied(long storageRevision) {
+    }
 }
diff --git a/parent/pom.xml b/parent/pom.xml
index 2616325..2f32870 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -63,7 +63,6 @@
         <jetbrains.annotations.version>20.1.0</jetbrains.annotations.version>
         <jmh.framework.version>1.13</jmh.framework.version>
         <junit.jupiter.version>5.7.0</junit.jupiter.version>
-        <log4j.version>1.2.17</log4j.version>
         <logback.version>1.2.3</logback.version>
         <micronaut.version>2.1.2</micronaut.version>
         <micronaut.test.junit5.version>2.3.1</micronaut.test.junit5.version>
@@ -230,12 +229,6 @@
             </dependency>
 
             <dependency>
-                <groupId>log4j</groupId>
-                <artifactId>log4j</artifactId>
-                <version>${log4j.version}</version>
-            </dependency>
-
-            <dependency>
                 <groupId>com.google.testing.compile</groupId>
                 <artifactId>compile-testing</artifactId>
                 <version>${compile.testing.library.version}</version>