You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ag...@apache.org on 2021/05/19 15:27:11 UTC

[ignite-3] branch ignite-14389 updated (c8e8014 -> 479ea87)

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

agura pushed a change to branch ignite-14389
in repository https://gitbox.apache.org/repos/asf/ignite-3.git.


 discard c8e8014  IGNITE-14389 Code review fixes
 discard e5056f3  IGNITE-14389 Code review fixes
    omit 9ec80e7  IGNITE-14389 Fixed tests
    omit f43ed26  IGNITE-14389 Fixed Idea inspections
    omit c55b8d8  IGNITE-14398 Fixed javadoc
    omit 743605b  IGNITE-14389 Fixed code style
    omit 51d3fb4  IGNITE-14389 MetaStorageService integration
    omit 67128be  WIP
    omit a0febd6  IGNITE-14389 Implemented cursor for ranges and watches.
    omit efe4f16  IGNITE-14389 Added putAll and removeAll. Started cursor management: ranges and watches (WIP)
    omit 54b1c42  IGNITE-14389 putAll initial (WIP)
    omit 7e0547d  IGNITE-14389 getAll and tests (WIP)
    omit 1a1e8be  IGNITE-14389 Added get and do smth semantic
    omit de979bd  IGNITE-14398: Meta storage: added update counter
    omit a1eb463  IGNITE-14389 Meta storage: in-memory implementation WIP
     add 81e5476  IGNITE-14077: Distributed schema manager. (#91)
     new 479ea87  IGNITE-14389 Added in-memory meta storage implementation.

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (c8e8014)
            \
             N -- N -- N   refs/heads/ignite-14389 (479ea87)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../schemas/table/TableConfigurationSchema.java    |   2 +-
 .../main/java/org/apache/ignite/table/Tuple.java   |  18 +
 .../configuration/internal/rest/JsonConverter.java |   2 +-
 .../org/apache/ignite/internal/manager/Event.java  |   2 +-
 .../ignite/internal/manager/EventParameters.java   |   2 +-
 .../apache/ignite/internal/manager/Producer.java   |  12 +-
 .../runner/app/DynamicTableCreationTest.java       | 162 +++++++
 .../internal/runner/app/TableCreationTest.java     | 190 ++++++++
 .../apache/ignite/internal/app/IgnitionImpl.java   |   4 +-
 modules/schema/pom.xml                             |   7 +-
 .../ignite/internal/schema/SchemaDescriptor.java   |  15 +
 .../ignite/internal/schema/SchemaManager.java      | 308 +++++++++++-
 .../ignite/internal/schema/SchemaRegistry.java}    |   8 +-
 .../ignite/internal/schema/event/SchemaEvent.java} |  15 +-
 .../schema/event/SchemaEventParameters.java}       |  42 +-
 .../SchemaRegistrationConflictException.java}      |  13 +-
 .../schema/registry/SchemaRegistryException.java}  |  20 +-
 .../schema/registry/SchemaRegistryImpl.java        | 147 ++++++
 .../internal/schema/registry}/package-info.java    |   4 +-
 .../internal/schema/SchemaRegistryImplTest.java    | 528 +++++++++++++++++++++
 .../ignite/distributed/ITDistributedTableTest.java |   4 +-
 .../schema/marshaller/TupleMarshaller.java         |   2 +-
 .../ignite/internal/table/AbstractTableView.java   |   9 +-
 .../ignite/internal/table/KVBinaryViewImpl.java    |  11 +-
 .../apache/ignite/internal/table/KVViewImpl.java   |   9 +-
 .../ignite/internal/table/RecordViewImpl.java      |   9 +-
 .../ignite/internal/table/RowChunkAdapter.java     |  16 +
 .../apache/ignite/internal/table/TableImpl.java    |  21 +-
 .../ignite/internal/table/TupleBuilderImpl.java    |  12 +
 .../ignite/internal/table/TupleMarshallerImpl.java |  13 +-
 .../internal/table/distributed/TableManager.java   |  76 ++-
 .../internal/table/event/TableEventParameters.java |  14 +-
 .../table/impl/DummySchemaManagerImpl.java         |   4 +-
 33 files changed, 1555 insertions(+), 146 deletions(-)
 create mode 100644 modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/DynamicTableCreationTest.java
 create mode 100644 modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/TableCreationTest.java
 rename modules/{table/src/main/java/org/apache/ignite/internal/table/TableSchemaView.java => schema/src/main/java/org/apache/ignite/internal/schema/SchemaRegistry.java} (85%)
 copy modules/{table/src/main/java/org/apache/ignite/internal/table/event/TableEvent.java => schema/src/main/java/org/apache/ignite/internal/schema/event/SchemaEvent.java} (76%)
 rename modules/{table/src/main/java/org/apache/ignite/internal/table/TableSchemaViewImpl.java => schema/src/main/java/org/apache/ignite/internal/schema/event/SchemaEventParameters.java} (55%)
 copy modules/schema/src/main/java/org/apache/ignite/internal/schema/{InvalidTypeException.java => registry/SchemaRegistrationConflictException.java} (71%)
 copy modules/{cli/src/main/java/org/apache/ignite/cli/IgniteCLIException.java => schema/src/main/java/org/apache/ignite/internal/schema/registry/SchemaRegistryException.java} (69%)
 create mode 100644 modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/SchemaRegistryImpl.java
 copy modules/{cli/src/main/java/org/apache/ignite/cli/builtins/init => schema/src/main/java/org/apache/ignite/internal/schema/registry}/package-info.java (86%)
 create mode 100644 modules/schema/src/test/java/org/apache/ignite/internal/schema/SchemaRegistryImplTest.java

[ignite-3] 01/01: IGNITE-14389 Added in-memory meta storage implementation.

Posted by ag...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 479ea87913ca6e46911046222e2144c92b6e1f2e
Author: Andrey Gura <ag...@apache.org>
AuthorDate: Tue Mar 30 21:21:07 2021 +0300

    IGNITE-14389 Added in-memory meta storage implementation.
---
 .../ignite/internal/affinity/AffinityManager.java  |   34 +-
 .../org/apache/ignite/internal/util}/Cursor.java   |   10 +-
 .../java/org/apache/ignite/lang/ByteArray.java     |   17 +-
 modules/metastorage-client/pom.xml                 |    6 +
 .../client/ITMetaStorageServiceTest.java           |  907 +++++----
 .../internal/metastorage/client/CursorImpl.java    |   84 +-
 .../internal/metastorage/client/EntryImpl.java}    |   73 +-
 .../metastorage/client/MetaStorageServiceImpl.java |  268 ++-
 .../metastorage/client}/CompactedException.java    |    2 +-
 .../ignite/metastorage/client/Condition.java       |  347 ++++
 .../ignite/metastorage/client}/Conditions.java     |   48 +-
 .../apache/ignite/metastorage/client}/Entry.java   |   19 +-
 .../ignite/metastorage/client/EntryEvent.java}     |   24 +-
 .../org/apache/ignite/metastorage/client}/Key.java |    2 +-
 .../metastorage/client/MetaStorageService.java     |  123 +-
 .../ignite/metastorage/client/Operation.java       |  166 ++
 .../client}/OperationTimeoutException.java         |    2 +-
 .../ignite/metastorage/client}/Operations.java     |   14 +-
 .../ignite/metastorage/client/WatchEvent.java      |   82 +
 .../ignite/metastorage/client}/WatchListener.java  |    6 +-
 .../internal/metastorage/common/ConditionType.java |   53 +
 .../metastorage/common/OperationType.java}         |   17 +-
 .../metastorage/common/command/ConditionInfo.java  |   89 +
 .../metastorage/common/command/ErrorResponse.java} |   33 +-
 .../metastorage/common/command/GetAllCommand.java  |   34 +-
 .../common/command/GetAndPutAllCommand.java        |   28 +-
 .../common/command/GetAndPutCommand.java           |   13 +-
 .../common/command/GetAndRemoveAllCommand.java     |   23 +-
 .../common/command/GetAndRemoveCommand.java        |   13 +-
 .../metastorage/common/command/GetCommand.java     |   22 +-
 .../metastorage/common/command/InvokeCommand.java  |   75 +
 ...moveCommand.java => MultipleEntryResponse.java} |   30 +-
 .../{PutCommand.java => OperationInfo.java}        |   54 +-
 .../metastorage/common/command/PutAllCommand.java  |   51 +-
 .../metastorage/common/command/PutCommand.java     |   13 +-
 .../metastorage/common/command/RangeCommand.java   |   29 +-
 .../common/command/RemoveAllCommand.java           |   27 +-
 .../metastorage/common/command/RemoveCommand.java  |   13 +-
 .../common/command/SingleEntryResponse.java        |   92 +
 .../common/command/WatchExactKeysCommand.java      |   29 +-
 .../common/command/WatchRangeKeysCommand.java      |   30 +-
 .../common/command/cursor/CursorCloseCommand.java  |    3 +-
 .../command/cursor/CursorHasNextCommand.java       |    3 +-
 .../common/command/cursor/CursorNextCommand.java   |    3 +-
 .../ignite/metastorage/common/Condition.java       |  370 ----
 .../ignite/metastorage/common/KeyValueStorage.java |   91 -
 .../metastorage/common/KeyValueStorageImpl.java    |  183 --
 .../ignite/metastorage/common/Operation.java       |   93 -
 modules/metastorage-server/pom.xml                 |   64 +
 .../metastorage/server/AbstractCondition.java}     |   27 +-
 .../metastorage/server}/CompactedException.java    |    2 +-
 .../internal/metastorage/server/Condition.java}    |   26 +-
 .../ignite/internal/metastorage/server/Entry.java  |  190 ++
 .../internal/metastorage/server/EntryEvent.java}   |   44 +-
 .../metastorage/server/ExistenceCondition.java     |   74 +
 .../metastorage/server/KeyValueStorage.java        |  206 +++
 .../internal/metastorage/server/Operation.java     |  100 +
 .../metastorage/server/RevisionCondition.java      |  110 ++
 .../server/SimpleInMemoryKeyValueStorage.java      |  775 ++++++++
 .../ignite/internal/metastorage/server/Value.java} |   55 +-
 .../metastorage/server/ValueCondition.java         |   82 +
 .../internal/metastorage/server/WatchEvent.java    |   72 +
 .../server}/raft/MetaStorageCommandListener.java   |  229 ++-
 .../metastorage/server/ExistenceConditionTest.java |   71 +
 .../metastorage/server/RevisionConditionTest.java  |  113 ++
 .../server/SimpleInMemoryKeyValueStorageTest.java  | 1945 ++++++++++++++++++++
 .../metastorage/server/ValueConditionTest.java     |   60 +
 modules/metastorage/pom.xml                        |    4 +-
 .../internal/metastorage/MetaStorageManager.java   |  173 +-
 .../metastorage/watch/AggregatedWatch.java         |    2 +-
 .../internal/metastorage/watch/KeyCriterion.java   |   44 +-
 .../metastorage/watch/WatchAggregator.java         |   49 +-
 .../internal/metastorage/WatchAggregatorTest.java  |  102 +-
 .../apache/ignite/internal/app/IgnitionImpl.java   |    5 +-
 .../storage/DistributedConfigurationStorage.java   |   52 +-
 .../ignite/internal/schema/SchemaManager.java      |   47 +-
 .../internal/table/distributed/TableManager.java   |   14 +-
 parent/pom.xml                                     |    6 +
 pom.xml                                            |    1 +
 79 files changed, 6521 insertions(+), 1971 deletions(-)

diff --git a/modules/affinity/src/main/java/org/apache/ignite/internal/affinity/AffinityManager.java b/modules/affinity/src/main/java/org/apache/ignite/internal/affinity/AffinityManager.java
index 6572a43..57f795a 100644
--- a/modules/affinity/src/main/java/org/apache/ignite/internal/affinity/AffinityManager.java
+++ b/modules/affinity/src/main/java/org/apache/ignite/internal/affinity/AffinityManager.java
@@ -33,11 +33,11 @@ import org.apache.ignite.internal.util.ByteUtils;
 import org.apache.ignite.internal.vault.VaultManager;
 import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.lang.IgniteLogger;
-import org.apache.ignite.metastorage.common.Conditions;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.Operations;
-import org.apache.ignite.metastorage.common.WatchEvent;
-import org.apache.ignite.metastorage.common.WatchListener;
+import org.apache.ignite.metastorage.client.Conditions;
+import org.apache.ignite.metastorage.client.EntryEvent;
+import org.apache.ignite.metastorage.client.Operations;
+import org.apache.ignite.metastorage.client.WatchEvent;
+import org.apache.ignite.metastorage.client.WatchListener;
 import org.apache.ignite.network.ClusterNode;
 import org.jetbrains.annotations.NotNull;
 
@@ -85,9 +85,9 @@ public class AffinityManager extends Producer<AffinityEvent, AffinityEventParame
         this.baselineMgr = baselineMgr;
         this.vaultMgr = vaultMgr;
 
