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/08/04 15:04:29 UTC

[ignite-3] branch main updated: IGNITE-15166 Implemented "rename" API for named lists configuration. (#244)

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 d3dc66f  IGNITE-15166 Implemented "rename" API for named lists configuration. (#244)
d3dc66f is described below

commit d3dc66f1eee0816944db746c8cc25be6f29bef0b
Author: ibessonov <be...@gmail.com>
AuthorDate: Wed Aug 4 18:02:28 2021 +0300

    IGNITE-15166 Implemented "rename" API for named lists configuration. (#244)
---
 .../configuration/ConfigurationChangerTest.java    |  72 +++--
 .../notifications/ConfigurationListenerTest.java   | 352 +++++++++++++++++++--
 .../configuration/tree/NamedListNodeTest.java      | 311 ++++++++++++++++++
 .../configuration/tree/NamedListOrderTest.java     | 239 --------------
 .../configuration/util/ConfigurationUtilTest.java  |  97 ++----
 .../validation/ValidationUtilTest.java             |   2 +-
 .../ignite/configuration/ConfigurationTree.java    |  12 +-
 .../ignite/configuration/ConfigurationValue.java   |   4 +-
 .../ignite/configuration/NamedListChange.java      |  22 +-
 .../notifications/ConfigurationListener.java       |   4 +-
 .../ConfigurationNamedListListener.java            |  24 +-
 .../configuration/ConfigurationChanger.java        |  54 ++--
 .../configuration/DynamicConfiguration.java        |  12 +-
 .../internal/configuration/DynamicProperty.java    |   4 +-
 .../internal/configuration/tree/NamedListNode.java | 230 ++++++++++++--
 .../configuration/util/ConfigurationFlattener.java |  25 +-
 .../util/ConfigurationNotificationsUtil.java       | 126 ++++++--
 .../configuration/util/ConfigurationUtil.java      | 285 +++++------------
 .../internal/table/distributed/TableManager.java   |  26 +-
 .../ignite/internal/table/TableManagerTest.java    |  10 +-
 20 files changed, 1191 insertions(+), 720 deletions(-)

diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
index ef7503f..741af6d 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
@@ -23,7 +23,9 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
 import org.apache.ignite.configuration.ConfigurationChangeException;
+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;
@@ -37,7 +39,8 @@ import org.apache.ignite.configuration.validation.Validator;
 import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
 import org.apache.ignite.internal.configuration.storage.Data;
 import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
-import org.apache.ignite.internal.configuration.tree.TraversableTreeNode;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Test;
 
@@ -45,7 +48,6 @@ import static java.lang.annotation.ElementType.FIELD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.apache.ignite.internal.configuration.AConfiguration.KEY;
-import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.superRootPatcher;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -112,11 +114,10 @@ public class ConfigurationChangerTest {
         changer.addRootKey(KEY);
         changer.register(storage);
 
-        AChange data = ((AChange)changer.createRootNode(KEY))
+        changer.change(source(KEY, (AChange parent) -> parent
             .changeChild(change -> change.changeIntCfg(1).changeStrCfg("1"))
-            .changeElements(change -> change.create("a", element -> element.changeStrCfg("1")));
-
-        changer.change(superRootPatcher(KEY, (TraversableTreeNode)data), null).get(1, SECONDS);
+            .changeElements(change -> change.create("a", element -> element.changeStrCfg("1")))
+        ), null).get(1, SECONDS);
 
         AView newRoot = (AView)changer.getRootNode(KEY);
 
@@ -140,20 +141,18 @@ public class ConfigurationChangerTest {
         changer2.addRootKey(KEY);
         changer2.register(storage);
 
-        AChange data1 = ((AChange)changer1.createRootNode(KEY))
+        changer1.change(source(KEY, (AChange parent) -> parent
             .changeChild(change -> change.changeIntCfg(1).changeStrCfg("1"))
-            .changeElements(change -> change.create("a", element -> element.changeStrCfg("1")));
-
-        changer1.change(superRootPatcher(KEY, (TraversableTreeNode)data1), null).get(1, SECONDS);
+            .changeElements(change -> change.create("a", element -> element.changeStrCfg("1")))
+        ), null).get(1, SECONDS);
 
-        AChange data2 = ((AChange)changer2.createRootNode(KEY))
+        changer2.change(source(KEY, (AChange parent) -> parent
             .changeChild(change -> change.changeIntCfg(2).changeStrCfg("2"))
             .changeElements(change -> change
-                .create("a", element -> element.changeStrCfg("2"))
+                .createOrUpdate("a", element -> element.changeStrCfg("2"))
                 .create("b", element -> element.changeStrCfg("2"))
-            );
-
-        changer2.change(superRootPatcher(KEY, (TraversableTreeNode)data2), null).get(1, SECONDS);
+            )
+        ), null).get(1, SECONDS);
 
         AView newRoot1 = (AView)changer1.getRootNode(KEY);
 
@@ -185,11 +184,10 @@ public class ConfigurationChangerTest {
         changer2.addRootKey(KEY);
         changer2.register(storage);
 
-        AChange data1 = ((AChange)changer1.createRootNode(KEY))
+        changer1.change(source(KEY, (AChange parent) -> parent
             .changeChild(change -> change.changeIntCfg(1).changeStrCfg("1"))
-            .changeElements(change -> change.create("a", element -> element.changeStrCfg("1")));
-
-        changer1.change(superRootPatcher(KEY, (TraversableTreeNode)data1), null).get(1, SECONDS);
+            .changeElements(change -> change.create("a", element -> element.changeStrCfg("1")))
+        ), null).get(1, SECONDS);
 
         changer2.addValidator(MaybeInvalid.class, new Validator<MaybeInvalid, Object>() {
             @Override public void validate(MaybeInvalid annotation, ValidationContext<Object> ctx) {
@@ -197,14 +195,13 @@ public class ConfigurationChangerTest {
             }
         });
 
-        AChange data2 = ((AChange)changer2.createRootNode(KEY))
+        assertThrows(ExecutionException.class, () -> changer2.change(source(KEY, (AChange parent) -> parent
             .changeChild(change -> change.changeIntCfg(2).changeStrCfg("2"))
             .changeElements(change -> change
                 .create("a", element -> element.changeStrCfg("2"))
                 .create("b", element -> element.changeStrCfg("2"))
-            );
-
-        assertThrows(ExecutionException.class, () -> changer2.change(superRootPatcher(KEY, (TraversableTreeNode)data2), null).get(1, SECONDS));
+            )
+        ), null).get(1, SECONDS));
 
         AView newRoot = (AView)changer2.getRootNode(KEY);
 
@@ -233,9 +230,9 @@ public class ConfigurationChangerTest {
 
         storage.fail(true);
 
-        AChange data = ((AChange)changer.createRootNode(KEY)).changeChild(child -> child.changeIntCfg(1));
-
-        assertThrows(ExecutionException.class, () -> changer.change(superRootPatcher(KEY, (TraversableTreeNode)data), null).get(1, SECONDS));
+        assertThrows(ExecutionException.class, () -> changer.change(source(KEY, (AChange parent) -> parent
+            .changeChild(child -> child.changeIntCfg(1))
+        ), null).get(1, SECONDS));
 
         storage.fail(false);
 
@@ -294,15 +291,28 @@ public class ConfigurationChangerTest {
         assertEquals("bar", root.child().defStr());
         assertEquals(List.of("xyz"), Arrays.asList(root.child().arr()));
 
-        DefaultsChange change = ((DefaultsChange)changer.createRootNode(DefaultsConfiguration.KEY)).changeChildsList(childs ->
-            childs.create("name", child -> {})
-        );
-
-        changer.change(superRootPatcher(DefaultsConfiguration.KEY, (TraversableTreeNode)change), null).get(1, SECONDS);
+        changer.change(source(DefaultsConfiguration.KEY, (DefaultsChange def) -> def
+            .changeChildsList(childs ->
+                childs.create("name", child -> {})
+            )
+        ), null).get(1, SECONDS);
 
         root = (DefaultsView)changer.getRootNode(DefaultsConfiguration.KEY);
 
         assertEquals("bar", root.childsList().get("name").defStr());
     }
 
+    private static <Change> ConfigurationSource source(RootKey<?, ? super Change> rootKey, Consumer<Change> changer) {
+        return new ConfigurationSource() {
+            @Override public void descend(ConstructableTreeNode node) {
+                ConfigurationSource changerSrc = new ConfigurationSource() {
+                    @Override public void descend(ConstructableTreeNode node) {
+                        changer.accept((Change)node);
+                    }
+                };
+
+                node.construct(rootKey.key(), changerSrc);
+            }
+        };
+    }
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
index 111e9c5..3a5d906 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
@@ -21,6 +21,8 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
 import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
@@ -34,6 +36,7 @@ import org.apache.ignite.internal.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 import static java.util.concurrent.CompletableFuture.completedFuture;
@@ -41,6 +44,8 @@ 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;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.fail;
 
 /** */
 public class ConfigurationListenerTest {
@@ -137,9 +142,9 @@ public class ConfigurationListenerTest {
         assertEquals(List.of("parent", "child", "str"), log);
     }
 
-    /** */
+    /** Tests notifications validity when a new named list element is created. */
     @Test
-    public void namedListNode() throws Exception {
+    public void namedListNodeOnCreate() throws Exception {
         List<String> log = new ArrayList<>();
 
         configuration.listen(ctx -> {
@@ -155,29 +160,98 @@ public class ConfigurationListenerTest {
         });
 
         configuration.elements().listen(ctx -> {
-            if (ctx.oldValue().size() == 0) {
-                ChildView newValue = ctx.newValue().get("name");
+            assertEquals(0, ctx.oldValue().size());
+
+            ChildView newValue = ctx.newValue().get("name");
+
+            assertNotNull(newValue);
+            assertEquals("default", 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);
             }
-            else if (ctx.newValue().size() == 0) {
-                ChildView oldValue = ctx.oldValue().get("name");
 
-                assertNotNull(oldValue);
-                assertEquals("foo", oldValue.str());
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onUpdate(ConfigurationNotificationEvent<ChildView> ctx) {
+                log.add("update");
+
+                return completedFuture(null);
             }
-            else {
-                ChildView oldValue = ctx.oldValue().get("name");
 
-                assertNotNull(oldValue);
-                assertEquals("default", oldValue.str());
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onRename(
+                String oldName,
+                String newName,
+                ConfigurationNotificationEvent<ChildView> ctx
+            ) {
+                log.add("rename");
 
-                ChildView newValue = ctx.newValue().get("name");
+                return completedFuture(null);
+            }
 
-                assertNotNull(newValue);
-                assertEquals("foo", newValue.str());
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onDelete(ConfigurationNotificationEvent<ChildView> ctx) {
+                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);
+    }
+
+    /** Tests notifications validity when a named list element is edited. */
+    @Test
+    public void namedListNodeOnUpdate() throws Exception {
+        configuration.change(parent ->
+            parent.changeElements(elements -> elements.create("name", element -> {}))
+        ).get(1, SECONDS);
+
+        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 -> {
+
+            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");
 
@@ -187,13 +261,6 @@ public class ConfigurationListenerTest {
         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);
@@ -217,14 +284,118 @@ public class ConfigurationListenerTest {
             }
 
             /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onRename(
+                String oldName,
+                String newName,
+                ConfigurationNotificationEvent<ChildView> ctx
+            ) {
+                log.add("rename");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
             @Override public CompletableFuture<?> onDelete(ConfigurationNotificationEvent<ChildView> ctx) {
-                assertNull(ctx.newValue());
+                log.add("delete");
+
+                return completedFuture(null);
+            }
+        });
+
+        configuration.change(parent ->
+            parent.changeElements(elements -> elements.createOrUpdate("name", element -> element.changeStr("foo")))
+        ).get(1, SECONDS);
+
+        assertEquals(List.of("parent", "elements", "update"), log);
+    }
+
+
+    /** Tests notifications validity when a named list element is renamed. */
+    @Test
+    public void namedListNodeOnRename() throws Exception {
+        configuration.change(parent ->
+            parent.changeElements(elements -> elements.create("name", element -> {}))
+        ).get(1, SECONDS);
+
+        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 -> {
+            assertEquals(1, ctx.oldValue().size());
+
+            ChildView oldValue = ctx.oldValue().get("name");
+
+            assertNotNull(oldValue);
+            assertEquals("default", oldValue.str());
+
+            assertEquals(1, ctx.newValue().size());
+
+            ChildView newValue = ctx.newValue().get("newName");
+
+            assertNotNull(newValue, ctx.newValue().namedListKeys().toString());
+            assertEquals("default", newValue.str());
+
+            assertSame(oldValue, newValue);
+
+            log.add("elements");
+
+            return completedFuture(null);
+        });
+
+        configuration.elements().listen(new ConfigurationNamedListListener<ChildView>() {
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onCreate(ConfigurationNotificationEvent<ChildView> ctx) {
+                log.add("create");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onUpdate(ConfigurationNotificationEvent<ChildView> ctx) {
+                log.add("update");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onRename(
+                String oldName,
+                String newName,
+                ConfigurationNotificationEvent<ChildView> ctx
+            ) {
+                assertEquals("name", oldName);
+                assertEquals("newName", newName);
 
                 ChildView oldValue = ctx.oldValue();
 
                 assertNotNull(oldValue);
-                assertEquals("foo", oldValue.str());
+                assertEquals("default", oldValue.str());
+
+                ChildView newValue = ctx.newValue();
+
+                assertNotNull(newValue);
+                assertEquals("default", newValue.str());
 
+                assertSame(oldValue, newValue);
+
+                log.add("rename");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onDelete(ConfigurationNotificationEvent<ChildView> ctx) {
                 log.add("delete");
 
                 return completedFuture(null);
@@ -232,20 +403,90 @@ public class ConfigurationListenerTest {
         });
 
         configuration.change(parent ->
-            parent.changeElements(elements -> elements.create("name", element -> {}))
+            parent.changeElements(elements -> elements.rename("name", "newName"))
         ).get(1, SECONDS);
 
-        assertEquals(List.of("parent", "elements", "create"), log);
-
-        log.clear();
+        assertEquals(List.of("parent", "elements", "rename"), log);
+    }
 
+    /** Tests notifications validity when a named list element is deleted. */
+    @Test
+    public void namedListNodeOnDelete() throws Exception {
         configuration.change(parent ->
-            parent.changeElements(elements -> elements.createOrUpdate("name", element -> element.changeStr("foo")))
+            parent.changeElements(elements -> elements.create("name", element -> {}))
         ).get(1, SECONDS);
 
-        assertEquals(List.of("parent", "elements", "update"), log);
+        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 -> {
+            assertEquals(0, ctx.newValue().size());
+
+            ChildView oldValue = ctx.oldValue().get("name");
+
+            assertNotNull(oldValue);
+            assertEquals("default", oldValue.str());
+
+            log.add("elements");
+
+            return completedFuture(null);
+        });
+
+        configuration.elements().listen(new ConfigurationNamedListListener<ChildView>() {
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onCreate(ConfigurationNotificationEvent<ChildView> ctx) {
+                log.add("create");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onUpdate(ConfigurationNotificationEvent<ChildView> ctx) {
+                log.add("update");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onRename(
+                String oldName,
+                String newName,
+                ConfigurationNotificationEvent<ChildView> ctx
+            ) {
+                log.add("rename");
+
+                return completedFuture(null);
+            }
+
+            /** {@inheritDoc} */
+            @Override public CompletableFuture<?> onDelete(ConfigurationNotificationEvent<ChildView> ctx) {
+                assertNull(ctx.newValue());
+
+                ChildView oldValue = ctx.oldValue();
 
-        log.clear();
+                assertNotNull(oldValue);
+                assertEquals("default", oldValue.str());
+
+                log.add("delete");
+
+                return completedFuture(null);
+            }
+        });
+
+        configuration.elements().get("name").listen(ctx -> {
+            return completedFuture(null);
+        });
 
         configuration.change(parent ->
             parent.changeElements(elements -> elements.delete("name"))
@@ -253,4 +494,53 @@ public class ConfigurationListenerTest {
 
         assertEquals(List.of("parent", "elements", "delete"), log);
     }
+
+    /** */
+    @Test
+    @Disabled("Will be fixed in https://issues.apache.org/jira/browse/IGNITE-15193")
+    public void dataRace() throws Exception {
+        configuration.change(parent -> parent.changeElements(elements ->
+            elements.create("name", e -> {}))
+        ).get(1, SECONDS);
+
+        CountDownLatch wait = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+
+        List<String> log = new ArrayList<>();
+
+        configuration.listen(ctx -> {
+            try {
+                wait.await(1, SECONDS);
+            }
+            catch (InterruptedException e) {
+                fail(e.getMessage());
+            }
+
+            release.countDown();
+
+            return completedFuture(null);
+        });
+
+        configuration.elements().get("name").listen(ctx -> {
+            assertNull(ctx.newValue());
+
+            log.add("deleted");
+
+            return completedFuture(null);
+        });
+
+        Future<Void> fut = configuration.change(parent -> parent.changeElements(elements ->
+            elements.delete("name"))
+        );
+
+        wait.countDown();
+
+        configuration.elements();
+
+        release.await(1, SECONDS);
+
+        fut.get(1, SECONDS);
+
+        assertEquals(List.of("deleted"), log);
+    }
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListNodeTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListNodeTest.java
new file mode 100644
index 0000000..4d2df6e
--- /dev/null
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListNodeTest.java
@@ -0,0 +1,311 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration.tree;
+
+import java.io.Serializable;
+import java.util.Map;
+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.apache.ignite.internal.configuration.TestConfigurationChanger;
+import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
+import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static java.lang.String.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.aMapWithSize;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/** Test for named list nodes. */
+public class NamedListNodeTest {
+    /** Root that has a single named list. */
+    @ConfigurationRoot(rootName = "a")
+    public static class AConfigurationSchema {
+        /** */
+        @NamedConfigValue
+        public BConfigurationSchema b;
+    }
+
+    /** Named list element node that in inself contains another named list. */
+    @Config
+    public static class BConfigurationSchema {
+        /** Every named list element node must have at least one configuration field that is not named list. */
+        @Value(hasDefault = true)
+        public String c = "foo";
+
+        @NamedConfigValue
+        public BConfigurationSchema b;
+    }
+
+    /** Runtime implementations generator. */
+    private static ConfigurationAsmGenerator cgen;
+
+    /** Test configuration storage. */
+    private TestConfigurationStorage storage;
+
+    /** Test configuration changer. */
+    private TestConfigurationChanger changer;
+
+    /** Instantiates {@link #cgen}. */
+    @BeforeAll
+    public static void beforeAll() {
+        cgen = new ConfigurationAsmGenerator();
+    }
+
+    /** Nullifies {@link #cgen} to prevent memory leak from having runtime ClassLoader accessible from GC root. */
+    @AfterAll
+    public static void afterAll() {
+        cgen = null;
+    }
+
+    /** */
+    @BeforeEach
+    public void before() {
+        storage = new TestConfigurationStorage();
+
+        changer = new TestConfigurationChanger(cgen);
+        changer.addRootKey(AConfiguration.KEY);
+        changer.register(storage);
+    }
+
+    /** */
+    @AfterEach
+    public void after() {
+        changer.stop();
+    }
+
+    /**
+     * Tests that there are no unnecessary {@code <order>} values in the storage after all basic named list operations.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void storageData() throws Exception {
+        // Manually instantiate configuration instance.
+        var a = (AConfiguration)cgen.instantiateCfg(AConfiguration.KEY, changer);
+
+        // Create values on several layers at the same time. They all should have <order> = 0.
+        a.b().change(b -> b.create("X", x -> x.changeB(xb -> xb.create("Z0", z0 -> {})))).get();
+
+        String xId = ((NamedListNode<?>)a.b().value()).internalId("X");
+        String z0Id = ((NamedListNode<?>)a.b().get("X").b().value()).internalId("Z0");
+
+        Map<String, Serializable> storageValues = storage.readAll().values();
+
+        assertThat(
+            storageValues,
+            is(Matchers.<Map<String, Serializable>>allOf(
+                aMapWithSize(6),
+                hasEntry(format("a.b.%s.c", xId), "foo"),
+                hasEntry(format("a.b.%s.<order>", xId), 0),
+                hasEntry(format("a.b.%s.<name>", xId), "X"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0")
+            ))
+        );
+
+        BConfiguration x = a.b().get("X");
+
+        // Append new key. It should have <order> = 1.
+        x.b().change(xb -> xb.create("Z5", z5 -> {})).get();
+
+        String z5Id = ((NamedListNode<?>)a.b().get("X").b().value()).internalId("Z5");
+
+        storageValues = storage.readAll().values();
+
+        assertThat(
+            storageValues,
+            is(Matchers.<Map<String, Serializable>>allOf(
+                aMapWithSize(9),
+                hasEntry(format("a.b.%s.c", xId), "foo"),
+                hasEntry(format("a.b.%s.<order>", xId), 0),
+                hasEntry(format("a.b.%s.<name>", xId), "X"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z5Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z5Id), 1),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z5Id), "Z5")
+            ))
+        );
+
+        // Insert new key somewhere in the middle. Index of Z5 should be updated to 2.
+        x.b().change(xb -> xb.create(1, "Z2", z2 -> {})).get();
+
+        String z2Id = ((NamedListNode<?>)a.b().get("X").b().value()).internalId("Z2");
+
+        storageValues = storage.readAll().values();
+
+        assertThat(
+            storageValues,
+            is(Matchers.<Map<String, Serializable>>allOf(
+                aMapWithSize(12),
+                hasEntry(format("a.b.%s.c", xId), "foo"),
+                hasEntry(format("a.b.%s.<order>", xId), 0),
+                hasEntry(format("a.b.%s.<name>", xId), "X"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z2Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z2Id), 1),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z2Id), "Z2"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z5Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z5Id), 2),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z5Id), "Z5")
+            ))
+        );
+
+        // Insert new key somewhere in the middle. Indexes of Z3 and Z5 should be updated to 2 and 3.
+        x.b().change(xb -> xb.createAfter("Z2", "Z3", z3 -> {})).get();
+
+        String z3Id = ((NamedListNode<?>)a.b().get("X").b().value()).internalId("Z3");
+
+        storageValues = storage.readAll().values();
+
+        assertThat(
+            storageValues,
+            is(Matchers.<Map<String, Serializable>>allOf(
+                aMapWithSize(15),
+                hasEntry(format("a.b.%s.c", xId), "foo"),
+                hasEntry(format("a.b.%s.<order>", xId), 0),
+                hasEntry(format("a.b.%s.<name>", xId), "X"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z2Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z2Id), 1),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z2Id), "Z2"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z3Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z3Id), 2),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z3Id), "Z3"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z5Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z5Id), 3),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z5Id), "Z5")
+            ))
+        );
+
+        // Delete keys from the middle. Indexes of Z3 should be updated to 1.
+        x.b().change(xb -> xb.delete("Z2").delete("Z5")).get();
+
+        storageValues = storage.readAll().values();
+
+        assertThat(
+            storageValues,
+            is(Matchers.<Map<String, Serializable>>allOf(
+                aMapWithSize(9),
+                hasEntry(format("a.b.%s.c", xId), "foo"),
+                hasEntry(format("a.b.%s.<order>", xId), 0),
+                hasEntry(format("a.b.%s.<name>", xId), "X"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z3Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z3Id), 1),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z3Id), "Z3")
+            ))
+        );
+
+        // Delete keys from the middle. Indexes of Z3 should be updated to 1.
+        x.b().change(xb -> xb.rename("Z0", "Z1")).get();
+
+        storageValues = storage.readAll().values();
+
+        assertThat(
+            storageValues,
+            is(Matchers.<Map<String, Serializable>>allOf(
+                aMapWithSize(9),
+                hasEntry(format("a.b.%s.c", xId), "foo"),
+                hasEntry(format("a.b.%s.<order>", xId), 0),
+                hasEntry(format("a.b.%s.<name>", xId), "X"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z1"),
+                hasEntry(format("a.b.%s.b.%s.c", xId, z3Id), "foo"),
+                hasEntry(format("a.b.%s.b.%s.<order>", xId, z3Id), 1),
+                hasEntry(format("a.b.%s.b.%s.<name>", xId, z3Id), "Z3")
+            ))
+        );
+
+        // Delete values on several layers simultaneously. Storage must be empty after that.
+        a.b().change(b -> b.delete("X")).get();
+
+        assertThat(storage.readAll().values(), is(anEmptyMap()));
+    }
+
+    /** Tests exceptions described in methods signatures. */
+    @Test
+    public void errors() throws Exception {
+        var b = new NamedListNode<>("name", () -> cgen.instantiateNode(BConfigurationSchema.class));
+
+        b.create("X", x -> {}).create("Y", y -> {});
+
+        // NPE in keys.
+        assertThrows(NullPointerException.class, () -> b.create(null, z -> {}));
+        assertThrows(NullPointerException.class, () -> b.createOrUpdate(null, z -> {}));
+        assertThrows(NullPointerException.class, () -> b.create(0, null, z -> {}));
+        assertThrows(NullPointerException.class, () -> b.createAfter(null, "Z", z -> {}));
+        assertThrows(NullPointerException.class, () -> b.createAfter("X", null, z -> {}));
+        assertThrows(NullPointerException.class, () -> b.rename(null, "Z"));
+        assertThrows(NullPointerException.class, () -> b.rename("X", null));
+        assertThrows(NullPointerException.class, () -> b.delete(null));
+
+        // NPE in closures.
+        assertThrows(NullPointerException.class, () -> b.create("Z", null));
+        assertThrows(NullPointerException.class, () -> b.createOrUpdate("Z", null));
+        assertThrows(NullPointerException.class, () -> b.create(0, "Z", null));
+        assertThrows(NullPointerException.class, () -> b.createAfter("X", "Z", null));
+
+        // Already existing keys.
+        assertThrows(IllegalArgumentException.class, () -> b.create("X", x -> {}));
+        assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x -> {}));
+        assertThrows(IllegalArgumentException.class, () -> b.createAfter("X", "Y", y -> {}));
+        assertThrows(IllegalArgumentException.class, () -> b.rename("X", "Y"));
+
+        // Nonexistent preceding key.
+        assertThrows(IllegalArgumentException.class, () -> b.createAfter("A", "Z", z -> {}));
+
+        // Wrong indexes.
+        assertThrows(IndexOutOfBoundsException.class, () -> b.create(-1, "Z", z -> {}));
+        assertThrows(IndexOutOfBoundsException.class, () -> b.create(3, "Z", z -> {}));
+
+        // Nonexisting key.
+        assertThrows(IllegalArgumentException.class, () -> b.rename("A", "Z"));
+
+        // Operations after delete.
+        b.delete("X");
+        assertThrows(IllegalArgumentException.class, () -> b.create("X", x -> {}));
+        assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x -> {}));
+        assertThrows(IllegalArgumentException.class, () -> b.rename("X", "Z"));
+        assertThrows(IllegalArgumentException.class, () -> b.rename("Y", "X"));
+
+        // Deletion of nonexistent elements doesn't break anything.
+        b.delete("X");
+        b.delete("Y");
+    }
+}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListOrderTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListOrderTest.java
deleted file mode 100644
index 16637c6..0000000
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListOrderTest.java
+++ /dev/null
@@ -1,239 +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.internal.configuration.tree;
-
-import java.util.Map;
-import org.apache.ignite.configuration.NamedListChange;
-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.apache.ignite.internal.configuration.TestConfigurationChanger;
-import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
-import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-/** Test for keys ordering in named list nodes. */
-public class NamedListOrderTest {
-    /** Root that has a single named list. */
-    @ConfigurationRoot(rootName = "a")
-    public static class AConfigurationSchema {
-        /** */
-        @NamedConfigValue
-        public BConfigurationSchema b;
-    }
-
-    /** Named list element node that in inself contains another named list. */
-    @Config
-    public static class BConfigurationSchema {
-        /** Every named list element node must have at least one configuration field that is not named list. */
-        @Value(hasDefault = true)
-        public String c = "foo";
-
-        @NamedConfigValue
-        public BConfigurationSchema b;
-    }
-
-    /** Runtime implementations generator. */
-    private static ConfigurationAsmGenerator cgen;
-
-    /** Test configuration storage. */
-    private TestConfigurationStorage storage;
-
-    /** Test configuration changer. */
-    private TestConfigurationChanger changer;
-
-    /** Instantiates {@link #cgen}. */
-    @BeforeAll
-    public static void beforeAll() {
-        cgen = new ConfigurationAsmGenerator();
-    }
-
-    /** Nullifies {@link #cgen} to prevent memory leak from having runtime ClassLoader accessible from GC root. */
-    @AfterAll
-    public static void afterAll() {
-        cgen = null;
-    }
-
-    /** */
-    @BeforeEach
-    public void before() {
-        storage = new TestConfigurationStorage();
-
-        changer = new TestConfigurationChanger(cgen);
-        changer.addRootKey(AConfiguration.KEY);
-        changer.register(storage);
-    }
-
-    /** */
-    @AfterEach
-    public void after() {
-        changer.stop();
-    }
-
-    /**
-     * Tests that there are no unnecessary {@code <idx>} values in the storage after all basic named list operations.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    public void storageData() throws Exception {
-        // Manually instantiate configuration instance.
-        var a = (AConfiguration)cgen.instantiateCfg(AConfiguration.KEY, changer);
-
-        // Create values on several layers at the same time. They all should have <idx> = 0.
-        a.b().change(b -> b.create("X", x -> x.changeB(xb -> xb.create("Z0", z0 -> {})))).get();
-
-        assertEquals(
-            Map.of(
-                "a.b.X.c", "foo",
-                "a.b.X.<idx>", 0,
-                "a.b.X.b.Z0.c", "foo",
-                "a.b.X.b.Z0.<idx>", 0
-            ),
-            storage.readAll().values()
-        );
-
-        BConfiguration x = a.b().get("X");
-
-        // Append new key. It should have <idx> = 1.
-        x.b().change(xb -> xb.create("Z5", z5 -> {})).get();
-
-        assertEquals(
-            Map.of(
-                "a.b.X.c", "foo",
-                "a.b.X.<idx>", 0,
-                "a.b.X.b.Z0.c", "foo",
-                "a.b.X.b.Z0.<idx>", 0,
-                "a.b.X.b.Z5.c", "foo",
-                "a.b.X.b.Z5.<idx>", 1
-            ),
-            storage.readAll().values()
-        );
-
-        // Insert new key somewhere in the middle. Index of Z5 should be updated to 2.
-        x.b().change(xb -> xb.create(1, "Z2", z2 -> {})).get();
-
-        assertEquals(
-            Map.of(
-                "a.b.X.c", "foo",
-                "a.b.X.<idx>", 0,
-                "a.b.X.b.Z0.c", "foo",
-                "a.b.X.b.Z0.<idx>", 0,
-                "a.b.X.b.Z2.c", "foo",
-                "a.b.X.b.Z2.<idx>", 1,
-                "a.b.X.b.Z5.c", "foo",
-                "a.b.X.b.Z5.<idx>", 2
-            ),
-            storage.readAll().values()
-        );
-
-        // Insert new key somewhere in the middle. Indexes of Z3 and Z5 should be updated to 2 and 3.
-        x.b().change(xb -> xb.createAfter("Z2", "Z3", z3 -> {})).get();
-
-        assertEquals(
-            Map.of(
-                "a.b.X.c", "foo",
-                "a.b.X.<idx>", 0,
-                "a.b.X.b.Z0.c", "foo",
-                "a.b.X.b.Z0.<idx>", 0,
-                "a.b.X.b.Z2.c", "foo",
-                "a.b.X.b.Z2.<idx>", 1,
-                "a.b.X.b.Z3.c", "foo",
-                "a.b.X.b.Z3.<idx>", 2,
-                "a.b.X.b.Z5.c", "foo",
-                "a.b.X.b.Z5.<idx>", 3
-            ),
-            storage.readAll().values()
-        );
-
-        // Delete key from the middle. Indexes of Z3 and Z5 should be updated to 1 and 2.
-        x.b().change(xb -> xb.delete("Z2")).get();
-
-        assertEquals(
-            Map.of(
-                "a.b.X.c", "foo",
-                "a.b.X.<idx>", 0,
-                "a.b.X.b.Z0.c", "foo",
-                "a.b.X.b.Z0.<idx>", 0,
-                "a.b.X.b.Z3.c", "foo",
-                "a.b.X.b.Z3.<idx>", 1,
-                "a.b.X.b.Z5.c", "foo",
-                "a.b.X.b.Z5.<idx>", 2
-            ),
-            storage.readAll().values()
-        );
-
-        // Delete values on several layers simultaneously. Storage must be empty after that.
-        a.b().change(b -> b.delete("X")).get();
-
-        assertEquals(
-            Map.of(),
-            storage.readAll().values()
-        );
-    }
-
-    /** Tests exceptions described in methods signatures. */
-    @Test
-    public void creationErrors() throws Exception {
-        // Manually instantiate configuration instance.
-        var a = (AConfiguration)cgen.instantiateCfg(AConfiguration.KEY, changer);
-
-        a.b().change(b -> b.create("X", x -> {}).create("Y", y -> {})).get();
-
-        // Dirty cast, but appropriate for this particular test.
-        var b = (NamedListChange<BView>)a.b().value();
-
-        // NPE in keys.
-        assertThrows(NullPointerException.class, () -> b.create(null, z -> {}));
-        assertThrows(NullPointerException.class, () -> b.createOrUpdate(null, z -> {}));
-        assertThrows(NullPointerException.class, () -> b.create(0, null, z -> {}));
-        assertThrows(NullPointerException.class, () -> b.createAfter(null, "Z", z -> {}));
-        assertThrows(NullPointerException.class, () -> b.createAfter("X", null, z -> {}));
-
-        // NPE in closures.
-        assertThrows(NullPointerException.class, () -> b.create("Z", null));
-        assertThrows(NullPointerException.class, () -> b.createOrUpdate("Z", null));
-        assertThrows(NullPointerException.class, () -> b.create(0, "Z", null));
-        assertThrows(NullPointerException.class, () -> b.createAfter("X", "Z", null));
-
-        // Already existing keys.
-        assertThrows(IllegalArgumentException.class, () -> b.create("X", x -> {}));
-        assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x -> {}));
-        assertThrows(IllegalArgumentException.class, () -> b.createAfter("X", "Y", y -> {}));
-
-        // Nonexistent preceding key.
-        assertThrows(IllegalArgumentException.class, () -> b.createAfter("A", "Z", z -> {}));
-
-        // Wrong indexes.
-        assertThrows(IndexOutOfBoundsException.class, () -> b.create(-1, "Z", z -> {}));
-        assertThrows(IndexOutOfBoundsException.class, () -> b.create(3, "Z", z -> {}));
-
-        // Create after delete.
-        b.delete("X");
-        assertThrows(IllegalArgumentException.class, () -> b.create("X", x -> {}));
-        assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x -> {}));
-    }
-}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
index 0b7fe20..9bf04cc 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
@@ -30,7 +30,6 @@ import org.apache.ignite.configuration.annotation.Value;
 import org.apache.ignite.internal.configuration.SuperRoot;
 import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
 import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
-import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
 import org.apache.ignite.internal.configuration.tree.InnerNode;
 import org.apache.ignite.internal.configuration.tree.TraversableTreeNode;
 import org.jetbrains.annotations.NotNull;
@@ -39,6 +38,7 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
 import static java.util.Collections.singletonMap;
+import static org.apache.ignite.internal.configuration.tree.NamedListNode.NAME;
 import static org.apache.ignite.internal.configuration.tree.NamedListNode.ORDER_IDX;
 import static org.apache.ignite.internal.configuration.util.ConfigurationFlattener.createFlattenedUpdatesMap;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -46,10 +46,11 @@ import static org.hamcrest.Matchers.aMapWithSize;
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.anEmptyMap;
 import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasToString;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.matchesPattern;
+import static org.hamcrest.Matchers.nullValue;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -258,13 +259,15 @@ public class ConfigurationUtilTest {
 
         ConfigurationUtil.fillFromPrefixMap(parentNode, Map.of(
             "elements", Map.of(
-                "name2", Map.of(
+                "0123456789abcde0123456789abcde", Map.of(
                     "child", Map.of("str", "value2"),
-                    ORDER_IDX, 1
+                    ORDER_IDX, 1,
+                    NAME, "name2"
                 ),
-                "name1", Map.of(
+                "12345689abcdef0123456789abcdef0", Map.of(
                     "child", Map.of("str", "value1"),
-                    ORDER_IDX, 0
+                    ORDER_IDX, 0,
+                    NAME, "name1"
                 )
             )
         ));
@@ -311,9 +314,10 @@ public class ConfigurationUtilTest {
                 )
             ),
             is(allOf(
-                aMapWithSize(2),
-                hasEntry("root.elements.name.child.str", (Serializable)"foo"),
-                hasEntry("root.elements.name.<idx>", 0)
+                aMapWithSize(3),
+                hasEntry(matchesPattern("root[.]elements[.]\\w{32}[.]child[.]str"), hasToString("foo")),
+                hasEntry(matchesPattern("root[.]elements[.]\\w{32}[.]<order>"), is(0)),
+                hasEntry(matchesPattern("root[.]elements[.]\\w{32}[.]<name>"), hasToString("name"))
             ))
         );
 
@@ -329,9 +333,10 @@ public class ConfigurationUtilTest {
                 .changeElements(elements -> elements.delete("name"))
             ),
             is(allOf(
-                aMapWithSize(2),
-                hasEntry("root.elements.name.child.str", null),
-                hasEntry("root.elements.name.<idx>", null)
+                aMapWithSize(3),
+                hasEntry(matchesPattern("root[.]elements[.]\\w{32}[.]child[.]str"), nullValue()),
+                hasEntry(matchesPattern("root[.]elements[.]\\w{32}[.]<order>"), nullValue()),
+                hasEntry(matchesPattern("root[.]elements[.]\\w{32}[.]<name>"), nullValue())
             ))
         );
     }
@@ -357,70 +362,4 @@ public class ConfigurationUtilTest {
         // Create flat diff between two super trees.
         return createFlattenedUpdatesMap(originalSuperRoot, superRoot);
     }
-
-    /**
-     * Tests basic invariants of {@link ConfigurationUtil#patch(ConstructableTreeNode, TraversableTreeNode)} method.
-     */
-    @Test
-    public void patch() {
-        var originalRoot = newParentInstance();
-
-        originalRoot.changeElements(elements ->
-            elements.create("name1", element ->
-                element.changeChild(child -> child.changeStr("value1"))
-            )
-        );
-
-        // Updating config.
-        ParentView updatedRoot = ConfigurationUtil.patch(originalRoot, (TraversableTreeNode)copy(originalRoot).changeElements(elements ->
-            elements.createOrUpdate("name1", element ->
-                element.changeChild(child -> child.changeStr("value2"))
-            )
-        ));
-
-        assertNotSame(originalRoot, updatedRoot);
-        assertNotSame(originalRoot.elements(), updatedRoot.elements());
-        assertNotSame(originalRoot.elements().get("name1"), updatedRoot.elements().get("name1"));
-        assertNotSame(originalRoot.elements().get("name1").child(), updatedRoot.elements().get("name1").child());
-
-        assertEquals("value1", originalRoot.elements().get("name1").child().str());
-        assertEquals("value2", updatedRoot.elements().get("name1").child().str());
-
-        // Expanding config.
-        ParentView expandedRoot = ConfigurationUtil.patch(originalRoot, (TraversableTreeNode)copy(originalRoot).changeElements(elements ->
-            elements.createOrUpdate("name2", element ->
-                element.changeChild(child -> child.changeStr("value2"))
-            )
-        ));
-
-        assertNotSame(originalRoot, expandedRoot);
-        assertNotSame(originalRoot.elements(), expandedRoot.elements());
-
-        assertSame(originalRoot.elements().get("name1"), expandedRoot.elements().get("name1"));
-        assertNull(originalRoot.elements().get("name2"));
-        assertNotNull(expandedRoot.elements().get("name2"));
-
-        assertEquals("value2", expandedRoot.elements().get("name2").child().str());
-
-        // Shrinking config.
-        ParentView shrinkedRoot = (ParentView)ConfigurationUtil.patch((InnerNode)expandedRoot, (TraversableTreeNode)copy(expandedRoot).changeElements(elements ->
-            elements.delete("name1")
-        ));
-
-        assertNotSame(expandedRoot, shrinkedRoot);
-        assertNotSame(expandedRoot.elements(), shrinkedRoot.elements());
-
-        assertNotNull(expandedRoot.elements().get("name1"));
-        assertNull(shrinkedRoot.elements().get("name1"));
-        assertNotNull(shrinkedRoot.elements().get("name2"));
-    }
-
-    /**
-     * @param parent {@link ParentView} object.
-     * @return Copy of the {@code parent} objects cast to {@link ParentChange}.
-     * @see ConstructableTreeNode#copy()
-     */
-    private ParentChange copy(ParentView parent) {
-        return (ParentChange)((ConstructableTreeNode)parent).copy();
-    }
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/validation/ValidationUtilTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/validation/ValidationUtilTest.java
index 1a95c85..8946f09 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/validation/ValidationUtilTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/validation/ValidationUtilTest.java
@@ -112,7 +112,7 @@ public class ValidationUtilTest {
     public void before() {
         root = cgen.instantiateNode(ValidatedRootConfigurationSchema.class);
 
-        ConfigurationUtil.addDefaults(root, root);
+        ConfigurationUtil.addDefaults(root);
     }
 
     /** */
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationTree.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationTree.java
index 0b7cbab..0336442 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationTree.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationTree.java
@@ -17,8 +17,7 @@
 
 package org.apache.ignite.configuration;
 
-import java.util.Map;
-import java.util.concurrent.Future;
+import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 
 /**
@@ -29,17 +28,10 @@ import java.util.function.Consumer;
  */
 public interface ConfigurationTree<VIEW, CHANGE> extends ConfigurationProperty<VIEW, CHANGE> {
     /**
-     * Children of the tree.
-     *
-     * @return Map from {@code String} to a corresponding {@link ConfigurationProperty}.
-     */
-    Map<String, ConfigurationProperty<?, ?>> members();
-
-    /**
      * Changes this configuration node value.
      *
      * @param change CHANGE object.
      * @return Future that is completed when configuration change is finished either successfully or not.
      */
-    Future<Void> change(Consumer<CHANGE> change);
+    CompletableFuture<Void> change(Consumer<CHANGE> change);
 }
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationValue.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationValue.java
index bd70cb1..8c8cb21 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationValue.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationValue.java
@@ -17,7 +17,7 @@
 
 package org.apache.ignite.configuration;
 
-import java.util.concurrent.Future;
+import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
 
 /**
@@ -34,5 +34,5 @@ public interface ConfigurationValue<VIEW> extends ConfigurationProperty<VIEW, VI
      * @return Future that signifies end of the update operation. Can also be completed with
      *      {@link ConfigurationValidationException} and {@link ConfigurationChangeException}.
      */
-    Future<Void> update(VIEW change);
+    CompletableFuture<Void> update(VIEW change);
 }
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
index ba42132..624b35f 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
@@ -18,6 +18,8 @@
 package org.apache.ignite.configuration;
 
 import java.util.function.Consumer;
+import org.apache.ignite.configuration.notifications.ConfigurationNamedListListener;
+import org.apache.ignite.configuration.notifications.ConfigurationNotificationEvent;
 
 /**
  * Closure parameter for {@link NamedConfigurationTree#change(Consumer)} method. Contains methods to modify named lists.
@@ -82,7 +84,25 @@ public interface NamedListChange<Change> extends NamedListView<Change> {
     NamedListChange<Change> createOrUpdate(String key, Consumer<Change> valConsumer);
 
     /**
-     * Remove the value from named list configuration.
+     * Renames the existing value in the named list configuration. Element with key {@code oldKey} must exist and key
+     * {@code newKey} must not. Error will occur if {@code newKey} has just been deleted on the same
+     * {@link NamedListChange} instance (to distinguish between
+     * {@link ConfigurationNamedListListener#onRename(String, String, ConfigurationNotificationEvent)} and
+     * {@link ConfigurationNamedListListener#onUpdate(ConfigurationNotificationEvent)} on {@code newKey}).
+     *
+     * @param oldKey Key for the value to be updated.
+     * @param newKey New key for the same value.
+     * @return {@code this} for chaining.
+     *
+     * @throws NullPointerException If one of parameters is null.
+     * @throws IllegalArgumentException If an element with name {@code newKey} already exists, or an element with name
+     *      {@code oldKey} doesn't exist, or {@link #delete(String)} has previously been invoked with either the
+     *      {@code newKey} or the {@code oldKey}.
+     */
+    NamedListChange<Change> rename(String oldKey, String newKey);
+
+    /**
+     * Removes the value from the named list configuration.
      *
      * @param key Key for the value to be removed.
      * @return {@code this} for chaining.
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationListener.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationListener.java
index 252a462..534d5ec 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationListener.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationListener.java
@@ -31,8 +31,8 @@ public interface ConfigurationListener<VIEW> {
      * Called on property value update.
      *
      * @param ctx Notification context.
-     * @return Future that signifies end of listener execution.
+     * @return Future that signifies the end of the listener execution.
      */
-    @NotNull CompletableFuture<?> onUpdate(ConfigurationNotificationEvent<VIEW> ctx);
+    @NotNull CompletableFuture<?> onUpdate(@NotNull ConfigurationNotificationEvent<VIEW> ctx);
 }
 
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNamedListListener.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNamedListListener.java
index 028a8d8..18d5549 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNamedListListener.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/notifications/ConfigurationNamedListListener.java
@@ -30,15 +30,31 @@ public interface ConfigurationNamedListListener<VIEW> extends ConfigurationListe
      * Called when new named list element is created.
      *
      * @param ctx Notification context.
-     * @return Future that signifies end of listener execution.
+     * @return Future that signifies the end of the listener execution.
      */
-    @NotNull CompletableFuture<?> onCreate(ConfigurationNotificationEvent<VIEW> ctx);
+    @NotNull CompletableFuture<?> onCreate(@NotNull ConfigurationNotificationEvent<VIEW> ctx);
+
+    /**
+     * Called when a named list element is renamed. Semantically equivalent to
+     * {@link #onUpdate(ConfigurationNotificationEvent)} with the difference that the content of the element might
+     * have not been changed. No separate {@link #onUpdate(ConfigurationNotificationEvent)} call is performed when
+     * {@link #onRename(String, String, ConfigurationNotificationEvent)} is already invoked.
+     *
+     * @param oldName Name, previously assigned to the element.
+     * @param newName New name of the element.
+     * @param ctx Notification context.
+     * @return Future that signifies the end of the listener execution.
+     */
+    @NotNull CompletableFuture<?> onRename(
+        @NotNull String oldName,
+        @NotNull String newName,
+        @NotNull ConfigurationNotificationEvent<VIEW> ctx);
 
     /**
      * Called when named list element is deleted.
      *
      * @param ctx Notification context.
-     * @return Future that signifies end of listener execution.
+     * @return Future that signifies the end of the listener execution.
      */
-    @NotNull CompletableFuture<?> onDelete(ConfigurationNotificationEvent<VIEW> ctx);
+    @NotNull CompletableFuture<?> onDelete(@NotNull ConfigurationNotificationEvent<VIEW> ctx);
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
index e54121a..58b1494 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
@@ -52,9 +52,8 @@ import static java.util.concurrent.CompletableFuture.completedFuture;
 import static java.util.stream.Collectors.toList;
 import static org.apache.ignite.internal.configuration.util.ConfigurationFlattener.createFlattenedUpdatesMap;
 import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.addDefaults;
+import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.dropNulls;
 import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.fillFromPrefixMap;
-import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.nodePatcher;
-import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.patch;
 import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.toPrefixMap;
 
 /**
@@ -233,32 +232,33 @@ public abstract class ConfigurationChanger {
 
         assert configurationStorage != null : storageType;
 
-        StorageRoots storageRoots = storagesRootsMap.get(storageType);
-
-        SuperRoot superRoot = storageRoots.roots;
-        SuperRoot defaultsNode = new SuperRoot(rootCreator());
+        try {
+            ConfigurationSource defaultsCfgSource = new ConfigurationSource() {
+                @Override public void descend(ConstructableTreeNode node) {
+                    addDefaults((InnerNode)node);
+                }
+            };
 
-        addDefaults(superRoot, defaultsNode);
+            changeInternally(defaultsCfgSource, configurationStorage).get();
+        }
+        catch (ExecutionException e) {
+            Throwable cause = e.getCause();
 
-        List<ValidationIssue> validationIssues = ValidationUtil.validate(
-            superRoot,
-            defaultsNode,
-            this::getRootNode,
-            cachedAnnotations,
-            validators
-        );
+            if (cause instanceof ConfigurationValidationException)
+                throw (ConfigurationValidationException)cause;
 
-        if (!validationIssues.isEmpty())
-            throw new ConfigurationValidationException(validationIssues);
+            if (cause instanceof ConfigurationChangeException)
+                throw (ConfigurationChangeException)cause;
 
-        try {
-            changeInternally(nodePatcher(defaultsNode), storageInstances.get(storageType)).get();
-        }
-        catch (InterruptedException | ExecutionException e) {
             throw new ConfigurationChangeException(
                 "Failed to write defalut configuration values into the storage " + configurationStorage.getClass(), e
             );
         }
+        catch (InterruptedException e) {
+            throw new ConfigurationChangeException(
+                "Failed to initialize configuration storage " + configurationStorage.getClass(), e
+            );
+        }
     }
 
     /**
@@ -358,23 +358,19 @@ public abstract class ConfigurationChanger {
 
                 src.descend(changes);
 
-                SuperRoot patchedSuperRoot = patch(curRoots, changes);
-
-                SuperRoot defaultsNode = new SuperRoot(rootCreator());
+                addDefaults(changes);
 
-                addDefaults(patchedSuperRoot, defaultsNode);
-
-                SuperRoot patchedChanges = patch(changes, defaultsNode);
-
-                Map<String, Serializable> allChanges = createFlattenedUpdatesMap(curRoots, patchedChanges);
+                Map<String, Serializable> allChanges = createFlattenedUpdatesMap(curRoots, changes);
 
                 // Unlikely but still possible.
                 if (allChanges.isEmpty())
                     return null;
 
+                dropNulls(changes);
+
                 List<ValidationIssue> validationIssues = ValidationUtil.validate(
                     curRoots,
-                    patch(patchedSuperRoot, defaultsNode),
+                    changes,
                     this::getRootNode,
                     cachedAnnotations,
                     validators
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfiguration.java
index 6fbc031..1ed111f 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicConfiguration.java
@@ -23,7 +23,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.RandomAccess;
-import java.util.concurrent.Future;
+import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 import org.apache.ignite.configuration.ConfigurationProperty;
 import org.apache.ignite.configuration.ConfigurationTree;
@@ -66,7 +66,7 @@ public abstract class DynamicConfiguration<VIEW, CHANGE> extends ConfigurationNo
     }
 
     /** {@inheritDoc} */
-    @Override public final Future<Void> change(Consumer<CHANGE> change) {
+    @Override public final CompletableFuture<Void> change(Consumer<CHANGE> change) {
         Objects.requireNonNull(change, "Configuration consumer cannot be null.");
 
         assert keys instanceof RandomAccess;
@@ -100,8 +100,12 @@ public abstract class DynamicConfiguration<VIEW, CHANGE> extends ConfigurationNo
         return refreshValue();
     }
 
-    /** {@inheritDoc} */
-    @Override public Map<String, ConfigurationProperty<?, ?>> members() {
+    /**
+     * Returns all child nodes of the current configuration tree node.
+     *
+     * @return Map from childs keys to a corresponding {@link ConfigurationProperty}.
+     */
+    public Map<String, ConfigurationProperty<?, ?>> members() {
         return Collections.unmodifiableMap(members);
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicProperty.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicProperty.java
index 5bd67c1..6f4b85d 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicProperty.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/DynamicProperty.java
@@ -21,7 +21,7 @@ import java.io.Serializable;
 import java.util.List;
 import java.util.Objects;
 import java.util.RandomAccess;
-import java.util.concurrent.Future;
+import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.configuration.ConfigurationValue;
 import org.apache.ignite.configuration.RootKey;
 import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
@@ -53,7 +53,7 @@ public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T
     }
 
     /** {@inheritDoc} */
-    @Override public Future<Void> update(T newValue) {
+    @Override public CompletableFuture<Void> update(T newValue) {
         Objects.requireNonNull(newValue, "Configuration value cannot be null.");
 
         assert keys instanceof RandomAccess;
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
index 6a90b15..d6b97ee 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
@@ -17,13 +17,18 @@
 
 package org.apache.ignite.internal.configuration.tree;
 
+import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.UUID;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 import org.apache.ignite.configuration.NamedListChange;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
+import org.apache.ignite.internal.configuration.util.ConfigurationUtil;
 
 /**
  * Configuration node implementation for the collection of named {@link InnerNode}s. Unlike implementations of
@@ -33,7 +38,12 @@ import org.apache.ignite.configuration.annotation.NamedConfigValue;
  */
 public final class NamedListNode<N extends InnerNode> implements NamedListChange<N>, TraversableTreeNode, ConstructableTreeNode {
     /** Name of a synthetic configuration property that describes the order of elements in a named list. */
-    public static final String ORDER_IDX = "<idx>";
+    public static final String ORDER_IDX = "<order>";
+
+    /**
+     * Name of a synthetic configuration property that's used to store "key" of the named list element in the storage.
+     */
+    public static final String NAME = "<name>";
 
     /** Configuration name for the synthetic key. */
     private final String syntheticKeyName;
@@ -41,8 +51,11 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
     /** Supplier of new node objects when new list element node has to be created. */
     private final Supplier<N> valSupplier;
 
-    /** Internal container for named list element. Maps keys to named list elements nodes. */
-    private final OrderedMap<N> map;
+    /** Internal container for named list element. Maps keys to named list elements nodes with their internal ids. */
+    private final OrderedMap<ElementDescriptor<N>> map;
+
+    /** Mapping from internal ids to public keys. */
+    private final Map<String, String> reverseIdMap;
 
     /**
      * Default constructor.
@@ -55,6 +68,7 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
         this.syntheticKeyName = syntheticKeyName;
         this.valSupplier = valSupplier;
         map = new OrderedMap<>();
+        reverseIdMap = new HashMap<>();
     }
 
     /**
@@ -65,7 +79,11 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
     private NamedListNode(NamedListNode<N> node) {
         syntheticKeyName = node.syntheticKeyName;
         valSupplier = node.valSupplier;
-        map = new OrderedMap<>(node.map);
+        map = new OrderedMap<>();
+        reverseIdMap = new HashMap<>(node.reverseIdMap);
+
+        for (String key : node.map.keys())
+            map.put(key, node.map.get(key).shallowCopy());
     }
 
     /** {@inheritDoc} */
@@ -80,12 +98,16 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
 
     /** {@inheritDoc} */
     @Override public final N get(String key) {
-        return map.get(key);
+        ElementDescriptor<N> element = map.get(key);
+
+        return element == null ? null : element.value;
     }
 
     /** {@inheritDoc} */
     @Override public N get(int index) throws IndexOutOfBoundsException {
-        return map.get(index);
+        ElementDescriptor<N> element = map.get(index);
+
+        return element == null ? null : element.value;
     }
 
     /** {@inheritDoc} */
@@ -100,11 +122,13 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
 
         checkNewKey(key);
 
-        N val = valSupplier.get();
+        ElementDescriptor<N> element = new ElementDescriptor<>(valSupplier.get());
+
+        map.put(key, element);
 
-        map.put(key, val);
+        reverseIdMap.put(element.internalId, key);
 
-        valConsumer.accept(val);
+        valConsumer.accept(element.value);
 
         return this;
     }
@@ -119,11 +143,13 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
 
         checkNewKey(key);
 
-        N val = valSupplier.get();
+        ElementDescriptor<N> element = new ElementDescriptor<>(valSupplier.get());
 
-        map.putByIndex(index, key, val);
+        map.putByIndex(index, key, element);
 
-        valConsumer.accept(val);
+        reverseIdMap.put(element.internalId, key);
+
+        valConsumer.accept(element.value);
 
         return this;
     }
@@ -139,11 +165,13 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
 
         checkNewKey(key);
 
-        N val = valSupplier.get();
+        ElementDescriptor<N> element = new ElementDescriptor<>(valSupplier.get());
+
+        map.putAfter(precedingKey, key, element);
 
-        map.putAfter(precedingKey, key, val);
+        reverseIdMap.put(element.internalId, key);
 
-        valConsumer.accept(val);
+        valConsumer.accept(element.value);
 
         return this;
     }
@@ -153,17 +181,47 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
         Objects.requireNonNull(key, "key");
         Objects.requireNonNull(valConsumer, "valConsumer");
 
-        if (map.containsKey(key) && map.get(key) == null)
+        if (map.containsKey(key) && map.get(key).value == null)
             throw new IllegalArgumentException("You can't create entity that has just been deleted [key=" + key + ']');
 
-        N val = map.get(key);
+        ElementDescriptor<N> element = map.get(key);
+
+        if (element == null) {
+            element = new ElementDescriptor<>(valSupplier.get());
 
-        if (val == null)
-            map.put(key, val = valSupplier.get());
+            reverseIdMap.put(element.internalId, key);
+        }
         else
-            map.put(key, val = (N)val.copy());
+            element = element.copy();
+
+        map.put(key, element);
+
+        valConsumer.accept(element.value);
+
+        return this;
+    }
 
-        valConsumer.accept(val);
+    /** {@inheritDoc} */
+    @Override public NamedListChange<N> rename(String oldKey, String newKey) {
+        Objects.requireNonNull(oldKey, "oldKey");
+        Objects.requireNonNull(newKey, "newKey");
+
+        if (!map.containsKey(oldKey))
+            throw new IllegalArgumentException("Element with name " + oldKey + " does not exist.");
+
+        ElementDescriptor<N> element = map.get(oldKey);
+
+        if (element.value == null) {
+            throw new IllegalArgumentException(
+                "Can't rename entity that has just been deleted [key=" + oldKey + ']'
+            );
+        }
+
+        checkNewKey(newKey);
+
+        map.rename(oldKey, newKey);
+
+        reverseIdMap.put(element.internalId, newKey);
 
         return this;
     }
@@ -187,7 +245,7 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
         Objects.requireNonNull(key, "key");
 
         if (map.containsKey(key))
-            map.put(key, null);
+            map.get(key).value = null;
 
         return this;
     }
@@ -202,12 +260,70 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
     }
 
     /**
+     * Sets an internal id for the value associated with the passed key. Should not be used in arbitrary code. Refer
+     * to {@link ConfigurationUtil#fillFromPrefixMap(ConstructableTreeNode, Map)} for further details on the usage.
+     *
+     * @param key Key to update. Should be present in the named list. Nothing will happen if the key is missing.
+     * @param internalId New id to associate with the key.
+     */
+    public void setInternalId(String key, String internalId) {
+        ElementDescriptor<N> element = map.get(key);
+
+        if (element != null) {
+            reverseIdMap.remove(element.internalId);
+
+            element.internalId = internalId;
+
+            reverseIdMap.put(internalId, key);
+        }
+    }
+
+    /**
+     * Returns internal id for the value associated with the passed key.
+     *
+     * @param key Key.
+     * @return Internal id.
+     *
+     * @throws IllegalArgumentException If {@code key} is not found in the named list.
+     */
+    public String internalId(String key) {
+        ElementDescriptor<N> element = map.get(key);
+
+        if (element == null)
+            throw new IllegalArgumentException("Element with name '" + key + "' does not exist.");
+
+        return element.internalId;
+    }
+
+    /**
+     * Returns public key associated with the internal id.
+     *
+     * @param internalId Internat id.
+     * @return Key.
+     */
+    public String keyByInternalId(String internalId) {
+        return reverseIdMap.get(internalId);
+    }
+
+    /**
+     * Returns collection of internal ids in this named list node.
+     *
+     * @return Set of internal ids.
+     */
+    public Collection<String> internalIds() {
+        return Collections.unmodifiableSet(reverseIdMap.keySet());
+    }
+
+    /**
      * Deletes named list element.
      *
      * @param key Element's key.
      */
     public void forceDelete(String key) {
-        map.remove(key);
+        ElementDescriptor<N> removed = map.remove(key);
+
+        if (removed != null)
+            reverseIdMap.remove(removed.internalId);
     }
 
     /**
@@ -222,20 +338,68 @@ public final class NamedListNode<N extends InnerNode> implements NamedListChange
     /** {@inheritDoc} */
     @Override public void construct(String key, ConfigurationSource src) {
         if (src == null)
-            map.put(key, null);
-        else {
-            N val = map.get(key);
-
-            val = val == null ? valSupplier.get() : (N)val.copy();
-
-            map.put(key, val);
-
-            src.descend(val);
-        }
+            delete(key);
+        else
+            createOrUpdate(key, src::descend);
     }
 
     /** {@inheritDoc} */
     @Override public NamedListNode<N> copy() {
         return new NamedListNode<>(this);
     }
+
+    /**
+     * Descriptor for internal named list element representation. Has node itself and its internal id.
+     *
+     * @param <N> Type of the node.
+     */
+    private static class ElementDescriptor<N extends InnerNode> {
+        /** Element's internal id. */
+        public String internalId;
+
+        /** Element node value. */
+        public N value;
+
+        /**
+         * Constructor.
+         *
+         * @param value Node instance.
+         */
+        ElementDescriptor(N value) {
+            this.value = value;
+            // Remove dashes so that id would be a bit shorter and easier to validate in tests.
+            // This string won't be visible by end users anyway.
+            internalId = UUID.randomUUID().toString().replace("-", "");
+        }
+
+        /**
+         * Private constructor with entire fields list.
+         *
+         * @param internalId Internal id.
+         * @param value Node instance.
+         */
+        private ElementDescriptor(String internalId, N value) {
+            this.internalId = internalId;
+            this.value = value;
+        }
+
+        /**
+         * Makes a copy of the element descriptor. Not to be confused with {@link #shallowCopy()}.
+         *
+         * @return New instance with the same internal id but copied node instance.
+         * @see InnerNode#copy()
+         */
+        public ElementDescriptor<N> copy() {
+            return new ElementDescriptor<>(internalId, (N)value.copy());
+        }
+
+        /**
+         * Makes a copy of the element descriptor, preserving same fields values.
+         *
+         * @return New instance with the same internal id and node instance.
+         */
+        public ElementDescriptor<N> shallowCopy() {
+            return new ElementDescriptor<>(internalId, value);
+        }
+    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
index 61600c9..a9f2739 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
@@ -148,10 +148,14 @@ public class ConfigurationFlattener {
             Map<String, Integer> newKeysToOrderIdxMap = deletion ? null
                 : keysToOrderIdx(newNode);
 
-            for (String namedListKey : newNode.namedListKeys()) {
-                withTracking(namedListKey, true, false, () -> {
-                    InnerNode oldNamedElement = oldNode.get(namedListKey);
-                    InnerNode newNamedElement = newNode.get(namedListKey);
+            for (String newNodeKey : newNode.namedListKeys()) {
+                String newNodeInternalId = newNode.internalId(newNodeKey);
+
+                withTracking(newNodeInternalId, false, false, () -> {
+                    InnerNode newNamedElement = newNode.get(newNodeKey);
+
+                    String oldNodeKey = oldNode.keyByInternalId(newNodeInternalId);
+                    InnerNode oldNamedElement = oldNode.get(oldNodeKey);
 
                     // Deletion of nonexistent element.
                     if (oldNamedElement == null && newNamedElement == null)
@@ -173,8 +177,8 @@ public class ConfigurationFlattener {
                         }
                     }
 
-                    Integer newIdx = newKeysToOrderIdxMap == null ? null : newKeysToOrderIdxMap.get(namedListKey);
-                    Integer oldIdx = oldKeysToOrderIdxMap == null ? null : oldKeysToOrderIdxMap.get(namedListKey);
+                    Integer newIdx = newKeysToOrderIdxMap == null ? null : newKeysToOrderIdxMap.get(newNodeKey);
+                    Integer oldIdx = oldKeysToOrderIdxMap == null ? null : oldKeysToOrderIdxMap.get(newNodeKey);
 
                     // We should "persist" changed indexes only.
                     if (newIdx != oldIdx || singleTreeTraversal || newNamedElement == null) {
@@ -183,6 +187,15 @@ public class ConfigurationFlattener {
                         resMap.put(orderKey, deletion || newNamedElement == null ? null : newIdx);
                     }
 
+                    // If it's creation / deletion / rename.
+                    if (singleTreeTraversal || oldNamedElement == null || newNamedElement == null
+                        || !oldNodeKey.equals(newNodeKey)
+                    ) {
+                        String idKey = currentKey() + NamedListNode.NAME;
+
+                        resMap.put(idKey, deletion || newNamedElement == null ? null : newNodeKey);
+                    }
+
                     return null;
                 });
             }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationNotificationsUtil.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationNotificationsUtil.java
index 34b0558..48b6cfa 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationNotificationsUtil.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationNotificationsUtil.java
@@ -18,15 +18,17 @@
 package org.apache.ignite.internal.configuration.util;
 
 import java.io.Serializable;
+import java.util.HashMap;
 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 java.util.function.BiFunction;
 import org.apache.ignite.configuration.ConfigurationProperty;
 import org.apache.ignite.configuration.NamedListView;
 import org.apache.ignite.configuration.notifications.ConfigurationListener;
+import org.apache.ignite.configuration.notifications.ConfigurationNamedListListener;
 import org.apache.ignite.configuration.notifications.ConfigurationNotificationEvent;
 import org.apache.ignite.internal.configuration.DynamicConfiguration;
 import org.apache.ignite.internal.configuration.DynamicProperty;
@@ -60,7 +62,14 @@ public class ConfigurationNotificationsUtil {
         if (oldInnerNode == null || oldInnerNode == newInnerNode)
             return;
 
-        notifyPublicListeners(cfgNode.listeners(), oldInnerNode, newInnerNode, storageRevision, futures);
+        notifyPublicListeners(
+            cfgNode.listeners(),
+            oldInnerNode,
+            newInnerNode,
+            storageRevision,
+            futures,
+            ConfigurationListener::onUpdate
+        );
 
         oldInnerNode.traverseChildren(new ConfigurationVisitor<Void>() {
             /** {@inheritDoc} */
@@ -70,7 +79,14 @@ public class ConfigurationNotificationsUtil {
                 if (newLeaf != oldLeaf) {
                     var dynProperty = (DynamicProperty<Serializable>)cfgNode.members().get(key);
 
-                    notifyPublicListeners(dynProperty.listeners(), oldLeaf, newLeaf, storageRevision, futures);
+                    notifyPublicListeners(
+                        dynProperty.listeners(),
+                        oldLeaf,
+                        newLeaf,
+                        storageRevision,
+                        futures,
+                        ConfigurationListener::onUpdate
+                    );
                 }
 
                 return null;
@@ -94,46 +110,100 @@ public class ConfigurationNotificationsUtil {
                 if (newNamedList != oldNamedList) {
                     var namedListCfg = (NamedListConfiguration<?, InnerNode, ?>)cfgNode.members().get(key);
 
-                    notifyPublicListeners(namedListCfg.listeners(), (NamedListView<InnerNode>)oldNamedList, newNamedList, storageRevision, futures);
+                    notifyPublicListeners(
+                        namedListCfg.listeners(),
+                        (NamedListView<InnerNode>)oldNamedList,
+                        newNamedList,
+                        storageRevision,
+                        futures,
+                        ConfigurationListener::onUpdate
+                    );
 
                     // This is optimization, we could use "NamedListConfiguration#get" directly, but we don't want to.
-
                     List<String> oldNames = oldNamedList.namedListKeys();
                     List<String> newNames = newNamedList.namedListKeys();
 
+                    //TODO https://issues.apache.org/jira/browse/IGNITE-15193
                     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());
+                    Set<String> deleted = new HashSet<>(oldNames);
+                    deleted.removeAll(newNames);
 
-                        for (String name : created)
-                            notifyPublicListeners(list, null, newNamedList.get(name), storageRevision, futures);
+                    Map<String, String> renamed = new HashMap<>();
+                    if (!created.isEmpty() && !deleted.isEmpty()) {
+                        Map<String, String> createdIds = new HashMap<>();
+
+                        for (String createdKey : created)
+                            createdIds.put(newNamedList.internalId(createdKey), createdKey);
+
+                        // Avoiding ConcurrentModificationException.
+                        for (String deletedKey : Set.copyOf(deleted)) {
+                            String internalId = oldNamedList.internalId(deletedKey);
+
+                            String maybeRenamedKey = createdIds.get(internalId);
+
+                            if (maybeRenamedKey == null)
+                                continue;
+
+                            deleted.remove(deletedKey);
+                            created.remove(maybeRenamedKey);
+                            renamed.put(deletedKey, maybeRenamedKey);
+                        }
                     }
 
-                    Set<String> deleted = new HashSet<>(oldNames);
-                    deleted.removeAll(newNames);
+                    if (!created.isEmpty()) {
+                        for (String name : created)
+                            notifyPublicListeners(
+                                namedListCfg.extendedListeners(),
+                                null,
+                                newNamedList.get(name),
+                                storageRevision,
+                                futures,
+                                ConfigurationNamedListListener::onCreate
+                            );
+                    }
 
                     if (!deleted.isEmpty()) {
-                        List<ConfigurationListener<InnerNode>> list = namedListCfg.extendedListeners()
-                            .stream()
-                            .map(l -> (ConfigurationListener<InnerNode>)l::onDelete)
-                            .collect(Collectors.toList());
+                        for (String name : deleted) {
+                            notifyPublicListeners(
+                                namedListCfg.extendedListeners(),
+                                oldNamedList.get(name),
+                                null,
+                                storageRevision,
+                                futures,
+                                ConfigurationNamedListListener::onDelete
+                            );
+                        }
+                    }
 
-                        for (String name : deleted)
-                            notifyPublicListeners(list, oldNamedList.get(name), null, storageRevision, futures);
+                    if (!renamed.isEmpty()) {
+                        for (Map.Entry<String, String> entry : renamed.entrySet()) {
+                            notifyPublicListeners(
+                                namedListCfg.extendedListeners(),
+                                oldNamedList.get(entry.getKey()),
+                                newNamedList.get(entry.getValue()),
+                                storageRevision,
+                                futures,
+                                (listener, evt) -> listener.onRename(entry.getKey(), entry.getValue(), evt)
+                            );
+                        }
                     }
 
                     for (String name : newNames) {
                         if (!oldNames.contains(name))
                             continue;
 
-                        notifyPublicListeners(namedListCfg.extendedListeners(), oldNamedList.get(name), newNamedList.get(name), storageRevision, futures);
+                        notifyPublicListeners(
+                            namedListCfg.extendedListeners(),
+                            oldNamedList.get(name),
+                            newNamedList.get(name),
+                            storageRevision,
+                            futures,
+                            ConfigurationListener::onUpdate
+                        );
 
                         var dynCfg = (DynamicConfiguration<InnerNode, ?>)namedListCfgMembers.get(name);
 
@@ -149,19 +219,23 @@ public class ConfigurationNotificationsUtil {
     /**
      * 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 updater Update closure to be invoked on the listener instance.
      * @param <V> Type of the node.
+     * @param <L> Type of the configuration listener.
      */
-    private static <V> void notifyPublicListeners(
-        List<? extends ConfigurationListener<V>> listeners,
+    private static <V, L extends ConfigurationListener<V>> void notifyPublicListeners(
+        List<? extends L> listeners,
         V oldVal,
         V newVal,
         long storageRevision,
-        List<CompletableFuture<?>> futures
+        List<CompletableFuture<?>> futures,
+        BiFunction<L, ConfigurationNotificationEvent<V>, CompletableFuture<?>> updater
     ) {
         ConfigurationNotificationEvent<V> evt = new ConfigurationNotificationEventImpl<>(
             oldVal,
@@ -169,9 +243,9 @@ public class ConfigurationNotificationsUtil {
             storageRevision
         );
 
-        for (ConfigurationListener<V> listener : listeners) {
+        for (L listener : listeners) {
             try {
-                CompletableFuture<?> future = listener.onUpdate(evt);
+                CompletableFuture<?> future = updater.apply(listener, evt);
 
                 if (future != null && (future.isCompletedExceptionally() || future.isCancelled() || !future.isDone()))
                     futures.add(future);
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
index 58e3a9d..5b33d68 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
@@ -27,7 +27,6 @@ import java.util.NoSuchElementException;
 import java.util.RandomAccess;
 import java.util.stream.Collectors;
 import org.apache.ignite.configuration.NamedListView;
-import org.apache.ignite.configuration.RootKey;
 import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
 import org.apache.ignite.internal.configuration.tree.ConfigurationVisitor;
 import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
@@ -37,6 +36,9 @@ import org.apache.ignite.internal.configuration.tree.TraversableTreeNode;
 
 /** */
 public class ConfigurationUtil {
+    /** Configuration source that copies values without modifying tham. */
+    private static final ConfigurationSource EMPTY_CFG_SRC = new ConfigurationSource() {};
+
     /**
      * Replaces all {@code .} and {@code \} characters with {@code \.} and {@code \\} respectively.
      *
@@ -263,7 +265,7 @@ public class ConfigurationUtil {
                     assert val == null || val instanceof Map || val instanceof Serializable;
 
                     // Ordering of indexes must be skipped here because they make no sense in this context.
-                    if (key.equals(NamedListNode.ORDER_IDX))
+                    if (key.equals(NamedListNode.ORDER_IDX) || key.equals(NamedListNode.NAME))
                         continue;
 
                     if (val == null)
@@ -289,21 +291,54 @@ public class ConfigurationUtil {
                 var orderedKeys = new ArrayList<>(((NamedListView<?>)node).namedListKeys());
 
                 for (Map.Entry<String, ?> entry : map.entrySet()) {
-                    String key = entry.getKey();
+                    String internalId = entry.getKey();
                     Object val = entry.getValue();
 
                     assert val == null || val instanceof Map || val instanceof Serializable;
 
+                    String oldKey = node.keyByInternalId(internalId);
+
                     if (val == null) {
                         // Given that this particular method is applied to modify existing trees rather than
                         // creating new trees, a "hack" is required in this place. "construct" is designed to create
                         // "change" objects, thus it would just nullify named list element instead of deleting it.
-                        node.forceDelete(key);
+                        node.forceDelete(oldKey);
                     }
                     else if (val instanceof Map) {
+                        Map<String, ?> map = (Map<String, ?>)val;
+                        int sizeDiff = 0;
+
                         // For every named list entry modification we must take its index into account.
                         // We do this by modifying "orderedKeys" when index is explicitly passed.
-                        Object idxObj = ((Map<?, ?>)val).get(NamedListNode.ORDER_IDX);
+                        Object idxObj = map.get(NamedListNode.ORDER_IDX);
+
+                        if (idxObj != null)
+                            sizeDiff++;
+
+                        String newKey = (String)map.get(NamedListNode.NAME);
+
+                        if (newKey != null)
+                            sizeDiff++;
+
+                        boolean construct = map.size() != sizeDiff;
+
+                        if (oldKey == null) {
+                            node.construct(newKey, new InnerConfigurationSource(map));
+
+                            node.setInternalId(newKey, internalId);
+                        }
+                        else if (newKey != null) {
+                            node.rename(oldKey, newKey);
+
+                            if (construct)
+                                node.construct(newKey, new InnerConfigurationSource(map));
+                        }
+                        else if (construct)
+                            node.construct(oldKey, new InnerConfigurationSource(map));
+                        // Else it's just index adjustment after new elements insertion.
+
+                        if (newKey == null)
+                            newKey = oldKey;
 
                         if (idxObj != null) {
                             assert idxObj instanceof Integer : val;
@@ -319,18 +354,16 @@ public class ConfigurationUtil {
                                 while (idx != orderedKeys.size())
                                     orderedKeys.add(null);
 
-                                orderedKeys.add(key);
+                                orderedKeys.add(newKey);
                             }
                             else
-                                orderedKeys.set(idx, key);
+                                orderedKeys.set(idx, newKey);
                         }
-
-                        node.construct(key, new InnerConfigurationSource((Map<String, ?>)val));
                     }
                     else {
                         assert val instanceof Serializable;
 
-                        node.construct(key, new LeafConfigurationSource((Serializable)val));
+                        node.construct(oldKey, new LeafConfigurationSource((Serializable)val));
                     }
                 }
 
@@ -365,111 +398,70 @@ public class ConfigurationUtil {
     }
 
     /**
-     * Convert tree node into patching configuration source.
+     * Fill {@code node} node with default values where nodes are {@code null}.
      *
-     * @param changes Tree node that contains prepared changes.
-     * @return Configuration source.
+     * @param node Node.
      */
-    public static ConfigurationSource nodePatcher(TraversableTreeNode changes) {
-        var scrVisitor = new ConfigurationVisitor<ConfigurationSource>() {
-            @Override public ConfigurationSource visitInnerNode(String key, InnerNode node) {
-                return new PatchInnerConfigurationSource(node);
-            }
+    public static void addDefaults(InnerNode node) {
+        node.traverseChildren(new ConfigurationVisitor<>() {
+            @Override public Object visitLeafNode(String key, Serializable val) {
+                // If source value is null then inititalise the same value on the destination node.
+                if (val == null)
+                    node.constructDefault(key);
 
-            @Override public <N extends InnerNode> ConfigurationSource visitNamedListNode(String key, NamedListNode<N> node) {
-                return new PatchNamedListConfigurationSource(node);
+                return null;
             }
-        };
 
-        return changes.accept(null, scrVisitor);
-    }
+            @Override public Object visitInnerNode(String key, InnerNode innerNode) {
+                // Instantiate field in destination node before doing something else or copy it if it wasn't null.
+                node.construct(key, EMPTY_CFG_SRC);
 
-    /**
-     * Convert root node into patching configuration source for the super root.
-     *
-     * @param rootKey Root key.
-     * @param changes Root node.
-     * @return Configuration source.
-     */
-    public static ConfigurationSource superRootPatcher(RootKey rootKey, TraversableTreeNode changes) {
-        ConfigurationSource rootSrc = nodePatcher(changes);
+                addDefaults(node.traverseChild(key, innerNodeVisitor()));
 
-        return new ConfigurationSource() {
-            @Override public void descend(ConstructableTreeNode node) {
-                node.construct(rootKey.key(), rootSrc);
+                return null;
             }
-        };
-    }
-
-    /**
-     * Apply changes on top of existing node. Creates completely new object while reusing parts of the original tree
-     * that weren't modified.
-     *
-     * @param root Immutable configuration node.
-     * @param changes Change or Init object to be applied.
-     * @param <C> Type of the root.
-     * @return Patched root.
-     */
-    public static <C extends ConstructableTreeNode> C patch(C root, TraversableTreeNode changes) {
-        assert root.getClass() == changes.getClass(); // Yes.
 
-        ConfigurationSource src = nodePatcher(changes);
+            @Override public <N extends InnerNode> Object visitNamedListNode(String key, NamedListNode<N> namedList) {
+                // Copy internal map.
+                node.construct(key, EMPTY_CFG_SRC);
 
-        assert src != null;
+                namedList = (NamedListNode<N>)node.traverseChild(key, namedListNodeVisitor());
 
-        C copy = (C)root.copy();
+                for (String namedListKey : namedList.namedListKeys()) {
+                    if (namedList.get(namedListKey) != null) {
+                        // Copy the element.
+                        namedList.construct(namedListKey, EMPTY_CFG_SRC);
 
-        src.descend(copy);
+                        addDefaults(namedList.get(namedListKey));
+                    }
+                }
 
-        return copy;
+                return null;
+            }
+        });
     }
 
     /**
-     * Fill {@code dst} node with default values, required to complete {@code src} node.
-     * These two objects can be the same, this would mean that all {@code null} values of {@code scr} will be
-     * replaced with defaults if it's possible.
+     * Recursively removes all nullified named list elements.
      *
-     * @param src Source node.
-     * @param dst Destination node.
+     * @param node Inner node for processing.
      */
-    public static void addDefaults(InnerNode src, InnerNode dst) {
-        assert src.getClass() == dst.getClass();
-
-        src.traverseChildren(new ConfigurationVisitor<>() {
-            @Override public Object visitLeafNode(String key, Serializable val) {
-                // If source value is null then inititalise the same value on the destination node.
-                if (val == null)
-                    dst.constructDefault(key);
+    public static void dropNulls(InnerNode node) {
+        node.traverseChildren(new ConfigurationVisitor<>() {
+            @Override public Object visitInnerNode(String key, InnerNode innerNode) {
+                dropNulls(innerNode);
 
                 return null;
             }
 
-            @Override public Object visitInnerNode(String key, InnerNode srcNode) {
-                // Instantiate field in destination node before doing something else.
-                // Not a big deal if it wasn't null.
-                dst.construct(key, new ConfigurationSource() {});
-
-                // Get that inner node from destination to continue the processing.
-                InnerNode dstNode = dst.traverseChild(key, innerNodeVisitor());
+            @Override public <N extends InnerNode> Object visitNamedListNode(String key, NamedListNode<N> namedList) {
+                for (String namedListKey : namedList.namedListKeys()) {
+                    N element = namedList.get(namedListKey);
 
-                // "dstNode" is guaranteed to not be null even if "src" and "dst" match.
-                // Null in "srcNode" means that we should initialize everything that we can in "dstNode"
-                // unconditionally. It's only possible if we pass it as a source as well.
-                addDefaults(srcNode == null ? dstNode : srcNode, dstNode);
-
-                return null;
-            }
-
-            @Override public <N extends InnerNode> Object visitNamedListNode(String key, NamedListNode<N> srcNamedList) {
-                // Here we don't need to preemptively initialise corresponsing field, because it can never be null.
-                NamedListNode<?> dstNamedList = dst.traverseChild(key, namedListNodeVisitor());
-
-                for (String namedListKey : srcNamedList.namedListKeys()) {
-                    // But, in order to get non-null value from "dstNamedList.get(namedListKey)" we must explicitly
-                    // ensure its existance.
-                    dstNamedList.construct(namedListKey, new ConfigurationSource() {});
-
-                    addDefaults(srcNamedList.get(namedListKey), dstNamedList.get(namedListKey));
+                    if (element == null)
+                        namedList.forceDelete(namedListKey);
+                    else
+                        dropNulls(element);
                 }
 
                 return null;
@@ -510,113 +502,4 @@ public class ConfigurationUtil {
             }
         };
     }
-
-    /** @see #patch(ConstructableTreeNode, TraversableTreeNode) */
-    private static class PatchLeafConfigurationSource implements ConfigurationSource {
-        /** */
-        private final Serializable val;
-
-        /**
-         * @param val Value.
-         */
-        PatchLeafConfigurationSource(Serializable val) {
-            this.val = val;
-        }
-
-        /** {@inheritDoc} */
-        @Override public <T> T unwrap(Class<T> clazz) {
-            assert clazz.isInstance(val);
-
-            return clazz.cast(val);
-        }
-
-        /** {@inheritDoc} */
-        @Override public void descend(ConstructableTreeNode node) {
-            throw new UnsupportedOperationException("descend");
-        }
-    }
-
-    /** @see #patch(ConstructableTreeNode, TraversableTreeNode) */
-    private static class PatchInnerConfigurationSource implements ConfigurationSource {
-        /** */
-        private final InnerNode srcNode;
-
-        /**
-         * @param srcNode Inner node.
-         */
-        PatchInnerConfigurationSource(InnerNode srcNode) {
-            this.srcNode = srcNode;
-        }
-
-        /** {@inheritDoc} */
-        @Override public <T> T unwrap(Class<T> clazz) {
-            throw new UnsupportedOperationException("unwrap");
-        }
-
-        /** {@inheritDoc} */
-        @Override public void descend(ConstructableTreeNode dstNode) {
-            assert srcNode.getClass() == dstNode.getClass() : srcNode.getClass() + " : " + dstNode.getClass();
-
-            if (srcNode == dstNode)
-                return;
-
-            srcNode.traverseChildren(new ConfigurationVisitor<>() {
-                @Override public Void visitLeafNode(String key, Serializable val) {
-                    if (val != null)
-                        dstNode.construct(key, new PatchLeafConfigurationSource(val));
-
-                    return null;
-                }
-
-                @Override public Void visitInnerNode(String key, InnerNode node) {
-                    if (node != null)
-                        dstNode.construct(key, new PatchInnerConfigurationSource(node));
-
-                    return null;
-                }
-
-                @Override public <N extends InnerNode> Void visitNamedListNode(String key, NamedListNode<N> node) {
-                    if (node != null)
-                        dstNode.construct(key, new PatchNamedListConfigurationSource(node));
-
-                    return null;
-                }
-            });
-        }
-    }
-
-    /** @see #patch(ConstructableTreeNode, TraversableTreeNode) */
-    private static class PatchNamedListConfigurationSource implements ConfigurationSource {
-        /** */
-        private final NamedListNode<?> srcNode;
-
-        /**
-         * @param srcNode Named list node.
-         */
-        PatchNamedListConfigurationSource(NamedListNode<?> srcNode) {
-            this.srcNode = srcNode;
-        }
-
-        /** {@inheritDoc} */
-        @Override public <T> T unwrap(Class<T> clazz) {
-            throw new UnsupportedOperationException("unwrap");
-        }
-
-        /** {@inheritDoc} */
-        @Override public void descend(ConstructableTreeNode dstNode) {
-            assert srcNode.getClass() == dstNode.getClass();
-
-            if (srcNode == dstNode)
-                return;
-
-            for (String key : srcNode.namedListKeys()) {
-                InnerNode node = srcNode.get(key);
-
-                if (node == null)
-                    ((NamedListNode<?>)dstNode).forceDelete(key); // Same as in fillFromPrefixMap.
-                else if (((NamedListView<?>)dstNode).get(key) != node)
-                    dstNode.construct(key, new PatchInnerConfigurationSource(node));
-            }
-        }
-    }
 }
diff --git a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java
index 46c7584..4485a6d 100644
--- a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java
+++ b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java
@@ -43,6 +43,7 @@ import org.apache.ignite.internal.affinity.AffinityManager;
 import org.apache.ignite.internal.affinity.event.AffinityEvent;
 import org.apache.ignite.internal.affinity.event.AffinityEventParameters;
 import org.apache.ignite.internal.configuration.ConfigurationManager;
+import org.apache.ignite.internal.configuration.tree.NamedListNode;
 import org.apache.ignite.internal.configuration.util.ConfigurationUtil;
 import org.apache.ignite.internal.manager.EventListener;
 import org.apache.ignite.internal.manager.IgniteComponent;
@@ -63,6 +64,7 @@ import org.apache.ignite.internal.table.distributed.raft.PartitionListener;
 import org.apache.ignite.internal.table.distributed.storage.InternalTableImpl;
 import org.apache.ignite.internal.table.event.TableEvent;
 import org.apache.ignite.internal.table.event.TableEventParameters;
+import org.apache.ignite.internal.util.ByteUtils;
 import org.apache.ignite.internal.util.Cursor;
 import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.lang.IgniteBiTuple;
@@ -708,26 +710,20 @@ public class TableManager extends Producer<TableEvent, TableEventParameters> imp
      *
      * @return A set of table names.
      */
-    private HashSet<String> tableNamesConfigured() {
+    //TODO This is an egregious violation of encapsulation. Current approach has to be revisited.
+    private Set<String> tableNamesConfigured() {
         IgniteBiTuple<ByteArray, ByteArray> range = toRange(new ByteArray(PUBLIC_PREFIX));
 
-        HashSet<String> tableNames = new HashSet<>();
+        Set<String> tableNames = new HashSet<>();
 
         try (Cursor<Entry> cursor = metaStorageMgr.range(range.get1(), range.get2())) {
             while (cursor.hasNext()) {
                 Entry entry = cursor.next();
 
-                String keyTail = entry.key().toString().substring(PUBLIC_PREFIX.length());
+                List<String> keySplit = ConfigurationUtil.split(entry.key().toString());
 
-                int idx = -1;
-
-                //noinspection StatementWithEmptyBody
-                while ((idx = keyTail.indexOf('.', idx + 1)) > 0 && keyTail.charAt(idx - 1) == '\\')
-                    ;
-
-                String tablName = keyTail.substring(0, idx);
-
-                tableNames.add(ConfigurationUtil.unescape(tablName));
+                if (keySplit.size() == 5 && NamedListNode.NAME.equals(keySplit.get(4)))
+                    tableNames.add(ByteUtils.fromBytes(entry.value()).toString());
             }
         }
         catch (Exception e) {
@@ -846,11 +842,7 @@ public class TableManager extends Producer<TableEvent, TableEventParameters> imp
      * @return True if table configured, false otherwise.
      */
     private boolean isTableConfigured(String name) {
-        return metaStorageMgr.invoke(Conditions.exists(
-            new ByteArray(PUBLIC_PREFIX + ConfigurationUtil.escape(name) + ".name")),
-            Operations.noop(),
-            Operations.noop()
-        ).join();
+        return tableNamesConfigured().contains(name);
     }
 
     /**
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/TableManagerTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/TableManagerTest.java
index aa67576..fe52cab 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/TableManagerTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/TableManagerTest.java
@@ -36,6 +36,7 @@ import org.apache.ignite.internal.affinity.AffinityManager;
 import org.apache.ignite.internal.affinity.event.AffinityEvent;
 import org.apache.ignite.internal.affinity.event.AffinityEventParameters;
 import org.apache.ignite.internal.configuration.ConfigurationManager;
+import org.apache.ignite.internal.configuration.tree.NamedListNode;
 import org.apache.ignite.internal.configuration.util.ConfigurationUtil;
 import org.apache.ignite.internal.manager.EventListener;
 import org.apache.ignite.internal.metastorage.MetaStorageManager;
@@ -49,6 +50,7 @@ import org.apache.ignite.internal.schema.configuration.SchemaConfigurationConver
 import org.apache.ignite.internal.schema.event.SchemaEvent;
 import org.apache.ignite.internal.schema.event.SchemaEventParameters;
 import org.apache.ignite.internal.table.distributed.TableManager;
+import org.apache.ignite.internal.util.ByteUtils;
 import org.apache.ignite.internal.util.Cursor;
 import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.lang.IgniteLogger;
@@ -67,6 +69,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.platform.commons.util.ReflectionUtils;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -87,6 +91,7 @@ import static org.mockito.Mockito.when;
  * Tests scenarios for table manager.
  */
 @ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
 public class TableManagerTest {
     /** The logger. */
     private static final IgniteLogger LOG = IgniteLogger.forClass(TableManagerTest.class);
@@ -488,8 +493,9 @@ public class TableManagerTest {
 
                     Entry mockEntry = mock(Entry.class);
 
-                    when(mockEntry.key()).thenReturn(new ByteArray(PUBLIC_PREFIX +
-                        ConfigurationUtil.escape(schemaTable.canonicalName()) + ".name"));
+                    when(mockEntry.key()).thenReturn(new ByteArray(PUBLIC_PREFIX + "uuid." + NamedListNode.NAME));
+
+                    when(mockEntry.value()).thenReturn(ByteUtils.toBytes(schemaTable.canonicalName()));
 
                     when(cursor.next()).thenReturn(mockEntry);