-        metaStorageMgr.registerWatchByPrefix(new Key(INTERNAL_PREFIX), new WatchListener() {
-            @Override public boolean onUpdate(@NotNull Iterable<WatchEvent> events) {
-                for (WatchEvent evt : events) {
+        metaStorageMgr.registerWatchByPrefix(new ByteArray(INTERNAL_PREFIX), new WatchListener() {
+            @Override public boolean onUpdate(@NotNull WatchEvent watchEvt) {
+                for (EntryEvent evt : watchEvt.entryEvents()) {
                     String tabIdVal = evt.newEntry().key().toString().substring(INTERNAL_PREFIX.length());
 
                     UUID tblId = UUID.fromString(tabIdVal);
@@ -112,7 +112,7 @@ public class AffinityManager extends Producer<AffinityEvent, AffinityEventParame
             }
 
             @Override public void onError(@NotNull Throwable e) {
-                LOG.error("Metastorage listener issue", e);
+                LOG.error("Meta storage listener issue", e);
             }
         });
     }
@@ -127,17 +127,15 @@ public class AffinityManager extends Producer<AffinityEvent, AffinityEventParame
         return vaultMgr
             .get(ByteArray.fromString(INTERNAL_PREFIX + tblId))
             .thenCompose(entry -> {
-                TableConfiguration tblConfig = configurationMgr
-                    .configurationRegistry()
-                    .getConfiguration(TablesConfiguration.KEY)
-                    .tables()
-                    .get(new String(entry.value(), StandardCharsets.UTF_8));
 
-                var key = new Key(INTERNAL_PREFIX + tblId);
+                            TableConfiguration tblConfig = configurationMgr.configurationRegistry()
+                                    .getConfiguration(TablesConfiguration.KEY).tables().get(new String(entry.value(), StandardCharsets.UTF_8));
+
+                var key = new ByteArray(INTERNAL_PREFIX + tblId);
 
                 // TODO: https://issues.apache.org/jira/browse/IGNITE-14716 Need to support baseline changes.
                 return metaStorageMgr.invoke(
-                    Conditions.key(key).value().eq(null),
+                    Conditions.notExists(key),
                     Operations.put(key, ByteUtils.toBytes(
                         RendezvousAffinityFunction.assignPartitions(
                             baselineMgr.nodes(),
@@ -157,10 +155,10 @@ public class AffinityManager extends Producer<AffinityEvent, AffinityEventParame
      * @return A future which will complete when assignment is removed.
      */
     public CompletableFuture<Boolean> removeAssignment(UUID tblId) {
-        var key = new Key(INTERNAL_PREFIX + tblId);
+        var key = new ByteArray(INTERNAL_PREFIX + tblId);
 
         return metaStorageMgr.invoke(
-            Conditions.key(key).value().ne(null),
+            Conditions.exists(key),
             Operations.remove(key),
             Operations.noop());
     }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java b/modules/core/src/main/java/org/apache/ignite/internal/util/Cursor.java
similarity index 79%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java
copy to modules/core/src/main/java/org/apache/ignite/internal/util/Cursor.java
index dcc1c94..e1ddaf2 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/Cursor.java
@@ -15,13 +15,15 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.internal.util;
+
+import java.util.Iterator;
 
 /**
  * Closeable cursor.
  *
- * @param <T>
+ * @param <T> Type of elements.
  */
-//TODO: Should be replaced by common entity from org.ignite.lang or org.ignite.core package.
-public interface Cursor<T> extends Iterable<T>, AutoCloseable {
+public interface Cursor<T> extends Iterator<T>, Iterable<T>, AutoCloseable {
+
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/lang/ByteArray.java b/modules/core/src/main/java/org/apache/ignite/lang/ByteArray.java
index c38be87..a5552f2 100644
--- a/modules/core/src/main/java/org/apache/ignite/lang/ByteArray.java
+++ b/modules/core/src/main/java/org/apache/ignite/lang/ByteArray.java
@@ -22,15 +22,17 @@ import java.util.Arrays;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * A class for handling byte array.
+ * A class wraps {@code byte[]} which provides {@link Object#equals}, {@link Object#hashCode} and
+ * lexicographical comparison implementation.
  */
 public final class ByteArray implements Comparable<ByteArray> {
-    /** Byte-wise representation of the {@code ByteArray}. */
+    /** Wrapped byte array. */
     @NotNull
     private final byte[] arr;
 
     /**
-     * Constructs {@code ByteArray} instance from the given byte array. <em>Note:</em> copy of the given byte array will not be
+     * Constructs {@code ByteArray} instance from the given byte array.
+     * <em>Note:</em> copy of the given byte array will not be
      * created in order to avoid redundant memory consumption.
      *
      * @param arr Byte array. Can't be {@code null}.
@@ -40,6 +42,15 @@ public final class ByteArray implements Comparable<ByteArray> {
     }
 
     /**
+     * Constructs {@code ByteArray} instance from the given string.
+     *
+     * @param s The string key representation. Can't be {@code null}.
+     */
+    public ByteArray(@NotNull String s) {
+        this(s.getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
      * Constructs {@code ByteArray} instance from the given string. {@link StandardCharsets#UTF_8} charset is used for
      * encoding the input string.
      *
diff --git a/modules/metastorage-client/pom.xml b/modules/metastorage-client/pom.xml
index d45bafd..9cb157f 100644
--- a/modules/metastorage-client/pom.xml
+++ b/modules/metastorage-client/pom.xml
@@ -74,5 +74,11 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-metastorage-server</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/modules/metastorage-client/src/integrationTest/java/org/apache/ignite/internal/metastorage/client/ITMetaStorageServiceTest.java b/modules/metastorage-client/src/integrationTest/java/org/apache/ignite/internal/metastorage/client/ITMetaStorageServiceTest.java
index f929c29..2ffdd30 100644
--- a/modules/metastorage-client/src/integrationTest/java/org/apache/ignite/internal/metastorage/client/ITMetaStorageServiceTest.java
+++ b/modules/metastorage-client/src/integrationTest/java/org/apache/ignite/internal/metastorage/client/ITMetaStorageServiceTest.java
@@ -18,37 +18,40 @@
 package org.apache.ignite.internal.metastorage.client;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.NavigableMap;
+import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
-import org.apache.ignite.internal.metastorage.common.DummyEntry;
-import org.apache.ignite.lang.IgniteLogger;
+import org.apache.ignite.internal.metastorage.common.OperationType;
+import org.apache.ignite.internal.util.Cursor;
 import org.apache.ignite.lang.IgniteUuid;
+import org.apache.ignite.lang.ByteArray;
+import org.apache.ignite.lang.IgniteLogger;
+import org.apache.ignite.internal.metastorage.server.KeyValueStorage;
+import org.apache.ignite.internal.metastorage.server.raft.MetaStorageCommandListener;
+import org.apache.ignite.metastorage.client.CompactedException;
+import org.apache.ignite.metastorage.client.Condition;
+import org.apache.ignite.metastorage.client.Conditions;
+import org.apache.ignite.metastorage.client.Entry;
+import org.apache.ignite.metastorage.client.EntryEvent;
 import org.apache.ignite.metastorage.client.MetaStorageService;
-import org.apache.ignite.metastorage.common.CompactedException;
-import org.apache.ignite.metastorage.common.Condition;
-import org.apache.ignite.metastorage.common.Cursor;
-import org.apache.ignite.metastorage.common.Entry;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.KeyValueStorage;
-import org.apache.ignite.metastorage.common.Operation;
-import org.apache.ignite.metastorage.common.OperationTimeoutException;
-import org.apache.ignite.metastorage.common.WatchEvent;
-import org.apache.ignite.metastorage.common.WatchListener;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.metastorage.client.Operation;
+import org.apache.ignite.metastorage.client.OperationTimeoutException;
+import org.apache.ignite.metastorage.client.Operations;
+import org.apache.ignite.metastorage.client.WatchEvent;
+import org.apache.ignite.metastorage.client.WatchListener;
 import org.apache.ignite.network.ClusterLocalConfiguration;
 import org.apache.ignite.network.ClusterService;
 import org.apache.ignite.network.ClusterServiceFactory;
 import org.apache.ignite.network.message.MessageSerializationRegistry;
-import org.apache.ignite.network.scalecube.TestScaleCubeClusterServiceFactory;
+import org.apache.ignite.network.scalecube.ScaleCubeClusterServiceFactory;
 import org.apache.ignite.network.scalecube.message.ScaleCubeMessage;
 import org.apache.ignite.network.scalecube.message.ScaleCubeMessageSerializationFactory;
 import org.apache.ignite.raft.client.Peer;
@@ -63,7 +66,6 @@ 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 org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
@@ -95,55 +97,76 @@ public class ITMetaStorageServiceTest {
     public static final int LATEST_REVISION = -1;
 
     /** Factory. */
-    private static RaftClientMessageFactory FACTORY = new RaftClientMessageFactoryImpl();
+    private static final RaftClientMessageFactory FACTORY = new RaftClientMessageFactoryImpl();
 
     /** Network factory. */
-    private static final ClusterServiceFactory NETWORK_FACTORY = new TestScaleCubeClusterServiceFactory();
+    private static final ClusterServiceFactory NETWORK_FACTORY = new ScaleCubeClusterServiceFactory();
 
     /** */
+    // TODO: IGNITE-14088 Uncomment and use real serializer provider
+    //    private static final MessageSerializationRegistry SERIALIZATION_REGISTRY = new MessageSerializationRegistry();
     private static final MessageSerializationRegistry SERIALIZATION_REGISTRY = new MessageSerializationRegistry()
-        .registerFactory(ScaleCubeMessage.TYPE, new ScaleCubeMessageSerializationFactory());
-
-    /** Expected result entry. */
-    private static final Entry EXPECTED_RESULT_ENTRY =
-        new DummyEntry(
-            new Key(new byte[] {1}),
-            new byte[] {2},
-            10,
-            2
-        );
+            .registerFactory(ScaleCubeMessage.TYPE, new ScaleCubeMessageSerializationFactory());
+
+    /**  Expected server result entry. */
+    private static final org.apache.ignite.internal.metastorage.server.Entry EXPECTED_SRV_RESULT_ENTRY =
+            new org.apache.ignite.internal.metastorage.server.Entry(
+                    new byte[] {1},
+                    new byte[] {2},
+                    10,
+                    2
+            );
+
+    /**  Expected server result entry. */
+    private static final EntryImpl EXPECTED_RESULT_ENTRY =
+            new EntryImpl(
+                    new ByteArray(new byte[] {1}),
+                    new byte[] {2},
+                    10,
+                    2
+            );
 
     /** Expected result map. */
-    private static final NavigableMap<Key, Entry> EXPECTED_RESULT_MAP;
+    private static final NavigableMap<ByteArray, Entry> EXPECTED_RESULT_MAP;
+
+    private static final Collection<org.apache.ignite.internal.metastorage.server.Entry> EXPECTED_SRV_RESULT_COLL;
 
     /** Cluster. */
     private ArrayList<ClusterService> cluster = new ArrayList<>();
 
-    /** Meta storage raft server. */
+    /**  Meta storage raft server. */
     private RaftServer metaStorageRaftSrv;
 
     static {
         EXPECTED_RESULT_MAP = new TreeMap<>();
 
-        EXPECTED_RESULT_MAP.put(
-            new Key(new byte[] {1}),
-            new DummyEntry(
-                new Key(new byte[] {1}),
-                new byte[] {2},
+        EntryImpl entry1 = new EntryImpl(
+                new ByteArray(new byte[]{1}),
+                new byte[]{2},
                 10,
                 2
-            )
         );
 
-        EXPECTED_RESULT_MAP.put(
-            new Key(new byte[] {3}),
-            new DummyEntry(
-                new Key(new byte[] {3}),
-                new byte[] {4},
+        EXPECTED_RESULT_MAP.put(entry1.key(), entry1);
+
+        EntryImpl entry2 = new EntryImpl(
+                new ByteArray(new byte[]{3}),
+                new byte[]{4},
                 10,
                 3
-            )
         );
+
+        EXPECTED_RESULT_MAP.put(entry2.key(), entry2);
+
+        EXPECTED_SRV_RESULT_COLL = new ArrayList<>();
+
+        EXPECTED_SRV_RESULT_COLL.add(new org.apache.ignite.internal.metastorage.server.Entry(
+                entry1.key().bytes(), entry1.value(), entry1.revision(), entry1.updateCounter()
+        ));
+
+        EXPECTED_SRV_RESULT_COLL.add(new org.apache.ignite.internal.metastorage.server.Entry(
+                entry2.key().bytes(), entry2.value(), entry2.revision(), entry2.updateCounter()
+        ));
     }
 
     /**
@@ -153,11 +176,11 @@ public class ITMetaStorageServiceTest {
     public void beforeTest() {
         for (int i = 0; i < NODES; i++) {
             cluster.add(
-                startClusterNode(
-                    "node_" + i,
-                    NODE_PORT_BASE + i,
-                    IntStream.range(NODE_PORT_BASE, NODE_PORT_BASE + NODES).boxed().
-                        map((port) -> "localhost:" + port).collect(Collectors.toList())));
+                    startClusterNode(
+                            "node_" + i,
+                            NODE_PORT_BASE + i,
+                            IntStream.range(NODE_PORT_BASE, NODE_PORT_BASE + NODES).boxed().
+                                    map((port) -> "localhost:" + port).collect(Collectors.toList())));
         }
 
         for (ClusterService node : cluster)
@@ -180,108 +203,105 @@ public class ITMetaStorageServiceTest {
     }
 
     /**
-     * Tests {@link MetaStorageService#get(Key)}.
+     * Tests {@link MetaStorageService#get(ByteArray)}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testGet() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull Entry get(byte[] key) {
-                    return EXPECTED_RESULT_ENTRY;
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry get(byte[] key) {
+                        return EXPECTED_SRV_RESULT_ENTRY;
+                    }
+                });
 
         assertEquals(EXPECTED_RESULT_ENTRY, metaStorageSvc.get(EXPECTED_RESULT_ENTRY.key()).get());
     }
 
     /**
-     * Tests {@link MetaStorageService#get(Key, long)}.
+     * Tests {@link MetaStorageService#get(ByteArray, long)}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testGetWithUpperBoundRevision() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull Entry get(byte[] key, long rev) {
-                    return EXPECTED_RESULT_ENTRY;
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry get(byte[] key, long rev) {
+                        return EXPECTED_SRV_RESULT_ENTRY;
+                    }
+                });
 
         assertEquals(
-            EXPECTED_RESULT_ENTRY,
-            metaStorageSvc.get(EXPECTED_RESULT_ENTRY.key(), EXPECTED_RESULT_ENTRY.revision()).get()
+                EXPECTED_RESULT_ENTRY,
+                metaStorageSvc.get(EXPECTED_RESULT_ENTRY.key(), EXPECTED_RESULT_ENTRY.revision()).get()
         );
     }
 
     /**
-     * Tests {@link MetaStorageService#getAll(Collection)}.
+     * Tests {@link MetaStorageService#getAll(Set)}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testGetAll() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull List<Entry> getAll(List<byte[]> keys) {
-                    return new ArrayList<>(EXPECTED_RESULT_MAP.values());
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull List<org.apache.ignite.internal.metastorage.server.Entry> getAll(List<byte[]> keys) {
+                        return new ArrayList<>(EXPECTED_SRV_RESULT_COLL);
+                    }
+                });
 
-        assertEquals(
-            EXPECTED_RESULT_MAP,
-            metaStorageSvc.getAll(EXPECTED_RESULT_MAP.keySet()).get()
-        );
+        assertEquals(EXPECTED_RESULT_MAP, metaStorageSvc.getAll(EXPECTED_RESULT_MAP.keySet()).get());
     }
 
     /**
-     * Tests {@link MetaStorageService#getAll(Collection, long)}.
+     * Tests {@link MetaStorageService#getAll(Set, long)}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testGetAllWithUpperBoundRevision() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull List<Entry> getAll(List<byte[]> keys, long revUpperBound) {
-                    return new ArrayList<>(EXPECTED_RESULT_MAP.values());
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull List<org.apache.ignite.internal.metastorage.server.Entry> getAll(List<byte[]> keys, long revUpperBound) {
+                        return new ArrayList<>(EXPECTED_SRV_RESULT_COLL);
+                    }
+                });
 
         assertEquals(
-            EXPECTED_RESULT_MAP,
-            metaStorageSvc.getAll(EXPECTED_RESULT_MAP.keySet(), 10).get()
+                EXPECTED_RESULT_MAP,
+                metaStorageSvc.getAll(EXPECTED_RESULT_MAP.keySet(), 10).get()
         );
     }
 
     /**
-     * Tests {@link MetaStorageService#put(Key, byte[])}.
+     * Tests {@link MetaStorageService#put(ByteArray, byte[])}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testPut() throws Exception {
-        Key expKey = new Key(new byte[]{1});
+        ByteArray expKey = new ByteArray(new byte[]{1});
 
         byte[] expVal = new byte[]{2};
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @SuppressWarnings("JavaAbbreviationUsage")
-                @Override public void put(byte[] key, byte[] value) {
-                    assertArrayEquals(expKey.bytes(), key);
+                new AbstractKeyValueStorage() {
+                    @SuppressWarnings("JavaAbbreviationUsage")
+                    @Override public void put(byte[] key, byte[] value) {
+                        assertArrayEquals(expKey.bytes(), key);
 
-                    assertArrayEquals(expVal, value);
-                }
-            });
+                        assertArrayEquals(expVal, value);
+                    }
+                });
 
         metaStorageSvc.put(expKey, expVal).get();
     }
 
     /**
-     * Tests {@link MetaStorageService#getAndPut(Key, byte[])}.
+     * Tests {@link MetaStorageService#getAndPut(ByteArray, byte[])}.
      *
      * @throws Exception If failed.
      */
@@ -290,20 +310,20 @@ public class ITMetaStorageServiceTest {
         byte[] expVal = new byte[]{2};
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @SuppressWarnings("JavaAbbreviationUsage")
-                @Override public @NotNull Entry getAndPut(byte[] key, byte[] value) {
-                    assertArrayEquals(EXPECTED_RESULT_ENTRY.key().bytes(), key);
+                new AbstractKeyValueStorage() {
+                    @SuppressWarnings("JavaAbbreviationUsage")
+                    @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry getAndPut(byte[] key, byte[] value) {
+                        assertArrayEquals(EXPECTED_RESULT_ENTRY.key().bytes(), key);
 
-                    assertArrayEquals(expVal, value);
+                        assertArrayEquals(expVal, value);
 
-                    return EXPECTED_RESULT_ENTRY;
-                }
-            });
+                        return EXPECTED_SRV_RESULT_ENTRY;
+                    }
+                });
 
         assertEquals(
-            EXPECTED_RESULT_ENTRY,
-            metaStorageSvc.getAndPut(EXPECTED_RESULT_ENTRY.key(), expVal).get()
+                EXPECTED_RESULT_ENTRY,
+                metaStorageSvc.getAndPut(EXPECTED_RESULT_ENTRY.key(), expVal).get()
         );
     }
 
@@ -315,35 +335,35 @@ public class ITMetaStorageServiceTest {
     @Test
     public void testPutAll() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public void putAll(List<byte[]> keys, List<byte[]> values) {
-                    // Assert keys equality.
-                    assertEquals(EXPECTED_RESULT_MAP.keySet().size(), keys.size());
+                new AbstractKeyValueStorage() {
+                    @Override public void putAll(List<byte[]> keys, List<byte[]> values) {
+                        // Assert keys equality.
+                        assertEquals(EXPECTED_RESULT_MAP.keySet().size(), keys.size());
 
-                    List<byte[]> expKeys = EXPECTED_RESULT_MAP.keySet().stream().
-                        map(Key::bytes).collect(Collectors.toList());
+                        List<byte[]> expKeys = EXPECTED_RESULT_MAP.keySet().stream().
+                                map(ByteArray::bytes).collect(Collectors.toList());
 
-                    for (int i = 0; i < expKeys.size(); i++)
-                        assertArrayEquals(expKeys.get(i), keys.get(i));
+                        for (int i = 0; i < expKeys.size(); i++)
+                            assertArrayEquals(expKeys.get(i), keys.get(i));
 
 
-                    // Assert values equality.
-                    assertEquals(EXPECTED_RESULT_MAP.values().size(), values.size());
+                        // Assert values equality.
+                        assertEquals(EXPECTED_RESULT_MAP.values().size(), values.size());
 
-                    List<byte[]> expVals = EXPECTED_RESULT_MAP.values().stream().
-                        map(Entry::value).collect(Collectors.toList());
+                        List<byte[]> expVals = EXPECTED_RESULT_MAP.values().stream().
+                                map(Entry::value).collect(Collectors.toList());
 
-                    for (int i = 0; i < expKeys.size(); i++)
-                        assertArrayEquals(expVals.get(i), values.get(i));
-                }
-            });
+                        for (int i = 0; i < expKeys.size(); i++)
+                            assertArrayEquals(expVals.get(i), values.get(i));
+                    }
+                });
 
         metaStorageSvc.putAll(
-            EXPECTED_RESULT_MAP.entrySet().stream()
-                .collect(Collectors.toMap(
-                    Map.Entry::getKey,
-                    e -> e.getValue().value())
-                )
+                EXPECTED_RESULT_MAP.entrySet().stream()
+                        .collect(Collectors.toMap(
+                                Map.Entry::getKey,
+                                e -> e.getValue().value())
+                        )
         ).get();
     }
 
@@ -355,297 +375,349 @@ public class ITMetaStorageServiceTest {
     @Test
     public void testGetAndPutAll() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull List<Entry> getAndPutAll(List<byte[]> keys, List<byte[]> values) {
-                    // Assert keys equality.
-                    assertEquals(EXPECTED_RESULT_MAP.keySet().size(), keys.size());
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull List<org.apache.ignite.internal.metastorage.server.Entry> getAndPutAll(List<byte[]> keys, List<byte[]> values) {
+                        // Assert keys equality.
+                        assertEquals(EXPECTED_RESULT_MAP.keySet().size(), keys.size());
 
-                    List<byte[]> expKeys = EXPECTED_RESULT_MAP.keySet().stream().
-                        map(Key::bytes).collect(Collectors.toList());
+                        List<byte[]> expKeys = EXPECTED_RESULT_MAP.keySet().stream().
+                                map(ByteArray::bytes).collect(Collectors.toList());
 
-                    for (int i = 0; i < expKeys.size(); i++)
-                        assertArrayEquals(expKeys.get(i), keys.get(i));
+                        for (int i = 0; i < expKeys.size(); i++)
+                            assertArrayEquals(expKeys.get(i), keys.get(i));
 
-                    // Assert values equality.
-                    assertEquals(EXPECTED_RESULT_MAP.values().size(), values.size());
+                        // Assert values equality.
+                        assertEquals(EXPECTED_RESULT_MAP.values().size(), values.size());
 
-                    List<byte[]> expVals = EXPECTED_RESULT_MAP.values().stream().
-                        map(Entry::value).collect(Collectors.toList());
+                        List<byte[]> expVals = EXPECTED_RESULT_MAP.values().stream().
+                                map(Entry::value).collect(Collectors.toList());
 
-                    for (int i = 0; i < expKeys.size(); i++)
-                        assertArrayEquals(expVals.get(i), values.get(i));
+                        for (int i = 0; i < expKeys.size(); i++)
+                            assertArrayEquals(expVals.get(i), values.get(i));
 
-                    return new ArrayList<>(EXPECTED_RESULT_MAP.values());
-                }
-            });
+                        return new ArrayList<>(EXPECTED_SRV_RESULT_COLL);
+                    }
+                });
 
-        Map<Key, Entry> gotRes = metaStorageSvc.getAndPutAll(
-            EXPECTED_RESULT_MAP.entrySet().stream()
-                .collect(Collectors.toMap(
-                    Map.Entry::getKey,
-                    e -> e.getValue().value())
-                )
+        Map<ByteArray, Entry> gotRes = metaStorageSvc.getAndPutAll(
+                EXPECTED_RESULT_MAP.entrySet().stream()
+                        .collect(Collectors.toMap(
+                                Map.Entry::getKey,
+                                e -> e.getValue().value())
+                        )
         ).get();
 
         assertEquals(EXPECTED_RESULT_MAP, gotRes);
     }
 
     /**
-     * Tests {@link MetaStorageService#remove(Key)}.
+     * Tests {@link MetaStorageService#remove(ByteArray)}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testRemove() throws Exception {
-        Key expKey = new Key(new byte[]{1});
+        ByteArray expKey = new ByteArray(new byte[]{1});
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public void remove(byte[] key) {
-                    assertArrayEquals(expKey.bytes(), key);
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public void remove(byte[] key) {
+                        assertArrayEquals(expKey.bytes(), key);
+                    }
+                });
 
         metaStorageSvc.remove(expKey).get();
     }
 
     /**
-     * Tests {@link MetaStorageService#getAndRemove(Key)}.
+     * Tests {@link MetaStorageService#getAndRemove(ByteArray)}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testGetAndRemove() throws Exception {
-        Entry expRes = new DummyEntry(
-            new Key(new byte[]{1}),
-            new byte[]{3},
-            10,
-            2
+        EntryImpl expRes = new EntryImpl(
+                new ByteArray(new byte[]{1}),
+                new byte[]{3},
+                10,
+                2
         );
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull Entry getAndRemove(byte[] key) {
-                    assertArrayEquals(expRes.key().bytes(), key);
-
-                    return expRes;
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry getAndRemove(byte[] key) {
+                        assertArrayEquals(expRes.key().bytes(), key);
+
+                        return new org.apache.ignite.internal.metastorage.server.Entry(
+                                expRes.key().bytes(),
+                                expRes.value(),
+                                expRes.revision(),
+                                expRes.updateCounter()
+                        );
+                    }
+                });
 
         assertEquals(expRes, metaStorageSvc.getAndRemove(expRes.key()).get());
     }
 
     /**
-     * Tests {@link MetaStorageService#removeAll(Collection)}.
+     * Tests {@link MetaStorageService#removeAll(Set)}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testRemoveAll() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public void removeAll(List<byte[]> keys) {
-                    assertEquals(EXPECTED_RESULT_MAP.keySet().size(), keys.size());
+                new AbstractKeyValueStorage() {
+                    @Override public void removeAll(List<byte[]> keys) {
+                        assertEquals(EXPECTED_RESULT_MAP.keySet().size(), keys.size());
 
-                    List<byte[]> expKeys = EXPECTED_RESULT_MAP.keySet().stream().
-                        map(Key::bytes).collect(Collectors.toList());
+                        List<byte[]> expKeys = EXPECTED_RESULT_MAP.keySet().stream().
+                                map(ByteArray::bytes).collect(Collectors.toList());
 
-                    for (int i = 0; i < expKeys.size(); i++)
-                        assertArrayEquals(expKeys.get(i), keys.get(i));
-                }
-            });
+                        for (int i = 0; i < expKeys.size(); i++)
+                            assertArrayEquals(expKeys.get(i), keys.get(i));
+                    }
+                });
 
         metaStorageSvc.removeAll(EXPECTED_RESULT_MAP.keySet()).get();
     }
 
     /**
-     * Tests {@link MetaStorageService#getAndRemoveAll(Collection)}.
+     * Tests {@link MetaStorageService#getAndRemoveAll(Set)}.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testGetAndRemoveAll() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull List<Entry> getAndRemoveAll(List<byte[]> keys) {
-                    // Assert keys equality.
-                    assertEquals(EXPECTED_RESULT_MAP.keySet().size(), keys.size());
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull List<org.apache.ignite.internal.metastorage.server.Entry> getAndRemoveAll(List<byte[]> keys) {
+                        // Assert keys equality.
+                        assertEquals(EXPECTED_RESULT_MAP.keySet().size(), keys.size());
 
-                    List<byte[]> expKeys = EXPECTED_RESULT_MAP.keySet().stream().
-                        map(Key::bytes).collect(Collectors.toList());
+                        List<byte[]> expKeys = EXPECTED_RESULT_MAP.keySet().stream().
+                                map(ByteArray::bytes).collect(Collectors.toList());
 
-                    for (int i = 0; i < expKeys.size(); i++)
-                        assertArrayEquals(expKeys.get(i), keys.get(i));
+                        for (int i = 0; i < expKeys.size(); i++)
+                            assertArrayEquals(expKeys.get(i), keys.get(i));
 
-                    return new ArrayList<>(EXPECTED_RESULT_MAP.values());
-                }
-            });
+                        return new ArrayList<>(EXPECTED_SRV_RESULT_COLL);
+                    }
+                });
 
-        Map<Key, Entry> gotRes = metaStorageSvc.getAndRemoveAll(EXPECTED_RESULT_MAP.keySet()).get();
+        Map<ByteArray, Entry> gotRes = metaStorageSvc.getAndRemoveAll(EXPECTED_RESULT_MAP.keySet()).get();
 
         assertEquals(EXPECTED_RESULT_MAP, gotRes);
     }
 
     /**
-     * Tests {@link MetaStorageService#range(Key, Key, long)}} with not null keyTo and explicit revUpperBound.
+     * Tests {@link MetaStorageService#range(ByteArray, ByteArray, long)}} with not null keyTo and explicit revUpperBound.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testRangeWitKeyToAndUpperBound() throws Exception {
-        Key expKeyFrom = new Key(new byte[]{1});
+        ByteArray expKeyFrom = new ByteArray(new byte[]{1});
 
-        Key expKeyTo = new Key(new byte[]{3});
+        ByteArray expKeyTo = new ByteArray(new byte[]{3});
 
         long expRevUpperBound = 10;
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
-                    assertArrayEquals(expKeyFrom.bytes(), keyFrom);
-
-                    assertArrayEquals(expKeyTo.bytes(), keyTo);
-
-                    assertEquals(expRevUpperBound, revUpperBound);
+                new AbstractKeyValueStorage() {
+                    @Override public Cursor<org.apache.ignite.internal.metastorage.server.Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
+                        assertArrayEquals(expKeyFrom.bytes(), keyFrom);
 
-                    return new Cursor<>() {
-                        @Override public void close() throws Exception {
+                        assertArrayEquals(expKeyTo.bytes(), keyTo);
 
-                        }
+                        assertEquals(expRevUpperBound, revUpperBound);
 
-                        @NotNull @Override public Iterator<Entry> iterator() {
-                            return new Iterator<>() {
+                        return new Cursor<>() {
+                            private final Iterator<org.apache.ignite.internal.metastorage.server.Entry> it = new Iterator<>() {
                                 @Override public boolean hasNext() {
                                     return false;
                                 }
 
-                                @Override public Entry next() {
+                                @Override public org.apache.ignite.internal.metastorage.server.Entry next() {
                                     return null;
                                 }
                             };
-                        }
-                    };
-                }
-            });
+
+
+                            @Override public void close() throws Exception {
+
+                            }
+
+                            @NotNull @Override public Iterator<org.apache.ignite.internal.metastorage.server.Entry> iterator() {
+                                return it;
+                            }
+
+                            @Override
+                            public boolean hasNext() {
+                                return it.hasNext();
+                            }
+
+                            @Override
+                            public org.apache.ignite.internal.metastorage.server.Entry next() {
+                                return it.next();
+                            }
+                        };
+                    }
+                });
 
         metaStorageSvc.range(expKeyFrom, expKeyTo, expRevUpperBound).close();
     }
 
     /**
-     * Tests {@link MetaStorageService#range(Key, Key, long)}} with not null keyTo.
+     * Tests {@link MetaStorageService#range(ByteArray, ByteArray, long)}} with not null keyTo.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testRangeWitKeyTo() throws Exception {
-        Key expKeyFrom = new Key(new byte[]{1});
+        ByteArray expKeyFrom = new ByteArray(new byte[]{1});
 
-        Key expKeyTo = new Key(new byte[]{3});
+        ByteArray expKeyTo = new ByteArray(new byte[]{3});
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
-                    assertArrayEquals(expKeyFrom.bytes(), keyFrom);
+                new AbstractKeyValueStorage() {
+                    @Override public Cursor<org.apache.ignite.internal.metastorage.server.Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
+                        assertArrayEquals(expKeyFrom.bytes(), keyFrom);
 
-                    assertArrayEquals(expKeyTo.bytes(), keyTo);
+                        assertArrayEquals(expKeyTo.bytes(), keyTo);
 
-                    assertEquals(LATEST_REVISION, revUpperBound);
+                        assertEquals(LATEST_REVISION, revUpperBound);
 
-                    return new Cursor<>() {
-                        @Override public void close() throws Exception {
-
-                        }
-
-                        @NotNull @Override public Iterator<Entry> iterator() {
-                            return new Iterator<>() {
-                                @Override public boolean hasNext() {
+                        return new Cursor<>() {
+                            private final Iterator<org.apache.ignite.internal.metastorage.server.Entry> it = new Iterator<>() {
+                                @Override
+                                public boolean hasNext() {
                                     return false;
                                 }
 
-                                @Override public Entry next() {
+                                @Override
+                                public org.apache.ignite.internal.metastorage.server.Entry next() {
                                     return null;
                                 }
                             };
-                        }
-                    };
-                }
-            });
+
+                            @Override public void close() throws Exception {
+
+                            }
+
+                            @NotNull @Override public Iterator<org.apache.ignite.internal.metastorage.server.Entry> iterator() {
+                                return it;
+                            }
+
+                            @Override public boolean hasNext() {
+                                return it.hasNext();
+                            }
+
+                            @Override public org.apache.ignite.internal.metastorage.server.Entry next() {
+                                return it.next();
+                            }
+                        };
+                    }
+                });
 
         metaStorageSvc.range(expKeyFrom, expKeyTo).close();
     }
 
     /**
-     * Tests {@link MetaStorageService#range(Key, Key, long)}} with null keyTo.
+     * Tests {@link MetaStorageService#range(ByteArray, ByteArray, long)}} with null keyTo.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testRangeWitNullAsKeyTo() throws Exception {
-        Key expKeyFrom = new Key(new byte[]{1});
+        ByteArray expKeyFrom = new ByteArray(new byte[]{1});
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
-                    assertArrayEquals(expKeyFrom.bytes(), keyFrom);
-
-                    assertNull(keyTo);
-
-                    assertEquals(LATEST_REVISION, revUpperBound);
+                new AbstractKeyValueStorage() {
+                    @Override public Cursor<org.apache.ignite.internal.metastorage.server.Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
+                        assertArrayEquals(expKeyFrom.bytes(), keyFrom);
 
-                    return new Cursor<>() {
-                        @Override public void close() throws Exception {
+                        assertNull(keyTo);
 
-                        }
+                        assertEquals(LATEST_REVISION, revUpperBound);
 
-                        @NotNull @Override public Iterator<Entry> iterator() {
-                            return new Iterator<>() {
+                        return new Cursor<>() {
+                            private final Iterator<org.apache.ignite.internal.metastorage.server.Entry> it = new Iterator<>() {
                                 @Override public boolean hasNext() {
                                     return false;
                                 }
 
-                                @Override public Entry next() {
+                                @Override public org.apache.ignite.internal.metastorage.server.Entry next() {
                                     return null;
                                 }
                             };
-                        }
-                    };
-                }
-            });
+
+                            @Override public void close() throws Exception {
+
+                            }
+
+                            @NotNull @Override public Iterator<org.apache.ignite.internal.metastorage.server.Entry> iterator() {
+                                return it;
+                            }
+
+                            @Override public boolean hasNext() {
+                                return it.hasNext();
+                            }
+
+                            @Override
+                            public org.apache.ignite.internal.metastorage.server.Entry next() {
+                                return it.next();
+                            }
+                        };
+                    }
+                });
 
         metaStorageSvc.range(expKeyFrom, null).close();
     }
 
     /**
-     * Tests {@link MetaStorageService#range(Key, Key, long)}} hasNext.
+     * Tests {@link MetaStorageService#range(ByteArray, ByteArray, long)}} hasNext.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testRangeHasNext() throws Exception {
-        Key expKeyFrom = new Key(new byte[]{1});
+        ByteArray expKeyFrom = new ByteArray(new byte[]{1});
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
-                    return new Cursor<>() {
-                        @Override public void close() throws Exception {
-
-                        }
-
-                        @NotNull @Override public Iterator<Entry> iterator() {
-                            return new Iterator<>() {
+                new AbstractKeyValueStorage() {
+                    @Override public Cursor<org.apache.ignite.internal.metastorage.server.Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
+                        return new Cursor<>() {
+                            private final Iterator<org.apache.ignite.internal.metastorage.server.Entry> it = new Iterator<>() {
                                 @Override public boolean hasNext() {
                                     return true;
                                 }
 
-                                @Override public Entry next() {
+                                @Override public org.apache.ignite.internal.metastorage.server.Entry next() {
                                     return null;
                                 }
                             };
-                        }
-                    };
-                }
-            });
+
+                            @Override public void close() throws Exception {
+
+                            }
+
+                            @NotNull @Override public Iterator<org.apache.ignite.internal.metastorage.server.Entry> iterator() {
+                                return it;
+                            }
+
+                            @Override public boolean hasNext() {
+                                return it.hasNext();
+                            }
+
+                            @Override
+                            public org.apache.ignite.internal.metastorage.server.Entry next() {
+                                return it.next();
+                            }
+                        };
+                    }
+                });
 
         Cursor<Entry> cursor = metaStorageSvc.range(expKeyFrom, null);
 
@@ -653,34 +725,45 @@ public class ITMetaStorageServiceTest {
     }
 
     /**
-     * Tests {@link MetaStorageService#range(Key, Key, long)}} next.
+     * Tests {@link MetaStorageService#range(ByteArray, ByteArray, long)}} next.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testRangeNext() throws Exception {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
-                    return new Cursor<>() {
-                        @Override public void close() throws Exception {
-
-                        }
-
-                        @NotNull @Override public Iterator<Entry> iterator() {
-                            return new Iterator<>() {
+                new AbstractKeyValueStorage() {
+                    @Override public Cursor<org.apache.ignite.internal.metastorage.server.Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
+                        return new Cursor<>() {
+                            private final Iterator<org.apache.ignite.internal.metastorage.server.Entry> it = new Iterator<>() {
                                 @Override public boolean hasNext() {
                                     return true;
                                 }
 
-                                @Override public Entry next() {
-                                    return EXPECTED_RESULT_ENTRY;
+                                @Override public org.apache.ignite.internal.metastorage.server.Entry next() {
+                                    return EXPECTED_SRV_RESULT_ENTRY;
                                 }
                             };
-                        }
-                    };
-                }
-            });
+
+                            @Override public void close() throws Exception {
+
+                            }
+
+                            @NotNull @Override public Iterator<org.apache.ignite.internal.metastorage.server.Entry> iterator() {
+                                return it;
+                            }
+
+                            @Override public boolean hasNext() {
+                                return it.hasNext();
+                            }
+
+                            @Override
+                            public org.apache.ignite.internal.metastorage.server.Entry next() {
+                                return it.next();
+                            }
+                        };
+                    }
+                });
 
         Cursor<Entry> cursor = metaStorageSvc.range(EXPECTED_RESULT_ENTRY.key(), null);
 
@@ -688,22 +771,22 @@ public class ITMetaStorageServiceTest {
     }
 
     /**
-     * Tests {@link MetaStorageService#range(Key, Key, long)}} close.
+     * Tests {@link MetaStorageService#range(ByteArray, ByteArray, long)}} close.
      *
      * @throws Exception If failed.
      */
     @Test
     public void testRangeClose() throws Exception {
-        Key expKeyFrom = new Key(new byte[]{1});
+        ByteArray expKeyFrom = new ByteArray(new byte[]{1});
 
         Cursor cursorMock = mock(Cursor.class);
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
-                    return cursorMock;
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public Cursor<org.apache.ignite.internal.metastorage.server.Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
+                        return cursorMock;
+                    }
+                });
 
         Cursor<Entry> cursor = metaStorageSvc.range(expKeyFrom, null);
 
@@ -714,101 +797,115 @@ public class ITMetaStorageServiceTest {
 
     @Test
     public void testWatchOnUpdate() throws Exception {
-        List<WatchEvent> returnedWatchEvents = Arrays.asList(
-            new WatchEvent(
-                new DummyEntry(
-                    new Key(new byte[]{2}),
-                    new byte[]{20},
-                    1,
-                    1
-                ),
-                new DummyEntry(
-                    new Key(new byte[]{2}),
-                    new byte[]{21},
-                    2,
-                    4
-                )
-            ),
-            new WatchEvent(
-                new DummyEntry(
-                    new Key(new byte[] {3}),
-                    new byte[] {20},
-                    1,
-                    2
+        org.apache.ignite.internal.metastorage.server.WatchEvent returnedWatchEvents = new org.apache.ignite.internal.metastorage.server.WatchEvent(List.of(
+                new org.apache.ignite.internal.metastorage.server.EntryEvent(
+                        new org.apache.ignite.internal.metastorage.server.Entry(
+                                new byte[]{2},
+                                new byte[]{20},
+                                1,
+                                1
+                        ),
+                        new org.apache.ignite.internal.metastorage.server.Entry(
+                                new byte[]{2},
+                                new byte[]{21},
+                                2,
+                                4
+                        )
                 ),
-                new DummyEntry(
-                    new Key(new byte[] {3}),
-                    null,
-                    2,
-                    5
-                )
-            ),
-            new WatchEvent(
-                new DummyEntry(
-                    new Key(new byte[] {4}),
-                    new byte[] {20},
-                    1,
-                    3
+                new org.apache.ignite.internal.metastorage.server.EntryEvent(
+                        new org.apache.ignite.internal.metastorage.server.Entry(
+                                new byte[] {3},
+                                new byte[] {20},
+                                1,
+                                2
+                        ),
+                        new org.apache.ignite.internal.metastorage.server.Entry(
+                                new byte[] {3},
+                                new byte[]{},
+                                2,
+                                5
+                        )
                 ),
-                new DummyEntry(
-                    new Key(new byte[] {4}),
-                    null,
-                    3,
-                    6
+                new org.apache.ignite.internal.metastorage.server.EntryEvent(
+                        new org.apache.ignite.internal.metastorage.server.Entry(
+                                new byte[] {4},
+                                new byte[] {20},
+                                1,
+                                3
+                        ),
+                        new org.apache.ignite.internal.metastorage.server.Entry(
+                                new byte[] {4},
+                                new byte[] {},
+                                3,
+                                6
+                        )
                 )
-            )
-        );
+        ));
 
-        Key keyFrom = new Key(new byte[]{1});
+        ByteArray keyFrom = new ByteArray(new byte[]{1});
 
-        Key keyTo = new Key(new byte[]{10});
+        ByteArray keyTo = new ByteArray(new byte[]{10});
 
         long rev = 2;
 
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public Cursor<WatchEvent> watch(byte[] keyFrom, byte[] keyTo, long rev) {
-                    return new Cursor<>() {
-                        AtomicInteger retirevedItemCnt = new AtomicInteger(0);
-
-                        @Override public void close() throws Exception {
-                            // No-op.
-                        }
-
-                        @NotNull @Override public Iterator<WatchEvent> iterator() {
-                            return new Iterator<WatchEvent>() {
+                new AbstractKeyValueStorage() {
+                    @Override public Cursor<org.apache.ignite.internal.metastorage.server.WatchEvent> watch(byte[] keyFrom, byte[] keyTo, long rev) {
+                        return new Cursor<>() {
+                            private final Iterator<org.apache.ignite.internal.metastorage.server.WatchEvent> it = new Iterator<>() {
                                 @Override public boolean hasNext() {
 
-                                    return retirevedItemCnt.get() < returnedWatchEvents.size();
+                                    return retirevedItemCnt.get() < returnedWatchEvents.entryEvents().size();
                                 }
 
-                                @Override public WatchEvent next() {
-                                    return returnedWatchEvents.get(retirevedItemCnt.getAndIncrement());
+                                @Override public org.apache.ignite.internal.metastorage.server.WatchEvent next() {
+                                    return returnedWatchEvents;
                                 }
                             };
-                        }
-                    };
-                }
-            });
+
+                            AtomicInteger retirevedItemCnt = new AtomicInteger(0);
+
+                            @Override public void close() throws Exception {
+                                // No-op.
+                            }
+
+                            @NotNull @Override public Iterator<org.apache.ignite.internal.metastorage.server.WatchEvent> iterator() {
+                                return it;
+                            }
+
+                            @Override public boolean hasNext() {
+                                return it.hasNext();
+                            }
+
+                            @Override
+                            public org.apache.ignite.internal.metastorage.server.WatchEvent next() {
+                                return it.next();
+                            }
+                        };
+                    }
+                });
 
         CountDownLatch latch = new CountDownLatch(1);
 
         IgniteUuid watchId = metaStorageSvc.watch(keyFrom, keyTo, rev, new WatchListener() {
-            @Override public boolean onUpdate(@NotNull Iterable<WatchEvent> events) {
+            @Override public boolean onUpdate(@NotNull WatchEvent events) {
                 List gotEvents = new ArrayList();
 
-                Iterator<WatchEvent> iter = events.iterator();
+                List returnedWatchEvents = new ArrayList(events.entryEvents());
+
+                Iterator<EntryEvent> iter = events.entryEvents().iterator();
 
                 while (iter.hasNext())
                     gotEvents.add(iter.next());
 
-                assertEquals(2, gotEvents.size());
+                assertEquals(3, gotEvents.size());
 
                 assertTrue(gotEvents.contains(returnedWatchEvents.get(0)));
 
                 assertTrue(gotEvents.contains(returnedWatchEvents.get(1)));
 
                 latch.countDown();
+
                 return true;
             }
 
@@ -823,11 +920,43 @@ public class ITMetaStorageServiceTest {
         metaStorageSvc.stopWatch(watchId).get();
     }
 
+    @Test
+    public void testInvoke() throws Exception {
+        ByteArray expKey = new ByteArray(new byte[]{1});
+
+        byte[] expVal = new byte[]{2};
+
+        Condition condition = Conditions.notExists(expKey);
+
+        Operation success = Operations.put(expKey, expVal);
+
+        Operation failure = Operations.noop();
+
+        MetaStorageService metaStorageSvc = prepareMetaStorage(
+                new AbstractKeyValueStorage() {
+                    @Override public boolean invoke(
+                            org.apache.ignite.internal.metastorage.server.Condition cond,
+                            Collection<org.apache.ignite.internal.metastorage.server.Operation> success,
+                            Collection<org.apache.ignite.internal.metastorage.server.Operation> failure) {
+                        assertArrayEquals(expKey.bytes(), cond.key());
+
+                        assertArrayEquals(expKey.bytes(), success.iterator().next().key());
+                        assertArrayEquals(expVal, success.iterator().next().value());
+
+                        assertEquals(OperationType.NO_OP, failure.iterator().next().type());
+
+                        return true;
+                    }
+                });
+
+        assertTrue(metaStorageSvc.invoke(condition, success, failure).get());
+    }
+
     // TODO: IGNITE-14693 Add tests for exception handling logic: onError,
     // TODO: (CompactedException | OperationTimeoutException)
 
     /**
-     * Tests {@link MetaStorageService#get(Key)}.
+     * Tests {@link MetaStorageService#get(ByteArray)}.
      *
      * @throws Exception If failed.
      */
@@ -835,17 +964,17 @@ public class ITMetaStorageServiceTest {
     @Test
     public void testGetThatThrowsCompactedException() {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull Entry get(byte[] key) {
-                    throw new CompactedException();
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry get(byte[] key) {
+                        throw new org.apache.ignite.internal.metastorage.server.CompactedException();
+                    }
+                });
 
         assertThrows(CompactedException.class, () -> metaStorageSvc.get(EXPECTED_RESULT_ENTRY.key()).get());
     }
 
     /**
-     * Tests {@link MetaStorageService#get(Key)}.
+     * Tests {@link MetaStorageService#get(ByteArray)}.
      *
      * @throws Exception If failed.
      */
@@ -853,11 +982,11 @@ public class ITMetaStorageServiceTest {
     @Test
     public void testGetThatThrowsOperationTimeoutException() {
         MetaStorageService metaStorageSvc = prepareMetaStorage(
-            new AbstractKeyValueStorage() {
-                @Override public @NotNull Entry get(byte[] key) {
-                    throw new OperationTimeoutException();
-                }
-            });
+                new AbstractKeyValueStorage() {
+                    @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry get(byte[] key) {
+                        throw new OperationTimeoutException();
+                    }
+                });
 
         assertThrows(OperationTimeoutException.class, () -> metaStorageSvc.get(EXPECTED_RESULT_ENTRY.key()).get());
     }
@@ -904,7 +1033,7 @@ public class ITMetaStorageServiceTest {
     }
 
     /**
-     * Prepares meta storage by instantiating corresponding raft server with {@link MetaStorageCommandListener} and
+     * Prepares meta storage by instantiating corresponding raft server with MetaStorageCommandListener and
      * {@link MetaStorageServiceImpl}.
      *
      * @param keyValStorageMock {@link KeyValueStorage} mock.
@@ -912,20 +1041,20 @@ public class ITMetaStorageServiceTest {
      */
     private MetaStorageService prepareMetaStorage(KeyValueStorage keyValStorageMock) {
         metaStorageRaftSrv = new RaftServerImpl(
-            cluster.get(0),
-            FACTORY,
-            1000,
-            Map.of(METASTORAGE_RAFT_GROUP_NAME, new MetaStorageCommandListener(keyValStorageMock))
+                cluster.get(0),
+                FACTORY,
+                1000,
+                Map.of(METASTORAGE_RAFT_GROUP_NAME, new MetaStorageCommandListener(keyValStorageMock))
         );
 
         RaftGroupService metaStorageRaftGrpSvc = new RaftGroupServiceImpl(
-            METASTORAGE_RAFT_GROUP_NAME,
-            cluster.get(1),
-            FACTORY,
-            10_000,
-            List.of(new Peer(cluster.get(0).topologyService().localMember())),
-            true,
-            200
+                METASTORAGE_RAFT_GROUP_NAME,
+                cluster.get(1),
+                FACTORY,
+                10_000,
+                List.of(new Peer(cluster.get(0).topologyService().localMember())),
+                true,
+                200
         );
 
         return new MetaStorageServiceImpl(metaStorageRaftGrpSvc);
@@ -947,28 +1076,28 @@ public class ITMetaStorageServiceTest {
         }
 
         /** {@inheritDoc} */
-        @Override public @NotNull Entry get(byte[] key) {
+        @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry get(byte[] key) {
             fail();
 
             return null;
         }
 
         /** {@inheritDoc} */
-        @Override public @NotNull Entry get(byte[] key, long rev) {
+        @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry get(byte[] key, long rev) {
             fail();
 
             return null;
         }
 
         /** {@inheritDoc} */
-        @Override public @NotNull Collection<Entry> getAll(List<byte[]> keys) {
+        @Override public @NotNull Collection<org.apache.ignite.internal.metastorage.server.Entry> getAll(List<byte[]> keys) {
             fail();
 
             return null;
         }
 
         /** {@inheritDoc} */
-        @Override public @NotNull Collection<Entry> getAll(List<byte[]> keys, long revUpperBound) {
+        @Override public @NotNull Collection<org.apache.ignite.internal.metastorage.server.Entry> getAll(List<byte[]> keys, long revUpperBound) {
             fail();
 
             return null;
@@ -980,7 +1109,7 @@ public class ITMetaStorageServiceTest {
         }
 
         /** {@inheritDoc} */
-        @Override public @NotNull Entry getAndPut(byte[] key, byte[] value) {
+        @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry getAndPut(byte[] key, byte[] value) {
             fail();
 
             return null;
@@ -992,7 +1121,7 @@ public class ITMetaStorageServiceTest {
         }
 
         /** {@inheritDoc} */
-        @Override public @NotNull Collection<Entry> getAndPutAll(List<byte[]> keys, List<byte[]> values) {
+        @Override public @NotNull Collection<org.apache.ignite.internal.metastorage.server.Entry> getAndPutAll(List<byte[]> keys, List<byte[]> values) {
             fail();
 
             return null;
@@ -1004,7 +1133,7 @@ public class ITMetaStorageServiceTest {
         }
 
         /** {@inheritDoc} */
-        @Override public @NotNull Entry getAndRemove(byte[] key) {
+        @Override public @NotNull org.apache.ignite.internal.metastorage.server.Entry getAndRemove(byte[] key) {
             fail();
 
             return null;
@@ -1016,7 +1145,7 @@ public class ITMetaStorageServiceTest {
         }
 
         /** {@inheritDoc} */
-        @Override public @NotNull Collection<Entry> getAndRemoveAll(List<byte[]> keys) {
+        @Override public @NotNull Collection<org.apache.ignite.internal.metastorage.server.Entry> getAndRemoveAll(List<byte[]> keys) {
             fail();
 
             return null;
@@ -1024,9 +1153,9 @@ public class ITMetaStorageServiceTest {
 
         /** {@inheritDoc} */
         @Override public boolean invoke(
-            Condition condition,
-            Collection<Operation> success,
-            Collection<Operation> failure
+                org.apache.ignite.internal.metastorage.server.Condition condition,
+                Collection<org.apache.ignite.internal.metastorage.server.Operation> success,
+                Collection<org.apache.ignite.internal.metastorage.server.Operation> failure
         ) {
             fail();
 
@@ -1034,35 +1163,35 @@ public class ITMetaStorageServiceTest {
         }
 
         /** {@inheritDoc} */
-        @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo) {
+        @Override public Cursor<org.apache.ignite.internal.metastorage.server.Entry> range(byte[] keyFrom, byte[] keyTo) {
             fail();
 
             return null;
         }
 
         /** {@inheritDoc} */
-        @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
+        @Override public Cursor<org.apache.ignite.internal.metastorage.server.Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
             fail();
 
             return null;
         }
 
         /** {@inheritDoc} */
-        @Override public Cursor<WatchEvent> watch(byte[] keyFrom, byte[] keyTo, long rev) {
+        @Override public Cursor<org.apache.ignite.internal.metastorage.server.WatchEvent> watch(byte[] keyFrom, byte[] keyTo, long rev) {
             fail();
 
             return null;
         }
 
         /** {@inheritDoc} */
-        @Override public Cursor<WatchEvent> watch(byte[] key, long rev) {
+        @Override public Cursor<org.apache.ignite.internal.metastorage.server.WatchEvent> watch(byte[] key, long rev) {
             fail();
 
             return null;
         }
 
         /** {@inheritDoc} */
-        @Override public Cursor<WatchEvent> watch(Collection<byte[]> keys, long rev) {
+        @Override public Cursor<org.apache.ignite.internal.metastorage.server.WatchEvent> watch(Collection<byte[]> keys, long rev) {
             fail();
 
             return null;
diff --git a/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/CursorImpl.java b/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/CursorImpl.java
index 9974266..dbbb2aa 100644
--- a/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/CursorImpl.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/CursorImpl.java
@@ -20,13 +20,14 @@ package org.apache.ignite.internal.metastorage.client;
 import java.util.Iterator;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
 import org.apache.ignite.internal.metastorage.common.command.cursor.CursorCloseCommand;
 import org.apache.ignite.internal.metastorage.common.command.cursor.CursorHasNextCommand;
 import org.apache.ignite.internal.metastorage.common.command.cursor.CursorNextCommand;
+import org.apache.ignite.internal.util.Cursor;
 import org.apache.ignite.lang.IgniteInternalException;
 import org.apache.ignite.lang.IgniteLogger;
 import org.apache.ignite.lang.IgniteUuid;
-import org.apache.ignite.metastorage.common.Cursor;
 import org.apache.ignite.raft.client.service.RaftGroupService;
 import org.jetbrains.annotations.NotNull;
 
@@ -39,49 +40,31 @@ public class CursorImpl<T> implements Cursor<T> {
     private static final IgniteLogger LOG = IgniteLogger.forClass(CursorImpl.class);
 
     /** Future that runs meta storage service operation that provides cursor. */
-    private CompletableFuture<IgniteUuid> initOp;
+    private final CompletableFuture<IgniteUuid> initOp;
 
     /** Meta storage raft group service. */
-    private RaftGroupService metaStorageRaftGrpSvc;
+    private final RaftGroupService metaStorageRaftGrpSvc;
+
+    /** */
+    private final Iterator<T> it;
+
+    /** */
+    private final Function<Object, T> fn;
 
     /**
      * @param metaStorageRaftGrpSvc Meta storage raft group service.
      * @param initOp Future that runs meta storage service operation that provides cursor.
      */
-    CursorImpl(RaftGroupService metaStorageRaftGrpSvc, CompletableFuture<IgniteUuid> initOp) {
+    CursorImpl(RaftGroupService metaStorageRaftGrpSvc, CompletableFuture<IgniteUuid> initOp, Function<Object, T> fn) {
         this.metaStorageRaftGrpSvc = metaStorageRaftGrpSvc;
         this.initOp = initOp;
+        this.it = new InnerIterator();
+        this.fn = fn;
     }
 
     /** {@inheritDoc} */
     @NotNull @Override public Iterator<T> iterator() {
-        return new Iterator<>() {
-            /** {@inheritDoc} */
-            @Override public boolean hasNext() {
-                try {
-                    return initOp.thenCompose(
-                        cursorId -> metaStorageRaftGrpSvc.<Boolean>run(new CursorHasNextCommand(cursorId))).get();
-                }
-                catch (InterruptedException | ExecutionException e) {
-                    LOG.error("Unable to evaluate cursor hasNext command", e);
-
-                    throw new IgniteInternalException(e);
-                }
-            }
-
-            /** {@inheritDoc} */
-            @Override public T next() {
-                try {
-                    return initOp.thenCompose(
-                        cursorId -> metaStorageRaftGrpSvc.<T>run(new CursorNextCommand(cursorId))).get();
-                }
-                catch (InterruptedException | ExecutionException e) {
-                    LOG.error("Unable to evaluate cursor hasNext command", e);
-
-                    throw new IgniteInternalException(e);
-                }
-            }
-        };
+        return it;
     }
 
     /** {@inheritDoc} */
@@ -96,4 +79,43 @@ public class CursorImpl<T> implements Cursor<T> {
             throw new IgniteInternalException(e);
         }
     }
+
+    /** {@inheritDoc} */
+    @Override public boolean hasNext() {
+        return it.hasNext();
+    }
+
+    /** {@inheritDoc} */
+    @Override public T next() {
+        return it.next();
+    }
+
+    /** */
+    private class InnerIterator implements Iterator<T> {
+        /** {@inheritDoc} */
+        @Override public boolean hasNext() {
+            try {
+                return initOp.thenCompose(
+                        cursorId -> metaStorageRaftGrpSvc.<Boolean>run(new CursorHasNextCommand(cursorId))).get();
+            }
+            catch (InterruptedException | ExecutionException e) {
+                LOG.error("Unable to evaluate cursor hasNext command", e);
+
+                throw new IgniteInternalException(e);
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public T next() {
+            try {
+                return initOp.thenCompose(
+                        cursorId -> metaStorageRaftGrpSvc.run(new CursorNextCommand(cursorId))).thenApply(fn).get();
+            }
+            catch (InterruptedException | ExecutionException e) {
+                LOG.error("Unable to evaluate cursor hasNext command", e);
+
+                throw new IgniteInternalException(e);
+            }
+        }
+    }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/DummyEntry.java b/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/EntryImpl.java
similarity index 60%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/DummyEntry.java
rename to modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/EntryImpl.java
index c4e6ed8..94164a0 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/DummyEntry.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/EntryImpl.java
@@ -15,90 +15,111 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.metastorage.common;
+package org.apache.ignite.internal.metastorage.client;
 
-import java.io.Serializable;
 import java.util.Arrays;
-import org.apache.ignite.metastorage.common.Entry;
-import org.apache.ignite.metastorage.common.Key;
+import org.apache.ignite.lang.ByteArray;
+import org.apache.ignite.metastorage.client.Entry;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-// TODO: IGNITE-14389 Tmp, used instead of real Entry implementation. Should be removed.
 /**
- * Dummy entry implementation.
+ * Meta storage entry.
  */
-public final class DummyEntry implements Entry, Serializable {
+public final class EntryImpl implements Entry {
     /** Key. */
-    @NotNull private Key key;
+    @NotNull
+    private final ByteArray key;
 
     /** Value. */
-    @Nullable private byte[] val;
+    @Nullable
+    private final byte[] val;
 
     /** Revision. */
-    private long revision;
+    private final long rev;
 
     /** Update counter. */
-    private long updateCntr;
+    private final long updCntr;
 
     /**
+     * Construct entry with given paramteters.
      *
      * @param key Key.
      * @param val Value.
-     * @param revision Revision.
-     * @param updateCntr Update counter.
+     * @param rev Revision.
+     * @param updCntr Update counter.
      */
-    public DummyEntry(@NotNull Key key, @Nullable byte[] val, long revision, long updateCntr) {
+    EntryImpl(@NotNull ByteArray key, @Nullable byte[] val, long rev, long updCntr) {
         this.key = key;
         this.val = val;
-        this.revision = revision;
-        this.updateCntr = updateCntr;
+        this.rev = rev;
+        this.updCntr = updCntr;
     }
 
-    /** {@inheritDoc} */
-    @Override public @NotNull Key key() {
+    /** {@inheritDoc} */    @NotNull
+    @Override public ByteArray key() {
         return key;
     }
 
     /** {@inheritDoc} */
-    @Override public @Nullable byte[] value() {
+    @Nullable
+    @Override public byte[] value() {
         return val;
     }
 
     /** {@inheritDoc} */
     @Override public long revision() {
-        return revision;
+        return rev;
     }
 
     /** {@inheritDoc} */
     @Override public long updateCounter() {
-        return updateCntr;
+        return updCntr;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean tombstone() {
+        return val == null && rev > 0 && updCntr > 0;
     }
 
     /** {@inheritDoc} */
+    @Override public boolean empty() {
+        return val == null && rev == 0 && updCntr == 0;
+    }
+
+
+    /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (this == o)
             return true;
+
         if (o == null || getClass() != o.getClass())
             return false;
 
-        DummyEntry entry = (DummyEntry)o;
+        EntryImpl entry = (EntryImpl)o;
 
-        if (revision != entry.revision)
+        if (rev != entry.rev)
             return false;
-        if (updateCntr != entry.updateCntr)
+
+        if (updCntr != entry.updCntr)
             return false;
+
         if (!key.equals(entry.key))
             return false;
+
         return Arrays.equals(val, entry.val);
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
         int res = key.hashCode();
+
         res = 31 * res + Arrays.hashCode(val);
-        res = 31 * res + (int)(revision ^ (revision >>> 32));
-        res = 31 * res + (int)(updateCntr ^ (updateCntr >>> 32));
+
+        res = 31 * res + (int)(rev ^ (rev >>> 32));
+
+        res = 31 * res + (int)(updCntr ^ (updCntr >>> 32));
+
         return res;
     }
 }
diff --git a/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/MetaStorageServiceImpl.java b/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/MetaStorageServiceImpl.java
index 4102805..3d7bde9 100644
--- a/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/MetaStorageServiceImpl.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/internal/metastorage/client/MetaStorageServiceImpl.java
@@ -19,37 +19,44 @@ package org.apache.ignite.internal.metastorage.client;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
-import java.util.stream.Collectors;
+import org.apache.ignite.internal.metastorage.common.OperationType;
+import org.apache.ignite.internal.metastorage.common.command.ConditionInfo;
 import org.apache.ignite.internal.metastorage.common.command.GetAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetAndPutAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetAndPutCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetAndRemoveAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetAndRemoveCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetCommand;
+import org.apache.ignite.internal.metastorage.common.command.InvokeCommand;
+import org.apache.ignite.internal.metastorage.common.command.MultipleEntryResponse;
+import org.apache.ignite.internal.metastorage.common.command.OperationInfo;
 import org.apache.ignite.internal.metastorage.common.command.PutAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.PutCommand;
 import org.apache.ignite.internal.metastorage.common.command.RangeCommand;
 import org.apache.ignite.internal.metastorage.common.command.RemoveAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.RemoveCommand;
+import org.apache.ignite.internal.metastorage.common.command.SingleEntryResponse;
 import org.apache.ignite.internal.metastorage.common.command.WatchExactKeysCommand;
 import org.apache.ignite.internal.metastorage.common.command.WatchRangeKeysCommand;
+import org.apache.ignite.internal.util.Cursor;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.lang.IgniteInternalException;
 import org.apache.ignite.lang.IgniteLogger;
 import org.apache.ignite.lang.IgniteUuid;
+import org.apache.ignite.metastorage.client.Condition;
+import org.apache.ignite.metastorage.client.Entry;
+import org.apache.ignite.metastorage.client.EntryEvent;
 import org.apache.ignite.metastorage.client.MetaStorageService;
-import org.apache.ignite.metastorage.common.Condition;
-import org.apache.ignite.metastorage.common.Cursor;
-import org.apache.ignite.metastorage.common.Entry;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.Operation;
-import org.apache.ignite.metastorage.common.WatchEvent;
-import org.apache.ignite.metastorage.common.WatchListener;
+import org.apache.ignite.metastorage.client.Operation;
+import org.apache.ignite.metastorage.client.WatchEvent;
+import org.apache.ignite.metastorage.client.WatchListener;
 import org.apache.ignite.raft.client.service.RaftGroupService;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -77,111 +84,117 @@ public class MetaStorageServiceImpl implements MetaStorageService {
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Entry> get(@NotNull Key key) {
-        return metaStorageRaftGrpSvc.run(new GetCommand(key));
+    @Override public @NotNull CompletableFuture<Entry> get(@NotNull ByteArray key) {
+        return metaStorageRaftGrpSvc.run(new GetCommand(key)).thenApply(MetaStorageServiceImpl::singleEntryResult);
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Entry> get(@NotNull Key key, long revUpperBound) {
-        return metaStorageRaftGrpSvc.run(new GetCommand(key, revUpperBound));
+    @Override public @NotNull CompletableFuture<Entry> get(@NotNull ByteArray key, long revUpperBound) {
+        return metaStorageRaftGrpSvc.run(new GetCommand(key, revUpperBound))
+                .thenApply(MetaStorageServiceImpl::singleEntryResult);
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Map<Key, Entry>> getAll(Collection<Key> keys) {
-        return metaStorageRaftGrpSvc.<Collection<Entry>>run(new GetAllCommand(keys)).
-            thenApply(entries -> entries.stream().collect(Collectors.toMap(Entry::key, Function.identity())));
+    @Override public @NotNull CompletableFuture<Map<ByteArray, Entry>> getAll(Set<ByteArray> keys) {
+        return metaStorageRaftGrpSvc.run(new GetAllCommand(keys))
+                .thenApply(MetaStorageServiceImpl::multipleEntryResult);
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Map<Key, Entry>> getAll(Collection<Key> keys, long revUpperBound) {
-        return metaStorageRaftGrpSvc.<Collection<Entry>>run(new GetAllCommand(keys, revUpperBound)).
-            thenApply(entries -> entries.stream().collect(Collectors.toMap(Entry::key, Function.identity())));
+    @Override public @NotNull CompletableFuture<Map<ByteArray, Entry>> getAll(Set<ByteArray> keys, long revUpperBound) {
+        return metaStorageRaftGrpSvc.run(new GetAllCommand(keys, revUpperBound)).
+                thenApply(MetaStorageServiceImpl::multipleEntryResult);
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Void> put(@NotNull Key key, @NotNull byte[] value) {
+    @Override public @NotNull CompletableFuture<Void> put(@NotNull ByteArray key, @NotNull byte[] value) {
         return metaStorageRaftGrpSvc.run(new PutCommand(key, value));
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Entry> getAndPut(@NotNull Key key, @NotNull byte[] value) {
-        return metaStorageRaftGrpSvc.run(new GetAndPutCommand(key, value));
+    @Override public @NotNull CompletableFuture<Entry> getAndPut(@NotNull ByteArray key, @NotNull byte[] value) {
+        return metaStorageRaftGrpSvc.run(new GetAndPutCommand(key, value))
+                .thenApply(MetaStorageServiceImpl::singleEntryResult);
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Void> putAll(@NotNull Map<Key, byte[]> vals) {
+    @Override public @NotNull CompletableFuture<Void> putAll(@NotNull Map<ByteArray, byte[]> vals) {
         return metaStorageRaftGrpSvc.run(new PutAllCommand(vals));
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Map<Key, Entry>> getAndPutAll(@NotNull Map<Key, byte[]> vals) {
-        List<Key> keys = new ArrayList<>();
-        List<byte[]> values = new ArrayList<>();
-
-        vals.forEach((key, value) -> {
-            keys.add(key);
-            values.add(value);
-        });
-
-        return metaStorageRaftGrpSvc.<Collection<Entry>>run(new GetAndPutAllCommand(keys, values)).
-            thenApply(entries -> entries.stream().collect(Collectors.toMap(Entry::key, Function.identity())));
+    @Override public @NotNull CompletableFuture<Map<ByteArray, Entry>> getAndPutAll(@NotNull Map<ByteArray, byte[]> vals) {
+        return metaStorageRaftGrpSvc.run(new GetAndPutAllCommand(vals)).
+            thenApply(MetaStorageServiceImpl::multipleEntryResult);
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Void> remove(@NotNull Key key) {
+    @Override public @NotNull CompletableFuture<Void> remove(@NotNull ByteArray key) {
         return metaStorageRaftGrpSvc.run(new RemoveCommand(key));
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Entry> getAndRemove(@NotNull Key key) {
-        return metaStorageRaftGrpSvc.run(new GetAndRemoveCommand(key));
+    @Override public @NotNull CompletableFuture<Entry> getAndRemove(@NotNull ByteArray key) {
+        return metaStorageRaftGrpSvc.run(new GetAndRemoveCommand(key))
+                .thenApply(MetaStorageServiceImpl::singleEntryResult);
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Void> removeAll(@NotNull Collection<Key> keys) {
+    @Override public @NotNull CompletableFuture<Void> removeAll(@NotNull Set<ByteArray> keys) {
         return metaStorageRaftGrpSvc.run(new RemoveAllCommand(keys));
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Map<Key, Entry>> getAndRemoveAll(@NotNull Collection<Key> keys) {
-        return metaStorageRaftGrpSvc.<Collection<Entry>>run(new GetAndRemoveAllCommand(keys)).
-            thenApply(entries -> entries.stream().collect(Collectors.toMap(Entry::key, Function.identity())));
+    @Override public @NotNull CompletableFuture<Map<ByteArray, Entry>> getAndRemoveAll(@NotNull Set<ByteArray> keys) {
+        return metaStorageRaftGrpSvc.run(new GetAndRemoveAllCommand(keys)).
+            thenApply(MetaStorageServiceImpl::multipleEntryResult);
     }
 
-    // TODO: IGNITE-14389 Implement.
-    /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Boolean> invoke(@NotNull Condition condition,
-        @NotNull Collection<Operation> success, @NotNull Collection<Operation> failure) {
-        return null;
+    @Override public @NotNull CompletableFuture<Boolean> invoke(
+            @NotNull Condition condition,
+            @NotNull Operation success,
+            @NotNull Operation failure
+    ) {
+        return invoke(condition, List.of(success), List.of(failure));
     }
 
-    // TODO: IGNITE-14389 Either implement or remove this method.
     /** {@inheritDoc} */
-    @Override public @NotNull CompletableFuture<Entry> getAndInvoke(@NotNull Key key, @NotNull Condition condition,
-        @NotNull Operation success, @NotNull Operation failure) {
-        return null;
+    @Override public @NotNull CompletableFuture<Boolean> invoke(
+            @NotNull Condition condition,
+            @NotNull Collection<Operation> success,
+            @NotNull Collection<Operation> failure
+    ) {
+        ConditionInfo cond = toConditionInfo(condition);
+
+        List<OperationInfo> successOps = toOperationInfos(success);
+
+        List<OperationInfo> failureOps = toOperationInfos(failure);
+
+        return metaStorageRaftGrpSvc.run(new InvokeCommand(cond, successOps, failureOps));
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull Cursor<Entry> range(@NotNull Key keyFrom, @Nullable Key keyTo, long revUpperBound) {
+    @Override public @NotNull Cursor<Entry> range(@NotNull ByteArray keyFrom, @Nullable ByteArray keyTo, long revUpperBound) {
         return new CursorImpl<>(
-            metaStorageRaftGrpSvc,
-            metaStorageRaftGrpSvc.run(new RangeCommand(keyFrom, keyTo, revUpperBound))
+                metaStorageRaftGrpSvc,
+                metaStorageRaftGrpSvc.run(new RangeCommand(keyFrom, keyTo, revUpperBound)),
+                MetaStorageServiceImpl::singleEntryResult
         );
     }
 
     /** {@inheritDoc} */
-    @Override public @NotNull Cursor<Entry> range(@NotNull Key keyFrom, @Nullable Key keyTo) {
+    @Override public @NotNull Cursor<Entry> range(@NotNull ByteArray keyFrom, @Nullable ByteArray keyTo) {
         return new CursorImpl<>(
-            metaStorageRaftGrpSvc,
-            metaStorageRaftGrpSvc.run(new RangeCommand(keyFrom, keyTo))
+                metaStorageRaftGrpSvc,
+                metaStorageRaftGrpSvc.run(new RangeCommand(keyFrom, keyTo)),
+                MetaStorageServiceImpl::singleEntryResult
         );
     }
 
     /** {@inheritDoc} */
     @Override public @NotNull CompletableFuture<IgniteUuid> watch(
-        @Nullable Key keyFrom,
-        @Nullable Key keyTo,
+        @Nullable ByteArray keyFrom,
+        @Nullable ByteArray keyTo,
         long revision,
         @NotNull WatchListener lsnr
     ) {
@@ -191,7 +204,7 @@ public class MetaStorageServiceImpl implements MetaStorageService {
         watchRes.thenAccept(
             watchId -> watchProcessor.addWatch(
                 watchId,
-                new CursorImpl<>(metaStorageRaftGrpSvc, watchRes),
+                new CursorImpl<>(metaStorageRaftGrpSvc, watchRes, MetaStorageServiceImpl::watchResponse),
                 lsnr
             )
         );
@@ -201,7 +214,7 @@ public class MetaStorageServiceImpl implements MetaStorageService {
 
     /** {@inheritDoc} */
     @Override public @NotNull CompletableFuture<IgniteUuid> watch(
-        @NotNull Key key,
+        @NotNull ByteArray key,
         long revision,
         @NotNull WatchListener lsnr
     ) {
@@ -210,7 +223,7 @@ public class MetaStorageServiceImpl implements MetaStorageService {
 
     /** {@inheritDoc} */
     @Override public @NotNull CompletableFuture<IgniteUuid> watch(
-        @NotNull Collection<Key> keys,
+        @NotNull Set<ByteArray> keys,
         long revision,
         @NotNull WatchListener lsnr
     ) {
@@ -220,7 +233,7 @@ public class MetaStorageServiceImpl implements MetaStorageService {
         watchRes.thenAccept(
             watchId -> watchProcessor.addWatch(
                 watchId,
-                new CursorImpl<>(metaStorageRaftGrpSvc, watchRes),
+                new CursorImpl<>(metaStorageRaftGrpSvc, watchRes, MetaStorageServiceImpl::watchResponse),
                 lsnr
             )
         );
@@ -233,12 +246,112 @@ public class MetaStorageServiceImpl implements MetaStorageService {
         return CompletableFuture.runAsync(() -> watchProcessor.stopWatch(id));
     }
 
-    // TODO: IGNITE-14389 Implement.
+    // TODO: IGNITE-14734 Implement.
     /** {@inheritDoc} */
     @Override public @NotNull CompletableFuture<Void> compact() {
         return null;
     }
 
+    /** */
+    private static List<OperationInfo> toOperationInfos(Collection<Operation> ops) {
+        List<OperationInfo> res = new ArrayList<>(ops.size());
+
+        for (Operation op : ops) {
+            OperationInfo info = null;
+
+            if (op.type() == OperationType.NO_OP)
+                info = new OperationInfo(null, null, OperationType.NO_OP);
+            else if (op.type() == OperationType.REMOVE)
+                info = new OperationInfo(((Operation.RemoveOp)op.inner()).key(), null, OperationType.REMOVE);
+            else if (op.type() == OperationType.PUT) {
+                Operation.PutOp inner = (Operation.PutOp)op.inner();
+
+                info = new OperationInfo(inner.key(), inner.value(), OperationType.PUT);
+            }
+            else
+                assert false : "Unknown operation type " + op.type();
+
+            res.add(info);
+        }
+
+        return res;
+    }
+
+    /** */
+    private static ConditionInfo toConditionInfo(@NotNull Condition condition) {
+        ConditionInfo cnd = null;
+
+        Object obj = condition.inner();
+
+        if (obj instanceof Condition.ExistenceCondition) {
+            Condition.ExistenceCondition inner = (Condition.ExistenceCondition)obj;
+
+            cnd = new ConditionInfo(inner.key(), inner.type(), null, 0);
+        }
+        else if (obj instanceof Condition.RevisionCondition) {
+            Condition.RevisionCondition inner = (Condition.RevisionCondition)obj;
+
+            cnd = new ConditionInfo(inner.key(), inner.type(), null, inner.revision());
+        }
+        else if (obj instanceof Condition.ValueCondition) {
+            Condition.ValueCondition inner = (Condition.ValueCondition)obj;
+
+            cnd = new ConditionInfo(inner.key(), inner.type(), inner.value(), 0);
+        }
+        else
+            assert false : "Unknown condition type: " + obj.getClass().getSimpleName();
+
+        return cnd;
+    }
+
+    /** */
+    private static Map<ByteArray, Entry> multipleEntryResult(Object obj) {
+        MultipleEntryResponse resp = (MultipleEntryResponse) obj;
+
+        Map<ByteArray, Entry> res = new HashMap<>();
+
+        for (SingleEntryResponse e : resp.entries()) {
+            ByteArray key = new ByteArray(e.key());
+
+            res.put(key, new EntryImpl(key, e.value(), e.revision(), e.updateCounter()));
+        }
+
+        return res;
+    }
+
+    /** */
+    private static Entry singleEntryResult(Object obj) {
+        SingleEntryResponse resp = (SingleEntryResponse) obj;
+
+        return new EntryImpl(new ByteArray(resp.key()), resp.value(), resp.revision(), resp.updateCounter());
+    }
+
+    /** */
+    private static WatchEvent watchResponse(Object obj) {
+        MultipleEntryResponse resp = (MultipleEntryResponse) obj;
+
+        List<EntryEvent> evts = new ArrayList<>(resp.entries().size() / 2);
+
+        Entry o = null;
+        Entry n;
+
+        for (int i = 0; i < resp.entries().size(); i++) {
+            SingleEntryResponse s = resp.entries().get(i);
+
+            EntryImpl e = new EntryImpl(new ByteArray(s.key()), s.value(), s.revision(), s.updateCounter());
+
+            if (i % 2 == 0)
+                o = e;
+            else {
+                n = e;
+
+                evts.add(new EntryEvent(o, n));
+            }
+        }
+
+        return new WatchEvent(evts);
+    }
+
     // TODO: IGNITE-14691 Temporally solution that should be removed after implementing reactive watches.
     /** Watch processor, that manages {@link Watcher} threads. */
     private final class WatchProcessor {
@@ -247,7 +360,7 @@ public class MetaStorageServiceImpl implements MetaStorageService {
 
         /**
          * Starts exclusive thread per watch that implement watch pulling logic and
-         * calls {@link WatchListener#onUpdate(Iterable)}} or {@link WatchListener#onError(Throwable)}.
+         * calls {@link WatchListener#onUpdate(WatchEvent)}} or {@link WatchListener#onError(Throwable)}.
          *
          * @param watchId Watch id.
          * @param cursor Watch Cursor.
@@ -272,6 +385,10 @@ public class MetaStorageServiceImpl implements MetaStorageService {
                 (k, v) -> {
                     CompletableFuture.runAsync(v::interrupt).thenRun(() -> {
                         try {
+                            v.stop = true;
+
+                            Thread.sleep(100);
+
                             v.cursor.close();
                         }
                         catch (InterruptedException e) {
@@ -289,6 +406,9 @@ public class MetaStorageServiceImpl implements MetaStorageService {
 
         /** Watcher thread, uses pulling logic in order to retrieve watch notifications from server */
         private final class Watcher extends Thread {
+            /** */
+            private volatile boolean stop = false;
+
             /** Watch event cursor. */
             private Cursor<WatchEvent> cursor;
 
@@ -309,13 +429,9 @@ public class MetaStorageServiceImpl implements MetaStorageService {
              * in the while(true) loop. Collects watch events with same revision and fires either onUpdate or onError().
              */
             @Override public void run() {
-                long rev = -1;
-
-                List<WatchEvent> sameRevisionEvts = new ArrayList<>();
-
                 Iterator<WatchEvent> watchEvtsIter = cursor.iterator();
 
-                while (true) {
+                while (!stop) {
                     try {
                         if (watchEvtsIter.hasNext()) {
                             WatchEvent watchEvt = null;
@@ -329,19 +445,7 @@ public class MetaStorageServiceImpl implements MetaStorageService {
 
                             assert watchEvt != null;
 
-                            if (watchEvt.newEntry().revision() == rev)
-                                sameRevisionEvts.add(watchEvt);
-                            else {
-                                rev = watchEvt.newEntry().revision();
-
-                                if (!sameRevisionEvts.isEmpty()) {
-                                    lsnr.onUpdate(sameRevisionEvts);
-
-                                    sameRevisionEvts.clear();
-                                }
-
-                                sameRevisionEvts.add(watchEvt);
-                            }
+                            lsnr.onUpdate(watchEvt);
                         }
                         else
                             Thread.sleep(10);
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/CompactedException.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/CompactedException.java
similarity index 97%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/CompactedException.java
copy to modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/CompactedException.java
index 6ea9f43..c256442 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/CompactedException.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/CompactedException.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.metastorage.client;
 
 /**
  * Thrown when a requested operation on meta storage could not be performed because target revisions were removed
diff --git a/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Condition.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Condition.java
new file mode 100644
index 0000000..f78b113
--- /dev/null
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Condition.java
@@ -0,0 +1,347 @@
+/*
+ * 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.metastorage.client;
+
+import java.util.Collection;
+import org.apache.ignite.internal.metastorage.common.ConditionType;
+
+/**
+ * Represents a condition for meta storage conditional update.
+ *
+ * @see MetaStorageService#invoke(Condition, Operation, Operation)
+ * @see MetaStorageService#invoke(Condition, Collection, Collection)
+ */
+public final class Condition {
+    /** Actual condition implementation. */
+    private final InnerCondition cond;
+
+    /**
+     * Constructs a condition which wraps the actual condition implementation.
+     *
+     * @param cond The actual condition implementation.
+     */
+    Condition(InnerCondition cond) {
+        this.cond = cond;
+    }
+
+    public InnerCondition inner() {
+        return cond;
+    }
+
+    public ConditionType type() {
+        return cond.type();
+    }
+
+    /**
+     * Represents condition on entry revision. Only one type of condition could be applied to
+     * the one instance of condition. Subsequent invocations of any method which produces condition will throw
+     * {@link IllegalStateException}.
+     */
+    public static final class RevisionCondition extends AbstractCondition {
+        /** The revision as the condition argument. */
+        private long rev;
+
+        /**
+         * Constructs a condition by a revision for an entry identified by the given key.
+         *
+         * @param key Identifies an entry which condition will be applied to.
+         */
+        RevisionCondition(byte[] key) {
+            super(key);
+        }
+
+        public long revision() {
+            return rev;
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#REV_EQUAL}. This condition tests the given revision on equality with
+         * target entry revision.
+         *
+         * @param rev The revision.
+         * @return The condition of type {@link ConditionType#REV_EQUAL}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition eq(long rev) {
+            validate(type());
+
+            type(ConditionType.REV_EQUAL);
+
+            this.rev = rev;
+
+            return new Condition(this);
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#REV_NOT_EQUAL}. This condition tests the given revision on inequality
+         * with target entry revision.
+         *
+         * @param rev The revision.
+         * @return The condition of type {@link ConditionType#REV_NOT_EQUAL}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition ne(long rev) {
+            validate(type());
+
+            type(ConditionType.REV_NOT_EQUAL);
+
+            this.rev = rev;
+
+            return new Condition(this);
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#REV_GREATER}. This condition tests that the target entry revision
+         * is greater than given revision.
+         *
+         * @param rev The revision.
+         * @return The condition of type {@link ConditionType#REV_GREATER}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition gt(long rev) {
+            validate(type());
+
+            type(ConditionType.REV_GREATER);
+
+            this.rev = rev;
+
+            return new Condition(this);
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#REV_GREATER_OR_EQUAL}. This condition tests that the target entry
+         * revision is greater than or equal to given revision.
+         *
+         * @param rev The revision.
+         * @return The condition of type {@link ConditionType#REV_GREATER_OR_EQUAL}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition ge(long rev) {
+            validate(type());
+
+            type(ConditionType.REV_GREATER_OR_EQUAL);
+
+            this.rev = rev;
+
+            return new Condition(this);
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#REV_LESS}. This condition tests that target entry revision
+         * is less than the given revision.
+         *
+         * @param rev The revision.
+         * @return The condition of type {@link ConditionType#REV_LESS}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition lt(long rev) {
+            validate(type());
+
+            type(ConditionType.REV_LESS);
+            this.rev = rev;
+
+            return new Condition(this);
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#REV_LESS_OR_EQUAL}. This condition tests that target entry revision
+         * is less than or equal to the given revision.
+         *
+         * @param rev The revision.
+         * @return The condition of type {@link ConditionType#REV_LESS_OR_EQUAL}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition le(long rev) {
+            validate(type());
+
+            type(ConditionType.REV_LESS_OR_EQUAL);
+
+            this.rev = rev;
+
+            return new Condition(this);
+        }
+    }
+
+    /**
+     * Represents condition on entry value. Only one type of condition could be applied to
+     * the one instance of condition. Subsequent invocations of any method which produces condition will throw
+     * {@link IllegalStateException}.
+     */
+    public static final class ValueCondition extends AbstractCondition {
+        /** The value as the condition argument. */
+        private byte[] val;
+
+        /**
+         * Constructs a condition by a value for an entry identified by the given key.
+         *
+         * @param key Identifies an entry which condition will be applied to.
+         */
+        ValueCondition(byte[] key) {
+            super(key);
+        }
+
+        public byte[] value() {
+            return val;
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#VAL_EQUAL}. This condition tests the given value on equality with
+         * target entry value.
+         *
+         * @param val The value.
+         * @return The condition of type {@link ConditionType#VAL_EQUAL}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition eq(byte[] val) {
+            validate(type());
+
+            type(ConditionType.VAL_EQUAL);
+
+            this.val = val;
+
+            return new Condition(this);
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#VAL_NOT_EQUAL}. This condition tests the given value on inequality
+         * with target entry value.
+         *
+         * @param val The value.
+         * @return The condition of type {@link ConditionType#VAL_NOT_EQUAL}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition ne(byte[] val) {
+            validate(type());
+
+            type(ConditionType.VAL_NOT_EQUAL);
+
+            this.val = val;
+
+            return new Condition(this);
+        }
+    }
+
+    /**
+     * Represents condition on an entry existence. Only one type of condition could be applied to
+     * the one instance of condition. Subsequent invocations of any method which produces condition will throw
+     * {@link IllegalStateException}.
+     */
+    public static final class ExistenceCondition extends AbstractCondition {
+        /**
+         * Constructs a condition on existence an entry identified by the given key.
+         *
+         * @param key Identifies an entry which condition will be applied to.
+         */
+        ExistenceCondition(byte[] key) {
+            super(key);
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#KEY_EXISTS}. This condition tests the existence of an entry
+         * identified by the given key.
+         *
+         * @return The condition of type {@link ConditionType#KEY_EXISTS}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition exists() {
+            validate(type());
+
+            type(ConditionType.KEY_EXISTS);
+
+            return new Condition(this);
+        }
+
+        /**
+         * Produces the condition of type {@link ConditionType#KEY_NOT_EXISTS}. This condition tests the non-existence of an entry
+         * identified by the given key.
+         *
+         * @return The condition of type {@link ConditionType#KEY_NOT_EXISTS}.
+         * @throws IllegalStateException In case when the condition is already defined.
+         */
+        public Condition notExists() {
+            validate(type());
+
+            type(ConditionType.KEY_NOT_EXISTS);
+
+            return new Condition(this);
+        }
+    }
+
+    /**
+     * Checks that condition is not defined yet. If the condition is already defined then exception will be thrown.
+     *
+     * @throws IllegalStateException In case when the condition is already defined.
+     */
+    private static void validate(Enum<?> type) {
+        if (type != null)
+            throw new IllegalStateException("Condition type " + type.name() + " is already defined.");
+    }
+
+    /**
+     * Defines condition interface.
+     */
+    private interface InnerCondition {
+        /**
+         * Returns key which identifies an entry which condition will be applied to.
+         *
+         * @return Key which identifies an entry which condition will be applied to.
+         */
+        byte[] key();
+
+        ConditionType type();
+    }
+
+    /**
+     * Defines an abstract condition with the key which identifies an entry which condition will be applied to.
+     */
+    private abstract static class AbstractCondition implements InnerCondition {
+        /** Entry key. */
+        private final byte[] key;
+
+        /**
+         * Condition type.
+         */
+        private ConditionType type;
+
+        /**
+         * Constructs a condition with the given entry key.
+         *
+         * @param key Key which identifies an entry which condition will be applied to.
+         */
+        private AbstractCondition(byte[] key) {
+            this.key = key;
+        }
+
+        /**
+         * Returns the key which identifies an entry which condition will be applied to.
+         *
+         * @return Key which identifies an entry which condition will be applied to.
+         */
+        @Override public byte[] key() {
+            return key;
+        }
+
+        @Override public ConditionType type() {
+            return type;
+        }
+
+        protected void type(ConditionType type) {
+            this.type = type;
+        }
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Conditions.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Conditions.java
similarity index 54%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Conditions.java
rename to modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Conditions.java
index 87fa238..3d4f370 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Conditions.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Conditions.java
@@ -15,7 +15,10 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.metastorage.client;
+
+import org.apache.ignite.lang.ByteArray;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * This class contains fabric methods which produce conditions needed for conditional multi update functionality
@@ -24,47 +27,46 @@ package org.apache.ignite.metastorage.common;
  * @see Condition
  */
 public final class Conditions {
-
-    /** Key. */
-    private Key key;
-
-    /**
-     * Creates new condition for entry with concrete key.
-     *
-     * @param key Key
-     */
-    private Conditions(Key key) {
-        this.key = key;
-    }
-
     /**
      * Creates condition on entry revision.
      *
+     * @param key Identifies an entry which condition will be applied to. Can't be {@code null}.
      * @return Condition on entry revision.
      * @see Condition.RevisionCondition
      */
-    public Condition.RevisionCondition revision() {
-        return new Condition.RevisionCondition(key);
+    public static Condition.RevisionCondition revision(@NotNull ByteArray key) {
+        return new Condition.RevisionCondition(key.bytes());
     }
 
     /**
      * Creates condition on entry value.
      *
+     * @param key Identifies an entry which condition will be applied to. Can't be {@code null}.
      * @return Condition on entry value.
      * @see Condition.ValueCondition
      */
-    public Condition.ValueCondition value() {
-        return new Condition.ValueCondition(key);
+    public static Condition.ValueCondition value(@NotNull ByteArray key) {
+        return new Condition.ValueCondition(key.bytes());
+    }
+
+    /**
+     * Creates condition on entry existence.
+     *
+     * @param key Identifies an entry which condition will be applied to. Can't be {@code null}.
+     * @return Condition on entry existence.
+     */
+    public static Condition exists(@NotNull ByteArray key) {
+        return new Condition.ExistenceCondition(key.bytes()).exists();
     }
 
     /**
-     * Creates key-based condition.
+     * Creates condition on entry not existence.
      *
-     * @param key Key of condition.
-     * @return Key-based condition instance.
+     * @param key Identifies an entry which condition will be applied to. Can't be {@code null}.
+     * @return Condition on entry not existence.
      */
-    public static Conditions key(Key key) {
-        return new Conditions(key);
+    public static Condition notExists(@NotNull ByteArray key) {
+        return new Condition.ExistenceCondition(key.bytes()).notExists();
     }
 
     /**
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Entry.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Entry.java
similarity index 77%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Entry.java
copy to modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Entry.java
index 7f653cc..8895aac 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Entry.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Entry.java
@@ -15,8 +15,9 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.metastorage.client;
 
+import org.apache.ignite.lang.ByteArray;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -34,7 +35,7 @@ public interface Entry {
      *
      * @return The key.
      */
-    @NotNull Key key();
+    @NotNull ByteArray key();
 
     /**
      * Returns a value. Could be {@code null} for empty entry.
@@ -56,4 +57,18 @@ public interface Entry {
      * @return Update counter.
      */
     long updateCounter();
+
+    /**
+     * Returns value which denotes whether entry is empty or not.
+     *
+     * @return {@code True} if entry is empty, otherwise - {@code false}.
+     */
+    boolean empty();
+
+    /**
+     * Returns value which denotes whether entry is tombstone or not.
+     *
+     * @return {@code True} if entry is tombstone, otherwise - {@code false}.
+     */
+    boolean tombstone();
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchEvent.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/EntryEvent.java
similarity index 81%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchEvent.java
rename to modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/EntryEvent.java
index 119167a..9e9a70d 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchEvent.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/EntryEvent.java
@@ -15,16 +15,16 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.metastorage.client;
 
 import java.io.Serializable;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Watch event which can be processed by {@link WatchListener}.
+ * Represent an update event for particular key and entry.
  */
-public final class WatchEvent implements Serializable {
-    /** Old (previous) entry */
+public final class EntryEvent implements Serializable {
+    /** Old (previous) entry. */
     @NotNull private final Entry oldEntry;
 
     /** New (updated) entry. */
@@ -34,9 +34,9 @@ public final class WatchEvent implements Serializable {
      * Constructs an event with given old and new entries.
      *
      * @param oldEntry Old entry.
-     * @param newEntry New entry/
+     * @param newEntry New entry.
      */
-    public WatchEvent(@NotNull Entry oldEntry, @NotNull Entry newEntry) {
+    public EntryEvent(Entry oldEntry, Entry newEntry) {
         this.oldEntry = oldEntry;
         this.newEntry = newEntry;
     }
@@ -46,7 +46,8 @@ public final class WatchEvent implements Serializable {
      *
      * @return Old entry.
      */
-    public @NotNull Entry oldEntry() {
+    @NotNull
+    public Entry oldEntry() {
         return oldEntry;
     }
 
@@ -55,7 +56,8 @@ public final class WatchEvent implements Serializable {
      *
      * @return New entry.
      */
-    public @NotNull Entry newEntry() {
+    @NotNull
+    public Entry newEntry() {
         return newEntry;
     }
 
@@ -63,20 +65,24 @@ public final class WatchEvent implements Serializable {
     @Override public boolean equals(Object o) {
         if (this == o)
             return true;
+
         if (o == null || getClass() != o.getClass())
             return false;
 
-        WatchEvent that = (WatchEvent)o;
+        EntryEvent that = (EntryEvent)o;
 
         if (!oldEntry.equals(that.oldEntry))
             return false;
+
         return newEntry.equals(that.newEntry);
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
         int res = oldEntry.hashCode();
+
         res = 31 * res + newEntry.hashCode();
+
         return res;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Key.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Key.java
similarity index 98%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Key.java
rename to modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Key.java
index 7cba39f..58d416e 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Key.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Key.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.metastorage.client;
 
 import java.io.Serializable;
 import java.nio.charset.StandardCharsets;
diff --git a/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/MetaStorageService.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/MetaStorageService.java
index 6a13637..787a463 100644
--- a/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/MetaStorageService.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/MetaStorageService.java
@@ -19,16 +19,11 @@ package org.apache.ignite.metastorage.client;
 
 import java.util.Collection;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.lang.IgniteUuid;
-import org.apache.ignite.metastorage.common.CompactedException;
-import org.apache.ignite.metastorage.common.Condition;
-import org.apache.ignite.metastorage.common.Cursor;
-import org.apache.ignite.metastorage.common.Entry;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.Operation;
-import org.apache.ignite.metastorage.common.OperationTimeoutException;
-import org.apache.ignite.metastorage.common.WatchListener;
+import org.apache.ignite.internal.util.Cursor;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -42,57 +37,57 @@ public interface MetaStorageService {
      * @param key Key. Couldn't be {@code null}.
      * @return An entry for the given key. Couldn't be {@code null}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Entry> get(@NotNull Key key);
+    CompletableFuture<Entry> get(@NotNull ByteArray key);
 
     /**
      * Retrieves an entry for the given key and the revision upper bound.
      *
      * @param key The key. Couldn't be {@code null}.
-     * @param revUpperBound  The upper bound for entry revisions. Must be positive.
+     * @param revUpperBound The upper bound for entry revisions. Must be positive.
      * @return An entry for the given key and maximum revision limited by {@code revUpperBound}.
      * Couldn't be {@code null}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
      * @throws CompactedException If the desired revisions are removed from the storage due to a compaction.
      * Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Entry> get(@NotNull Key key, long revUpperBound);
+    CompletableFuture<Entry> get(@NotNull ByteArray key, long revUpperBound);
 
     /**
      * Retrieves entries for given keys.
      *
-     * @param keys The collection of keys. Couldn't be {@code null} or empty.
-     *             Collection elements couldn't be {@code null}.
+     * @param keys The set of keys. Couldn't be {@code null} or empty.
+     *             Set elements couldn't be {@code null}.
      * @return A map of entries for given keys. Couldn't be {@code null}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Map<Key, Entry>> getAll(Collection<Key> keys);
+    CompletableFuture<Map<ByteArray, Entry>> getAll(Set<ByteArray> keys);
 
     /**
      * Retrieves entries for given keys and the revision upper bound.
      *
-     * @param keys The collection of keys. Couldn't be {@code null} or empty.
-     *             Collection elements couldn't be {@code null}.
-     * @param revUpperBound  The upper bound for entry revisions. Must be positive.
+     * @param keys The set of keys. Couldn't be {@code null} or empty.
+     *             Set elements couldn't be {@code null}.
+     * @param revUpperBound The upper bound for entry revisions. Must be positive.
      * @return A map of entries for given keys and maximum revision limited by {@code revUpperBound}.
      * Couldn't be {@code null}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
      * @throws CompactedException If the desired revisions are removed from the storage due to a compaction.
      * Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Map<Key, Entry>> getAll(Collection<Key> keys, long revUpperBound);
+    CompletableFuture<Map<ByteArray, Entry>> getAll(Set<ByteArray> keys, long revUpperBound);
 
     /**
      * Inserts or updates an entry with the given key and the given value.
@@ -101,11 +96,11 @@ public interface MetaStorageService {
      * @param value The value. Couldn't be {@code null}.
      * @return Completed future.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Void> put(@NotNull Key key, @NotNull byte[] value);
+    CompletableFuture<Void> put(@NotNull ByteArray key, @NotNull byte[] value);
 
     /**
      * Inserts or updates an entry with the given key and the given value and
@@ -115,11 +110,11 @@ public interface MetaStorageService {
      * @param value The value. Couldn't be {@code null}.
      * @return A previous entry for the given key. Couldn't be {@code null}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Entry> getAndPut(@NotNull Key key, @NotNull byte[] value);
+    CompletableFuture<Entry> getAndPut(@NotNull ByteArray key, @NotNull byte[] value);
 
     /**
      * Inserts or updates entries with given keys and given values.
@@ -127,11 +122,11 @@ public interface MetaStorageService {
      * @param vals The map of keys and corresponding values. Couldn't be {@code null} or empty.
      * @return Completed future.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Void> putAll(@NotNull Map<Key, byte[]> vals);
+    CompletableFuture<Void> putAll(@NotNull Map<ByteArray, byte[]> vals);
 
     /**
      * Inserts or updates entries with given keys and given values and
@@ -140,11 +135,11 @@ public interface MetaStorageService {
      * @param vals The map of keys and corresponding values. Couldn't be {@code null} or empty.
      * @return A map of entries for given keys. Couldn't be {@code null}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Map<Key, Entry>> getAndPutAll(@NotNull Map<Key, byte[]> vals);
+    CompletableFuture<Map<ByteArray, Entry>> getAndPutAll(@NotNull Map<ByteArray, byte[]> vals);
 
     /**
      * Removes an entry for the given key.
@@ -152,11 +147,11 @@ public interface MetaStorageService {
      * @param key The key. Couldn't be {@code null}.
      * @return Completed future.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Void> remove(@NotNull Key key);
+    CompletableFuture<Void> remove(@NotNull ByteArray key);
 
     /**
      * Removes an entry for the given key.
@@ -164,23 +159,23 @@ public interface MetaStorageService {
      * @param key The key. Couldn't be {@code null}.
      * @return A previous entry for the given key. Couldn't be {@code null}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Entry> getAndRemove(@NotNull Key key);
+    CompletableFuture<Entry> getAndRemove(@NotNull ByteArray key);
 
     /**
      * Removes entries for given keys.
      *
-     * @param keys The keys collection. Couldn't be {@code null}.
+     * @param keys The keys set. Couldn't be {@code null} or empty.
      * @return Completed future.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Void> removeAll(@NotNull Collection<Key> keys);
+    CompletableFuture<Void> removeAll(@NotNull Set<ByteArray> keys);
 
     /**
      * Removes entries for given keys and retrieves previous entries.
@@ -190,12 +185,11 @@ public interface MetaStorageService {
      * The order of entries in the result list corresponds to the traversal order of {@code keys} collection.
      * Couldn't be {@code null}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<Map<Key, Entry>> getAndRemoveAll(@NotNull Collection<Key> keys);
-
+    CompletableFuture<Map<ByteArray, Entry>> getAndRemoveAll(@NotNull Set<ByteArray> keys);
 
     /**
      * Updates an entry for the given key conditionally.
@@ -203,11 +197,11 @@ public interface MetaStorageService {
      * <p>Conditional update could be treated as <i>if(condition)-then(success)-else(failure)</i> expression.</p>
      *
      * @param condition The condition.
-     * @param success Batch of updates which will be atomically applied in case of condition evaluation yields {@code true}.
-     * @param failure Batch of updates which will be atomically applied in case of condition evaluation yields {@code false}.
-     * @return Future result {@code true} if {@code success} updates were applied, otherwise {@code false}.
+     * @param success The update which will be applied in case of condition evaluation yields {@code true}.
+     * @param failure The update which will be applied in case of condition evaluation yields {@code false}.
+     * @return Future result {@code true} if {@code success} update was applied, otherwise {@code false}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      * @see Condition
      * @see Operation
@@ -215,28 +209,27 @@ public interface MetaStorageService {
     // TODO: https://issues.apache.org/jira/browse/IGNITE-14269: will be replaced by conditional multi update.
     @NotNull
     CompletableFuture<Boolean> invoke(@NotNull Condition condition,
-        @NotNull Collection<Operation> success, @NotNull Collection<Operation> failure);
+                                      @NotNull Operation success, @NotNull Operation failure);
 
     /**
      * Updates an entry for the given key conditionally.
      *
      * <p>Conditional update could be treated as <i>if(condition)-then(success)-else(failure)</i> expression.</p>
      *
-     * @param key The key. Couldn't be {@code null}.
      * @param condition The condition.
-     * @param success The update which will be applied in case of condition evaluation yields {@code true}.
-     * @param failure The update which will be applied in case of condition evaluation yields {@code false}.
-     * @return A previous entry for the given key.
+     * @param success The updates which will be applied in case of condition evaluation yields {@code true}.
+     * @param failure The updates which will be applied in case of condition evaluation yields {@code false}.
+     * @return Future result {@code true} if {@code success} update was applied, otherwise {@code false}.
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      * @see Condition
      * @see Operation
      */
-    //TODO: https://issues.apache.org/jira/browse/IGNITE-14269: will be replaced by conditional multi update.
+    // TODO: https://issues.apache.org/jira/browse/IGNITE-14269: will be replaced by conditional multi update.
     @NotNull
-    CompletableFuture<Entry> getAndInvoke(@NotNull Key key, @NotNull Condition condition,
-                                          @NotNull Operation success, @NotNull Operation failure);
+    CompletableFuture<Boolean> invoke(@NotNull Condition condition,
+                                      @NotNull Collection<Operation> success, @NotNull Collection<Operation> failure);
 
     /**
      * Retrieves entries for the given key range in lexicographic order. Entries will be filtered out by upper bound
@@ -248,26 +241,26 @@ public interface MetaStorageService {
      * @return Cursor built upon entries corresponding to the given range and revision.
      * @throws OperationTimeoutException If the operation is timed out.
      * @throws CompactedException If the desired revisions are removed from the storage due to a compaction.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    Cursor<Entry> range(@NotNull Key keyFrom, @Nullable Key keyTo, long revUpperBound);
+    Cursor<Entry> range(@NotNull ByteArray keyFrom, @Nullable ByteArray keyTo, long revUpperBound);
 
     /**
      * Retrieves entries for the given key range in lexicographic order. Short cut for
-     * {@link #range(Key, Key, long)} where {@code revUpperBound == -1}.
+     * {@link #range(ByteArray, ByteArray, long)} where {@code revUpperBound == -1}.
      *
      * @param keyFrom Start key of range (inclusive). Couldn't be {@code null}.
      * @param keyTo End key of range (exclusive). Could be {@code null}.
      * @return Cursor built upon entries corresponding to the given range and revision.
      * @throws OperationTimeoutException If the operation is timed out.
      * @throws CompactedException If the desired revisions are removed from the storage due to a compaction.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    Cursor<Entry> range(@NotNull Key keyFrom, @Nullable Key keyTo);
+    Cursor<Entry> range(@NotNull ByteArray keyFrom, @Nullable ByteArray keyTo);
 
     /**
      * Subscribes on meta storage updates matching the parameters.
@@ -281,11 +274,11 @@ public interface MetaStorageService {
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
      * @throws CompactedException If the desired revisions are removed from the storage due to a compaction.
      * Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<IgniteUuid> watch(@Nullable Key keyFrom, @Nullable Key keyTo,
+    CompletableFuture<IgniteUuid> watch(@Nullable ByteArray keyFrom, @Nullable ByteArray keyTo,
                                   long revision, @NotNull WatchListener lsnr);
 
     /**
@@ -299,16 +292,16 @@ public interface MetaStorageService {
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
      * @throws CompactedException If the desired revisions are removed from the storage due to a compaction.
      * Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<IgniteUuid> watch(@NotNull Key key, long revision, @NotNull WatchListener lsnr);
+    CompletableFuture<IgniteUuid> watch(@NotNull ByteArray key, long revision, @NotNull WatchListener lsnr);
 
     /**
      * Subscribes on meta storage updates for given keys.
      *
-     * @param keys Collection of target keys. Could be {@code null}.
+     * @param keys Set of target keys. Could be {@code null}.
      * @param revision Start revision inclusive. {@code 0} - all revision,
      * {@code -1} - latest revision (accordingly to current meta storage state).
      * @param lsnr Listener which will be notified for each update.
@@ -316,11 +309,11 @@ public interface MetaStorageService {
      * @throws OperationTimeoutException If the operation is timed out. Will be thrown on getting future result.
      * @throws CompactedException If the desired revisions are removed from the storage due to a compaction.
      * Will be thrown on getting future result.
-     * @see Key
+     * @see ByteArray
      * @see Entry
      */
     @NotNull
-    CompletableFuture<IgniteUuid> watch(@NotNull Collection<Key> keys, long revision, @NotNull WatchListener lsnr);
+    CompletableFuture<IgniteUuid> watch(@NotNull Set<ByteArray> keys, long revision, @NotNull WatchListener lsnr);
 
     /**
      * Cancels subscription for the given identifier.
diff --git a/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Operation.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Operation.java
new file mode 100644
index 0000000..cf7a43e
--- /dev/null
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Operation.java
@@ -0,0 +1,166 @@
+/*
+ * 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.metastorage.client;
+
+import org.apache.ignite.internal.metastorage.common.OperationType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Defines operation for meta storage conditional update (invoke).
+ */
+public final class Operation {
+    /** Actual operation implementation. */
+    private final InnerOp upd;
+
+    /**
+     * Constructs an operation which wraps the actual operation implementation.
+     *
+     * @param upd The actual operation implementation.
+     */
+    Operation(InnerOp upd) {
+        this.upd = upd;
+    }
+
+    /** */
+    public InnerOp inner() {
+        return upd;
+    }
+
+    /**
+     * Returns operation type.
+     *
+     * @return Operation type.
+     */
+    public OperationType type() {
+        return upd.type();
+    }
+
+    /**
+     * Represents operation of type <i>remove</i>.
+     */
+    public static final class RemoveOp extends AbstractOp {
+        /**
+         * Default no-op constructor.
+         *
+         * @param key Identifies an entry which operation will be applied to.
+         */
+        RemoveOp(byte[] key) {
+            super(key, OperationType.REMOVE);
+        }
+    }
+
+    /**
+     * Represents operation of type <i>put</i>.
+     */
+    public static final class PutOp extends AbstractOp {
+        /** Value. */
+        private final byte[] val;
+
+        /**
+         * Constructs operation of type <i>put</i>.
+         *
+         * @param key Identifies an entry which operation will be applied to.
+         * @param val The value to which the entry should be updated.
+         */
+        PutOp(byte[] key, byte[] val) {
+            super(key, OperationType.PUT);
+
+            this.val = val;
+        }
+
+        /**
+         * Returns value.
+         *
+         * @return Value.
+         */
+        public byte[] value() {
+            return val;
+        }
+    }
+
+    /**
+     * Represents operation of type <i>no-op</i>.
+     */
+    public static final class NoOp extends AbstractOp {
+        /**
+         * Default no-op constructor.
+         */
+        NoOp() {
+            super(null, OperationType.NO_OP);
+        }
+    }
+
+    /**
+     * Defines operation interface.
+     */
+    private interface InnerOp {
+        /**
+         * Returns key.
+         *
+         * @return Key.
+         */
+        @Nullable byte[] key();
+
+        /**
+         * Returns operation type.
+         *
+         * @return Operation type.
+         */
+        @NotNull OperationType type();
+    }
+
+    /** */
+    private static class AbstractOp implements InnerOp {
+        /** Key. */
+        @Nullable private final byte[] key;
+
+        /** Operation type. */
+        @NotNull private final OperationType type;
+
+        /**
+         * Ctor.
+         * @param key Key.
+         * @param type Operation type.
+         */
+        private AbstractOp(@Nullable byte[] key, OperationType type) {
+            this.key = key;
+            this.type = type;
+        }
+
+        /**
+         * Returns key.
+         *
+         * @return Key.
+         */
+        @Nullable
+        @Override public byte[] key() {
+            return key;
+        }
+
+        /**
+         * Returns operation type.
+         *
+         * @return Operation type.
+         */
+        @NotNull
+        @Override public OperationType type() {
+            return type;
+        }
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/OperationTimeoutException.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/OperationTimeoutException.java
similarity index 97%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/OperationTimeoutException.java
rename to modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/OperationTimeoutException.java
index 4edcf02..3428f39 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/OperationTimeoutException.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/OperationTimeoutException.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.metastorage.client;
 
 /**
  * Thrown when an operation is not executed within a specified time period. Usually in such cases the operation
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Operations.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Operations.java
similarity index 79%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Operations.java
rename to modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Operations.java
index 31c7449..87f806f 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Operations.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/Operations.java
@@ -15,7 +15,9 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.metastorage.client;
+
+import org.apache.ignite.lang.ByteArray;
 
 /**
  * This class contains fabric methods which produce operations needed for conditional multi update functionality
@@ -30,20 +32,22 @@ public final class Operations {
     /**
      * Creates operation of type <i>remove</i>. This type of operation removes entry.
      *
+     * @param key Identifies an entry which operation will be applied to.
      * @return Operation of type <i>remove</i>.
      */
-    public static Operation remove(Key key) {
-        return new Operation(new Operation.RemoveOp(key));
+    public static Operation remove(ByteArray key) {
+        return new Operation(new Operation.RemoveOp(key.bytes()));
     }
 
     /**
      * Creates operation of type <i>put</i>. This type of operation inserts or updates value of entry.
      *
+     * @param key Identifies an entry which operation will be applied to.
      * @param value Value.
      * @return Operation of type <i>put</i>.
      */
-    public static Operation put(Key key, byte[] value) {
-        return new Operation(new Operation.PutOp(key, value));
+    public static Operation put(ByteArray key, byte[] value) {
+        return new Operation(new Operation.PutOp(key.bytes(), value));
     }
 
     /**
diff --git a/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/WatchEvent.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/WatchEvent.java
new file mode 100644
index 0000000..147ff63
--- /dev/null
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/WatchEvent.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.metastorage.client;
+
+import java.util.Collection;
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Watch event contains all entry updates done under one revision. Each particular entry update in this revision
+ * is represented by {@link EntryEvent} entity.
+ */
+public class WatchEvent {
+    /** Events about each entry update in the revision. */
+    private final List<EntryEvent> entryEvts;
+
+    /** Designates that watch event contains only one update revision. */
+    private final boolean single;
+
+    /**
+     * Constructs an watch event with given entry events collection.
+     *
+     * @param entryEvts Events for entries corresponding to an update under one revision.
+     */
+    public WatchEvent(List<EntryEvent> entryEvts) {
+        assert entryEvts != null && !entryEvts.isEmpty();
+
+        this.single = entryEvts.size() == 1;
+        this.entryEvts = entryEvts;
+    }
+
+    /**
+     * Constructs watch event with single entry update.
+     *
+     * @param entryEvt Entry event.
+     */
+    public WatchEvent(@NotNull EntryEvent entryEvt) {
+        this(List.of(entryEvt));
+    }
+
+    /**
+     * Returns {@code true} if watch event contains only one entry event.
+     *
+     * @return {@code True} if watch event contains only one entry event.
+     */
+    public boolean single() {
+        return single;
+    }
+
+    /**
+     * Returns collection of entry entry event done under one revision.
+     *
+     * @return Collection of entry entry event done under one revision.
+     */
+    public Collection<EntryEvent> entryEvents() {
+        return entryEvts;
+    }
+
+    /**
+     * Returns entry event. It is useful method in case when we know that only one event was modified.
+     *
+     * @return Entry event.
+     */
+    public EntryEvent entryEvent() {
+        return entryEvts.get(0);
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/WatchListener.java
similarity index 87%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java
copy to modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/WatchListener.java
index 1ed196e..37c6a6f 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java
+++ b/modules/metastorage-client/src/main/java/org/apache/ignite/metastorage/client/WatchListener.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.metastorage.client;
 
 import org.jetbrains.annotations.NotNull;
 
@@ -26,11 +26,11 @@ public interface WatchListener {
     /**
      * The method will be called on each meta storage update.
      *
-     * @param events A single event or a batch. The batch always contains updates for specific revision.
+     * @param evt A single event or a batch. The batch always contains updates for specific revision.
      * @return {@code True} if listener must continue event handling. If returns {@code false} then the listener and
      * corresponding watch will be unregistered.
      */
-    boolean onUpdate(@NotNull Iterable<WatchEvent> events);
+    boolean onUpdate(@NotNull WatchEvent evt);
 
     /**
      * The method will be called in case of an error occurred. The listener and corresponding watch will be
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/ConditionType.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/ConditionType.java
new file mode 100644
index 0000000..522bd8f
--- /dev/null
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/ConditionType.java
@@ -0,0 +1,53 @@
+/*
+ * 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.metastorage.common;
+
+/**
+ * Defines possible condition types which can be applied to the revision.
+ */
+public enum ConditionType {
+    /** Equality condition type for revision. */
+    REV_EQUAL,
+
+    /** Inequality condition type for revision. */
+    REV_NOT_EQUAL,
+
+    /** Greater than condition type for revision. */
+    REV_GREATER,
+
+    /** Less than condition type for revision. */
+    REV_LESS,
+
+    /** Less than or equal to condition type for revision. */
+    REV_LESS_OR_EQUAL,
+
+    /** Greater than or equal to condition type for revision. */
+    REV_GREATER_OR_EQUAL,
+
+    /** Equality condition type for value. */
+    VAL_EQUAL,
+
+    /** Inequality condition type for value. */
+    VAL_NOT_EQUAL,
+
+    /** Existence condition type for key. */
+    KEY_EXISTS,
+
+    /** Non-existence condition type for key. */
+    KEY_NOT_EXISTS
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/OperationType.java
similarity index 76%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java
copy to modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/OperationType.java
index dcc1c94..7c97859 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/OperationType.java
@@ -15,13 +15,18 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.internal.metastorage.common;
 
 /**
- * Closeable cursor.
- *
- * @param <T>
+ * Defines possible operation types.
  */
-//TODO: Should be replaced by common entity from org.ignite.lang or org.ignite.core package.
-public interface Cursor<T> extends Iterable<T>, AutoCloseable {
+public enum OperationType {
+    /** No-op operation. */
+    NO_OP,
+
+    /** Put (insert/replace) operation. */
+    PUT,
+
+    /** Remove operation. */
+    REMOVE
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/ConditionInfo.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/ConditionInfo.java
new file mode 100644
index 0000000..998be42
--- /dev/null
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/ConditionInfo.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.metastorage.common.command;
+
+import java.io.Serializable;
+import org.apache.ignite.internal.metastorage.common.ConditionType;
+
+/**
+ * Defines condition for InvokeCommand.
+ */
+public class ConditionInfo implements Serializable {
+    /** Key. */
+    private final byte[] key;
+
+    /** Type. */
+    private final ConditionType type;
+
+    /** Value. */
+    private final byte[] val;
+
+    /** Revision. */
+    private final long rev;
+
+    /**
+     * Construct condition with given parameters.
+     *
+     * @param key Key.
+     * @param type Condition type.
+     * @param val Value.
+     * @param rev Revision.
+     */
+    public ConditionInfo(byte[] key, ConditionType type, byte[] val, long rev) {
+        this.key = key;
+        this.type = type;
+        this.val = val;
+        this.rev = rev;
+    }
+
+    /**
+     * Returns key.
+     *
+     * @return Key.
+     */
+    public byte[] key() {
+        return key;
+    }
+
+    /**
+     * Returns condition type.
+     *
+     * @return Condition type.
+     */
+    public ConditionType type() {
+        return type;
+    }
+
+    /**
+     * Returns value.
+     *
+     * @return Value.
+     */
+    public byte[] value() {
+        return val;
+    }
+
+    /**
+     * Returns revision.
+     *
+     * @return Revision.
+     */
+    public long revision() {
+        return rev;
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/ErrorResponse.java
similarity index 60%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java
rename to modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/ErrorResponse.java
index dcc1c94..8ef1744 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Cursor.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/ErrorResponse.java
@@ -15,13 +15,30 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.internal.metastorage.common.command;
 
-/**
- * Closeable cursor.
- *
- * @param <T>
- */
-//TODO: Should be replaced by common entity from org.ignite.lang or org.ignite.core package.
-public interface Cursor<T> extends Iterable<T>, AutoCloseable {
+import java.io.Serializable;
+
+/** Defines error response. */
+public class ErrorResponse implements Serializable {
+    /** Error code. */
+    private final int errCode;
+
+    /**
+     * Constructs error response.
+     *
+     * @param errCode Error code
+     */
+    public ErrorResponse(int errCode) {
+        this.errCode = errCode;
+    }
+
+    /**
+     * Returns error code.
+     *
+     * @return Error code.
+     */
+    public int errorCode() {
+        return errCode;
+    }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAllCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAllCommand.java
index 227ba38..c129931 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAllCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAllCommand.java
@@ -17,37 +17,35 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.Collection;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import java.util.List;
+import java.util.Set;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.ReadCommand;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
- * Get all command for {@link MetaStorageCommandListener} that retrieves entries
+ * Get all command for MetaStorageCommandListener that retrieves entries
  * for given keys and the revision upper bound, if latter is present.
  */
 public final class GetAllCommand implements ReadCommand {
-    /** The collection of keys. */
-    @NotNull private final Collection<Key> keys;
+    /** The list of keys. */
+    @NotNull private final List<byte[]> keys;
 
     /** The upper bound for entry revisions. Must be positive. */
-    @Nullable private Long revUpperBound;
+    private long revUpperBound;
 
     /**
      * @param keys The collection of keys. Couldn't be {@code null} or empty. Collection elements couldn't be {@code
      * null}.
      */
-    public GetAllCommand(@NotNull Collection<Key> keys) {
+    public GetAllCommand(@NotNull Set<ByteArray> keys) {
         assert !keys.isEmpty();
 
-        if (keys instanceof Serializable)
-            this.keys = keys;
-        else
-            this.keys = new ArrayList<>(keys);
+        this.keys = new ArrayList<>(keys.size());
+
+        for (ByteArray key : keys)
+            this.keys.add(key.bytes());
     }
 
     /**
@@ -55,7 +53,7 @@ public final class GetAllCommand implements ReadCommand {
      * null}.
      * @param revUpperBound The upper bound for entry revisions. Must be positive.
      */
-    public GetAllCommand(@NotNull Collection<Key> keys, @NotNull Long revUpperBound) {
+    public GetAllCommand(@NotNull Set<ByteArray> keys, long revUpperBound) {
         this(keys);
 
         assert revUpperBound > 0;
@@ -64,16 +62,16 @@ public final class GetAllCommand implements ReadCommand {
     }
 
     /**
-     * @return The collection of keys.
+     * @return The list of keys.
      */
-    public @NotNull Collection<Key> keys() {
+    public @NotNull List<byte[]> keys() {
         return keys;
     }
 
     /**
      * @return The upper bound for entry revisions. Must be positive.
      */
-    public @Nullable Long revision() {
+    public long revision() {
         return revUpperBound;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndPutAllCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndPutAllCommand.java
index d5bc3e5..cf11013 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndPutAllCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndPutAllCommand.java
@@ -17,40 +17,44 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.List;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import java.util.Map;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Get and put all command for {@link MetaStorageCommandListener} that inserts or updates entries
+ * Get and put all command for MetaStorageCommandListener that inserts or updates entries
  * with given keys and given values and retrieves a previous entries for given keys.
  */
 public final class GetAndPutAllCommand implements WriteCommand {
     /** Keys. */
-    @NotNull private final List<Key> keys;
+    @NotNull private final List<byte[]> keys;
 
     /** Values. */
     @NotNull private final List<byte[]> vals;
 
     /**
-     * @param keys Keys.
      * @param vals Values.
      */
-    public GetAndPutAllCommand(@NotNull List<Key> keys, @NotNull List<byte[]> vals) {
-        assert keys instanceof Serializable;
-        assert vals instanceof Serializable;
+    public GetAndPutAllCommand(@NotNull Map<ByteArray, byte[]> vals) {
+        int size = vals.size();
 
-        this.keys = keys;
-        this.vals = vals;
+        this.keys = new ArrayList<>(size);
+        this.vals = new ArrayList<>(size);
+
+        for (Map.Entry<ByteArray, byte[]> e : vals.entrySet()) {
+            this.keys.add(e.getKey().bytes());
+
+            this.vals.add(e.getValue());
+        }
     }
 
     /**
      * @return Keys.
      */
-    public @NotNull List<Key> keys() {
+    public @NotNull List<byte[]> keys() {
         return keys;
     }
 
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndPutCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndPutCommand.java
index 95581ad..26bfbe5 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndPutCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndPutCommand.java
@@ -17,18 +17,17 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Get and put command for {@link MetaStorageCommandListener} that inserts or updates an entry
+ * Get and put command for MetaStorageCommandListener that inserts or updates an entry
  * with the given key and the given value and retrieves a previous entry for the given key.
  */
 public final class GetAndPutCommand implements WriteCommand {
     /** The key. Couldn't be {@code null}. */
-    @NotNull private final Key key;
+    @NotNull private final byte[] key;
 
     /** The value. Couldn't be {@code null}. */
     @NotNull private final byte[] val;
@@ -37,15 +36,15 @@ public final class GetAndPutCommand implements WriteCommand {
      * @param key The key. Couldn't be {@code null}.
      * @param val The value. Couldn't be {@code null}.
      */
-    public GetAndPutCommand(@NotNull Key key, @NotNull byte[] val) {
-        this.key = key;
+    public GetAndPutCommand(@NotNull ByteArray key, @NotNull byte[] val) {
+        this.key = key.bytes();
         this.val = val;
     }
 
     /**
      * @return The key. Couldn't be {@code null}.
      */
-    public @NotNull Key key() {
+    public @NotNull byte[] key() {
         return key;
     }
 
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndRemoveAllCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndRemoveAllCommand.java
index bea5d1a..0ff8738 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndRemoveAllCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndRemoveAllCommand.java
@@ -17,38 +17,35 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.Collection;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import java.util.List;
+import java.util.Set;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Get and remove all command for {@link MetaStorageCommandListener} that removes entries
+ * Get and remove all command for MetaStorageCommandListener that removes entries
  * for given keys and retrieves previous entries.
  */
 public final class GetAndRemoveAllCommand implements WriteCommand {
     /** The keys collection. Couldn't be {@code null}. */
-    @NotNull private final Collection<Key> keys;
+    @NotNull private final List<byte[]> keys;
 
     /**
      * @param keys The keys collection. Couldn't be {@code null}.
      */
-    public GetAndRemoveAllCommand(@NotNull Collection<Key> keys) {
-        assert !keys.isEmpty();
+    public GetAndRemoveAllCommand(@NotNull Set<ByteArray> keys) {
+        this.keys = new ArrayList<>(keys.size());
 
-        if (keys instanceof Serializable)
-            this.keys = keys;
-        else
-            this.keys = new ArrayList<>(keys);
+        for (ByteArray key : keys)
+            this.keys.add(key.bytes());
     }
 
     /**
      * @return The keys collection. Couldn't be {@code null}.
      */
-    public @NotNull Collection<Key> keys() {
+    public @NotNull List<byte[]> keys() {
         return keys;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndRemoveCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndRemoveCommand.java
index 066fc48..57288ad 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndRemoveCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetAndRemoveCommand.java
@@ -17,30 +17,29 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Get and remove command for {@link MetaStorageCommandListener} that removes an entry
+ * Get and remove command for MetaStorageCommandListener that removes an entry
  * for the given key and retrieves a previous entry for the given key.
  */
 public final class GetAndRemoveCommand implements WriteCommand {
     /** The key. Couldn't be {@code null}. */
-    @NotNull private final Key key;
+    @NotNull private final byte[] key;
 
     /**
      * @param key The key. Couldn't be {@code null}.
      */
-    public GetAndRemoveCommand(@NotNull Key key) {
-        this.key = key;
+    public GetAndRemoveCommand(@NotNull ByteArray key) {
+        this.key = key.bytes();
     }
 
     /**
      * @return The key. Couldn't be {@code null}.
      */
-    public @NotNull Key key() {
+    public @NotNull byte[] key() {
         return key;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetCommand.java
index e3468ec..381a8aa 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/GetCommand.java
@@ -17,36 +17,34 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.ReadCommand;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
- * Get command for {@link MetaStorageCommandListener} that retrieves an entry
+ * Get command for MetaStorageCommandListener that retrieves an entry
  * for the given key and the revision upper bound, if latter is present.
  */
 public final class GetCommand implements ReadCommand {
     /** Key. */
-    @NotNull private final Key key;
+    @NotNull private final byte[] key;
 
     /** The upper bound for entry revisions. Must be positive. */
-    @Nullable private Long revUpperBound;
+    private long revUpperBound;
 
     /**
      * @param key Key. Couldn't be {@code null}.
      */
-    public GetCommand(@NotNull Key key) {
-        this.key = key;
+    public GetCommand(@NotNull ByteArray key) {
+        this.key = key.bytes();
     }
 
     /**
      * @param key Key. Couldn't be {@code null}.
      * @param revUpperBound The upper bound for entry revisions. Must be positive.
      */
-    public GetCommand(@NotNull Key key, @NotNull Long revUpperBound) {
-        this.key = key;
+    public GetCommand(@NotNull ByteArray key, long revUpperBound) {
+        this.key = key.bytes();
 
         assert revUpperBound > 0;
 
@@ -56,14 +54,14 @@ public final class GetCommand implements ReadCommand {
     /**
      * @return Key.
      */
-    public @NotNull Key key() {
+    public @NotNull byte[] key() {
         return key;
     }
 
     /**
      * @return The upper bound for entry revisions, or {@code null} if wasn't specified.
      */
-    public @Nullable Long revision() {
+    public long revision() {
         return revUpperBound;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/InvokeCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/InvokeCommand.java
new file mode 100644
index 0000000..71ef3f0
--- /dev/null
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/InvokeCommand.java
@@ -0,0 +1,75 @@
+/*
+ * 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.metastorage.common.command;
+
+import java.util.List;
+import org.apache.ignite.raft.client.WriteCommand;
+
+/**
+ * Represents invoke command for meta storage.
+ */
+public class InvokeCommand implements WriteCommand {
+    /** Condition. */
+    private final ConditionInfo cond;
+
+    /** Success operations. */
+    private final List<OperationInfo> success;
+
+    /** Failure operations. */
+    private final List<OperationInfo> failure;
+
+    /**
+     * Constructs invoke command instance.
+     *
+     * @param cond Condition.
+     * @param success Success operations.
+     * @param failure Failure operations.
+     */
+    public InvokeCommand(ConditionInfo cond, List<OperationInfo> success, List<OperationInfo> failure) {
+        this.cond = cond;
+        this.success = success;
+        this.failure = failure;
+    }
+
+    /**
+     * Returns condition.
+     *
+     * @return Condition.
+     */
+    public ConditionInfo condition() {
+        return cond;
+    }
+
+    /**
+     * Returns success operations.
+     *
+     * @return Success operations.
+     */
+    public List<OperationInfo> success() {
+        return success;
+    }
+
+    /**
+     * Returns failure operations.
+     *
+     * @return Failure operations.
+     */
+    public List<OperationInfo> failure() {
+        return failure;
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/MultipleEntryResponse.java
similarity index 57%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java
copy to modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/MultipleEntryResponse.java
index b1fe4cb..a11a3e2 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/MultipleEntryResponse.java
@@ -17,29 +17,31 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
-import org.apache.ignite.raft.client.WriteCommand;
-import org.jetbrains.annotations.NotNull;
+import java.io.Serializable;
+import java.util.List;
 
 /**
- * Remove command for {@link MetaStorageCommandListener} that removes an entry for the given key.
+ * Defines response for command which returns a number of results.
  */
-public final class RemoveCommand implements WriteCommand {
-    /** The key. Couldn't be {@code null}. */
-    @NotNull private final Key key;
+public class MultipleEntryResponse implements Serializable {
+    /** Single responses. */
+    private final List<SingleEntryResponse> entries;
 
     /**
-     * @param key he key. Couldn't be {@code null}.
+     * Constructs multiple entries response.
+     *
+     * @param entries The ;list of single responses.
      */
-    public RemoveCommand(@NotNull Key key) {
-        this.key = key;
+    public MultipleEntryResponse(List<SingleEntryResponse> entries) {
+        this.entries = entries;
     }
 
     /**
-     * @return The key. Couldn't be {@code null}.
+     * Returns the list of single responses.
+     *
+     * @return The list of single responses.
      */
-    public @NotNull Key key() {
-        return key;
+    public List<SingleEntryResponse> entries() {
+        return entries;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/OperationInfo.java
similarity index 51%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutCommand.java
copy to modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/OperationInfo.java
index 16205d8..21776a2 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/OperationInfo.java
@@ -17,42 +17,60 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
-import org.apache.ignite.raft.client.WriteCommand;
-import org.jetbrains.annotations.NotNull;
+import org.apache.ignite.internal.metastorage.common.OperationType;
+
+import java.io.Serializable;
 
 /**
- * Put command for {@link MetaStorageCommandListener} that inserts or updates an entry
- * with the given key and the given value and retrieves a previous entry for the given key.
+ * Defines operation.
  */
-public final class PutCommand implements WriteCommand {
-    /** The key. Couldn't be {@code null}. */
-    @NotNull private final Key key;
+public class OperationInfo implements Serializable {
+    /** Key. */
+    private final byte[] key;
+
+    /** Value. */
+    private final byte[] val;
 
-    /** The value. Couldn't be {@code null}. */
-    @NotNull private final byte[] val;
+    /** Operation type. */
+    private final OperationType type;
 
     /**
-     * @param key The key. Couldn't be {@code null}.
-     * @param val The value. Couldn't be {@code null}.
+     * Constructs operation with given parameters.
+     *
+     * @param key Key.
+     * @param val Value.
+     * @param type Operation type.
      */
-    public PutCommand(@NotNull Key key, @NotNull byte[] val) {
+    public OperationInfo(byte[] key, byte[] val, OperationType type) {
         this.key = key;
         this.val = val;
+        this.type = type;
+    }
+
+    /**
+     * Returns operation type.
+     *
+     * @return Operation type.
+     */
+    public OperationType type() {
+        return type;
     }
 
     /**
-     * @return The key. Couldn't be {@code null}.
+     * Returns key.
+     *
+     * @return Key.
      */
-    public @NotNull Key key() {
+    public byte[] key() {
         return key;
     }
 
     /**
-     * @return The value. Couldn't be {@code null}.
+     * Returns value.
+     *
+     * @return Value.
      */
-    public @NotNull byte[] value() {
+    public byte[] value() {
         return val;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutAllCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutAllCommand.java
index 0e87623..fe25f90 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutAllCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutAllCommand.java
@@ -17,38 +17,61 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import java.io.Serializable;
-import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Put all command for {@link MetaStorageCommandListener} that inserts or updates entries
+ * Put all command for MetaStorageCommandListener that inserts or updates entries
  * with given keys and given values.
  */
 public final class PutAllCommand implements WriteCommand {
-    /** The map of keys and corresponding values. Couldn't be {@code null} or empty. */
-    @NotNull private final Map<Key, byte[]> vals;
+    /** List of keys. */
+    private final List<byte[]> keys;
+
+    /** List of values. */
+    private final List<byte[]> vals;
 
     /**
      * @param vals he map of keys and corresponding values. Couldn't be {@code null} or empty.
      */
-    public PutAllCommand(@NotNull Map<Key, byte[]> vals) {
+    public PutAllCommand(@NotNull Map<ByteArray, byte[]> vals) {
         assert !vals.isEmpty();
 
-        if (vals instanceof Serializable)
-            this.vals = vals;
-        else
-            this.vals = new HashMap<>(vals);
+        int size = vals.size();
+
+        this.keys = new ArrayList<>(size);
+
+        this.vals = new ArrayList<>(size);
+
+        for (Map.Entry<ByteArray, byte[]> e : vals.entrySet()) {
+            byte[] key = e.getKey().bytes();
+
+            byte[] val = e.getValue();
+
+            assert key != null : "Key could not be null.";
+            assert val != null : "Value could not be null.";
+
+            this.keys.add(key);
+
+            this.vals.add(val);
+        }
+    }
+
+    /**
+     * @return Entries values.
+     */
+    public @NotNull List<byte[]> keys() {
+        return keys;
     }
 
     /**
-     * @return The map of keys and corresponding values. Couldn't be  or empty.
+     * @return Entries values.
      */
-    public @NotNull Map<Key, byte[]> values() {
+    public @NotNull List<byte[]> values() {
         return vals;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutCommand.java
index 16205d8..fe24ffe 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/PutCommand.java
@@ -17,18 +17,17 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Put command for {@link MetaStorageCommandListener} that inserts or updates an entry
+ * Put command for MetaStorageCommandListener that inserts or updates an entry
  * with the given key and the given value and retrieves a previous entry for the given key.
  */
 public final class PutCommand implements WriteCommand {
     /** The key. Couldn't be {@code null}. */
-    @NotNull private final Key key;
+    @NotNull private final byte[] key;
 
     /** The value. Couldn't be {@code null}. */
     @NotNull private final byte[] val;
@@ -37,15 +36,15 @@ public final class PutCommand implements WriteCommand {
      * @param key The key. Couldn't be {@code null}.
      * @param val The value. Couldn't be {@code null}.
      */
-    public PutCommand(@NotNull Key key, @NotNull byte[] val) {
-        this.key = key;
+    public PutCommand(@NotNull ByteArray key, @NotNull byte[] val) {
+        this.key = key.bytes();
         this.val = val;
     }
 
     /**
      * @return The key. Couldn't be {@code null}.
      */
-    public @NotNull Key key() {
+    public @NotNull byte[] key() {
         return key;
     }
 
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RangeCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RangeCommand.java
index 9027512..b531454 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RangeCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RangeCommand.java
@@ -17,31 +17,30 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Range command for {@link MetaStorageCommandListener} that retrieves entries for the given
+ * Range command for MetaStorageCommandListener that retrieves entries for the given
  * key range in lexicographic order. Entries will be filtered out by upper bound of given revision number.
  */
 public final class RangeCommand implements WriteCommand {
     /** Start key of range (inclusive). Couldn't be {@code null}. */
-    @NotNull private final Key keyFrom;
+    @NotNull private final byte[] keyFrom;
 
     /** End key of range (exclusive). Could be {@code null}. */
-    @Nullable private final Key keyTo;
+    @Nullable private final byte[] keyTo;
 
     /** The upper bound for entry revision. {@code -1} means latest revision. */
-    @NotNull private final Long revUpperBound;
+    @NotNull private final long revUpperBound;
 
     /**
      * @param keyFrom Start key of range (inclusive).
      * @param keyTo End key of range (exclusive).
      */
-    public RangeCommand(@NotNull Key keyFrom, @Nullable Key keyTo) {
+    public RangeCommand(@NotNull ByteArray keyFrom, @Nullable ByteArray keyTo) {
         this(keyFrom, keyTo, -1L);
     }
 
@@ -51,33 +50,33 @@ public final class RangeCommand implements WriteCommand {
      * @param revUpperBound The upper bound for entry revision. {@code -1} means latest revision.
      */
     public RangeCommand(
-        @NotNull Key keyFrom,
-        @Nullable Key keyTo,
-        @NotNull Long revUpperBound
+        @NotNull ByteArray keyFrom,
+        @Nullable ByteArray keyTo,
+        long revUpperBound
     ) {
-        this.keyFrom = keyFrom;
-        this.keyTo = keyTo;
+        this.keyFrom = keyFrom.bytes();
+        this.keyTo = keyTo == null ? null : keyTo.bytes();
         this.revUpperBound = revUpperBound;
     }
 
     /**
      * @return Start key of range (inclusive). Couldn't be {@code null}.
      */
-    public @NotNull Key keyFrom() {
+    public @NotNull byte[] keyFrom() {
         return keyFrom;
     }
 
     /**
      * @return End key of range (exclusive). Could be {@code null}.
      */
-    public @Nullable Key keyTo() {
+    public @Nullable byte[] keyTo() {
         return keyTo;
     }
 
     /**
      * @return The upper bound for entry revision. Means latest revision.
      */
-    public @NotNull Long revUpperBound() {
+    public @NotNull long revUpperBound() {
         return revUpperBound;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveAllCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveAllCommand.java
index 437c368..b40854b 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveAllCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveAllCommand.java
@@ -17,35 +17,34 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.Collection;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import java.util.List;
+import java.util.Set;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Remove all command for {@link MetaStorageCommandListener} that removes entries for given keys.
+ * Remove all command for MetaStorageCommandListener that removes entries for given keys.
  */
 public final class RemoveAllCommand implements WriteCommand {
-    /** The keys collection. Couldn't be {@code null}. */
-    @NotNull private final Collection<Key> keys;
+    /** The keys list. Couldn't be {@code null}. */
+    @NotNull private final List<byte[]> keys;
 
     /**
      * @param keys The keys collection. Couldn't be {@code null}.
      */
-    public RemoveAllCommand(@NotNull Collection<Key> keys) {
-        if (keys instanceof Serializable)
-            this.keys = keys;
-        else
-            this.keys = new ArrayList<>(keys);
+    public RemoveAllCommand(@NotNull Set<ByteArray> keys) {
+        this.keys = new ArrayList<>(keys.size());
+
+        for (ByteArray key : keys)
+            this.keys.add(key.bytes());
     }
 
     /**
-     * @return The keys collection. Couldn't be {@code null}.
+     * @return The keys list. Couldn't be {@code null}.
      */
-    public @NotNull Collection<Key> keys() {
+    public @NotNull List<byte[]> keys() {
         return keys;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java
index b1fe4cb..b98b829 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java
@@ -17,29 +17,28 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Remove command for {@link MetaStorageCommandListener} that removes an entry for the given key.
+ * Remove command for MetaStorageCommandListener that removes an entry for the given key.
  */
 public final class RemoveCommand implements WriteCommand {
     /** The key. Couldn't be {@code null}. */
-    @NotNull private final Key key;
+    @NotNull private final byte[] key;
 
     /**
      * @param key he key. Couldn't be {@code null}.
      */
-    public RemoveCommand(@NotNull Key key) {
-        this.key = key;
+    public RemoveCommand(@NotNull ByteArray key) {
+        this.key = key.bytes();
     }
 
     /**
      * @return The key. Couldn't be {@code null}.
      */
-    public @NotNull Key key() {
+    public @NotNull byte[] key() {
         return key;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/SingleEntryResponse.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/SingleEntryResponse.java
new file mode 100644
index 0000000..678ec1a
--- /dev/null
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/SingleEntryResponse.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.metastorage.common.command;
+
+import java.io.Serializable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Defines response for command which returns exactly one result (entry).
+ */
+public class SingleEntryResponse implements Serializable {
+    /** Key. */
+    @NotNull
+    private final byte[] key;
+
+    /** Value. */
+    @Nullable
+    private final byte[] val;
+
+    /** Revision. */
+    private final long rev;
+
+    /** Update counter */
+    private final long updCntr;
+
+    /**
+     * Constructs single entry response.
+     *
+     * @param key Key. Couldn't be {@code null}.
+     * @param val Value. Could be {@code null} for empty and tombstone entries.
+     * @param rev Revision number.
+     * @param updCntr Update counter.
+     */
+    public SingleEntryResponse(byte[] key, byte[] val, long rev, long updCntr) {
+        this.key = key;
+        this.val = val;
+        this.rev = rev;
+        this.updCntr = updCntr;
+    }
+
+    /**
+     * Returns key.
+     *
+     * @return Entry key. Couldn't be {@code null}.
+     */
+    @NotNull public byte[] key() {
+        return key;
+    }
+
+    /**
+     * Returns value.
+     *
+     * @return Entry value. Could be {@code null} for empty and tombstone entries.
+     */
+    @Nullable public byte[] value() {
+        return val;
+    }
+
+    /**
+     * Returns revision.
+     *
+     * @return Entry revision.
+     */
+    public long revision() {
+        return rev;
+    }
+
+    /**
+     * Returns update counter.
+     *
+     * @return Entry update counter.
+     */
+    public long updateCounter() {
+        return updCntr;
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/WatchExactKeysCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/WatchExactKeysCommand.java
index 0bb9080..87635a3 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/WatchExactKeysCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/WatchExactKeysCommand.java
@@ -17,41 +17,40 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.Collection;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import java.util.List;
+import java.util.Set;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Watch command for {@link MetaStorageCommandListener} that subscribes on meta storage updates matching the parameters.
+ * Watch command for MetaStorageCommandListener that subscribes on meta storage updates matching the parameters.
  */
 public final class WatchExactKeysCommand implements WriteCommand {
-    /** The keys collection. Couldn't be {@code null}. */
-    @NotNull private final Collection<Key> keys;
+    /** The keys list. Couldn't be {@code null}. */
+    @NotNull private final List<byte[]> keys;
 
     /** Start revision inclusive. {@code 0} - all revisions. */
-    @NotNull private final Long revision;
+    private final long revision;
 
     /**
      * @param keys The keys collection. Couldn't be {@code null}.
      * @param revision Start revision inclusive. {@code 0} - all revisions.
      */
-    public WatchExactKeysCommand(@NotNull Collection<Key> keys, @NotNull Long revision) {
-        if (keys instanceof Serializable)
-            this.keys = keys;
-        else
-            this.keys = new ArrayList<>(keys);
+    public WatchExactKeysCommand(@NotNull Set<ByteArray> keys, long revision) {
+        this.keys = new ArrayList<>(keys.size());
+
+        for (ByteArray key : keys)
+            this.keys.add(key.bytes());
 
         this.revision = revision;
     }
 
     /**
-     * @return The keys collection. Couldn't be {@code null}.
+     * @return The keys list. Couldn't be {@code null}.
      */
-    public @NotNull Collection<Key> keys() {
+    public @NotNull List<byte[]> keys() {
         return keys;
     }
 
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/WatchRangeKeysCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/WatchRangeKeysCommand.java
index f959039..49839cf 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/WatchRangeKeysCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/WatchRangeKeysCommand.java
@@ -17,30 +17,28 @@
 
 package org.apache.ignite.internal.metastorage.common.command;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
+import org.apache.ignite.lang.ByteArray;
 import org.apache.ignite.raft.client.WriteCommand;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Watch command for {@link MetaStorageCommandListener} that subscribes on meta storage updates matching the parameters.
+ * Watch command for MetaStorageCommandListener that subscribes on meta storage updates matching the parameters.
  */
 public final class WatchRangeKeysCommand implements WriteCommand {
     /** Start key of range (inclusive). Couldn't be {@code null}. */
-    @Nullable private final Key keyFrom;
+    @Nullable private final byte[] keyFrom;
 
     /** End key of range (exclusive). Could be {@code null}. */
-    @Nullable private final Key keyTo;
+    @Nullable private final byte[] keyTo;
 
     /** Start revision inclusive. {@code 0} - all revisions. */
-    @NotNull private final Long revision;
+    private final long revision;
 
     /**
      * @param keyFrom Start key of range (inclusive).
      * @param keyTo End key of range (exclusive).
      */
-    public WatchRangeKeysCommand(@Nullable Key keyFrom, @Nullable Key keyTo) {
+    public WatchRangeKeysCommand(@Nullable ByteArray keyFrom, @Nullable ByteArray keyTo) {
         this(keyFrom, keyTo, 0L);
     }
 
@@ -50,33 +48,33 @@ public final class WatchRangeKeysCommand implements WriteCommand {
      * @param revision Start revision inclusive. {@code 0} - all revisions.
      */
     public WatchRangeKeysCommand(
-        @Nullable Key keyFrom,
-        @Nullable Key keyTo,
-        @NotNull Long revision
+        @Nullable ByteArray keyFrom,
+        @Nullable ByteArray keyTo,
+        long revision
     ) {
-        this.keyFrom = keyFrom;
-        this.keyTo = keyTo;
+        this.keyFrom = keyFrom == null ? null : keyFrom.bytes();
+        this.keyTo = keyTo == null ? null : keyTo.bytes();
         this.revision = revision;
     }
 
     /**
      * @return Start key of range (inclusive). Couldn't be {@code null}.
      */
-    public @Nullable Key keyFrom() {
+    public @Nullable byte[] keyFrom() {
         return keyFrom;
     }
 
     /**
      * @return End key of range (exclusive). Could be {@code null}.
      */
-    public @Nullable Key keyTo() {
+    public @Nullable byte[] keyTo() {
         return keyTo;
     }
 
     /**
      * @return Start revision inclusive. {@code 0} - all revisions.
      */
-    public @NotNull Long revision() {
+    public long revision() {
         return revision;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorCloseCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorCloseCommand.java
index d07b9a8..aae9c32 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorCloseCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorCloseCommand.java
@@ -18,12 +18,11 @@
 package org.apache.ignite.internal.metastorage.common.command.cursor;
 
 import org.apache.ignite.lang.IgniteUuid;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Cursor close command for {@link MetaStorageCommandListener} that closes cursor with given id.
+ * Cursor close command for MetaStorageCommandListener that closes cursor with given id.
  */
 public class CursorCloseCommand implements WriteCommand {
     /** Cursor id. */
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorHasNextCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorHasNextCommand.java
index 142369a..461c2ce 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorHasNextCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorHasNextCommand.java
@@ -18,12 +18,11 @@
 package org.apache.ignite.internal.metastorage.common.command.cursor;
 
 import org.apache.ignite.lang.IgniteUuid;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
 import org.apache.ignite.raft.client.ReadCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Cursor {@code hasNext} command for {@link MetaStorageCommandListener} that checks whether next element is available.
+ * Cursor {@code hasNext} command for MetaStorageCommandListener that checks whether next element is available.
  */
 public class CursorHasNextCommand implements ReadCommand {
     /** Cursor id. */
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorNextCommand.java b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorNextCommand.java
index e9ba6a7..78b36a6 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorNextCommand.java
+++ b/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/cursor/CursorNextCommand.java
@@ -18,12 +18,11 @@
 package org.apache.ignite.internal.metastorage.common.command.cursor;
 
 import org.apache.ignite.lang.IgniteUuid;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Cursor {@code next} command for {@link MetaStorageCommandListener} that returns next element and moves cursor.
+ * Cursor {@code next} command for MetaStorageCommandListener that returns next element and moves cursor.
  */
 public class CursorNextCommand implements WriteCommand {
     /** Cursor id. */
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Condition.java b/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Condition.java
deleted file mode 100644
index 54049de..0000000
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Condition.java
+++ /dev/null
@@ -1,370 +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.metastorage.common;
-
-import java.util.Arrays;
-
-/**
- * Represents a condition for conditional update.
- */
-public final class Condition {
-    /** Actual condition implementation. */
-    private final InnerCondition cond;
-
-    /**
-     * Constructs a condition which wraps the actual condition implementation.
-     *
-     * @param cond The actual condition implementation.
-     */
-    Condition(InnerCondition cond) {
-        this.cond = cond;
-    }
-
-    /**
-     * Tests the given entry on satisfaction of the condition.
-     *
-     * @param e Entry.
-     * @return The result of condition test. {@code true} - if the entry satisfies to the condition,
-     * otherwise - {@code false}.
-     */
-    public boolean test(Entry e) {
-        return cond.test(e);
-    }
-
-    /**
-     * Represents condition on entry revision. Only one type of condition could be applied to
-     * the one instance of condition. Subsequent invocations of any method which produces condition will throw
-     * {@link IllegalStateException}.
-     */
-    public static final class RevisionCondition implements InnerCondition {
-        /**
-         * The type of condition.
-         *
-         * @see Type
-         */
-        private Type type;
-
-        /** The revision as the condition argument. */
-        private long rev;
-
-        /** Key of entry, which will be tested for condition. */
-        private final Key key;
-
-        /**
-         * Creates a new condition for the given {@code key}.
-         *
-         * @param key Key of entry, to be tested for the condition.
-         */
-        RevisionCondition(Key key) {
-            this.key = key;
-        }
-
-        /**
-         * Produces the condition of type {@link Type#EQUAL}. This condition tests the given revision on equality with
-         * target entry revision.
-         *
-         * @param rev The revision.
-         * @return The condition of type {@link Type#EQUAL}.
-         * @throws IllegalStateException In case when the condition is already defined.
-         */
-        public Condition eq(long rev) {
-            validate(type);
-
-            this.type = Type.EQUAL;
-            this.rev = rev;
-
-            return new Condition(this);
-        }
-
-        /**
-         * Produces the condition of type {@link Type#NOT_EQUAL}. This condition tests the given revision on inequality
-         * with target entry revision.
-         *
-         * @param rev The revision.
-         * @return The condition of type {@link Type#NOT_EQUAL}.
-         * @throws IllegalStateException In case when the condition is already defined.
-         */
-        public Condition ne(long rev) {
-            validate(type);
-
-            this.type = Type.NOT_EQUAL;
-            this.rev = rev;
-
-            return new Condition(this);
-        }
-
-        /**
-         * Produces the condition of type {@link Type#GREATER}. This condition tests that the target entry revision
-         * is greater than given revision.
-         *
-         * @param rev The revision.
-         * @return The condition of type {@link Type#GREATER}.
-         * @throws IllegalStateException In case when the condition is already defined.
-         */
-        public Condition gt(long rev) {
-            validate(type);
-
-            this.type = Type.GREATER;
-            this.rev = rev;
-
-            return new Condition(this);
-        }
-
-        /**
-         * Produces the condition of type {@link Type#GREATER_OR_EQUAL}. This condition tests that the target entry
-         * revision is greater than or equal to given revision.
-         *
-         * @param rev The revision.
-         * @return The condition of type {@link Type#GREATER_OR_EQUAL}.
-         * @throws IllegalStateException In case when the condition is already defined.
-         */
-        public Condition ge(long rev) {
-            validate(type);
-
-            this.type = Type.GREATER_OR_EQUAL;
-            this.rev = rev;
-
-            return new Condition(this);
-        }
-
-        /**
-         * Produces the condition of type {@link Type#LESS}. This condition tests that target entry revision
-         * is less than the given revision.
-         *
-         * @param rev The revision.
-         * @return The condition of type {@link Type#LESS}.
-         * @throws IllegalStateException In case when the condition is already defined.
-         */
-        public Condition lt(long rev) {
-            validate(type);
-
-            this.type = Type.LESS;
-            this.rev = rev;
-
-            return new Condition(this);
-        }
-
-        /**
-         * Produces the condition of type {@link Type#LESS_OR_EQUAL}. This condition tests that target entry revision
-         * is less than or equal to the given revision.
-         *
-         * @param rev The revision.
-         * @return The condition of type {@link Type#LESS_OR_EQUAL}.
-         * @throws IllegalStateException In case when the condition is already defined.
-         */
-        public Condition le(long rev) {
-            validate(type);
-
-            this.type = Type.LESS_OR_EQUAL;
-            this.rev = rev;
-
-            return new Condition(this);
-        }
-
-        /** {@inheritDoc} */
-        @Override public boolean test(Entry e) {
-            if ((e.key() == key) || (e.key() != null && e.key().equals(key))) {
-                int res = Long.compare(e.revision(), rev);
-
-                return type.test(res);
-            }
-            else
-                return false;
-        }
-
-        /**
-         * Defines possible condition types which can be applied to the revision.
-         */
-        enum Type {
-            /** Equality condition type. */
-            EQUAL {
-                @Override public boolean test(long res) {
-                    return res == 0;
-                }
-            },
-
-            /** Inequality condition type. */
-            NOT_EQUAL {
-                @Override public boolean test(long res) {
-                    return res != 0;
-                }
-            },
-
-            /** Greater than condition type. */
-            GREATER {
-                @Override public boolean test(long res) {
-                    return res > 0;
-                }
-            },
-
-            /** Less than condition type. */
-            LESS {
-                @Override public boolean test(long res) {
-                    return res < 0;
-                }
-            },
-
-            /** Less than or equal to condition type. */
-            LESS_OR_EQUAL {
-                @Override public boolean test(long res) {
-                    return res <= 0;
-                }
-            },
-
-            /** Greater than or equal to condition type. */
-            GREATER_OR_EQUAL {
-                @Override public boolean test(long res) {
-                    return res >= 0;
-                }
-            };
-
-            /**
-             * Interprets comparison result.
-             *
-             * @param res The result of comparison.
-             * @return The interpretation of the comparison result.
-             */
-            public abstract boolean test(long res);
-        }
-    }
-
-    /**
-     * Represents condition on entry value. Only one type of condition could be applied to
-     * the one instance of condition. Subsequent invocations of any method which produces condition will throw
-     * {@link IllegalStateException}.
-     */
-    public static final class ValueCondition implements InnerCondition {
-        /**
-         * The type of condition.
-         *
-         * @see Type
-         */
-        private Type type;
-
-        /** The value as the condition argument. */
-        private byte[] val;
-
-        /** Key of entry, which will be tested for condition. */
-        private final Key key;
-
-        /**
-         * Creates a new condition for the given {@code key}.
-         *
-         * @param key Key of entry, to be tested for the condition.
-         */
-        ValueCondition(Key key) {
-            this.key = key;
-        }
-
-        /**
-         * Produces the condition of type {@link Type#EQUAL}. This condition tests the given value on equality with
-         * target entry value.
-         *
-         * @param val The value.
-         * @return The condition of type {@link Type#EQUAL}.
-         * @throws IllegalStateException In case when the condition is already defined.
-         */
-        public Condition eq(byte[] val) {
-            validate(type);
-
-            this.type = Type.EQUAL;
-            this.val = val;
-
-            return new Condition(this);
-        }
-
-        /**
-         * Produces the condition of type {@link Type#NOT_EQUAL}. This condition tests the given value on inequality
-         * with target entry value.
-         *
-         * @param val The value.
-         * @return The condition of type {@link Type#NOT_EQUAL}.
-         * @throws IllegalStateException In case when the condition is already defined.
-         */
-        public Condition ne(byte[] val) {
-            validate(type);
-
-            this.type = Type.NOT_EQUAL;
-            this.val = val;
-
-            return new Condition(this);
-        }
-
-        /** {@inheritDoc} */
-        @Override public boolean test(Entry e) {
-            if ((e.key() == key) || (e.key() != null && e.key().equals(key))) {
-                int res = Arrays.compare(e.value(), val);
-
-                return type.test(res);
-            }
-            else
-               return false;
-        }
-
-        /**
-         * Defines possible condition types which can be applied to the value.
-         */
-        enum Type {
-            /** Equality condition type. */
-            EQUAL {
-                @Override public boolean test(long res) {
-                    return res == 0;
-                }
-            },
-
-            /** Inequality condition type. */
-            NOT_EQUAL {
-                @Override public boolean test(long res) {
-                    return res != 0;
-                }
-            };
-
-            /**
-             * Interprets comparison result.
-             *
-             * @param res The result of comparison.
-             * @return The interpretation of the comparison result.
-             */
-            public abstract boolean test(long res);
-        }
-    }
-
-    /**
-     * Checks that condition is not defined yet. If the condition is already defined then exception will be thrown.
-     *
-     * @throws IllegalStateException In case when the condition is already defined.
-     */
-    private static void validate(Enum<?> type) {
-        if (type != null)
-            throw new IllegalStateException("Condition type " + type.name() + " is already defined.");
-    }
-
-    /**
-     * Defines condition interface.
-     */
-    private interface InnerCondition {
-        /**
-         * Tests the given entry on satisfaction of the condition.
-         *
-         * @param e Entry.
-         * @return The result of condition test. {@code true} - if the entry satisfies to the condition,
-         * otherwise - {@code false}.
-         */
-        boolean test(Entry e);
-    }
-}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/KeyValueStorage.java b/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/KeyValueStorage.java
deleted file mode 100644
index 6155112..0000000
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/KeyValueStorage.java
+++ /dev/null
@@ -1,91 +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.metastorage.common;
-
-import java.util.Collection;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
-
-// TODO: IGNITE-14389 Tmp, used instead of real KeyValueStorage interface from metastorage-server module.
-/**
- *
- */
-@SuppressWarnings("unused") public interface KeyValueStorage {
-    /** */
-    long revision();
-
-    /** */
-    long updateCounter();
-
-    /** */
-    @NotNull Entry get(byte[] key);
-
-    /** */
-    @NotNull Entry get(byte[] key, long rev);
-
-    /** */
-    @NotNull Collection<Entry> getAll(List<byte[]> keys);
-
-    /** */
-    @NotNull Collection<Entry> getAll(List<byte[]> keys, long revUpperBound);
-
-    /** */
-    void put(byte[] key, byte[] value);
-
-    /** */
-    @NotNull Entry getAndPut(byte[] key, byte[] value);
-
-    /** */
-    void putAll(List<byte[]> keys, List<byte[]> values);
-
-    /** */
-    @NotNull Collection<Entry> getAndPutAll(List<byte[]> keys, List<byte[]> values);
-
-    /** */
-    void remove(byte[] key);
-
-    /** */
-    @NotNull Entry getAndRemove(byte[] key);
-
-    /** */
-    void removeAll(List<byte[]> key);
-
-    /** */
-    @NotNull Collection<Entry> getAndRemoveAll(List<byte[]> keys);
-
-    /** */
-    boolean invoke(Condition condition, Collection<Operation> success, Collection<Operation> failure);
-
-    /** */
-    Cursor<Entry> range(byte[] keyFrom, byte[] keyTo);
-
-    /** */
-    Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound);
-
-    /** */
-    Cursor<WatchEvent> watch(byte[] keyFrom, byte[] keyTo, long rev);
-
-    /** */
-    Cursor<WatchEvent> watch(byte[] key, long rev);
-
-    /** */
-    Cursor<WatchEvent> watch(Collection<byte[]> keys, long rev);
-
-    /** */
-    void compact();
-}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/KeyValueStorageImpl.java b/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/KeyValueStorageImpl.java
deleted file mode 100644
index cb59227..0000000
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/KeyValueStorageImpl.java
+++ /dev/null
@@ -1,183 +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.metastorage.common;
-
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import org.apache.ignite.internal.metastorage.common.DummyEntry;
-import org.jetbrains.annotations.NotNull;
-
-// TODO: IGNITE-14389 Tmp, should be removed.
-/**
- *
- */
-@SuppressWarnings("ConstantConditions")
-public class KeyValueStorageImpl implements KeyValueStorage {
-    /** {@inheritDoc} */
-    @Override public long revision() {
-        return 0;
-    }
-
-    /** {@inheritDoc} */
-    @Override public long updateCounter() {
-        return 0;
-    }
-
-    /** {@inheritDoc} */
-    @Override public @NotNull Entry get(byte[] key) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public @NotNull Entry get(byte[] key, long rev) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public @NotNull Collection<Entry> getAll(List<byte[]> keys) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public @NotNull Collection<Entry> getAll(List<byte[]> keys, long revUpperBound) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public void put(byte[] key, byte[] value) {
-
-    }
-
-    /** {@inheritDoc} */
-    @Override public @NotNull Entry getAndPut(byte[] key, byte[] value) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public void putAll(List<byte[]> keys, List<byte[]> values) {
-
-    }
-
-    /** {@inheritDoc} */
-    @Override public @NotNull Collection<Entry> getAndPutAll(List<byte[]> keys, List<byte[]> values) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public void remove(byte[] key) {
-
-    }
-
-    /** {@inheritDoc} */
-    @Override public @NotNull Entry getAndRemove(byte[] key) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public void removeAll(List<byte[]> keys) {
-
-    }
-
-    /** {@inheritDoc} */
-    @Override public @NotNull Collection<Entry> getAndRemoveAll(List<byte[]> keys) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public boolean invoke(Condition condition, Collection<Operation> success, Collection<Operation> failure) {
-        return false;
-    }
-
-    /** {@inheritDoc} */
-    @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
-        return new Cursor<Entry>() {
-            @NotNull @Override public Iterator<Entry> iterator() {
-                return new Iterator<Entry>() {
-                    @Override public boolean hasNext() {
-                        return false;
-                    }
-
-                    @Override public Entry next() {
-                        return null;
-                    }
-                };
-            }
-
-            @Override public void close() throws Exception {
-
-            }
-        };
-    }
-
-    /** {@inheritDoc} */
-    @Override public Cursor<WatchEvent> watch(byte[] keyFrom, byte[] keyTo, long rev) {
-        return new Cursor<>() {
-            /** {@inheritDoc} */
-            @Override public void close(){
-
-            }
-
-            /** {@inheritDoc} */
-            @NotNull @Override public Iterator<WatchEvent> iterator() {
-                return new Iterator<>() {
-                    @Override public boolean hasNext() {
-                        return true;
-                    }
-
-                    @Override public WatchEvent next() {
-                        return new WatchEvent(
-                            new DummyEntry(
-                                new Key(new byte[] {1}),
-                                new byte[] {2},
-                                1L,
-                                1L
-                            ),
-                            new DummyEntry(
-                                new Key(new byte[] {1}),
-                                new byte[] {3},
-                                2L,
-                                2L
-                            )
-                        );
-                    }
-                };
-            }
-        };
-    }
-
-    /** {@inheritDoc} */
-    @Override public Cursor<WatchEvent> watch(byte[] key, long rev) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public Cursor<WatchEvent> watch(Collection<byte[]> keys, long rev) {
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public void compact() {
-
-    }
-}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Operation.java b/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Operation.java
deleted file mode 100644
index 9810fe5..0000000
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Operation.java
+++ /dev/null
@@ -1,93 +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.metastorage.common;
-
-/**
- * Defines operation for meta storage conditional update (invoke).
- */
-public final class Operation {
-    /** Actual operation implementation. */
-    @SuppressWarnings({"FieldCanBeLocal", "unused"})
-    private final InnerOp upd;
-
-    /**
-     * Constructs an operation which wraps the actual operation implementation.
-     *
-     * @param upd The actual operation implementation.
-     */
-    Operation(InnerOp upd) {
-        this.upd = upd;
-    }
-
-    /**
-     * Represents operation of type <i>remove</i>.
-     */
-    public static final class RemoveOp implements InnerOp {
-        /** Key. */
-        private final Key key;
-
-        /**
-         * Creates a new remove operation for the given {@code key}.
-         *
-         * @param key Key.
-         */
-        RemoveOp(Key key) {
-            this.key = key;
-        }
-    }
-
-    /**
-     * Represents operation of type <i>put</i>.
-     */
-    public static final class PutOp implements InnerOp {
-        /** Key. */
-        private final Key key;
-
-        /** Value. */
-        private final byte[] val;
-
-        /**
-         * Constructs operation of type <i>put</i>.
-         *
-         * @param val The value to which the entry should be updated.
-         */
-        PutOp(Key key, byte[] val) {
-            this.key = key;
-            this.val = val;
-        }
-    }
-
-    /**
-     * Represents operation of type <i>no-op</i>.
-     */
-    public static final class NoOp implements InnerOp {
-        /**
-         * Default no-op constructor.
-         */
-        NoOp() {
-            // No-op.
-        }
-    }
-
-    /**
-     * Defines operation interface.
-     */
-    private interface InnerOp {
-        // Marker interface.
-    }
-}
diff --git a/modules/metastorage-server/pom.xml b/modules/metastorage-server/pom.xml
new file mode 100644
index 0000000..5f0f453
--- /dev/null
+++ b/modules/metastorage-server/pom.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-parent</artifactId>
+        <version>1</version>
+        <relativePath>../../parent/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>ignite-metastorage-server</artifactId>
+    <version>3.0.0-SNAPSHOT</version>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-metastorage-common</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+        </dependency>
+
+        <!-- Test dependencies. -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/AbstractCondition.java
similarity index 58%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java
copy to modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/AbstractCondition.java
index b1fe4cb..df4acbe 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/internal/metastorage/common/command/RemoveCommand.java
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/AbstractCondition.java
@@ -15,31 +15,30 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.metastorage.common.command;
+package org.apache.ignite.internal.metastorage.server;
 
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.raft.MetaStorageCommandListener;
-import org.apache.ignite.raft.client.WriteCommand;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Remove command for {@link MetaStorageCommandListener} that removes an entry for the given key.
+ * An abstract condition which could be applied to an entry identified by the key.
  */
-public final class RemoveCommand implements WriteCommand {
-    /** The key. Couldn't be {@code null}. */
-    @NotNull private final Key key;
+public abstract class AbstractCondition implements Condition {
+    /** Entry key. */
+    @NotNull
+    private final byte[] key;
 
     /**
-     * @param key he key. Couldn't be {@code null}.
+     * Constructs a condition with the given entry key.
+     *
+     * @param key Key identifies an entry which the condition will applied to.
      */
-    public RemoveCommand(@NotNull Key key) {
+    public AbstractCondition(@NotNull byte[] key) {
         this.key = key;
     }
 
-    /**
-     * @return The key. Couldn't be {@code null}.
-     */
-    public @NotNull Key key() {
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public byte[] key() {
         return key;
     }
 }
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/CompactedException.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/CompactedException.java
similarity index 97%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/CompactedException.java
rename to modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/CompactedException.java
index 6ea9f43..85cbf3d 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/CompactedException.java
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/CompactedException.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.internal.metastorage.server;
 
 /**
  * Thrown when a requested operation on meta storage could not be performed because target revisions were removed
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Condition.java
similarity index 55%
copy from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java
copy to modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Condition.java
index 1ed196e..e20459a 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Condition.java
@@ -15,28 +15,30 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.internal.metastorage.server;
 
 import org.jetbrains.annotations.NotNull;
 
+import java.util.Collection;
+
 /**
- * The listener which receives and handles watch updates.
+ * Defines interface for condition which could be applied to an entry.
+ * 
+ * @see KeyValueStorage#invoke(Condition, Collection, Collection) 
  */
-public interface WatchListener {
+public interface Condition {
     /**
-     * The method will be called on each meta storage update.
+     * Returns the key which identifies an entry which condition will applied to.
      *
-     * @param events A single event or a batch. The batch always contains updates for specific revision.
-     * @return {@code True} if listener must continue event handling. If returns {@code false} then the listener and
-     * corresponding watch will be unregistered.
+     * @return The key which identifies an entry which condition will applied to.
      */
-    boolean onUpdate(@NotNull Iterable<WatchEvent> events);
+    @NotNull byte[] key();
 
     /**
-     * The method will be called in case of an error occurred. The listener and corresponding watch will be
-     * unregistered.
+     * Tests the given entry on condition.
      *
-     * @param e Exception.
+     * @param e Entry which will be tested on the condition. Can't be {@code null}.
+     * @return {@code True} if the given entry satisfies to the condition, otherwise - {@code false}.
      */
-    void onError(@NotNull Throwable e);
+    boolean test(@NotNull Entry e);
 }
diff --git a/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Entry.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Entry.java
new file mode 100644
index 0000000..0b7670b
--- /dev/null
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Entry.java
@@ -0,0 +1,190 @@
+/*
+ * 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.metastorage.server;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Represents a storage unit as entry with key, value and revision, where
+ * <ul>
+ *     <li>key - an unique entry's key represented by an array of bytes. Keys are comparable in lexicographic manner.</li>
+ *     <li>value - a data which is associated with a key and represented as an array of bytes.</li>
+ *     <li>revision - a number which denotes a version of whole meta storage.
+ *     Each change (which could include multiple entries) increments the revision. </li>
+ *     <li>updateCounter - a number which increments on every update in the change under one revision.</li>
+ * </ul>
+ *
+ * Instance of {@link #Entry} could represents:
+ * <ul>
+ *     <li>A regular entry which stores a particular key, a value and a revision number.</li>
+ *     <li>An empty entry which denotes absence a regular entry in the meta storage for a given key.
+ *     A revision is 0 for such kind of entry.</li>
+ *     <li>A tombstone entry which denotes that a regular entry for a given key was removed from storage on some revision.</li>
+ * </ul>
+ */
+public class Entry {
+    /** Entry key. Couldn't be {@code null}. */
+    @NotNull
+    private final byte[] key;
+
+    /**
+     * Entry value.
+     * <p>
+     *     {@code val == null} only for {@link #empty()} and {@link #tombstone()} entries.
+     * </p>
+     */
+    @Nullable
+    private final byte[] val;
+
+    /**
+     * Revision number corresponding to this particular entry.
+     * <p>
+     *     {@code rev == 0} for {@link #empty()} entry,
+     *     {@code rev > 0} for regular and {@link #tombstone()} entries.
+     * </p>
+     */
+    private final long rev;
+
+    /**
+     * Update counter corresponds to this particular entry.
+     * <p>
+     *     {@code updCntr == 0} for {@link #empty()} entry,
+     *     {@code updCntr > 0} for regular and {@link #tombstone()} entries.
+     * </p>
+     */
+    private final long updCntr;
+
+    /**
+     * Constructor.
+     *
+     * @param key Key bytes. Couldn't be {@code null}.
+     * @param val Value bytes. Couldn't be {@code null}.
+     * @param rev Revision.
+     * @param updCntr Update counter.
+     */
+    // TODO: It seems user will never create Entry, so we can reduce constructor scope to protected or package-private and reuse it from two-place private constructor.
+    public Entry(@NotNull byte[] key, @NotNull byte[] val, long rev, long updCntr) {
+        assert key != null : "key can't be null";
+        assert val != null : "value can't be null";
+
+        this.key = key;
+        this.val = val;
+        this.rev = rev;
+        this.updCntr = updCntr;
+    }
+
+    /**
+     * Constructor for empty and tombstone entries.
+     *
+     * @param key Key bytes. Couldn't be {@code null}.
+     * @param rev Revision.
+     * @param updCntr Update counter.
+     */
+    private Entry(@NotNull byte[] key, long rev, long updCntr) {
+        assert key != null : "key can't be null";
+
+        this.key = key;
+        this.val = null;
+        this.rev = rev;
+        this.updCntr = updCntr;
+    }
+
+    /**
+     * Creates an instance of empty entry for a given key.
+     *
+     * @param key Key bytes. Couldn't be {@code null}.
+     * @return Empty entry.
+     */
+    @NotNull
+    public static Entry empty(byte[] key) {
+        return new Entry(key, 0, 0);
+    }
+
+    /**
+     * Creates an instance of tombstone entry for a given key and a revision.
+     *
+     * @param key Key bytes. Couldn't be {@code null}.
+     * @param rev Revision.
+     * @param updCntr Update counter.
+     * @return Empty entry.
+     */
+    @NotNull
+    public static Entry tombstone(byte[] key, long rev, long updCntr) {
+        assert rev > 0 : "rev must be positive for tombstone entry.";
+        assert updCntr > 0 : "updCntr must be positive for tombstone entry.";
+
+        return new Entry(key, rev, updCntr);
+    }
+
+    /**
+     * Returns a key.
+     *
+     * @return Key.
+     */
+    @NotNull
+    public byte[] key() {
+        return key;
+    }
+
+    /**
+     * Returns a value.
+     *
+     * @return Value.
+     */
+    @Nullable
+    public byte[] value() {
+        return val;
+    }
+
+    /**
+     * Returns a revision.
+     *
+     * @return Revision.
+     */
+    public long revision() {
+        return rev;
+    }
+
+    /**
+     * Returns a update counter.
+     *
+     * @return Update counter.
+     */
+    public long updateCounter() {
+        return updCntr;
+    }
+
+    /**
+     * Returns value which denotes whether entry is tombstone or not.
+     *
+     * @return {@code True} if entry is tombstone, otherwise - {@code false}.
+     */
+    public boolean tombstone() {
+        return val == null && rev > 0 && updCntr > 0;
+    }
+
+    /**
+     * Returns value which denotes whether entry is empty or not.
+     *
+     * @return {@code True} if entry is empty, otherwise - {@code false}.
+     */
+    public boolean empty() {
+        return val == null && rev == 0 && updCntr == 0;
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/EntryEvent.java
similarity index 52%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java
rename to modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/EntryEvent.java
index 1ed196e..ef0adba 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/WatchListener.java
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/EntryEvent.java
@@ -15,28 +15,44 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
-
-import org.jetbrains.annotations.NotNull;
+package org.apache.ignite.internal.metastorage.server;
 
 /**
- * The listener which receives and handles watch updates.
+ * Represent an update event for particular key and entry.
  */
-public interface WatchListener {
+public class EntryEvent {
+    /** Old (previous) entry. */
+    private final Entry oldEntry;
+
+    /** New (current) entry. */
+    private final Entry entry;
+
+    /**
+     * Constructs event with given old and new entries.
+     *
+     * @param oldEntry Old entry.
+     * @param curEntry New entry.
+     */
+    public EntryEvent(Entry oldEntry, Entry curEntry) {
+        this.oldEntry = oldEntry;
+        this.entry = curEntry;
+    }
+
     /**
-     * The method will be called on each meta storage update.
+     * Returns old entry.
      *
-     * @param events A single event or a batch. The batch always contains updates for specific revision.
-     * @return {@code True} if listener must continue event handling. If returns {@code false} then the listener and
-     * corresponding watch will be unregistered.
+     * @return Old entry.
      */
-    boolean onUpdate(@NotNull Iterable<WatchEvent> events);
+    public Entry oldEntry() {
+        return oldEntry;
+    }
 
     /**
-     * The method will be called in case of an error occurred. The listener and corresponding watch will be
-     * unregistered.
+     * Rreturns new entry.
      *
-     * @param e Exception.
+     * @return New entry.
      */
-    void onError(@NotNull Throwable e);
+    public Entry entry() {
+        return entry;
+    }
 }
diff --git a/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/ExistenceCondition.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/ExistenceCondition.java
new file mode 100644
index 0000000..506e23b
--- /dev/null
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/ExistenceCondition.java
@@ -0,0 +1,74 @@
+/*
+ * 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.metastorage.server;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Condition tests an entry on existence in meta storage.
+ * Entry exists if it is not empty and not tombstone.
+ */
+public class ExistenceCondition extends AbstractCondition {
+    /** Condition type. */
+    @NotNull
+    private final Type type;
+
+    /**
+     * Constructs existence condition with the given type and for entry identified by the given key.
+     *
+     * @param type Condition type. Can't be {@code null}.
+     * @param key Key of entry which condition will be applied to. Can't be {@code null}.
+     */
+    public ExistenceCondition(@NotNull Type type, @NotNull byte[] key) {
+        super(key);
+
+        this.type = type;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean test(@NotNull Entry e) {
+        boolean res = !(e.empty() || e.tombstone());
+
+        return type.test(res);
+    }
+
+    /** Defines existence condition types. */
+    public enum Type {
+        /** Equality condition type. */
+        EXISTS {
+            @Override public boolean test(boolean res) {
+                return res;
+            }
+        },
+
+        /** Inequality condition type. */
+        NOT_EXISTS {
+            @Override public boolean test(boolean res) {
+                return !res;
+            }
+        };
+
+        /**
+         * Interprets comparison result.
+         *
+         * @param res The result of comparison.
+         * @return The interpretation of the comparison result.
+         */
+        public abstract boolean test(boolean res);
+    }
+}
diff --git a/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/KeyValueStorage.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/KeyValueStorage.java
new file mode 100644
index 0000000..d4c7da5
--- /dev/null
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/KeyValueStorage.java
@@ -0,0 +1,206 @@
+/*
+ * 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.metastorage.server;
+
+import java.util.Collection;
+import java.util.List;
+import org.apache.ignite.internal.util.Cursor;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Defines key/value storage interface.
+ */
+public interface KeyValueStorage {
+    /**
+     * Returns storage revision.
+     *
+     * @return Storage revision.
+     */
+    long revision();
+
+    /**
+     * Returns update counter.
+     *
+     * @return Update counter.
+     */
+    long updateCounter();
+
+    /**
+     * Returns an entry by the given key.
+     *
+     * @param key The key.
+     * @return Value corresponding to the given key.
+     */
+    @NotNull Entry get(byte[] key);
+
+    /**
+     * Returns an entry by the given key and revision.
+     *
+     * @param key The key.
+     * @param rev The revision.
+     * @return Value corresponding to the given key.
+     */
+    @NotNull Entry get(byte[] key, long rev);
+
+    /**
+     * Returns all entries corresponding to given keys.
+     *
+     * @param keys Keys collection.
+     * @return Entries corresponding to given keys.
+     */
+    @NotNull Collection<Entry> getAll(List<byte[]> keys);
+
+    /**
+     * Returns all entries corresponding to given keys and bounded by the given revision.
+     *
+     * @param keys Keys collection.
+     * @param revUpperBound Upper bound of revision.
+     * @return Entries corresponding to given keys.
+     */
+    @NotNull Collection<Entry> getAll(List<byte[]> keys, long revUpperBound);
+
+    /**
+     * Inserts an entry with the given key and given value.
+     *
+     * @param key The key.
+     * @param value The value.
+     */
+    void put(@NotNull byte[] key, @NotNull byte[] value);
+
+    /**
+     * Inserts an entry with the given key and given value and returns previous entry.
+     *
+     * @param key The key.
+     * @param value The value.
+     * @return Previous entry corresponding to the given key.
+     */
+    @NotNull Entry getAndPut(byte[] key, byte[] value);
+
+    /**
+     * Inserts entries with given keys and given values.
+     *
+     * @param keys The key list.
+     * @param values The values list.
+     */
+    void putAll(List<byte[]> keys, List<byte[]> values);
+
+    /**
+     * Inserts entries with given keys and given values and returns previous entries.
+     *
+     * @param keys The key list.
+     * @param values The values list.
+     * @return Collection of previous entries corresponding to given keys.
+     */
+    @NotNull Collection<Entry> getAndPutAll(List<byte[]> keys, List<byte[]> values);
+
+    /**
+     * Removes an entry with the given key.
+     *
+     * @param key The key.
+     */
+    void remove(byte[] key);
+
+    /**
+     * Removes an entry with the given key and returns previous entry.
+     *
+     * @param key The key.
+     * @return Previous entry.
+     */
+    @NotNull Entry getAndRemove(byte[] key);
+
+    /**
+     * Remove all entries corresponding to given keys.
+     *
+     * @param keys The keys list.
+     */
+    void removeAll(List<byte[]> keys);
+
+    /**
+     * Remove all entries corresponding to given keys and returns previous entries.
+     *
+     * @param keys The keys list.
+     * @return Previous entries.
+     */
+    @NotNull Collection<Entry> getAndRemoveAll(List<byte[]> keys);
+
+    /**
+     * Performs {@code success} operation if condition is {@code true}, otherwise performs
+     * {@code failure} operations.
+     *
+     * @param condition Condition.
+     * @param success Success operations.
+     * @param failure Failure operations.
+     * @return Result of test condition.
+     */
+    boolean invoke(Condition condition, Collection<Operation> success, Collection<Operation> failure);
+
+    /**
+     * Returns cursor by entries which correspond to the given keys range.
+     *
+     * @param keyFrom Start key of range (inclusive).
+     * @param keyTo Last key of range (exclusive).
+     * @return Cursor by entries which correspond to the given keys range.
+     */
+    Cursor<Entry> range(byte[] keyFrom, byte[] keyTo);
+
+    /**
+     * Returns cursor by entries which correspond to the given keys range and bounded by revision number..
+     *
+     * @param keyFrom Start key of range (inclusive).
+     * @param keyTo Last key of range (exclusive).
+     * @param revUpperBound Upper bound of revision.
+     * @return Cursor by entries which correspond to the given keys range.
+     */
+    Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound);
+
+    /**
+     * Creates subscription on updates of entries corresponding to the given keys range and starting from
+     * the given revision number.
+     *
+     * @param keyFrom Start key of range (inclusive).
+     * @param keyTo Last key of range (exclusive).
+     * @param rev Start revision number.
+     * @return Cursor by update events.
+     */
+    Cursor<WatchEvent> watch(byte[] keyFrom, byte[] keyTo, long rev);
+
+    /**
+     * Creates subscription on updates of entries corresponding to the given keys range (where upper bound is unlimited)
+     * and starting from the given revision number.
+     *
+     * @param key Start key of range (inclusive).
+     * @param rev Start revision number.
+     * @return Cursor by update events.
+     */
+    Cursor<WatchEvent> watch(byte[] key, long rev);
+
+    /**
+     * Creates subscription on updates of entries corresponding to the given keys collection
+     * and starting from the given revision number.
+     *
+     * @param keys Collection of keys
+     * @param rev Start revision number.
+     * @return Cursor by update events.
+     */
+    Cursor<WatchEvent> watch(Collection<byte[]> keys, long rev);
+
+    /**
+     * Compacts storage (removes tombstones).
+     */
+    void compact();
+}
diff --git a/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Operation.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Operation.java
new file mode 100644
index 0000000..4c69d7d
--- /dev/null
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Operation.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.metastorage.server;
+
+import java.util.Objects;
+import org.apache.ignite.internal.metastorage.common.OperationType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Defines operation which will be applied to an entry identified by the key.
+ * <p>
+ * Invariants:
+ * <ul>
+ *     <li>Any operation identifies a target entry by not null {@code key} except of {@link OperationType#NO_OP}.</li>
+ *     <li>Only {@link OperationType#PUT} operation contains value which will be written to meta storage.</li>
+ * </ul>
+ */
+public final class Operation {
+    /**
+     * Key identifies an entry which operation will be applied to. Key is {@code null} for {@link OperationType#NO_OP} operation.
+     */
+    @Nullable
+    private final byte[] key;
+
+    /**
+     * Value which will be associated with the {@link #key}. Value is not {@code null} only for {@link OperationType#PUT}
+     * operation.
+     */
+    @Nullable
+    private final byte[] val;
+
+    /**
+     * Operation type.
+     * @see OperationType
+     */
+    @NotNull
+    private final OperationType type;
+
+    /**
+     * Constructs operation which will be applied to an entry identified by the given key.
+     *
+     * @param type Operation type. Can't be {@code null}.
+     * @param key Key identifies an entry which operation will be applied to.
+     * @param val Value will be associated with an entry identified by the {@code key}.
+     */
+    public Operation(@NotNull OperationType type, @Nullable byte[] key, @Nullable byte[] val) {
+        assert (type == OperationType.NO_OP && key == null && val == null)
+                || (type == OperationType.PUT && key != null && val != null)
+                || (type == OperationType.REMOVE && key != null && val == null)
+                : "Invalid operation parameters: [type=" + type + ", key=" + Objects.toString(key,"null") +
+                ", val=" + Objects.toString(key,"null") + ']';
+
+        this.key = key;
+        this.val = val;
+        this.type = type;
+    }
+
+    /**
+     * Returns a key which identifies an entry which operation will be applied to.
+     *
+     * @return A key which identifies an entry which operation will be applied to.
+     */
+    @Nullable public byte[] key() {
+        return key;
+    }
+
+    /**
+     * Returns a value which will be associated with an entry identified by the {@code key}.
+     *
+     * @return A value which will be associated with an entry identified by the {@code key}.
+     */
+    @Nullable public byte[] value() {
+        return val;
+    }
+
+    /**
+     * Returns an operation type.
+     *
+     * @return An operation type.
+     */
+    @NotNull public OperationType type() {
+        return type;
+    }
+}
diff --git a/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/RevisionCondition.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/RevisionCondition.java
new file mode 100644
index 0000000..94e8b71
--- /dev/null
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/RevisionCondition.java
@@ -0,0 +1,110 @@
+/*
+ * 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.metastorage.server;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Defines condition on entry revision.
+ */
+public class RevisionCondition extends AbstractCondition {
+    /** Condition type. */
+    @NotNull
+    private final Type type;
+
+    /** Revision which will be tested against an entry revision. Must be positive. */
+    private final long rev;
+
+    /**
+     * Constructs revision condition with the given type, the key and revision.
+     *
+     * @param type Condition type. Can't be {@code null}.
+     * @param key Key identifies an entry which condition will be applied to. Can't be {@code null}.
+     * @param rev Revision which will be tested against an entry revision. Must be positive.
+     */
+    public RevisionCondition(@NotNull Type type, @NotNull byte[] key, long rev) {
+        super(key);
+
+        assert rev > 0 : "Revision must be positive.";
+
+        this.type = type;
+        this.rev = rev;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean test(@NotNull Entry e) {
+        int res = Long.compare(e.revision(), rev);
+
+        return type.test(res);
+    }
+
+    /**
+     * Defines possible condition types which can be applied to the revision.
+     */
+    public enum Type {
+        /** Equality condition type. */
+        EQUAL {
+            @Override public boolean test(long res) {
+                return res == 0;
+            }
+        },
+
+        /** Inequality condition type. */
+        NOT_EQUAL {
+            @Override public boolean test(long res) {
+                return res != 0;
+            }
+        },
+
+        /** Greater than condition type. */
+        GREATER {
+            @Override public boolean test(long res) {
+                return res > 0;
+            }
+        },
+
+        /** Less than condition type. */
+        LESS {
+            @Override public boolean test(long res) {
+                return res < 0;
+            }
+        },
+
+        /** Less than or equal to condition type. */
+        LESS_OR_EQUAL {
+            @Override public boolean test(long res) {
+                return res <= 0;
+            }
+        },
+
+        /** Greater than or equal to condition type. */
+        GREATER_OR_EQUAL {
+            @Override public boolean test(long res) {
+                return res >= 0;
+            }
+        };
+
+        /**
+         * Interprets comparison result.
+         *
+         * @param res The result of comparison.
+         * @return The interpretation of the comparison result.
+         */
+        public abstract boolean test(long res);
+    }
+}
diff --git a/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/SimpleInMemoryKeyValueStorage.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/SimpleInMemoryKeyValueStorage.java
new file mode 100644
index 0000000..271b8df
--- /dev/null
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/SimpleInMemoryKeyValueStorage.java
@@ -0,0 +1,775 @@
+/*
+ * 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.metastorage.server;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+import org.apache.ignite.internal.util.Cursor;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.ignite.internal.metastorage.server.Value.TOMBSTONE;
+
+/**
+ * Simple in-memory key/value storage.
+ *
+ * WARNING: Only for test purposes.
+ */
+public class SimpleInMemoryKeyValueStorage implements KeyValueStorage {
+    /** Lexicographical comparator. */
+    private static final Comparator<byte[]> CMP = Arrays::compare;
+
+    /**
+     * Special value for revision number which means that operation should be applied
+     * to the latest revision of an entry.
+     */
+    private static final long LATEST_REV = -1;
+
+    /** Keys index. Value is the list of all revisions under which entry corresponding to the key was modified. */
+    private NavigableMap<byte[], List<Long>> keysIdx = new TreeMap<>(CMP);
+
+    /**  Revisions index. Value contains all entries which were modified under particular revision. */
+    private NavigableMap<Long, NavigableMap<byte[], Value>> revsIdx = new TreeMap<>();
+
+    /** Revision. Will be incremented for each single-entry or multi-entry update operation. */
+    private long rev;
+
+    /** Update counter. Will be incremented for each update of any particular entry. */
+    private long updCntr;
+
+    /** All operations are queued on this lock. */
+    private final Object mux = new Object();
+
+    /** {@inheritDoc} */
+    @Override public long revision() {
+        return rev;
+    }
+
+    /** {@inheritDoc} */
+    @Override public long updateCounter() {
+        return updCntr;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void put(byte[] key, byte[] value) {
+        synchronized (mux) {
+            long curRev = rev + 1;
+
+            doPut(key, value, curRev);
+
+            rev = curRev;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public Entry getAndPut(byte[] key, byte[] bytes) {
+        synchronized (mux) {
+            long curRev = rev + 1;
+
+            long lastRev = doPut(key, bytes, curRev);
+
+            rev = curRev;
+
+            // Return previous value.
+            return doGetValue(key, lastRev);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void putAll(List<byte[]> keys, List<byte[]> values) {
+        synchronized (mux) {
+            long curRev = rev + 1;
+
+            doPutAll(curRev, keys, values);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public Collection<Entry> getAndPutAll(List<byte[]> keys, List<byte[]> values) {
+        Collection<Entry> res;
+
+        synchronized (mux) {
+            long curRev = rev + 1;
+
+            res = doGetAll(keys, curRev);
+
+            doPutAll(curRev, keys, values);
+        }
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public Entry get(byte[] key) {
+        synchronized (mux) {
+            return doGet(key, LATEST_REV, false);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public Entry get(byte[] key, long rev) {
+        synchronized (mux) {
+            return doGet(key, rev, true);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public Collection<Entry> getAll(List<byte[]> keys) {
+        return doGetAll(keys, LATEST_REV);
+    }
+
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public Collection<Entry> getAll(List<byte[]> keys, long revUpperBound) {
+        return doGetAll(keys, revUpperBound);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void remove(byte[] key) {
+        synchronized (mux) {
+            long curRev = rev + 1;
+
+            if (doRemove(key, curRev))
+                rev = curRev;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public Entry getAndRemove(byte[] key) {
+        synchronized (mux) {
+            Entry e = doGet(key, LATEST_REV, false);
+
+            if (e.empty() || e.tombstone())
+                return e;
+
+            return getAndPut(key, TOMBSTONE);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void removeAll(List<byte[]> keys) {
+        synchronized (mux) {
+            long curRev = rev + 1;
+
+            List<byte[]> existingKeys = new ArrayList<>(keys.size());
+
+            List<byte[]> vals = new ArrayList<>(keys.size());
+
+            for (byte[] key : keys) {
+                Entry e = doGet(key, LATEST_REV, false);
+
+                if (e.empty() || e.tombstone())
+                    continue;
+
+                existingKeys.add(key);
+
+                vals.add(TOMBSTONE);
+            }
+
+            doPutAll(curRev, existingKeys, vals);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @NotNull
+    @Override public Collection<Entry> getAndRemoveAll(List<byte[]> keys) {
+        Collection<Entry> res = new ArrayList<>(keys.size());
+
+        synchronized (mux) {
+            long curRev = rev + 1;
+
+            List<byte[]> existingKeys = new ArrayList<>(keys.size());
+
+            List<byte[]> vals = new ArrayList<>(keys.size());
+
+            for (byte[] key : keys) {
+                Entry e = doGet(key, LATEST_REV, false);
+
+                res.add(e);
+
+                if (e.empty() || e.tombstone())
+                    continue;
+
+                existingKeys.add(key);
+
+                vals.add(TOMBSTONE);
+            }
+
+            doPutAll(curRev, existingKeys, vals);
+        }
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean invoke(Condition condition, Collection<Operation> success, Collection<Operation> failure) {
+        synchronized (mux) {
+            Entry e = get(condition.key());
+
+            boolean branch = condition.test(e);
+
+            Collection<Operation> ops = branch ? success : failure;
+
+            long curRev = rev + 1;
+
+            boolean modified = false;
+
+            for (Operation op : ops) {
+                switch (op.type()) {
+                    case PUT:
+                        doPut(op.key(), op.value(), curRev);
+
+                        modified = true;
+
+                        break;
+
+                    case REMOVE:
+                        modified |= doRemove(op.key(), curRev);
+
+                        break;
+
+                    case NO_OP:
+                        break;
+
+                    default:
+                        throw new IllegalArgumentException("Unknown operation type: " + op.type());
+                }
+            }
+
+            if (modified)
+                rev = curRev;
+
+            return branch;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo) {
+        return new RangeCursor(keyFrom, keyTo, rev);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Cursor<Entry> range(byte[] keyFrom, byte[] keyTo, long revUpperBound) {
+        return new RangeCursor(keyFrom, keyTo, revUpperBound);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Cursor<WatchEvent> watch(byte[] keyFrom, byte[] keyTo, long rev) {
+        assert keyFrom != null;
+        assert rev > 0;
+
+        return new WatchCursor(rev, k ->
+            CMP.compare(keyFrom, k) <= 0 && (keyTo == null || CMP.compare(k, keyTo) < 0)
+        );
+    }
+
+    /** {@inheritDoc} */
+    @Override public Cursor<WatchEvent> watch(byte[] key, long rev) {
+        assert key != null;
+        assert rev > 0;
+
+        return new WatchCursor(rev, k -> CMP.compare(k, key) == 0);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Cursor<WatchEvent> watch(Collection<byte[]> keys, long rev) {
+        assert keys != null && !keys.isEmpty();
+        assert rev > 0;
+
+        TreeSet<byte[]> keySet = new TreeSet<>(CMP);
+
+        keySet.addAll(keys);
+
+        return new WatchCursor(rev, keySet::contains);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void compact() {
+        synchronized (mux) {
+            NavigableMap<byte[], List<Long>> compactedKeysIdx = new TreeMap<>(CMP);
+
+            NavigableMap<Long, NavigableMap<byte[], Value>> compactedRevsIdx = new TreeMap<>();
+
+            keysIdx.forEach((key, revs) -> compactForKey(key, revs, compactedKeysIdx, compactedRevsIdx));
+
+            keysIdx = compactedKeysIdx;
+
+            revsIdx = compactedRevsIdx;
+        }
+    }
+
+    /** */
+    private boolean doRemove(byte[] key, long curRev) {
+        Entry e = doGet(key, LATEST_REV, false);
+
+        if (e.empty() || e.tombstone())
+            return false;
+
+        doPut(key, TOMBSTONE, curRev);
+
+        return true;
+    }
+
+    /** */
+    private void compactForKey(
+            byte[] key,
+            List<Long> revs,
+            NavigableMap<byte[], List<Long>> compactedKeysIdx,
+            NavigableMap<Long, NavigableMap<byte[], Value>> compactedRevsIdx
+    ) {
+        Long lastRev = lastRevision(revs);
+
+        NavigableMap<byte[], Value> kv = revsIdx.get(lastRev);
+
+        Value lastVal = kv.get(key);
+
+        if (!lastVal.tombstone()) {
+            compactedKeysIdx.put(key, listOf(lastRev));
+
+            NavigableMap<byte[], Value> compactedKv = compactedRevsIdx.computeIfAbsent(
+                    lastRev,
+                    k -> new TreeMap<>(CMP)
+            );
+
+            compactedKv.put(key, lastVal);
+        }
+    }
+
+    /** */
+    @NotNull
+    private Collection<Entry> doGetAll(List<byte[]> keys, long rev) {
+        assert keys != null : "keys list can't be null.";
+        assert !keys.isEmpty() : "keys list can't be empty.";
+        assert rev > 0 || rev == LATEST_REV : "Revision must be positive.";
+
+        Collection<Entry> res = new ArrayList<>(keys.size());
+
+        synchronized (mux) {
+            for (byte[] key : keys) {
+                res.add(doGet(key, rev, false));
+            }
+        }
+
+        return res;
+    }
+
+    /** */
+    @NotNull
+    private Entry doGet(byte[] key, long rev, boolean exactRev) {
+        assert rev == LATEST_REV && !exactRev || rev > LATEST_REV :
+                "Invalid arguments: [rev=" + rev + ", exactRev=" + exactRev + ']';
+
+        List<Long> revs = keysIdx.get(key);
+
+        if (revs == null || revs.isEmpty())
+            return Entry.empty(key);
+
+        long lastRev;
+
+        if (rev == LATEST_REV)
+            lastRev = lastRevision(revs);
+        else
+            lastRev = exactRev ? rev : maxRevision(revs, rev);
+
+        // lastRev can be -1 if maxRevision return -1.
+        if (lastRev == -1)
+            return Entry.empty(key);
+
+        return doGetValue(key, lastRev);
+    }
+
+    /**
+     * Returns maximum revision which must be less or equal to {@code upperBoundRev}. If there is no such revision then
+     * {@code -1} will be returned.
+     *
+     * @param revs Revisions list.
+     * @param upperBoundRev Revision upper bound.
+     * @return Appropriate revision or {@code -1} if there is no such revision.
+     */
+    private static long maxRevision(List<Long> revs, long upperBoundRev) {
+        int i = revs.size() - 1;
+
+        for (; i >= 0; i--) {
+            long rev = revs.get(i);
+
+            if (rev <= upperBoundRev)
+                return rev;
+        }
+
+        return -1;
+    }
+
+    /** */
+    @NotNull
+    private Entry doGetValue(byte[] key, long lastRev) {
+        if (lastRev == 0)
+            return Entry.empty(key);
+
+        NavigableMap<byte[], Value> lastRevVals = revsIdx.get(lastRev);
+
+        if (lastRevVals == null || lastRevVals.isEmpty())
+            return Entry.empty(key);
+
+        Value lastVal = lastRevVals.get(key);
+
+        if (lastVal.tombstone())
+            return Entry.tombstone(key, lastRev, lastVal.updateCounter());
+
+        return new Entry(key, lastVal.bytes(), lastRev, lastVal.updateCounter());
+    }
+
+    /** */
+    private long doPut(byte[] key, byte[] bytes, long curRev) {
+        long curUpdCntr = ++updCntr;
+
+        // Update keysIdx.
+        List<Long> revs = keysIdx.computeIfAbsent(key, k -> new ArrayList<>());
+
+        long lastRev = revs.isEmpty() ? 0 : lastRevision(revs);
+
+        revs.add(curRev);
+
+        // Update revsIdx.
+        Value val = new Value(bytes, curUpdCntr);
+
+        revsIdx.compute(
+                curRev,
+                (rev, entries) -> {
+                    if (entries == null)
+                        entries = new TreeMap<>(CMP);
+
+                    entries.put(key, val);
+
+                    return entries;
+                }
+        );
+
+        return lastRev;
+    }
+
+    /** */
+    private long doPutAll(long curRev, List<byte[]> keys, List<byte[]> bytesList) {
+        synchronized (mux) {
+            // Update revsIdx.
+            NavigableMap<byte[], Value> entries = new TreeMap<>(CMP);
+
+            for (int i = 0; i < keys.size(); i++) {
+                byte[] key = keys.get(i);
+
+                byte[] bytes = bytesList.get(i);
+
+                long curUpdCntr = ++updCntr;
+
+                // Update keysIdx.
+                List<Long> revs = keysIdx.computeIfAbsent(key, k -> new ArrayList<>());
+
+                revs.add(curRev);
+
+                Value val = new Value(bytes, curUpdCntr);
+
+                entries.put(key, val);
+
+                revsIdx.put(curRev, entries);
+            }
+
+            rev = curRev;
+
+            return curRev;
+        }
+    }
+
+    /** */
+    private static long lastRevision(List<Long> revs) {
+        return revs.get(revs.size() - 1);
+    }
+
+    /** */
+    private static List<Long> listOf(long val) {
+        List<Long> res = new ArrayList<>();
+
+        res.add(val);
+
+        return res;
+    }
+
+    /** */
+    private class RangeCursor implements Cursor<Entry> {
+        /** */
+        private final byte[] keyFrom;
+
+        /** */
+        private final byte[] keyTo;
+
+        /** */
+        private final long rev;
+
+        /** */
+        private final Iterator<Entry> it;
+
+        /** */
+        private Entry nextRetEntry;
+
+        /** */
+        private byte[] lastRetKey;
+
+        /** */
+        private boolean finished;
+
+        /** */
+        RangeCursor(byte[] keyFrom, byte[] keyTo, long rev) {
+            this.keyFrom = keyFrom;
+            this.keyTo = keyTo;
+            this.rev = rev;
+            this.it = createIterator();
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean hasNext() {
+            return it.hasNext();
+        }
+
+        /** {@inheritDoc} */
+        @Override public Entry next() {
+            return it.next();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void close() throws Exception {
+            // TODO: implement.
+        }
+
+        /** {@inheritDoc} */
+        @NotNull
+        @Override public Iterator<Entry> iterator() {
+            return it;
+        }
+
+        @NotNull
+        Iterator<Entry> createIterator() {
+            return new Iterator<>() {
+                /** {@inheritDoc} */
+                @Override public boolean hasNext() {
+                    synchronized (mux) {
+                        while (true) {
+                            if (finished)
+                                return false;
+
+                            if (nextRetEntry != null)
+                                return true;
+
+                            byte[] key = lastRetKey;
+
+                            while (!finished || nextRetEntry == null) {
+                                Map.Entry<byte[], List<Long>> e =
+                                        key == null ? keysIdx.ceilingEntry(keyFrom) : keysIdx.higherEntry(key);
+
+                                if (e == null) {
+                                    finished = true;
+
+                                    break;
+                                }
+
+                                key = e.getKey();
+
+                                if (keyTo != null && CMP.compare(key, keyTo) >= 0) {
+                                    finished = true;
+
+                                    break;
+                                }
+
+                                List<Long> revs = e.getValue();
+
+                                assert revs != null && !revs.isEmpty() :
+                                        "Revisions should not be empty: [revs=" + revs + ']';
+
+                                long lastRev = maxRevision(revs, rev);
+
+                                if (lastRev == -1)
+                                    continue;
+
+                                Entry entry = doGetValue(key, lastRev);
+
+                                assert !entry.empty() : "Iterator should not return empty entry.";
+
+                                nextRetEntry = entry;
+
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                /** {@inheritDoc} */
+                @Override public Entry next() {
+                    synchronized (mux) {
+                        while (true) {
+                            if (finished)
+                                throw new NoSuchElementException();
+
+                            if (nextRetEntry != null) {
+                                Entry e = nextRetEntry;
+
+                                nextRetEntry = null;
+
+                                lastRetKey = e.key();
+
+                                return e;
+                            } else
+                                hasNext();
+                        }
+                    }
+                }
+            };
+        }
+    }
+
+    /** */
+    private class WatchCursor implements Cursor<WatchEvent> {
+        /** */
+        private final Predicate<byte[]> p;
+
+        /** */
+        private final Iterator<WatchEvent> it;
+
+        /** */
+        private long lastRetRev;
+
+        /** */
+        private long nextRetRev = -1;
+
+        /** */
+        WatchCursor(long rev, Predicate<byte[]> p) {
+            this.p = p;
+            this.lastRetRev = rev - 1;
+            this.it = createIterator();
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean hasNext() {
+            return it.hasNext();
+        }
+
+        /** {@inheritDoc} */
+        @Override public WatchEvent next() {
+            return it.next();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void close() throws Exception {
+            // TODO: implement.
+        }
+
+        /** {@inheritDoc} */
+        @NotNull
+        @Override public Iterator<WatchEvent> iterator() {
+            return it;
+        }
+
+        @NotNull
+        Iterator<WatchEvent> createIterator() {
+            return new Iterator<>() {
+                /** {@inheritDoc} */
+                @Override public boolean hasNext() {
+                    synchronized (mux) {
+                        if (nextRetRev != -1)
+                            return true;
+
+                        while (true) {
+                            long curRev = lastRetRev + 1;
+
+                            NavigableMap<byte[], Value> entries = revsIdx.get(curRev);
+
+                            if (entries == null)
+                                return false;
+
+                            for (byte[] key : entries.keySet()) {
+                                if (p.test(key)) {
+                                    nextRetRev = curRev;
+
+                                    return true;
+                                }
+                            }
+
+                            lastRetRev++;
+                        }
+                    }
+                }
+
+                /** {@inheritDoc} */
+                @Override public WatchEvent next() {
+                    synchronized (mux) {
+                        while (true) {
+                            if (nextRetRev != -1) {
+                                NavigableMap<byte[], Value> entries = revsIdx.get(nextRetRev);
+
+                                if (entries == null)
+                                    return null;
+
+                                List<EntryEvent> evts = new ArrayList<>(entries.size());
+
+                                for (Map.Entry<byte[], Value> e : entries.entrySet()) {
+                                    byte[] key = e.getKey();
+
+                                    Value val = e.getValue();
+
+                                    if (p.test(key)) {
+                                        Entry newEntry;
+
+                                        if (val.tombstone())
+                                            newEntry = Entry.tombstone(key, nextRetRev, val.updateCounter());
+                                        else
+                                            newEntry = new Entry(key, val.bytes(), nextRetRev, val.updateCounter());
+
+                                        Entry oldEntry = doGet(key, nextRetRev - 1, false);
+
+                                        evts.add(new EntryEvent(oldEntry, newEntry));
+                                    }
+                                }
+
+                                if (evts.isEmpty())
+                                    continue;
+
+                                lastRetRev = nextRetRev;
+
+                                nextRetRev = -1;
+
+                                return new WatchEvent(evts);
+                            } else if (!hasNext())
+                                return null;
+                        }
+                    }
+                }
+            };
+        }
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Entry.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Value.java
similarity index 50%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Entry.java
rename to modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Value.java
index 7f653cc..ce79b17 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/Entry.java
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/Value.java
@@ -15,45 +15,58 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common;
+package org.apache.ignite.internal.metastorage.server;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
- * Represents a storage unit as entry with key, value and revision, where
- * <ul>
- *     <li>key - an unique entry's key. Keys are comparable in lexicographic manner.</li>
- *     <li>value - a data which is associated with a key and represented as an array of bytes.</li>
- *     <li>revision - a number which denotes a version of whole meta storage. Each change increments the revision.</li>
- * </ul>
+ * Meta storage value with update counter.
  */
-public interface Entry {
+public class Value {
+    /** Tombstone constant. */
+    public static final byte[] TOMBSTONE = new byte[0];
+
+    /** Value. */
+    private final byte[] bytes;
+
+    /** Update counter. */
+    private final long updCntr;
+
     /**
-     * Returns a key.
+     * Constructs value.
      *
-     * @return The key.
+     * @param bytes Value bytes or {@code #TOMBSTONE}.
+     * @param updCntr Update cpunter.
      */
-    @NotNull Key key();
+    public Value(@NotNull byte[] bytes, long updCntr) {
+        this.bytes = bytes;
+        this.updCntr = updCntr;
+    }
 
     /**
-     * Returns a value. Could be {@code null} for empty entry.
+     * Returns value bytes.
      *
-     * @return Value.
+     * @return Value bytes.
      */
-    @Nullable byte[] value();
+    public byte[] bytes() {
+        return bytes;
+    }
 
     /**
-     * Returns a revision.
+     * Returns update counter.
      *
-     * @return Revision.
+     * @return Update counter.
      */
-    long revision();
+    public long updateCounter() {
+        return updCntr;
+    }
 
     /**
-     * Returns an update counter.
+     * Returns {@code true} if value is tombstone.
      *
-     * @return Update counter.
+     * @return {@code True} if value is tombstone, otherwise - {@code false}.
      */
-    long updateCounter();
+    boolean tombstone() {
+        return bytes == TOMBSTONE;
+    }
 }
diff --git a/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/ValueCondition.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/ValueCondition.java
new file mode 100644
index 0000000..f33ff9b
--- /dev/null
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/ValueCondition.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.metastorage.server;
+
+import java.util.Arrays;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Defines condition on entry value.
+ */
+public class ValueCondition extends AbstractCondition {
+    /** Condition type. */
+    @NotNull
+    private final Type type;
+
+    /** Value which will be tested against an entry value. */
+    @NotNull
+    private final byte[] val;
+
+    /**
+     * Constructs value condition with the given type, key and value.
+     *
+     * @param type Condition type. Can't be {@code null}.
+     * @param key Key identifies an entry which condition will be applied to. Can't be {@code null}.
+     * @param val Value which will be tested against an entry value. Can't be {@code null}.
+     */
+    public ValueCondition(@NotNull Type type, @NotNull byte[] key, @NotNull byte[] val) {
+        super(key);
+
+        this.type = type;
+        this.val = val;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean test(@NotNull Entry e) {
+        int res = Arrays.compare(e.value(), val);
+
+        return type.test(res);
+    }
+
+    /**
+     * Defines possible condition types which can be applied to the value.
+     */
+    public enum Type {
+        /** Equality condition type. */
+        EQUAL {
+            @Override public boolean test(long res) {
+                return res == 0;
+            }
+        },
+
+        /** Inequality condition type. */
+        NOT_EQUAL {
+            @Override public boolean test(long res) {
+                return res != 0;
+            }
+        };
+
+        /**
+         * Interprets comparison result.
+         *
+         * @param res The result of comparison.
+         * @return The interpretation of the comparison result.
+         */
+        public abstract boolean test(long res);
+    }
+}
diff --git a/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/WatchEvent.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/WatchEvent.java
new file mode 100644
index 0000000..24b1e98
--- /dev/null
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/WatchEvent.java
@@ -0,0 +1,72 @@
+/*
+ * 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.metastorage.server;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Watch event contains all entry updates done under one revision. Each particular entry update in this revision
+ * is represented by {@link EntryEvent} entity.
+ */
+public class WatchEvent {
+    /** Events about each entry update in the revision. */
+    private final List<EntryEvent> entryEvts;
+
+    /** Designates that watch event contains only one update revision. */
+    private final boolean single;
+
+    /**
+     * Constructs an watch event with given entry events collection.
+     *
+     * @param entryEvts Events for entries corresponding to an update under one revision.
+     */
+    public WatchEvent(List<EntryEvent> entryEvts) {
+        assert entryEvts != null && !entryEvts.isEmpty();
+
+        this.single = entryEvts.size() == 1;
+        this.entryEvts = entryEvts;
+    }
+
+    /**
+     * Returns {@code true} if watch event contains only one entry event.
+     *
+     * @return {@code True} if watch event contains only one entry event.
+     */
+    public boolean single() {
+        return single;
+    }
+
+    /**
+     * Returns collection of entry entry event done under one revision.
+     *
+     * @return Collection of entry entry event done under one revision.
+     */
+    public Collection<EntryEvent> entryEvents() {
+        return entryEvts;
+    }
+
+    /**
+     * Returns entry event. It is useful method in case when we know that only one event was modified.
+     *
+     * @return Entry event.
+     */
+    public EntryEvent entryEvent() {
+        return entryEvts.get(0);
+    }
+}
diff --git a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/raft/MetaStorageCommandListener.java b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/raft/MetaStorageCommandListener.java
similarity index 53%
rename from modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/raft/MetaStorageCommandListener.java
rename to modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/raft/MetaStorageCommandListener.java
index 464d871..77a5a13 100644
--- a/modules/metastorage-common/src/main/java/org/apache/ignite/metastorage/common/raft/MetaStorageCommandListener.java
+++ b/modules/metastorage-server/src/main/java/org/apache/ignite/internal/metastorage/server/raft/MetaStorageCommandListener.java
@@ -15,47 +15,54 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.metastorage.common.raft;
+package org.apache.ignite.internal.metastorage.server.raft;
 
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
+import org.apache.ignite.internal.metastorage.common.ConditionType;
+import org.apache.ignite.internal.metastorage.common.command.ConditionInfo;
 import org.apache.ignite.internal.metastorage.common.command.GetAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetAndPutAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetAndPutCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetAndRemoveAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetAndRemoveCommand;
 import org.apache.ignite.internal.metastorage.common.command.GetCommand;
+import org.apache.ignite.internal.metastorage.common.command.InvokeCommand;
+import org.apache.ignite.internal.metastorage.common.command.MultipleEntryResponse;
+import org.apache.ignite.internal.metastorage.common.command.OperationInfo;
 import org.apache.ignite.internal.metastorage.common.command.PutAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.PutCommand;
 import org.apache.ignite.internal.metastorage.common.command.RangeCommand;
 import org.apache.ignite.internal.metastorage.common.command.RemoveAllCommand;
 import org.apache.ignite.internal.metastorage.common.command.RemoveCommand;
+import org.apache.ignite.internal.metastorage.common.command.SingleEntryResponse;
 import org.apache.ignite.internal.metastorage.common.command.WatchExactKeysCommand;
 import org.apache.ignite.internal.metastorage.common.command.WatchRangeKeysCommand;
 import org.apache.ignite.internal.metastorage.common.command.cursor.CursorCloseCommand;
 import org.apache.ignite.internal.metastorage.common.command.cursor.CursorHasNextCommand;
 import org.apache.ignite.internal.metastorage.common.command.cursor.CursorNextCommand;
+import org.apache.ignite.internal.metastorage.server.Condition;
+import org.apache.ignite.internal.metastorage.server.Entry;
+import org.apache.ignite.internal.metastorage.server.EntryEvent;
+import org.apache.ignite.internal.metastorage.server.ExistenceCondition;
+import org.apache.ignite.internal.metastorage.server.KeyValueStorage;
+import org.apache.ignite.internal.metastorage.server.Operation;
+import org.apache.ignite.internal.metastorage.server.RevisionCondition;
+import org.apache.ignite.internal.metastorage.server.ValueCondition;
+import org.apache.ignite.internal.metastorage.server.WatchEvent;
+import org.apache.ignite.internal.util.Cursor;
 import org.apache.ignite.lang.IgniteBiTuple;
 import org.apache.ignite.lang.IgniteLogger;
 import org.apache.ignite.lang.IgniteUuid;
-import org.apache.ignite.metastorage.common.CompactedException;
-import org.apache.ignite.metastorage.common.Cursor;
-import org.apache.ignite.metastorage.common.Entry;
-import org.apache.ignite.metastorage.common.Key;
-import org.apache.ignite.metastorage.common.KeyValueStorage;
-import org.apache.ignite.metastorage.common.OperationTimeoutException;
-import org.apache.ignite.metastorage.common.WatchEvent;
 import org.apache.ignite.raft.client.ReadCommand;
 import org.apache.ignite.raft.client.WriteCommand;
 import org.apache.ignite.raft.client.service.CommandClosure;
 import org.apache.ignite.raft.client.service.RaftGroupCommandListener;
-import org.jetbrains.annotations.NotNull;
 
 /**
  * Meta storage command listener.
@@ -68,7 +75,7 @@ public class MetaStorageCommandListener implements RaftGroupCommandListener {
     private final KeyValueStorage storage;
 
     /** Cursors map. */
-    private final Map<IgniteUuid, IgniteBiTuple<@NotNull Cursor, @NotNull Iterator>> cursors;
+    private final Map<IgniteUuid, IgniteBiTuple<Cursor<?>, CursorType>> cursors;
 
     /**
      * @param storage Storage.
@@ -87,37 +94,47 @@ public class MetaStorageCommandListener implements RaftGroupCommandListener {
                 if (clo.command() instanceof GetCommand) {
                     GetCommand getCmd = (GetCommand)clo.command();
 
-                    if (getCmd.revision() != null)
-                        clo.success(storage.get(getCmd.key().bytes(), getCmd.revision()));
+                    Entry e;
+
+                    if (getCmd.revision() != 0)
+                        e = storage.get(getCmd.key(), getCmd.revision());
                     else
-                        clo.success(storage.get(getCmd.key().bytes()));
+                        e = storage.get(getCmd.key());
+
+                    SingleEntryResponse resp = new SingleEntryResponse(
+                            e.key(), e.value(), e.revision(), e.updateCounter()
+                    );
+
+                    clo.success(resp);
                 }
                 else if (clo.command() instanceof GetAllCommand) {
                     GetAllCommand getAllCmd = (GetAllCommand)clo.command();
 
-                    if (getAllCmd.revision() != null) {
-                        clo.success(storage.getAll(
-                            getAllCmd.keys().stream().map(Key::bytes).collect(Collectors.toList()),
-                            getAllCmd.revision())
-                        );
-                    }
-                    else {
-                        clo.success(storage.getAll(
-                            getAllCmd.keys().stream().map(Key::bytes).collect(Collectors.toList()))
-                        );
-                    }
+                    Collection<Entry> entries;
+
+                    if (getAllCmd.revision() != 0)
+                        entries = storage.getAll(getAllCmd.keys(), getAllCmd.revision());
+                    else
+                        entries = storage.getAll(getAllCmd.keys());
+
+                    List<SingleEntryResponse> res = new ArrayList<>(entries.size());
+
+                    for (Entry e : entries)
+                        res.add(new SingleEntryResponse(e.key(), e.value(), e.revision(), e.updateCounter()));
+
+                    clo.success(new MultipleEntryResponse(res));
                 }
                 else if (clo.command() instanceof CursorHasNextCommand) {
                     CursorHasNextCommand cursorHasNextCmd = (CursorHasNextCommand)clo.command();
 
                     assert cursors.containsKey(cursorHasNextCmd.cursorId());
 
-                    clo.success(cursors.get(cursorHasNextCmd.cursorId()).getValue().hasNext());
+                    clo.success(cursors.get(cursorHasNextCmd.cursorId()).getKey().hasNext());
                 }
                 else
                     assert false : "Command was not found [cmd=" + clo.command() + ']';
             }
-            catch (CompactedException | OperationTimeoutException e) {
+            catch (Exception e) {
                 // TODO: IGNITE-14693 Implement Meta storage exception handling logic.
                 LOG.warn("Unable to evaluate command [cmd=" + clo.command() + ']', e);
 
@@ -140,67 +157,79 @@ public class MetaStorageCommandListener implements RaftGroupCommandListener {
                 if (clo.command() instanceof PutCommand) {
                     PutCommand putCmd = (PutCommand)clo.command();
 
-                    storage.put(putCmd.key().bytes(), putCmd.value());
+                    storage.put(putCmd.key(), putCmd.value());
 
                     clo.success(null);
                 }
                 else if (clo.command() instanceof GetAndPutCommand) {
                     GetAndPutCommand getAndPutCmd = (GetAndPutCommand)clo.command();
 
-                    clo.success(storage.getAndPut(getAndPutCmd.key().bytes(), getAndPutCmd.value()));
+                    Entry e = storage.getAndPut(getAndPutCmd.key(), getAndPutCmd.value());
+
+                    clo.success(new SingleEntryResponse(e.key(), e.value(), e.revision(), e.updateCounter()));
                 }
                 else if (clo.command() instanceof PutAllCommand) {
                     PutAllCommand putAllCmd = (PutAllCommand)clo.command();
 
-                    storage.putAll(
-                        putAllCmd.values().keySet().stream().map(Key::bytes).collect(Collectors.toList()),
-                        new ArrayList<>(putAllCmd.values().values()));
+                    storage.putAll(putAllCmd.keys(), putAllCmd.values());
 
                     clo.success(null);
                 }
                 else if (clo.command() instanceof GetAndPutAllCommand) {
                     GetAndPutAllCommand getAndPutAllCmd = (GetAndPutAllCommand)clo.command();
 
-                    Collection<Entry> entries = storage.getAndPutAll(
-                        getAndPutAllCmd.keys().stream().map(Key::bytes).collect(Collectors.toList()),
-                        getAndPutAllCmd.vals()
-                    );
+                    Collection<Entry> entries = storage.getAndPutAll(getAndPutAllCmd.keys(), getAndPutAllCmd.vals());
+
+                    List<SingleEntryResponse> resp = new ArrayList<>(entries.size());
 
-                    if (!(entries instanceof Serializable))
-                        entries = new ArrayList<>(entries);
+                    for (Entry e : entries)
+                        resp.add(new SingleEntryResponse(e.key(), e.value(), e.revision(), e.updateCounter()));
 
-                    clo.success(entries);
+                    clo.success(new MultipleEntryResponse(resp));
                 }
                 else if (clo.command() instanceof RemoveCommand) {
                     RemoveCommand rmvCmd = (RemoveCommand)clo.command();
 
-                    storage.remove(rmvCmd.key().bytes());
+                    storage.remove(rmvCmd.key());
 
                     clo.success(null);
                 }
                 else if (clo.command() instanceof GetAndRemoveCommand) {
                     GetAndRemoveCommand getAndRmvCmd = (GetAndRemoveCommand)clo.command();
 
-                    clo.success(storage.getAndRemove(getAndRmvCmd.key().bytes()));
+                    Entry e = storage.getAndRemove(getAndRmvCmd.key());
+
+                    clo.success(new SingleEntryResponse(e.key(), e.value(), e.revision(), e.updateCounter()));
                 }
                 else if (clo.command() instanceof RemoveAllCommand) {
                     RemoveAllCommand rmvAllCmd = (RemoveAllCommand)clo.command();
 
-                    storage.removeAll(rmvAllCmd.keys().stream().map(Key::bytes).collect(Collectors.toList()));
+                    storage.removeAll(rmvAllCmd.keys());
 
                     clo.success(null);
                 }
                 else if (clo.command() instanceof GetAndRemoveAllCommand) {
                     GetAndRemoveAllCommand getAndRmvAllCmd = (GetAndRemoveAllCommand)clo.command();
 
-                    Collection<Entry> entries = storage.getAndRemoveAll(
-                        getAndRmvAllCmd.keys().stream().map(Key::bytes).collect(Collectors.toList())
-                    );
+                    Collection<Entry> entries = storage.getAndRemoveAll(getAndRmvAllCmd.keys());
+
+                    List<SingleEntryResponse> resp = new ArrayList<>(entries.size());
 
-                    if (!(entries instanceof Serializable))
-                        entries = new ArrayList<>(entries);
+                    for (Entry e : entries)
+                        resp.add(new SingleEntryResponse(e.key(), e.value(), e.revision(), e.updateCounter()));
 
-                    clo.success(entries);
+                    clo.success(new MultipleEntryResponse(resp));
+                }
+                else if (clo.command() instanceof InvokeCommand) {
+                    InvokeCommand cmd = (InvokeCommand)clo.command();
+
+                    boolean res = storage.invoke(
+                            toCondition(cmd.condition()),
+                            toOperations(cmd.success()),
+                            toOperations(cmd.failure())
+                    );
+
+                    clo.success(res);
                 }
                 else if (clo.command() instanceof RangeCommand) {
                     RangeCommand rangeCmd = (RangeCommand)clo.command();
@@ -208,15 +237,12 @@ public class MetaStorageCommandListener implements RaftGroupCommandListener {
                     IgniteUuid cursorId = new IgniteUuid(UUID.randomUUID(), 0L);
 
                     Cursor<Entry> cursor = storage.range(
-                        rangeCmd.keyFrom().bytes(),
-                        rangeCmd.keyTo() == null ? null : rangeCmd.keyTo().bytes(),
+                        rangeCmd.keyFrom(),
+                        rangeCmd.keyTo(),
                         rangeCmd.revUpperBound()
                     );
 
-                    cursors.put(
-                        cursorId,
-                        new IgniteBiTuple<>(cursor, cursor.iterator())
-                    );
+                    cursors.put(cursorId, new IgniteBiTuple<>(cursor, CursorType.RANGE));
 
                     clo.success(cursorId);
                 }
@@ -225,7 +251,31 @@ public class MetaStorageCommandListener implements RaftGroupCommandListener {
 
                     assert cursors.containsKey(cursorNextCmd.cursorId());
 
-                    clo.success(cursors.get(cursorNextCmd.cursorId()).getValue().next());
+                    IgniteBiTuple<Cursor<?>, CursorType> cursorDesc = cursors.get(cursorNextCmd.cursorId());
+
+                    if (cursorDesc.getValue() == CursorType.RANGE) {
+                        Entry e = (Entry)cursorDesc.getKey().next();
+
+                        clo.success(new SingleEntryResponse(e.key(), e.value(), e.revision(), e.updateCounter()));
+                    }
+                    else if (cursorDesc.getValue() == CursorType.WATCH) {
+                        WatchEvent evt = (WatchEvent)cursorDesc.getKey().next();
+
+                        List<SingleEntryResponse> resp = new ArrayList<>(evt.entryEvents().size() * 2);
+
+                        for (EntryEvent e : evt.entryEvents()) {
+                            Entry o = e.oldEntry();
+
+                            Entry n = e.entry();
+
+                            resp.add(new SingleEntryResponse(o.key(), o.value(), o.revision(), o.updateCounter()));
+
+                            resp.add(new SingleEntryResponse(n.key(), n.value(), n.revision(), n.updateCounter()));
+                        }
+
+                        clo.success(new MultipleEntryResponse(resp));
+                    }
+
                 }
                 else if (clo.command() instanceof CursorCloseCommand) {
                     CursorCloseCommand cursorCloseCmd = (CursorCloseCommand)clo.command();
@@ -250,15 +300,10 @@ public class MetaStorageCommandListener implements RaftGroupCommandListener {
 
                     IgniteUuid cursorId = new IgniteUuid(UUID.randomUUID(), 0L);
 
-                    Cursor<WatchEvent> cursor = storage.watch(
-                        watchCmd.keyFrom() == null ? null : watchCmd.keyFrom().bytes(),
-                        watchCmd.keyTo() == null ? null : watchCmd.keyTo().bytes(),
-                        watchCmd.revision());
+                    Cursor<WatchEvent> cursor =
+                            storage.watch(watchCmd.keyFrom(), watchCmd.keyTo(), watchCmd.revision());
 
-                    cursors.put(
-                        cursorId,
-                        new IgniteBiTuple<>(cursor, cursor.iterator())
-                    );
+                    cursors.put(cursorId, new IgniteBiTuple<>(cursor, CursorType.WATCH));
 
                     clo.success(cursorId);
                 }
@@ -267,21 +312,16 @@ public class MetaStorageCommandListener implements RaftGroupCommandListener {
 
                     IgniteUuid cursorId = new IgniteUuid(UUID.randomUUID(), 0L);
 
-                    Cursor<WatchEvent> cursor = storage.watch(
-                        watchCmd.keys().stream().map(Key::bytes).collect(Collectors.toList()),
-                        watchCmd.revision());
+                    Cursor<WatchEvent> cursor = storage.watch(watchCmd.keys(), watchCmd.revision());
 
-                    cursors.put(
-                        cursorId,
-                        new IgniteBiTuple<>(cursor, cursor.iterator())
-                    );
+                    cursors.put(cursorId, new IgniteBiTuple<>(cursor, CursorType.WATCH));
 
                     clo.success(cursorId);
                 }
                 else
                     assert false : "Command was not found [cmd=" + clo.command() + ']';
             }
-            catch (CompactedException | OperationTimeoutException e) {
+            catch (Exception e) {
                 // TODO: IGNITE-14693 Implement Meta storage exception handling logic.
                 LOG.warn("Unable to evaluate command [cmd=" + clo.command() + ']', e);
 
@@ -294,4 +334,49 @@ public class MetaStorageCommandListener implements RaftGroupCommandListener {
             }
         }
     }
+
+    /** */
+    private static Condition toCondition(ConditionInfo info) {
+        byte[] key = info.key();
+
+        ConditionType type = info.type();
+
+        if (type == ConditionType.KEY_EXISTS)
+            return new ExistenceCondition(ExistenceCondition.Type.EXISTS, key);
+        else if (type == ConditionType.KEY_NOT_EXISTS)
+            return new ExistenceCondition(ExistenceCondition.Type.NOT_EXISTS, key);
+        else if (type == ConditionType.VAL_EQUAL)
+            return new ValueCondition(ValueCondition.Type.EQUAL, key, info.value());
+        else if (type == ConditionType.VAL_NOT_EQUAL)
+            return new ValueCondition(ValueCondition.Type.NOT_EQUAL, key, info.value());
+        else if (type == ConditionType.REV_EQUAL)
+            return new RevisionCondition(RevisionCondition.Type.EQUAL, key, info.revision());
+        else if (type == ConditionType.REV_NOT_EQUAL)
+            return new RevisionCondition(RevisionCondition.Type.NOT_EQUAL, key, info.revision());
+        else if (type == ConditionType.REV_GREATER)
+            return new RevisionCondition(RevisionCondition.Type.GREATER, key, info.revision());
+        else if (type == ConditionType.REV_GREATER_OR_EQUAL)
+            return new RevisionCondition(RevisionCondition.Type.GREATER_OR_EQUAL, key, info.revision());
+        else if (type == ConditionType.REV_LESS)
+            return new RevisionCondition(RevisionCondition.Type.LESS, key, info.revision());
+        else if (type == ConditionType.REV_LESS_OR_EQUAL)
+            return new RevisionCondition(RevisionCondition.Type.LESS_OR_EQUAL, key, info.revision());
+        else
+            throw new IllegalArgumentException("Unknown condition type: " + type);
+    }
+
+    /** */
+    private static List<Operation> toOperations(List<OperationInfo> infos) {
+        List<Operation> ops = new ArrayList<>(infos.size());
+
+        for (OperationInfo info : infos)
+            ops.add(new Operation(info.type(), info.key(), info.value()));
+
+        return ops;
+    }
+
+    /** Cursor type. */
+    private enum CursorType {
+        RANGE, WATCH;
+    }
 }
diff --git a/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/ExistenceConditionTest.java b/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/ExistenceConditionTest.java
new file mode 100644
index 0000000..ec4a573
--- /dev/null
+++ b/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/ExistenceConditionTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.metastorage.server;
+
+import org.junit.jupiter.api.Test;
+
+import static org.apache.ignite.internal.metastorage.server.ExistenceCondition.Type.EXISTS;
+import static org.apache.ignite.internal.metastorage.server.ExistenceCondition.Type.NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for entry existence condition.
+ *
+ * @see ExistenceCondition
+ */
+public class ExistenceConditionTest {
+    /** Entry key. */
+    private static final byte[] KEY = new byte[] {1};
+
+    /** Entry value. */
+    private static final byte[] VAL = new byte[] {1};
+
+    /** Regular entry. */
+    private static final Entry ENTRY = new Entry(KEY, VAL, 1, 1);
+
+    /** Empty entry. */
+    private static final Entry EMPTY = Entry.empty(KEY);
+
+    /** Tombstone entry. */
+    private static final Entry TOMBSTONE = Entry.tombstone(KEY, 1, 1);
+
+    /**
+     * Tests {@link ExistenceCondition.Type#EXISTS} condition for regular, empty and tombstone entries.
+     */
+    @Test
+    public void exists() {
+        Condition cond = new ExistenceCondition(EXISTS, KEY);
+
+        assertTrue(cond.test(ENTRY));
+        assertFalse(cond.test(EMPTY));
+        assertFalse(cond.test(TOMBSTONE));
+    }
+
+    /**
+     * Tests {@link ExistenceCondition.Type#NOT_EXISTS} condition for regular, empty and tombstone entries.
+     */
+    @Test
+    public void notExists() {
+        Condition cond = new ExistenceCondition(NOT_EXISTS, KEY);
+
+        assertFalse(cond.test(ENTRY));
+        assertTrue(cond.test(EMPTY));
+        assertTrue(cond.test(TOMBSTONE));
+    }
+}
diff --git a/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/RevisionConditionTest.java b/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/RevisionConditionTest.java
new file mode 100644
index 0000000..2c461aa
--- /dev/null
+++ b/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/RevisionConditionTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.metastorage.server;
+
+import org.junit.jupiter.api.Test;
+
+import static org.apache.ignite.internal.metastorage.server.RevisionCondition.Type.EQUAL;
+import static org.apache.ignite.internal.metastorage.server.RevisionCondition.Type.GREATER;
+import static org.apache.ignite.internal.metastorage.server.RevisionCondition.Type.GREATER_OR_EQUAL;
+import static org.apache.ignite.internal.metastorage.server.RevisionCondition.Type.LESS;
+import static org.apache.ignite.internal.metastorage.server.RevisionCondition.Type.LESS_OR_EQUAL;
+import static org.apache.ignite.internal.metastorage.server.RevisionCondition.Type.NOT_EQUAL;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for entry revision condition.
+ *
+ * @see RevisionCondition
+ */
+public class RevisionConditionTest {
+    /** Entry key. */
+    private static final byte[] KEY = new byte[] {1};
+
+    /** Entry value. */
+    private static final byte[] VAL = new byte[] {2};
+
+    /**
+     * Tests revisions equality.
+     */
+    @Test
+    public void eq() {
+        Condition cond = new RevisionCondition(EQUAL, KEY, 1);
+
+        // 1 == 1.
+        assertTrue(cond.test(new Entry(KEY, VAL, 1, 1)));
+    }
+
+    /**
+     * Tests revisions inequality.
+     */
+    @Test
+    public void ne() {
+        Condition cond = new RevisionCondition(NOT_EQUAL, KEY, 1);
+
+        // 2 != 1.
+        assertTrue(cond.test(new Entry(KEY, VAL, 2, 1)));
+    }
+
+    /**
+     * Tests that revision is greater than another one.
+     */
+    @Test
+    public void gt() {
+        Condition cond = new RevisionCondition(GREATER, KEY, 1);
+
+        // 2 > 1.
+        assertTrue(cond.test(new Entry(KEY, VAL, 2, 1)));
+    }
+
+    /**
+     * Tests that revision is greater than or equal to another one .
+     */
+    @Test
+    public void ge() {
+        Condition cond = new RevisionCondition(GREATER_OR_EQUAL, KEY, 1);
+
+        // 2 >= 1 (2 > 1).
+        assertTrue(cond.test(new Entry(KEY, VAL, 2, 1)));
+
+        // 1 >= 1 (1 == 1).
+        assertTrue(cond.test(new Entry(KEY, VAL, 1, 1)));
+    }
+
+    /**
+     * Tests that revision is less than another one.
+     */
+    @Test
+    public void lt() {
+        Condition cond = new RevisionCondition(LESS, KEY, 2);
+
+        // 1 < 2
+        assertTrue(cond.test(new Entry(KEY, VAL, 1, 1)));
+    }
+
+    /**
+     * Tests that revision is less than or equal to another one .
+     */
+    @Test
+    public void le() {
+        Condition cond = new RevisionCondition(LESS_OR_EQUAL, KEY, 2);
+
+        // 1 <= 2 (1 < 2)
+        assertTrue(cond.test(new Entry(KEY, VAL, 1, 1)));
+
+        // 1 <= 1 (1 == 1).
+        assertTrue(cond.test(new Entry(KEY, VAL, 1, 1)));
+    }
+}
diff --git a/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/SimpleInMemoryKeyValueStorageTest.java b/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/SimpleInMemoryKeyValueStorageTest.java
new file mode 100644
index 0000000..342b4ba
--- /dev/null
+++ b/modules/metastorage-server/src/test/java/org/apache/ignite/internal/metastorage/server/SimpleInMemoryKeyValueStorageTest.java
@@ -0,0 +1,1945 @@
+/*
+ * 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.metastorage.server;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.metastorage.common.OperationType;
+import org.apache.ignite.internal.util.Cursor;
+import org.apache.ignite.lang.ByteArray;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static java.util.function.Function.identity;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Tests for in-memory meta storage implementation.
+ */
+class SimpleInMemoryKeyValueStorageTest {
+    /** */
+    private KeyValueStorage storage;
+
+    @BeforeEach
+    public void setUp() {
+        storage = new SimpleInMemoryKeyValueStorage();
+    }
+
+    @Test
+    public void put() {
+        byte[] key = k(1);
+        byte[] val = kv(1, 1);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+        assertTrue(storage.get(key).empty());
+
+        storage.put(key, val);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        Entry e = storage.get(key);
+
+        assertFalse(e.empty());
+        assertFalse(e.tombstone());
+        assertEquals(1, e.revision());
+        assertEquals(1, e.updateCounter());
+
+        storage.put(key, val);
+
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+
+        e = storage.get(key);
+
+        assertFalse(e.empty());
+        assertFalse(e.tombstone());
+        assertEquals(2, e.revision());
+        assertEquals(2, e.updateCounter());
+    }
+
+    @Test
+    void getAll() {
+        byte[] key1 = k(1);
+        byte[] val1 = kv(1, 1);
+
+        byte[] key2 = k(2);
+        byte[] val2_1 = kv(2, 21);
+        byte[] val2_2 = kv(2, 22);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        byte[] key4 = k(4);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Regular put.
+        storage.put(key1, val1);
+
+        // Rewrite.
+        storage.put(key2, val2_1);
+        storage.put(key2, val2_2);
+
+        // Remove.
+        storage.put(key3, val3);
+        storage.remove(key3);
+
+        assertEquals(5, storage.revision());
+        assertEquals(5, storage.updateCounter());
+
+        Collection<Entry> entries = storage.getAll(List.of(key1, key2, key3, key4));
+
+        assertEquals(4, entries.size());
+
+        Map<ByteArray, Entry> map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        Entry e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(1, e1.revision());
+        assertEquals(1, e1.updateCounter());
+        assertFalse(e1.tombstone());
+        assertFalse(e1.empty());
+        assertArrayEquals(val1, e1.value());
+
+        // Test rewritten value.
+        Entry e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(3, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertFalse(e2.tombstone());
+        assertFalse(e2.empty());
+        assertArrayEquals(val2_2, e2.value());
+
+        // Test removed value.
+        Entry e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertEquals(5, e3.revision());
+        assertEquals(5, e3.updateCounter());
+        assertTrue(e3.tombstone());
+        assertFalse(e3.empty());
+
+        // Test empty value.
+        Entry e4 = map.get(new ByteArray(key4));
+
+        assertNotNull(e4);
+        assertFalse(e4.tombstone());
+        assertTrue(e4.empty());
+    }
+
+    @Test
+    void getAllWithRevisionBound() {
+        byte[] key1 = k(1);
+        byte[] val1 = kv(1, 1);
+
+        byte[] key2 = k(2);
+        byte[] val2_1 = kv(2, 21);
+        byte[] val2_2 = kv(2, 22);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        byte[] key4 = k(4);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Regular put.
+        storage.put(key1, val1);
+
+        // Rewrite.
+        storage.put(key2, val2_1);
+        storage.put(key2, val2_2);
+
+        // Remove.
+        storage.put(key3, val3);
+        storage.remove(key3);
+
+        assertEquals(5, storage.revision());
+        assertEquals(5, storage.updateCounter());
+
+        // Bounded by revision 2.
+        Collection<Entry> entries = storage.getAll(List.of(key1, key2, key3, key4), 2);
+
+        assertEquals(4, entries.size());
+
+        Map<ByteArray, Entry> map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        Entry e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(1, e1.revision());
+        assertEquals(1, e1.updateCounter());
+        assertFalse(e1.tombstone());
+        assertFalse(e1.empty());
+        assertArrayEquals(val1, e1.value());
+
+        // Test while not rewritten value.
+        Entry e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(2, e2.revision());
+        assertEquals(2, e2.updateCounter());
+        assertFalse(e2.tombstone());
+        assertFalse(e2.empty());
+        assertArrayEquals(val2_1, e2.value());
+
+        // Values with larger revision don't exist yet.
+        Entry e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertTrue(e3.empty());
+
+        Entry e4 = map.get(new ByteArray(key4));
+
+        assertNotNull(e4);
+        assertTrue(e4.empty());
+
+        // Bounded by revision 4.
+        entries = storage.getAll(List.of(key1, key2, key3, key4), 4);
+
+        assertEquals(4, entries.size());
+
+        map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(1, e1.revision());
+        assertEquals(1, e1.updateCounter());
+        assertFalse(e1.tombstone());
+        assertFalse(e1.empty());
+        assertArrayEquals(val1, e1.value());
+
+        // Test rewritten value.
+        e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(3, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertFalse(e2.tombstone());
+        assertFalse(e2.empty());
+        assertArrayEquals(val2_2, e2.value());
+
+        // Test not removed value.
+        e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertEquals(4, e3.revision());
+        assertEquals(4, e3.updateCounter());
+        assertFalse(e3.tombstone());
+        assertFalse(e3.empty());
+        assertArrayEquals(val3, e3.value());
+
+        // Value with larger revision doesn't exist yet.
+        e4 = map.get(new ByteArray(key4));
+
+        assertNotNull(e4);
+        assertTrue(e4.empty());
+    }
+
+    @Test
+    public void getAndPut() {
+        byte[] key = k(1);
+        byte[] val = kv(1, 1);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+        assertTrue(storage.get(key).empty());
+
+        Entry e = storage.getAndPut(key, val);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+        assertTrue(e.empty());
+        assertFalse(e.tombstone());
+        assertEquals(0, e.revision());
+        assertEquals(0, e.updateCounter());
+
+        e = storage.getAndPut(key, val);
+
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+        assertFalse(e.empty());
+        assertFalse(e.tombstone());
+        assertEquals(1, e.revision());
+        assertEquals(1, e.updateCounter());
+    }
+
+    @Test
+    public void putAll() {
+        byte[] key1 = k(1);
+        byte[] val1 = kv(1, 1);
+
+        byte[] key2 = k(2);
+        byte[] val2_1 = kv(2, 21);
+        byte[] val2_2 = kv(2, 22);
+
+        byte[] key3 = k(3);
+        byte[] val3_1 = kv(3, 31);
+        byte[] val3_2 = kv(3, 32);
+
+        byte[] key4 = k(4);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Must be rewritten.
+        storage.put(key2, val2_1);
+
+        // Remove. Tombstone must be replaced by new value.
+        storage.put(key3, val3_1);
+        storage.remove(key3);
+
+        assertEquals(3, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        storage.putAll(List.of(key1, key2, key3), List.of(val1, val2_2, val3_2));
+
+        assertEquals(4, storage.revision());
+        assertEquals(6, storage.updateCounter());
+
+        Collection<Entry> entries = storage.getAll(List.of(key1, key2, key3, key4));
+
+        assertEquals(4, entries.size());
+
+        Map<ByteArray, Entry> map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        Entry e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(4, e1.revision());
+        assertEquals(4, e1.updateCounter());
+        assertFalse(e1.tombstone());
+        assertFalse(e1.empty());
+        assertArrayEquals(val1, e1.value());
+
+        // Test rewritten value.
+        Entry e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(4, e2.revision());
+        assertEquals(5, e2.updateCounter());
+        assertFalse(e2.tombstone());
+        assertFalse(e2.empty());
+        assertArrayEquals(val2_2, e2.value());
+
+        // Test removed value.
+        Entry e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertEquals(4, e3.revision());
+        assertEquals(6, e3.updateCounter());
+        assertFalse(e3.tombstone());
+        assertFalse(e3.empty());
+
+        // Test empty value.
+        Entry e4 = map.get(new ByteArray(key4));
+
+        assertNotNull(e4);
+        assertFalse(e4.tombstone());
+        assertTrue(e4.empty());
+    }
+
+    @Test
+    public void getAndPutAll() {
+        byte[] key1 = k(1);
+        byte[] val1 = kv(1, 1);
+
+        byte[] key2 = k(2);
+        byte[] val2_1 = kv(2, 21);
+        byte[] val2_2 = kv(2, 22);
+
+        byte[] key3 = k(3);
+        byte[] val3_1 = kv(3, 31);
+        byte[] val3_2 = kv(3, 32);
+
+        byte[] key4 = k(4);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Must be rewritten.
+        storage.put(key2, val2_1);
+
+        // Remove. Tombstone must be replaced by new value.
+        storage.put(key3, val3_1);
+        storage.remove(key3);
+
+        assertEquals(3, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Collection<Entry> entries = storage.getAndPutAll(List.of(key1, key2, key3), List.of(val1, val2_2, val3_2));
+
+        assertEquals(4, storage.revision());
+        assertEquals(6, storage.updateCounter());
+
+        assertEquals(3, entries.size());
+
+        Map<ByteArray, Entry> map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        Entry e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(0, e1.revision());
+        assertEquals(0, e1.updateCounter());
+        assertFalse(e1.tombstone());
+        assertTrue(e1.empty());
+
+        // Test rewritten value.
+        Entry e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(1, e2.revision());
+        assertEquals(1, e2.updateCounter());
+        assertFalse(e2.tombstone());
+        assertFalse(e2.empty());
+        assertArrayEquals(val2_1, e2.value());
+
+        // Test removed value.
+        Entry e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertEquals(3, e3.revision());
+        assertEquals(3, e3.updateCounter());
+        assertTrue(e3.tombstone());
+        assertFalse(e3.empty());
+
+        // Test state after putAll.
+        entries = storage.getAll(List.of(key1, key2, key3, key4));
+
+        assertEquals(4, entries.size());
+
+        map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(4, e1.revision());
+        assertEquals(4, e1.updateCounter());
+        assertFalse(e1.tombstone());
+        assertFalse(e1.empty());
+        assertArrayEquals(val1, e1.value());
+
+        // Test rewritten value.
+        e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(4, e2.revision());
+        assertEquals(5, e2.updateCounter());
+        assertFalse(e2.tombstone());
+        assertFalse(e2.empty());
+        assertArrayEquals(val2_2, e2.value());
+
+        // Test removed value.
+        e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertEquals(4, e3.revision());
+        assertEquals(6, e3.updateCounter());
+        assertFalse(e3.tombstone());
+        assertFalse(e3.empty());
+
+        // Test empty value.
+        Entry e4 = map.get(new ByteArray(key4));
+
+        assertNotNull(e4);
+        assertFalse(e4.tombstone());
+        assertTrue(e4.empty());
+    }
+
+    @Test
+    public void remove() {
+        byte[] key = k(1);
+        byte[] val = kv(1, 1);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+        assertTrue(storage.get(key).empty());
+
+        // Remove non-existent entry.
+        storage.remove(key);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+        assertTrue(storage.get(key).empty());
+
+        storage.put(key, val);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        // Remove existent entry.
+        storage.remove(key);
+
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+
+        Entry e = storage.get(key);
+
+        assertFalse(e.empty());
+        assertTrue(e.tombstone());
+        assertEquals(2, e.revision());
+        assertEquals(2, e.updateCounter());
+
+        // Remove already removed entry (tombstone can't be removed).
+        storage.remove(key);
+
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+
+        e = storage.get(key);
+
+        assertFalse(e.empty());
+        assertTrue(e.tombstone());
+        assertEquals(2, e.revision());
+        assertEquals(2, e.updateCounter());
+    }
+
+    @Test
+    public void getAndRemove() {
+        byte[] key = k(1);
+        byte[] val = kv(1, 1);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+        assertTrue(storage.get(key).empty());
+
+        // Remove non-existent entry.
+        Entry e = storage.getAndRemove(key);
+
+        assertTrue(e.empty());
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+        assertTrue(storage.get(key).empty());
+
+        storage.put(key, val);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        // Remove existent entry.
+        e = storage.getAndRemove(key);
+
+        assertFalse(e.empty());
+        assertFalse(e.tombstone());
+        assertEquals(1, e.revision());
+        assertEquals(1, e.updateCounter());
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+
+        e = storage.get(key);
+
+        assertFalse(e.empty());
+        assertTrue(e.tombstone());
+        assertEquals(2, e.revision());
+        assertEquals(2, e.updateCounter());
+
+        // Remove already removed entry (tombstone can't be removed).
+        e = storage.getAndRemove(key);
+
+        assertFalse(e.empty());
+        assertTrue(e.tombstone());
+        assertEquals(2, e.revision());
+        assertEquals(2, e.updateCounter());
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+
+        e = storage.get(key);
+
+        assertFalse(e.empty());
+        assertTrue(e.tombstone());
+        assertEquals(2, e.revision());
+        assertEquals(2, e.updateCounter());
+    }
+
+    @Test
+    public void removeAll() {
+        byte[] key1 = k(1);
+        byte[] val1 = kv(1, 1);
+
+        byte[] key2 = k(2);
+        byte[] val2_1 = kv(2, 21);
+        byte[] val2_2 = kv(2, 22);
+
+        byte[] key3 = k(3);
+        byte[] val3_1 = kv(3, 31);
+
+        byte[] key4 = k(4);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Regular put.
+        storage.put(key1, val1);
+
+        // Rewrite.
+        storage.put(key2, val2_1);
+        storage.put(key2, val2_2);
+
+        // Remove. Tombstone must not be removed again.
+        storage.put(key3, val3_1);
+        storage.remove(key3);
+
+        assertEquals(5, storage.revision());
+        assertEquals(5, storage.updateCounter());
+
+        storage.removeAll(List.of(key1, key2, key3, key4));
+
+        assertEquals(6, storage.revision());
+        assertEquals(7, storage.updateCounter()); // Only two keys are updated.
+
+        Collection<Entry> entries = storage.getAll(List.of(key1, key2, key3, key4));
+
+        assertEquals(4, entries.size());
+
+        Map<ByteArray, Entry> map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        Entry e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(6, e1.revision());
+        assertEquals(6, e1.updateCounter());
+        assertTrue(e1.tombstone());
+        assertFalse(e1.empty());
+
+        // Test rewritten value.
+        Entry e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(6, e2.revision());
+        assertEquals(7, e2.updateCounter());
+        assertTrue(e2.tombstone());
+        assertFalse(e2.empty());
+
+        // Test removed value.
+        Entry e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertEquals(5, e3.revision());
+        assertEquals(5, e3.updateCounter());
+        assertTrue(e3.tombstone());
+        assertFalse(e3.empty());
+
+        // Test empty value.
+        Entry e4 = map.get(new ByteArray(key4));
+
+        assertNotNull(e4);
+        assertFalse(e4.tombstone());
+        assertTrue(e4.empty());
+    }
+
+    @Test
+    public void getAndRemoveAll() {
+        byte[] key1 = k(1);
+        byte[] val1 = kv(1, 1);
+
+        byte[] key2 = k(2);
+        byte[] val2_1 = kv(2, 21);
+        byte[] val2_2 = kv(2, 22);
+
+        byte[] key3 = k(3);
+        byte[] val3_1 = kv(3, 31);
+
+        byte[] key4 = k(4);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Regular put.
+        storage.put(key1, val1);
+
+        // Rewrite.
+        storage.put(key2, val2_1);
+        storage.put(key2, val2_2);
+
+        // Remove. Tombstone must not be removed again.
+        storage.put(key3, val3_1);
+        storage.remove(key3);
+
+        assertEquals(5, storage.revision());
+        assertEquals(5, storage.updateCounter());
+
+        Collection<Entry> entries = storage.getAndRemoveAll(List.of(key1, key2, key3, key4));
+
+        assertEquals(6, storage.revision());
+        assertEquals(7, storage.updateCounter()); // Only two keys are updated.
+
+        assertEquals(4, entries.size());
+
+        Map<ByteArray, Entry> map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        Entry e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(1, e1.revision());
+        assertEquals(1, e1.updateCounter());
+        assertFalse(e1.tombstone());
+        assertFalse(e1.empty());
+
+        // Test rewritten value.
+        Entry e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(3, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertFalse(e2.tombstone());
+        assertFalse(e2.empty());
+
+        // Test removed value.
+        Entry e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertEquals(5, e3.revision());
+        assertEquals(5, e3.updateCounter());
+        assertTrue(e3.tombstone());
+        assertFalse(e3.empty());
+
+        // Test empty value.
+        Entry e4 = map.get(new ByteArray(key4));
+
+        assertNotNull(e4);
+        assertFalse(e4.tombstone());
+        assertTrue(e4.empty());
+
+        // Test state after getAndRemoveAll.
+        entries = storage.getAll(List.of(key1, key2, key3, key4));
+
+        assertEquals(4, entries.size());
+
+        map = entries.stream().collect(Collectors.toMap(e -> new ByteArray(e.key()), identity()));
+
+        // Test regular put value.
+        e1 = map.get(new ByteArray(key1));
+
+        assertNotNull(e1);
+        assertEquals(6, e1.revision());
+        assertEquals(6, e1.updateCounter());
+        assertTrue(e1.tombstone());
+        assertFalse(e1.empty());
+
+        // Test rewritten value.
+        e2 = map.get(new ByteArray(key2));
+
+        assertNotNull(e2);
+        assertEquals(6, e2.revision());
+        assertEquals(7, e2.updateCounter());
+        assertTrue(e2.tombstone());
+        assertFalse(e2.empty());
+
+        // Test removed value.
+        e3 = map.get(new ByteArray(key3));
+
+        assertNotNull(e3);
+        assertEquals(5, e3.revision());
+        assertEquals(5, e3.updateCounter());
+        assertTrue(e3.tombstone());
+        assertFalse(e3.empty());
+
+        // Test empty value.
+        e4 = map.get(new ByteArray(key4));
+
+        assertNotNull(e4);
+        assertFalse(e4.tombstone());
+        assertTrue(e4.empty());
+    }
+
+    @Test
+    public void getAfterRemove() {
+        byte[] key = k(1);
+        byte[] val = kv(1, 1);
+
+        storage.getAndPut(key, val);
+
+        storage.getAndRemove(key);
+
+        Entry e = storage.get(key);
+
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+        assertEquals(2, e.revision());
+        assertTrue(e.tombstone());
+    }
+
+    @Test
+    public void getAndPutAfterRemove() {
+        byte[] key = k(1);
+        byte[] val = kv(1, 1);
+
+        storage.getAndPut(key, val);
+
+        storage.getAndRemove(key);
+
+        Entry e = storage.getAndPut(key, val);
+
+        assertEquals(3, storage.revision());
+        assertEquals(3, storage.updateCounter());
+        assertEquals(2, e.revision());
+        assertTrue(e.tombstone());
+    }
+
+    @Test
+    public void putGetRemoveCompact() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 1);
+        byte[] val1_3 = kv(1, 3);
+
+        byte[] key2 = k(2);
+        byte[] val2_2 = kv(2, 2);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Previous entry is empty.
+        Entry emptyEntry = storage.getAndPut(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+        assertTrue(emptyEntry.empty());
+
+        // Entry with rev == 1.
+        Entry e1_1 = storage.get(key1);
+
+        assertFalse(e1_1.empty());
+        assertFalse(e1_1.tombstone());
+        assertArrayEquals(key1, e1_1.key());
+        assertArrayEquals(val1_1, e1_1.value());
+        assertEquals(1, e1_1.revision());
+        assertEquals(1, e1_1.updateCounter());
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        // Previous entry is empty.
+        emptyEntry = storage.getAndPut(key2, val2_2);
+
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+        assertTrue(emptyEntry.empty());
+
+        // Entry with rev == 2.
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertArrayEquals(key2, e2.key());
+        assertArrayEquals(val2_2, e2.value());
+        assertEquals(2, e2.revision());
+        assertEquals(2, e2.updateCounter());
+        assertEquals(2, storage.revision());
+        assertEquals(2, storage.updateCounter());
+
+        // Previous entry is not empty.
+        e1_1 = storage.getAndPut(key1, val1_3);
+
+        assertFalse(e1_1.empty());
+        assertFalse(e1_1.tombstone());
+        assertArrayEquals(key1, e1_1.key());
+        assertArrayEquals(val1_1, e1_1.value());
+        assertEquals(1, e1_1.revision());
+        assertEquals(1, e1_1.updateCounter());
+        assertEquals(3, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        // Entry with rev == 3.
+        Entry e1_3 = storage.get(key1);
+
+        assertFalse(e1_3.empty());
+        assertFalse(e1_3.tombstone());
+        assertArrayEquals(key1, e1_3.key());
+        assertArrayEquals(val1_3, e1_3.value());
+        assertEquals(3, e1_3.revision());
+        assertEquals(3, e1_3.updateCounter());
+        assertEquals(3, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        // Remove existing entry.
+        Entry e2_2 = storage.getAndRemove(key2);
+
+        assertFalse(e2_2.empty());
+        assertFalse(e2_2.tombstone());
+        assertArrayEquals(key2, e2_2.key());
+        assertArrayEquals(val2_2, e2_2.value());
+        assertEquals(2, e2_2.revision());
+        assertEquals(2, e2_2.updateCounter());
+        assertEquals(4, storage.revision()); // Storage revision is changed.
+        assertEquals(4, storage.updateCounter());
+
+        // Remove already removed entry.
+        Entry tombstoneEntry = storage.getAndRemove(key2);
+
+        assertFalse(tombstoneEntry.empty());
+        assertTrue(tombstoneEntry.tombstone());
+        assertEquals(4, storage.revision()); // Storage revision is not changed.
+        assertEquals(4, storage.updateCounter());
+
+        // Compact and check that tombstones are removed.
+        storage.compact();
+
+        assertEquals(4, storage.revision());
+        assertEquals(4, storage.updateCounter());
+        assertTrue(storage.getAndRemove(key2).empty());
+        assertTrue(storage.get(key2).empty());
+
+        // Remove existing entry.
+        e1_3 = storage.getAndRemove(key1);
+
+        assertFalse(e1_3.empty());
+        assertFalse(e1_3.tombstone());
+        assertArrayEquals(key1, e1_3.key());
+        assertArrayEquals(val1_3, e1_3.value());
+        assertEquals(3, e1_3.revision());
+        assertEquals(3, e1_3.updateCounter());
+        assertEquals(5, storage.revision()); // Storage revision is changed.
+        assertEquals(5, storage.updateCounter());
+
+        // Remove already removed entry.
+        tombstoneEntry = storage.getAndRemove(key1);
+
+        assertFalse(tombstoneEntry.empty());
+        assertTrue(tombstoneEntry.tombstone());
+        assertEquals(5, storage.revision()); // // Storage revision is not changed.
+        assertEquals(5, storage.updateCounter());
+
+        // Compact and check that tombstones are removed.
+        storage.compact();
+
+        assertEquals(5, storage.revision());
+        assertEquals(5, storage.updateCounter());
+        assertTrue(storage.getAndRemove(key1).empty());
+        assertTrue(storage.get(key1).empty());
+    }
+
+    @Test
+    public void invokeWithRevisionCondition_successBranch() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 11);
+        byte[] val1_2 = kv(1, 12);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        boolean branch = storage.invoke(
+                new RevisionCondition(RevisionCondition.Type.EQUAL, key1, 1),
+                List.of(
+                        new Operation(OperationType.PUT, key1, val1_2),
+                        new Operation(OperationType.PUT, key2, val2)
+                ),
+                List.of(new Operation(OperationType.PUT, key3, val3))
+        );
+
+        // "Success" branch is applied.
+        assertTrue(branch);
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e1 = storage.get(key1);
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertEquals(2, e1.revision());
+        assertEquals(2, e1.updateCounter());
+        assertArrayEquals(val1_2, e1.value());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertArrayEquals(val2, e2.value());
+
+        // "Failure" branch isn't applied.
+        Entry e3 = storage.get(key3);
+
+        assertTrue(e3.empty());
+    }
+
+    @Test
+    public void invokeWithRevisionCondition_failureBranch() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 11);
+        byte[] val1_2 = kv(1, 12);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        boolean branch = storage.invoke(
+                new RevisionCondition(RevisionCondition.Type.EQUAL, key1, 2),
+                List.of(new Operation(OperationType.PUT, key3, val3)),
+                List.of(
+                        new Operation(OperationType.PUT, key1, val1_2),
+                        new Operation(OperationType.PUT, key2, val2)
+                )
+        );
+
+        // "Failure" branch is applied.
+        assertFalse(branch);
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e1 = storage.get(key1);
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertEquals(2, e1.revision());
+        assertEquals(2, e1.updateCounter());
+        assertArrayEquals(val1_2, e1.value());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertArrayEquals(val2, e2.value());
+
+        // "Success" branch isn't applied.
+        Entry e3 = storage.get(key3);
+
+        assertTrue(e3.empty());
+    }
+
+    @Test
+    public void invokeWithExistsCondition_successBranch() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 11);
+        byte[] val1_2 = kv(1, 12);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        boolean branch = storage.invoke(
+                new ExistenceCondition(ExistenceCondition.Type.EXISTS, key1),
+                List.of(
+                        new Operation(OperationType.PUT, key1, val1_2),
+                        new Operation(OperationType.PUT, key2, val2)
+                ),
+                List.of(new Operation(OperationType.PUT, key3, val3))
+        );
+
+        // "Success" branch is applied.
+        assertTrue(branch);
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e1 = storage.get(key1);
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertEquals(2, e1.revision());
+        assertEquals(2, e1.updateCounter());
+        assertArrayEquals(val1_2, e1.value());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertArrayEquals(val2, e2.value());
+
+        // "Failure" branch isn't applied.
+        Entry e3 = storage.get(key3);
+
+        assertTrue(e3.empty());
+    }
+
+    @Test
+    public void invokeWithExistsCondition_failureBranch() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 11);
+        byte[] val1_2 = kv(1, 12);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        boolean branch = storage.invoke(
+                new ExistenceCondition(ExistenceCondition.Type.EXISTS, key3),
+                List.of(new Operation(OperationType.PUT, key3, val3)),
+                List.of(
+                        new Operation(OperationType.PUT, key1, val1_2),
+                        new Operation(OperationType.PUT, key2, val2)
+                )
+        );
+
+        // "Failure" branch is applied.
+        assertFalse(branch);
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e1 = storage.get(key1);
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertEquals(2, e1.revision());
+        assertEquals(2, e1.updateCounter());
+        assertArrayEquals(val1_2, e1.value());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertArrayEquals(val2, e2.value());
+
+        // "Success" branch isn't applied.
+        Entry e3 = storage.get(key3);
+
+        assertTrue(e3.empty());
+    }
+
+    @Test
+    public void invokeWithNotExistsCondition_successBranch() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 11);
+        byte[] val1_2 = kv(1, 12);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        boolean branch = storage.invoke(
+                new ExistenceCondition(ExistenceCondition.Type.NOT_EXISTS, key2),
+                List.of(
+                        new Operation(OperationType.PUT, key1, val1_2),
+                        new Operation(OperationType.PUT, key2, val2)
+                ),
+                List.of(new Operation(OperationType.PUT, key3, val3))
+        );
+
+        // "Success" branch is applied.
+        assertTrue(branch);
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e1 = storage.get(key1);
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertEquals(2, e1.revision());
+        assertEquals(2, e1.updateCounter());
+        assertArrayEquals(val1_2, e1.value());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertArrayEquals(val2, e2.value());
+
+        // "Failure" branch isn't applied.
+        Entry e3 = storage.get(key3);
+
+        assertTrue(e3.empty());
+    }
+
+    @Test
+    public void invokeWithNotExistsCondition_failureBranch() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 11);
+        byte[] val1_2 = kv(1, 12);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        boolean branch = storage.invoke(
+                new ExistenceCondition(ExistenceCondition.Type.NOT_EXISTS, key1),
+                List.of(new Operation(OperationType.PUT, key3, val3)),
+                List.of(
+                        new Operation(OperationType.PUT, key1, val1_2),
+                        new Operation(OperationType.PUT, key2, val2)
+                )
+        );
+
+        // "Failure" branch is applied.
+        assertFalse(branch);
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e1 = storage.get(key1);
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertEquals(2, e1.revision());
+        assertEquals(2, e1.updateCounter());
+        assertArrayEquals(val1_2, e1.value());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertArrayEquals(val2, e2.value());
+
+        // "Success" branch isn't applied.
+        Entry e3 = storage.get(key3);
+
+        assertTrue(e3.empty());
+    }
+
+    @Test
+    public void invokeWithValueCondition_successBranch() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 11);
+        byte[] val1_2 = kv(1, 12);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        boolean branch = storage.invoke(
+                new ValueCondition(ValueCondition.Type.EQUAL, key1, val1_1),
+                List.of(
+                        new Operation(OperationType.PUT, key1, val1_2),
+                        new Operation(OperationType.PUT, key2, val2)
+                ),
+                List.of(new Operation(OperationType.PUT, key3, val3))
+        );
+
+        // "Success" branch is applied.
+        assertTrue(branch);
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e1 = storage.get(key1);
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertEquals(2, e1.revision());
+        assertEquals(2, e1.updateCounter());
+        assertArrayEquals(val1_2, e1.value());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertArrayEquals(val2, e2.value());
+
+        // "Failure" branch isn't applied.
+        Entry e3 = storage.get(key3);
+
+        assertTrue(e3.empty());
+    }
+
+    @Test
+    public void invokeWithValueCondition_failureBranch() {
+        byte[] key1 = k(1);
+        byte[] val1_1 = kv(1, 11);
+        byte[] val1_2 = kv(1, 12);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1_1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        boolean branch = storage.invoke(
+                new ValueCondition(ValueCondition.Type.EQUAL, key1, val1_2),
+                List.of(new Operation(OperationType.PUT, key3, val3)),
+                List.of(
+                        new Operation(OperationType.PUT, key1, val1_2),
+                        new Operation(OperationType.PUT, key2, val2)
+                )
+        );
+
+        // "Failure" branch is applied.
+        assertFalse(branch);
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e1 = storage.get(key1);
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertEquals(2, e1.revision());
+        assertEquals(2, e1.updateCounter());
+        assertArrayEquals(val1_2, e1.value());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(3, e2.updateCounter());
+        assertArrayEquals(val2, e2.value());
+
+        // "Success" branch isn't applied.
+        Entry e3 = storage.get(key3);
+
+        assertTrue(e3.empty());
+    }
+
+    @Test
+    public void invokeOperations() {
+        byte[] key1 = k(1);
+        byte[] val1 = kv(1, 1);
+
+        byte[] key2 = k(2);
+        byte[] val2 = kv(2, 2);
+
+        byte[] key3 = k(3);
+        byte[] val3 = kv(3, 3);
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        storage.put(key1, val1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        // No-op.
+        boolean branch = storage.invoke(
+                new ValueCondition(ValueCondition.Type.EQUAL, key1, val1),
+                List.of(new Operation(OperationType.NO_OP, null, null)),
+                List.of(new Operation(OperationType.NO_OP, null, null))
+        );
+
+        assertTrue(branch);
+
+        // No updates.
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        // Put.
+        branch = storage.invoke(
+                new ValueCondition(ValueCondition.Type.EQUAL, key1, val1),
+                List.of(
+                        new Operation(OperationType.PUT, key2, val2),
+                        new Operation(OperationType.PUT, key3, val3)
+                ),
+                List.of(new Operation(OperationType.NO_OP, null, null))
+        );
+
+        assertTrue(branch);
+
+        // +1 for revision, +2 for update counter.
+        assertEquals(2, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        Entry e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertEquals(2, e2.revision());
+        assertEquals(2, e2.updateCounter());
+        assertArrayEquals(key2, e2.key());
+        assertArrayEquals(val2, e2.value());
+
+        Entry e3 = storage.get(key3);
+
+        assertFalse(e3.empty());
+        assertFalse(e3.tombstone());
+        assertEquals(2, e3.revision());
+        assertEquals(3, e3.updateCounter());
+        assertArrayEquals(key3, e3.key());
+        assertArrayEquals(val3, e3.value());
+
+        // Remove.
+        branch = storage.invoke(
+                new ValueCondition(ValueCondition.Type.EQUAL, key1, val1),
+                List.of(
+                        new Operation(OperationType.REMOVE, key2, null),
+                        new Operation(OperationType.REMOVE, key3, null)
+                ),
+                List.of(new Operation(OperationType.NO_OP, null, null))
+        );
+
+        assertTrue(branch);
+
+        // +1 for revision, +2 for update counter.
+        assertEquals(3, storage.revision());
+        assertEquals(5, storage.updateCounter());
+
+        e2 = storage.get(key2);
+
+        assertFalse(e2.empty());
+        assertTrue(e2.tombstone());
+        assertEquals(3, e2.revision());
+        assertEquals(4, e2.updateCounter());
+        assertArrayEquals(key2, e2.key());
+
+        e3 = storage.get(key3);
+
+        assertFalse(e3.empty());
+        assertTrue(e3.tombstone());
+        assertEquals(3, e3.revision());
+        assertEquals(5, e3.updateCounter());
+        assertArrayEquals(key3, e3.key());
+    }
+
+    @Test
+    public void compact() {
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Compact empty.
+        storage.compact();
+
+        assertEquals(0, storage.revision());
+        assertEquals(0, storage.updateCounter());
+
+        // Compact non-empty.
+        fill(storage, 1, 1);
+
+        assertEquals(1, storage.revision());
+        assertEquals(1, storage.updateCounter());
+
+        fill(storage, 2, 2);
+
+        assertEquals(3, storage.revision());
+        assertEquals(3, storage.updateCounter());
+
+        fill(storage, 3, 3);
+
+        assertEquals(6, storage.revision());
+        assertEquals(6, storage.updateCounter());
+
+        storage.getAndRemove(k(3));
+
+        assertEquals(7, storage.revision());
+        assertEquals(7, storage.updateCounter());
+        assertTrue(storage.get(k(3)).tombstone());
+
+        storage.compact();
+
+        assertEquals(7, storage.revision());
+        assertEquals(7, storage.updateCounter());
+
+        Entry e1 = storage.get(k(1));
+
+        assertFalse(e1.empty());
+        assertFalse(e1.tombstone());
+        assertArrayEquals(k(1), e1.key());
+        assertArrayEquals(kv(1,1), e1.value());
+        assertEquals(1, e1.revision());
+        assertEquals(1, e1.updateCounter());
+
+        Entry e2 = storage.get(k(2));
+
+        assertFalse(e2.empty());
+        assertFalse(e2.tombstone());
+        assertArrayEquals(k(2), e2.key());
+        assertArrayEquals(kv(2,2), e2.value());
+        assertTrue(storage.get(k(2), 2).empty());
... 1693 lines suppressed ...