diff --git a/environment/os/amzn-2.sh b/environment/os/amzn-2.sh
index a9cc3e4b2..bac62233e 100755
--- a/environment/os/amzn-2.sh
+++ b/environment/os/amzn-2.sh
@@ -45,6 +45,7 @@ MEMGRAPH_BUILD_DEPS=(
     readline-devel # for memgraph console
     python3-devel # for query modules
     openssl-devel
+    openssl
     libseccomp-devel
     python3 python3-pip nmap-ncat # for tests
     #
diff --git a/environment/os/centos-7.sh b/environment/os/centos-7.sh
index d9fc93912..0e029fab5 100755
--- a/environment/os/centos-7.sh
+++ b/environment/os/centos-7.sh
@@ -43,6 +43,7 @@ MEMGRAPH_BUILD_DEPS=(
     readline-devel # for memgraph console
     python3-devel # for query modules
     openssl-devel
+    openssl
     libseccomp-devel
     python3 python-virtualenv python3-pip nmap-ncat # for qa, macro_benchmark and stress tests
     #
diff --git a/import/n2mg_cypherl.sh b/import/n2mg_cypherl.sh
index b11f5d3e3..2605fc6c7 100755
--- a/import/n2mg_cypherl.sh
+++ b/import/n2mg_cypherl.sh
@@ -20,14 +20,18 @@ if [ ! -f "$INPUT" ]; then
 fi
 
 echo -e "${COLOR_ORANGE}NOTE:${COLOR_NULL} BEGIN and COMMIT are required because variables share the same name (e.g. row)"
-echo -e "${COLOR_ORANGE}NOTE:${COLOR_NULL} CONSTRAINTS are just skipped -> ${COLOR_RED}please create consraints manually if needed${COLOR_NULL}"
+echo -e "${COLOR_ORANGE}NOTE:${COLOR_NULL} CONSTRAINTS are just skipped -> ${COLOR_RED}please create constraints manually if needed${COLOR_NULL}"
+
+echo 'CREATE INDEX ON :`UNIQUE IMPORT LABEL`(`UNIQUE IMPORT ID`);' > "$OUTPUT"
 
 sed -e 's/^:begin/BEGIN/g; s/^BEGIN$/BEGIN;/g;' \
     -e 's/^:commit/COMMIT/g; s/^COMMIT$/COMMIT;/g;' \
     -e '/^CALL/d; /^SCHEMA AWAIT/d;' \
     -e 's/CREATE RANGE INDEX FOR (n:/CREATE INDEX ON :/g;' \
     -e 's/) ON (n./(/g;' \
-    -e '/^CREATE CONSTRAINT/d; /^DROP CONSTRAINT/d;' "$INPUT" > "$OUTPUT"
+    -e '/^CREATE CONSTRAINT/d; /^DROP CONSTRAINT/d;' "$INPUT" >> "$OUTPUT"
+
+echo 'DROP INDEX ON :`UNIQUE IMPORT LABEL`(`UNIQUE IMPORT ID`);' >> "$OUTPUT"
 
 echo ""
 echo -e "${COLOR_GREEN}DONE!${COLOR_NULL} Please find Memgraph compatible cypherl|.cypher file under $OUTPUT"
diff --git a/import/n2mg_separate_files_cypherl.sh b/import/n2mg_separate_files_cypherl.sh
new file mode 100755
index 000000000..98049f193
--- /dev/null
+++ b/import/n2mg_separate_files_cypherl.sh
@@ -0,0 +1,61 @@
+#!/bin/bash -e
+COLOR_ORANGE="\e[38;5;208m"
+COLOR_GREEN="\e[38;5;35m"
+COLOR_RED="\e[0;31m"
+COLOR_NULL="\e[0m"
+
+print_help() {
+    echo -e "${COLOR_ORANGE}HOW TO RUN:${COLOR_NULL} $0 input_file_schema_path input_file_nodes_path input_file_relationships_path input_file_cleanup_path output_file_path"
+    exit 1
+}
+
+if [ "$#" -ne 5 ]; then
+    print_help
+fi
+INPUT_SCHEMA="$1"
+INPUT_NODES="$2"
+INPUT_RELATIONSHIPS="$3"
+INPUT_CLEANUP="$4"
+OUTPUT="$5"
+
+if [ ! -f "$INPUT_SCHEMA" ]; then
+    echo -e "${COLOR_RED}ERROR:${COLOR_NULL} input_file_path is not a file!"
+    print_help
+fi
+
+if [ ! -f "$INPUT_NODES" ]; then
+    echo -e "${COLOR_RED}ERROR:${COLOR_NULL} input_file_path is not a file!"
+    print_help
+fi
+
+if [ ! -f "$INPUT_RELATIONSHIPS" ]; then
+    echo -e "${COLOR_RED}ERROR:${COLOR_NULL} input_file_path is not a file!"
+    print_help
+fi
+
+if [ ! -f "$INPUT_CLEANUP" ]; then
+    echo -e "${COLOR_RED}ERROR:${COLOR_NULL} input_file_path is not a file!"
+    print_help
+fi
+
+echo -e "${COLOR_ORANGE}NOTE:${COLOR_NULL} BEGIN and COMMIT are required because variables share the same name (e.g. row)"
+echo -e "${COLOR_ORANGE}NOTE:${COLOR_NULL} CONSTRAINTS are just skipped -> ${COLOR_RED}please create constraints manually if needed${COLOR_NULL}"
+
+
+echo 'CREATE INDEX ON :`UNIQUE IMPORT LABEL`(`UNIQUE IMPORT ID`);' > "$OUTPUT"
+
+sed -e 's/CREATE RANGE INDEX FOR (n:/CREATE INDEX ON :/g;' \
+    -e 's/) ON (n./(/g;' \
+    -e '/^CREATE CONSTRAINT/d' $INPUT_SCHEMA >> "$OUTPUT"
+
+cat "$INPUT_NODES" >> "$OUTPUT"
+cat "$INPUT_RELATIONSHIPS" >> "$OUTPUT"
+
+sed -e '/^DROP CONSTRAINT/d' "$INPUT_CLEANUP" >> "$OUTPUT"
+
+echo 'DROP INDEX ON :`UNIQUE IMPORT LABEL`(`UNIQUE IMPORT ID`);' >> "$OUTPUT"
+
+echo ""
+echo -e "${COLOR_GREEN}DONE!${COLOR_NULL} Please find Memgraph compatible cypherl|.cypher file under $OUTPUT"
+echo ""
+echo "Please import data by executing => \`cat $OUTPUT | mgconsole\`"
diff --git a/import/n2mg_separate_files_cypherls.sh b/import/n2mg_separate_files_cypherls.sh
new file mode 100755
index 000000000..5b9057e24
--- /dev/null
+++ b/import/n2mg_separate_files_cypherls.sh
@@ -0,0 +1,64 @@
+#!/bin/bash -e
+COLOR_ORANGE="\e[38;5;208m"
+COLOR_GREEN="\e[38;5;35m"
+COLOR_RED="\e[0;31m"
+COLOR_NULL="\e[0m"
+
+print_help() {
+    echo -e "${COLOR_ORANGE}HOW TO RUN:${COLOR_NULL} $0 input_file_schema_path input_file_nodes_path input_file_relationships_path input_file_cleanup_path output_file_schema_path output_file_nodes_path output_file_relationships_path output_file_cleanup_path"
+    exit 1
+}
+
+if [ "$#" -ne 8 ]; then
+    print_help
+fi
+INPUT_SCHEMA="$1"
+INPUT_NODES="$2"
+INPUT_RELATIONSHIPS="$3"
+INPUT_CLEANUP="$4"
+OUTPUT_SCHEMA="$5"
+OUTPUT_NODES="$6"
+OUTPUT_RELATIONSHIPS="$7"
+OUTPUT_CLEANUP="$8"
+
+if [ ! -f "$INPUT_SCHEMA" ]; then
+    echo -e "${COLOR_RED}ERROR:${COLOR_NULL} input_file_path is not a file!"
+    print_help
+fi
+
+if [ ! -f "$INPUT_NODES" ]; then
+    echo -e "${COLOR_RED}ERROR:${COLOR_NULL} input_file_path is not a file!"
+    print_help
+fi
+
+if [ ! -f "$INPUT_RELATIONSHIPS" ]; then
+    echo -e "${COLOR_RED}ERROR:${COLOR_NULL} input_file_path is not a file!"
+    print_help
+fi
+
+if [ ! -f "$INPUT_CLEANUP" ]; then
+    echo -e "${COLOR_RED}ERROR:${COLOR_NULL} input_file_path is not a file!"
+    print_help
+fi
+
+echo -e "${COLOR_ORANGE}NOTE:${COLOR_NULL} BEGIN and COMMIT are required because variables share the same name (e.g. row)"
+echo -e "${COLOR_ORANGE}NOTE:${COLOR_NULL} CONSTRAINTS are just skipped -> ${COLOR_RED}please create constraints manually if needed${COLOR_NULL}"
+
+
+echo 'CREATE INDEX ON :`UNIQUE IMPORT LABEL`(`UNIQUE IMPORT ID`);' > "$OUTPUT_SCHEMA"
+
+sed -e 's/CREATE RANGE INDEX FOR (n:/CREATE INDEX ON :/g;' \
+    -e 's/) ON (n./(/g;' \
+    -e '/^CREATE CONSTRAINT/d' $INPUT_SCHEMA >> "$OUTPUT_SCHEMA"
+
+cat "$INPUT_NODES" > "$OUTPUT_NODES"
+cat "$INPUT_RELATIONSHIPS" > "$OUTPUT_RELATIONSHIPS"
+
+sed -e '/^DROP CONSTRAINT/d' "$INPUT_CLEANUP" >> "$OUTPUT_CLEANUP"
+
+echo 'DROP INDEX ON :`UNIQUE IMPORT LABEL`(`UNIQUE IMPORT ID`);' >> "$OUTPUT_CLEANUP"
+
+echo ""
+echo -e "${COLOR_GREEN}DONE!${COLOR_NULL} Please find Memgraph compatible cypherl|.cypher files under $OUTPUT_SCHEMA, $OUTPUT_NODES, $OUTPUT_RELATIONSHIPS and $OUTPUT_CLEANUP"
+echo ""
+echo "Please import data by executing => \`cat $OUTPUT_SCHEMA | mgconsole\`, \`cat $OUTPUT_NODES | mgconsole\`, \`cat $OUTPUT_RELATIONSHIPS | mgconsole\` and \`cat $OUTPUT_CLEANUP | mgconsole\`"
diff --git a/query_modules/schema.cpp b/query_modules/schema.cpp
index 848ccedc4..9c2380284 100644
--- a/query_modules/schema.cpp
+++ b/query_modules/schema.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -9,10 +9,11 @@
 // by the Apache License, Version 2.0, included in the file
 // licenses/APL.txt.
 
+#include <boost/functional/hash.hpp>
 #include <mgp.hpp>
 #include "utils/string.hpp"
 
-#include <optional>
+#include <unordered_set>
 
 namespace Schema {
 
@@ -37,6 +38,7 @@ constexpr std::string_view kParameterIndices = "indices";
 constexpr std::string_view kParameterUniqueConstraints = "unique_constraints";
 constexpr std::string_view kParameterExistenceConstraints = "existence_constraints";
 constexpr std::string_view kParameterDropExisting = "drop_existing";
+constexpr int kInitialNumberOfPropertyOccurances = 1;
 
 std::string TypeOf(const mgp::Type &type);
 
@@ -108,83 +110,79 @@ void Schema::ProcessPropertiesRel(mgp::Record &record, const std::string_view &t
   record.Insert(std::string(kReturnMandatory).c_str(), mandatory);
 }
 
-struct Property {
-  std::string name;
-  mgp::Value value;
+struct PropertyInfo {
+  std::unordered_set<std::string> property_types;  // property types
+  int64_t number_of_property_occurrences = 0;
 
-  Property(const std::string &name, mgp::Value &&value) : name(name), value(std::move(value)) {}
+  PropertyInfo() = default;
+  explicit PropertyInfo(std::string &&property_type)
+      : property_types({std::move(property_type)}),
+        number_of_property_occurrences(Schema::kInitialNumberOfPropertyOccurances) {}
+};
+
+struct LabelsInfo {
+  std::unordered_map<std::string, PropertyInfo> properties;  // key is a property name
+  int64_t number_of_label_occurrences = 0;
 };
 
 struct LabelsHash {
-  std::size_t operator()(const std::set<std::string> &set) const {
-    std::size_t seed = set.size();
-    for (const auto &i : set) {
-      seed ^= std::hash<std::string>{}(i) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
-    }
-    return seed;
-  }
+  std::size_t operator()(const std::set<std::string> &s) const { return boost::hash_range(s.begin(), s.end()); }
 };
 
 struct LabelsComparator {
   bool operator()(const std::set<std::string> &lhs, const std::set<std::string> &rhs) const { return lhs == rhs; }
 };
 
-struct PropertyComparator {
-  bool operator()(const Property &lhs, const Property &rhs) const { return lhs.name < rhs.name; }
-};
-
-struct PropertyInfo {
-  std::set<Property, PropertyComparator> properties;
-  bool mandatory;
-};
-
 void Schema::NodeTypeProperties(mgp_list * /*args*/, mgp_graph *memgraph_graph, mgp_result *result,
                                 mgp_memory *memory) {
   mgp::MemoryDispatcherGuard guard{memory};
   const auto record_factory = mgp::RecordFactory(result);
   try {
-    std::unordered_map<std::set<std::string>, PropertyInfo, LabelsHash, LabelsComparator> node_types_properties;
+    std::unordered_map<std::set<std::string>, LabelsInfo, LabelsHash, LabelsComparator> node_types_properties;
 
-    for (auto node : mgp::Graph(memgraph_graph).Nodes()) {
+    for (const auto node : mgp::Graph(memgraph_graph).Nodes()) {
       std::set<std::string> labels_set = {};
-      for (auto label : node.Labels()) {
+      for (const auto label : node.Labels()) {
         labels_set.emplace(label);
       }
 
-      if (node_types_properties.find(labels_set) == node_types_properties.end()) {
-        node_types_properties[labels_set] = PropertyInfo{std::set<Property, PropertyComparator>(), true};
-      }
+      node_types_properties[labels_set].number_of_label_occurrences++;
 
       if (node.Properties().empty()) {
-        node_types_properties[labels_set].mandatory = false;  // if there is node with no property, it is not mandatory
         continue;
       }
 
-      auto &property_info = node_types_properties.at(labels_set);
-      for (auto &[key, prop] : node.Properties()) {
-        property_info.properties.emplace(key, std::move(prop));
-        if (property_info.mandatory) {
-          property_info.mandatory =
-              property_info.properties.size() == 1;  // if there is only one property, it is mandatory
+      auto &labels_info = node_types_properties.at(labels_set);
+      for (const auto &[key, prop] : node.Properties()) {
+        auto prop_type = TypeOf(prop.Type());
+        if (labels_info.properties.find(key) == labels_info.properties.end()) {
+          labels_info.properties[key] = PropertyInfo{std::move(prop_type)};
+        } else {
+          labels_info.properties[key].property_types.emplace(prop_type);
+          labels_info.properties[key].number_of_property_occurrences++;
         }
       }
     }
 
-    for (auto &[labels, property_info] : node_types_properties) {
+    for (auto &[node_type, labels_info] : node_types_properties) {  // node type is a set of labels
       std::string label_type;
-      mgp::List labels_list = mgp::List();
-      for (auto const &label : labels) {
+      auto labels_list = mgp::List();
+      for (const auto &label : node_type) {
         label_type += ":`" + std::string(label) + "`";
         labels_list.AppendExtend(mgp::Value(label));
       }
-      for (auto const &prop : property_info.properties) {
+      for (const auto &prop : labels_info.properties) {
+        auto prop_types = mgp::List();
+        for (const auto &prop_type : prop.second.property_types) {
+          prop_types.AppendExtend(mgp::Value(prop_type));
+        }
+        bool mandatory = prop.second.number_of_property_occurrences == labels_info.number_of_label_occurrences;
         auto record = record_factory.NewRecord();
-        ProcessPropertiesNode(record, label_type, labels_list, prop.name, TypeOf(prop.value.Type()),
-                              property_info.mandatory);
+        ProcessPropertiesNode(record, label_type, labels_list, prop.first, prop_types, mandatory);
       }
-      if (property_info.properties.empty()) {
+      if (labels_info.properties.empty()) {
         auto record = record_factory.NewRecord();
-        ProcessPropertiesNode<std::string>(record, label_type, labels_list, "", "", false);
+        ProcessPropertiesNode<mgp::List>(record, label_type, labels_list, "", mgp::List(), false);
       }
     }
 
@@ -197,40 +195,45 @@ void Schema::NodeTypeProperties(mgp_list * /*args*/, mgp_graph *memgraph_graph,
 void Schema::RelTypeProperties(mgp_list * /*args*/, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
   mgp::MemoryDispatcherGuard guard{memory};
 
-  std::unordered_map<std::string, PropertyInfo> rel_types_properties;
+  std::unordered_map<std::string, LabelsInfo> rel_types_properties;
   const auto record_factory = mgp::RecordFactory(result);
   try {
-    const mgp::Graph graph = mgp::Graph(memgraph_graph);
-    for (auto rel : graph.Relationships()) {
+    const auto graph = mgp::Graph(memgraph_graph);
+    for (const auto rel : graph.Relationships()) {
       std::string rel_type = std::string(rel.Type());
-      if (rel_types_properties.find(rel_type) == rel_types_properties.end()) {
-        rel_types_properties[rel_type] = PropertyInfo{std::set<Property, PropertyComparator>(), true};
-      }
+
+      rel_types_properties[rel_type].number_of_label_occurrences++;
 
       if (rel.Properties().empty()) {
-        rel_types_properties[rel_type].mandatory = false;  // if there is rel with no property, it is not mandatory
         continue;
       }
 
-      auto &property_info = rel_types_properties.at(rel_type);
+      auto &labels_info = rel_types_properties.at(rel_type);
       for (auto &[key, prop] : rel.Properties()) {
-        property_info.properties.emplace(key, std::move(prop));
-        if (property_info.mandatory) {
-          property_info.mandatory =
-              property_info.properties.size() == 1;  // if there is only one property, it is mandatory
+        auto prop_type = TypeOf(prop.Type());
+        if (labels_info.properties.find(key) == labels_info.properties.end()) {
+          labels_info.properties[key] = PropertyInfo{std::move(prop_type)};
+        } else {
+          labels_info.properties[key].property_types.emplace(prop_type);
+          labels_info.properties[key].number_of_property_occurrences++;
         }
       }
     }
 
-    for (auto &[type, property_info] : rel_types_properties) {
-      std::string type_str = ":`" + std::string(type) + "`";
-      for (auto const &prop : property_info.properties) {
+    for (auto &[rel_type, labels_info] : rel_types_properties) {
+      std::string type_str = ":`" + std::string(rel_type) + "`";
+      for (const auto &prop : labels_info.properties) {
+        auto prop_types = mgp::List();
+        for (const auto &prop_type : prop.second.property_types) {
+          prop_types.AppendExtend(mgp::Value(prop_type));
+        }
+        bool mandatory = prop.second.number_of_property_occurrences == labels_info.number_of_label_occurrences;
         auto record = record_factory.NewRecord();
-        ProcessPropertiesRel(record, type_str, prop.name, TypeOf(prop.value.Type()), property_info.mandatory);
+        ProcessPropertiesRel(record, type_str, prop.first, prop_types, mandatory);
       }
-      if (property_info.properties.empty()) {
+      if (labels_info.properties.empty()) {
         auto record = record_factory.NewRecord();
-        ProcessPropertiesRel<std::string>(record, type_str, "", "", false);
+        ProcessPropertiesRel<mgp::List>(record, type_str, "", mgp::List(), false);
       }
     }
 
diff --git a/src/communication/result_stream_faker.hpp b/src/communication/result_stream_faker.hpp
index 779d039cc..c0a40cecf 100644
--- a/src/communication/result_stream_faker.hpp
+++ b/src/communication/result_stream_faker.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/src/coordination/CMakeLists.txt b/src/coordination/CMakeLists.txt
index 936d7a5c2..ef9376a70 100644
--- a/src/coordination/CMakeLists.txt
+++ b/src/coordination/CMakeLists.txt
@@ -16,11 +16,14 @@ target_sources(mg-coordination
         include/coordination/raft_state.hpp
         include/coordination/rpc_errors.hpp
 
+        include/nuraft/raft_log_action.hpp
+        include/nuraft/coordinator_cluster_state.hpp
         include/nuraft/coordinator_log_store.hpp
         include/nuraft/coordinator_state_machine.hpp
         include/nuraft/coordinator_state_manager.hpp
 
         PRIVATE
+        coordinator_config.cpp
         coordinator_client.cpp
         coordinator_state.cpp
         coordinator_rpc.cpp
@@ -33,6 +36,7 @@ target_sources(mg-coordination
         coordinator_log_store.cpp
         coordinator_state_machine.cpp
         coordinator_state_manager.cpp
+        coordinator_cluster_state.cpp
 )
 target_include_directories(mg-coordination PUBLIC include)
 
diff --git a/src/coordination/coordinator_client.cpp b/src/coordination/coordinator_client.cpp
index 84044b04a..8530faff3 100644
--- a/src/coordination/coordinator_client.cpp
+++ b/src/coordination/coordinator_client.cpp
@@ -16,6 +16,7 @@
 
 #include "coordination/coordinator_config.hpp"
 #include "coordination/coordinator_rpc.hpp"
+#include "replication_coordination_glue/common.hpp"
 #include "replication_coordination_glue/messages.hpp"
 #include "utils/result.hpp"
 
@@ -30,7 +31,7 @@ auto CreateClientContext(memgraph::coordination::CoordinatorClientConfig const &
 }  // namespace
 
 CoordinatorClient::CoordinatorClient(CoordinatorInstance *coord_instance, CoordinatorClientConfig config,
-                                     HealthCheckCallback succ_cb, HealthCheckCallback fail_cb)
+                                     HealthCheckClientCallback succ_cb, HealthCheckClientCallback fail_cb)
     : rpc_context_{CreateClientContext(config)},
       rpc_client_{io::network::Endpoint(io::network::Endpoint::needs_resolving, config.ip_address, config.port),
                   &rpc_context_},
@@ -40,7 +41,9 @@ CoordinatorClient::CoordinatorClient(CoordinatorInstance *coord_instance, Coordi
       fail_cb_{std::move(fail_cb)} {}
 
 auto CoordinatorClient::InstanceName() const -> std::string { return config_.instance_name; }
-auto CoordinatorClient::SocketAddress() const -> std::string { return rpc_client_.Endpoint().SocketAddress(); }
+
+auto CoordinatorClient::CoordinatorSocketAddress() const -> std::string { return config_.CoordinatorSocketAddress(); }
+auto CoordinatorClient::ReplicationSocketAddress() const -> std::string { return config_.ReplicationSocketAddress(); }
 
 auto CoordinatorClient::InstanceDownTimeoutSec() const -> std::chrono::seconds {
   return config_.instance_down_timeout_sec;
@@ -63,11 +66,15 @@ void CoordinatorClient::StartFrequentCheck() {
       [this, instance_name = config_.instance_name] {
         try {
           spdlog::trace("Sending frequent heartbeat to machine {} on {}", instance_name,
-                        rpc_client_.Endpoint().SocketAddress());
+                        config_.CoordinatorSocketAddress());
           {  // NOTE: This is intentionally scoped so that stream lock could get released.
             auto stream{rpc_client_.Stream<memgraph::replication_coordination_glue::FrequentHeartbeatRpc>()};
             stream.AwaitResponse();
           }
+          // Subtle race condition:
+          // acquiring of lock needs to happen before function call, as function callback can be changed
+          // for instance after lock is already acquired
+          // (failover case when instance is promoted to MAIN)
           succ_cb_(coord_instance_, instance_name);
         } catch (rpc::RpcFailedException const &) {
           fail_cb_(coord_instance_, instance_name);
@@ -79,11 +86,6 @@ void CoordinatorClient::StopFrequentCheck() { instance_checker_.Stop(); }
 void CoordinatorClient::PauseFrequentCheck() { instance_checker_.Pause(); }
 void CoordinatorClient::ResumeFrequentCheck() { instance_checker_.Resume(); }
 
-auto CoordinatorClient::SetCallbacks(HealthCheckCallback succ_cb, HealthCheckCallback fail_cb) -> void {
-  succ_cb_ = std::move(succ_cb);
-  fail_cb_ = std::move(fail_cb);
-}
-
 auto CoordinatorClient::ReplicationClientInfo() const -> ReplClientInfo { return config_.replication_client_info; }
 
 auto CoordinatorClient::SendPromoteReplicaToMainRpc(const utils::UUID &uuid,
@@ -117,7 +119,7 @@ auto CoordinatorClient::DemoteToReplica() const -> bool {
   return false;
 }
 
-auto CoordinatorClient::SendSwapMainUUIDRpc(const utils::UUID &uuid) const -> bool {
+auto CoordinatorClient::SendSwapMainUUIDRpc(utils::UUID const &uuid) const -> bool {
   try {
     auto stream{rpc_client_.Stream<replication_coordination_glue::SwapMainUUIDRpc>(uuid)};
     if (!stream.AwaitResponse().success) {
@@ -131,7 +133,7 @@ auto CoordinatorClient::SendSwapMainUUIDRpc(const utils::UUID &uuid) const -> bo
   return false;
 }
 
-auto CoordinatorClient::SendUnregisterReplicaRpc(std::string const &instance_name) const -> bool {
+auto CoordinatorClient::SendUnregisterReplicaRpc(std::string_view instance_name) const -> bool {
   try {
     auto stream{rpc_client_.Stream<UnregisterReplicaRpc>(instance_name)};
     if (!stream.AwaitResponse().success) {
@@ -171,5 +173,17 @@ auto CoordinatorClient::SendEnableWritingOnMainRpc() const -> bool {
   return false;
 }
 
+auto CoordinatorClient::SendGetInstanceTimestampsRpc() const
+    -> utils::BasicResult<GetInstanceUUIDError, replication_coordination_glue::DatabaseHistories> {
+  try {
+    auto stream{rpc_client_.Stream<coordination::GetDatabaseHistoriesRpc>()};
+    return stream.AwaitResponse().database_histories;
+
+  } catch (const rpc::RpcFailedException &) {
+    spdlog::error("RPC error occured while sending GetInstance UUID RPC");
+    return GetInstanceUUIDError::RPC_EXCEPTION;
+  }
+}
+
 }  // namespace memgraph::coordination
 #endif
diff --git a/src/coordination/coordinator_cluster_state.cpp b/src/coordination/coordinator_cluster_state.cpp
new file mode 100644
index 000000000..cf6e1a574
--- /dev/null
+++ b/src/coordination/coordinator_cluster_state.cpp
@@ -0,0 +1,147 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#ifdef MG_ENTERPRISE
+
+#include "nuraft/coordinator_cluster_state.hpp"
+#include "utils/logging.hpp"
+
+#include <shared_mutex>
+
+namespace memgraph::coordination {
+
+void to_json(nlohmann::json &j, InstanceState const &instance_state) {
+  j = nlohmann::json{{"config", instance_state.config}, {"status", instance_state.status}};
+}
+
+void from_json(nlohmann::json const &j, InstanceState &instance_state) {
+  j.at("config").get_to(instance_state.config);
+  j.at("status").get_to(instance_state.status);
+}
+
+CoordinatorClusterState::CoordinatorClusterState(std::map<std::string, InstanceState, std::less<>> instances)
+    : instances_{std::move(instances)} {}
+
+CoordinatorClusterState::CoordinatorClusterState(CoordinatorClusterState const &other) : instances_{other.instances_} {}
+
+CoordinatorClusterState &CoordinatorClusterState::operator=(CoordinatorClusterState const &other) {
+  if (this == &other) {
+    return *this;
+  }
+  instances_ = other.instances_;
+  return *this;
+}
+
+CoordinatorClusterState::CoordinatorClusterState(CoordinatorClusterState &&other) noexcept
+    : instances_{std::move(other.instances_)} {}
+
+CoordinatorClusterState &CoordinatorClusterState::operator=(CoordinatorClusterState &&other) noexcept {
+  if (this == &other) {
+    return *this;
+  }
+  instances_ = std::move(other.instances_);
+  return *this;
+}
+
+auto CoordinatorClusterState::MainExists() const -> bool {
+  auto lock = std::shared_lock{log_lock_};
+  return std::ranges::any_of(instances_,
+                             [](auto const &entry) { return entry.second.status == ReplicationRole::MAIN; });
+}
+
+auto CoordinatorClusterState::IsMain(std::string_view instance_name) const -> bool {
+  auto lock = std::shared_lock{log_lock_};
+  auto const it = instances_.find(instance_name);
+  return it != instances_.end() && it->second.status == ReplicationRole::MAIN;
+}
+
+auto CoordinatorClusterState::IsReplica(std::string_view instance_name) const -> bool {
+  auto lock = std::shared_lock{log_lock_};
+  auto const it = instances_.find(instance_name);
+  return it != instances_.end() && it->second.status == ReplicationRole::REPLICA;
+}
+
+auto CoordinatorClusterState::InsertInstance(std::string instance_name, InstanceState instance_state) -> void {
+  auto lock = std::lock_guard{log_lock_};
+  instances_.insert_or_assign(std::move(instance_name), std::move(instance_state));
+}
+
+auto CoordinatorClusterState::DoAction(TRaftLog log_entry, RaftLogAction log_action) -> void {
+  auto lock = std::lock_guard{log_lock_};
+  switch (log_action) {
+    case RaftLogAction::REGISTER_REPLICATION_INSTANCE: {
+      auto const &config = std::get<CoordinatorClientConfig>(log_entry);
+      instances_[config.instance_name] = InstanceState{config, ReplicationRole::REPLICA};
+      break;
+    }
+    case RaftLogAction::UNREGISTER_REPLICATION_INSTANCE: {
+      auto const instance_name = std::get<std::string>(log_entry);
+      instances_.erase(instance_name);
+      break;
+    }
+    case RaftLogAction::SET_INSTANCE_AS_MAIN: {
+      auto const instance_name = std::get<std::string>(log_entry);
+      auto it = instances_.find(instance_name);
+      MG_ASSERT(it != instances_.end(), "Instance does not exist as part of raft state!");
+      it->second.status = ReplicationRole::MAIN;
+      break;
+    }
+    case RaftLogAction::SET_INSTANCE_AS_REPLICA: {
+      auto const instance_name = std::get<std::string>(log_entry);
+      auto it = instances_.find(instance_name);
+      MG_ASSERT(it != instances_.end(), "Instance does not exist as part of raft state!");
+      it->second.status = ReplicationRole::REPLICA;
+      break;
+    }
+    case RaftLogAction::UPDATE_UUID: {
+      uuid_ = std::get<utils::UUID>(log_entry);
+      break;
+    }
+  }
+}
+
+auto CoordinatorClusterState::Serialize(ptr<buffer> &data) -> void {
+  auto lock = std::shared_lock{log_lock_};
+
+  auto const log = nlohmann::json(instances_).dump();
+
+  data = buffer::alloc(sizeof(uint32_t) + log.size());
+  buffer_serializer bs(data);
+  bs.put_str(log);
+}
+
+auto CoordinatorClusterState::Deserialize(buffer &data) -> CoordinatorClusterState {
+  buffer_serializer bs(data);
+  auto const j = nlohmann::json::parse(bs.get_str());
+  auto instances = j.get<std::map<std::string, InstanceState, std::less<>>>();
+
+  return CoordinatorClusterState{std::move(instances)};
+}
+
+auto CoordinatorClusterState::GetInstances() const -> std::vector<InstanceState> {
+  auto lock = std::shared_lock{log_lock_};
+  return instances_ | ranges::views::values | ranges::to<std::vector<InstanceState>>;
+}
+
+auto CoordinatorClusterState::GetUUID() const -> utils::UUID { return uuid_; }
+
+auto CoordinatorClusterState::FindCurrentMainInstanceName() const -> std::optional<std::string> {
+  auto lock = std::shared_lock{log_lock_};
+  auto const it =
+      std::ranges::find_if(instances_, [](auto const &entry) { return entry.second.status == ReplicationRole::MAIN; });
+  if (it == instances_.end()) {
+    return {};
+  }
+  return it->first;
+}
+
+}  // namespace memgraph::coordination
+#endif
diff --git a/src/coordination/coordinator_config.cpp b/src/coordination/coordinator_config.cpp
new file mode 100644
index 000000000..a1147d3b6
--- /dev/null
+++ b/src/coordination/coordinator_config.cpp
@@ -0,0 +1,54 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#ifdef MG_ENTERPRISE
+
+#include "coordination/coordinator_config.hpp"
+
+namespace memgraph::coordination {
+
+void to_json(nlohmann::json &j, ReplClientInfo const &config) {
+  j = nlohmann::json{{"instance_name", config.instance_name},
+                     {"replication_mode", config.replication_mode},
+                     {"replication_ip_address", config.replication_ip_address},
+                     {"replication_port", config.replication_port}};
+}
+
+void from_json(nlohmann::json const &j, ReplClientInfo &config) {
+  config.instance_name = j.at("instance_name").get<std::string>();
+  config.replication_mode = j.at("replication_mode").get<replication_coordination_glue::ReplicationMode>();
+  config.replication_ip_address = j.at("replication_ip_address").get<std::string>();
+  config.replication_port = j.at("replication_port").get<uint16_t>();
+}
+
+void to_json(nlohmann::json &j, CoordinatorClientConfig const &config) {
+  j = nlohmann::json{{"instance_name", config.instance_name},
+                     {"ip_address", config.ip_address},
+                     {"port", config.port},
+                     {"instance_health_check_frequency_sec", config.instance_health_check_frequency_sec.count()},
+                     {"instance_down_timeout_sec", config.instance_down_timeout_sec.count()},
+                     {"instance_get_uuid_frequency_sec", config.instance_get_uuid_frequency_sec.count()},
+                     {"replication_client_info", config.replication_client_info}};
+}
+
+void from_json(nlohmann::json const &j, CoordinatorClientConfig &config) {
+  config.instance_name = j.at("instance_name").get<std::string>();
+  config.ip_address = j.at("ip_address").get<std::string>();
+  config.port = j.at("port").get<uint16_t>();
+  config.instance_health_check_frequency_sec =
+      std::chrono::seconds{j.at("instance_health_check_frequency_sec").get<int>()};
+  config.instance_down_timeout_sec = std::chrono::seconds{j.at("instance_down_timeout_sec").get<int>()};
+  config.instance_get_uuid_frequency_sec = std::chrono::seconds{j.at("instance_get_uuid_frequency_sec").get<int>()};
+  config.replication_client_info = j.at("replication_client_info").get<ReplClientInfo>();
+}
+
+}  // namespace memgraph::coordination
+#endif
diff --git a/src/coordination/coordinator_handlers.cpp b/src/coordination/coordinator_handlers.cpp
index f605069fe..637360267 100644
--- a/src/coordination/coordinator_handlers.cpp
+++ b/src/coordination/coordinator_handlers.cpp
@@ -57,6 +57,17 @@ void CoordinatorHandlers::Register(memgraph::coordination::CoordinatorServer &se
         spdlog::info("Received GetInstanceUUIDRpc on coordinator server");
         CoordinatorHandlers::GetInstanceUUIDHandler(replication_handler, req_reader, res_builder);
       });
+
+  server.Register<coordination::GetDatabaseHistoriesRpc>(
+      [&replication_handler](slk::Reader *req_reader, slk::Builder *res_builder) -> void {
+        spdlog::info("Received GetDatabasesHistoryRpc on coordinator server");
+        CoordinatorHandlers::GetDatabaseHistoriesHandler(replication_handler, req_reader, res_builder);
+      });
+}
+
+void CoordinatorHandlers::GetDatabaseHistoriesHandler(replication::ReplicationHandler &replication_handler,
+                                                      slk::Reader * /*req_reader*/, slk::Builder *res_builder) {
+  slk::Save(coordination::GetDatabaseHistoriesRes{replication_handler.GetDatabasesHistories()}, res_builder);
 }
 
 void CoordinatorHandlers::SwapMainUUIDHandler(replication::ReplicationHandler &replication_handler,
diff --git a/src/coordination/coordinator_instance.cpp b/src/coordination/coordinator_instance.cpp
index 90674cf3c..791ffbc59 100644
--- a/src/coordination/coordinator_instance.cpp
+++ b/src/coordination/coordinator_instance.cpp
@@ -15,10 +15,12 @@
 
 #include "coordination/coordinator_exceptions.hpp"
 #include "coordination/fmt.hpp"
+#include "dbms/constants.hpp"
 #include "nuraft/coordinator_state_machine.hpp"
 #include "nuraft/coordinator_state_manager.hpp"
 #include "utils/counter.hpp"
 #include "utils/functional.hpp"
+#include "utils/resource_lock.hpp"
 
 #include <range/v3/view.hpp>
 #include <shared_mutex>
@@ -30,144 +32,156 @@ using nuraft::srv_config;
 
 CoordinatorInstance::CoordinatorInstance()
     : raft_state_(RaftState::MakeRaftState(
-          [this] { std::ranges::for_each(repl_instances_, &ReplicationInstance::StartFrequentCheck); },
-          [this] { std::ranges::for_each(repl_instances_, &ReplicationInstance::StopFrequentCheck); })) {
-  auto find_repl_instance = [](CoordinatorInstance *self,
-                               std::string_view repl_instance_name) -> ReplicationInstance & {
-    auto repl_instance =
-        std::ranges::find_if(self->repl_instances_, [repl_instance_name](ReplicationInstance const &instance) {
-          return instance.InstanceName() == repl_instance_name;
-        });
+          [this]() {
+            spdlog::info("Leader changed, starting all replication instances!");
+            auto const instances = raft_state_.GetInstances();
+            auto replicas = instances | ranges::views::filter([](auto const &instance) {
+                              return instance.status == ReplicationRole::REPLICA;
+                            });
 
-    MG_ASSERT(repl_instance != self->repl_instances_.end(), "Instance {} not found during callback!",
-              repl_instance_name);
-    return *repl_instance;
+            std::ranges::for_each(replicas, [this](auto &replica) {
+              spdlog::info("Started pinging replication instance {}", replica.config.instance_name);
+              repl_instances_.emplace_back(this, replica.config, client_succ_cb_, client_fail_cb_,
+                                           &CoordinatorInstance::ReplicaSuccessCallback,
+                                           &CoordinatorInstance::ReplicaFailCallback);
+            });
+
+            auto main = instances | ranges::views::filter(
+                                        [](auto const &instance) { return instance.status == ReplicationRole::MAIN; });
+
+            std::ranges::for_each(main, [this](auto &main_instance) {
+              spdlog::info("Started pinging main instance {}", main_instance.config.instance_name);
+              repl_instances_.emplace_back(this, main_instance.config, client_succ_cb_, client_fail_cb_,
+                                           &CoordinatorInstance::MainSuccessCallback,
+                                           &CoordinatorInstance::MainFailCallback);
+            });
+
+            std::ranges::for_each(repl_instances_, [this](auto &instance) {
+              instance.SetNewMainUUID(raft_state_.GetUUID());
+              instance.StartFrequentCheck();
+            });
+          },
+          [this]() {
+            spdlog::info("Leader changed, stopping all replication instances!");
+            repl_instances_.clear();
+          })) {
+  client_succ_cb_ = [](CoordinatorInstance *self, std::string_view repl_instance_name) -> void {
+    auto lock = std::lock_guard{self->coord_instance_lock_};
+    auto &repl_instance = self->FindReplicationInstance(repl_instance_name);
+    std::invoke(repl_instance.GetSuccessCallback(), self, repl_instance_name);
   };
 
-  replica_succ_cb_ = [find_repl_instance](CoordinatorInstance *self, std::string_view repl_instance_name) -> void {
+  client_fail_cb_ = [](CoordinatorInstance *self, std::string_view repl_instance_name) -> void {
     auto lock = std::lock_guard{self->coord_instance_lock_};
-    spdlog::trace("Instance {} performing replica successful callback", repl_instance_name);
-    auto &repl_instance = find_repl_instance(self, repl_instance_name);
-
-    // We need to get replicas UUID from time to time to ensure replica is listening to correct main
-    // and that it didn't go down for less time than we could notice
-    // We need to get id of main replica is listening to
-    // and swap if necessary
-    if (!repl_instance.EnsureReplicaHasCorrectMainUUID(self->GetMainUUID())) {
-      spdlog::error("Failed to swap uuid for replica instance {} which is alive", repl_instance.InstanceName());
-      return;
-    }
-
-    repl_instance.OnSuccessPing();
-  };
-
-  replica_fail_cb_ = [find_repl_instance](CoordinatorInstance *self, std::string_view repl_instance_name) -> void {
-    auto lock = std::lock_guard{self->coord_instance_lock_};
-    spdlog::trace("Instance {} performing replica failure callback", repl_instance_name);
-    auto &repl_instance = find_repl_instance(self, repl_instance_name);
-    repl_instance.OnFailPing();
-  };
-
-  main_succ_cb_ = [find_repl_instance](CoordinatorInstance *self, std::string_view repl_instance_name) -> void {
-    auto lock = std::lock_guard{self->coord_instance_lock_};
-    spdlog::trace("Instance {} performing main successful callback", repl_instance_name);
-
-    auto &repl_instance = find_repl_instance(self, repl_instance_name);
-
-    if (repl_instance.IsAlive()) {
-      repl_instance.OnSuccessPing();
-      return;
-    }
-
-    const auto &repl_instance_uuid = repl_instance.GetMainUUID();
-    MG_ASSERT(repl_instance_uuid.has_value(), "Instance must have uuid set.");
-
-    auto const curr_main_uuid = self->GetMainUUID();
-    if (curr_main_uuid == repl_instance_uuid.value()) {
-      if (!repl_instance.EnableWritingOnMain()) {
-        spdlog::error("Failed to enable writing on main instance {}", repl_instance_name);
-        return;
-      }
-
-      repl_instance.OnSuccessPing();
-      return;
-    }
-
-    // TODO(antoniof) make demoteToReplica idempotent since main can be demoted to replica but
-    // swapUUID can fail
-    if (repl_instance.DemoteToReplica(self->replica_succ_cb_, self->replica_fail_cb_)) {
-      repl_instance.OnSuccessPing();
-      spdlog::info("Instance {} demoted to replica", repl_instance_name);
-    } else {
-      spdlog::error("Instance {} failed to become replica", repl_instance_name);
-      return;
-    }
-
-    if (!repl_instance.SendSwapAndUpdateUUID(curr_main_uuid)) {
-      spdlog::error(fmt::format("Failed to swap uuid for demoted main instance {}", repl_instance.InstanceName()));
-      return;
-    }
-  };
-
-  main_fail_cb_ = [find_repl_instance](CoordinatorInstance *self, std::string_view repl_instance_name) -> void {
-    auto lock = std::lock_guard{self->coord_instance_lock_};
-    spdlog::trace("Instance {} performing main failure callback", repl_instance_name);
-    auto &repl_instance = find_repl_instance(self, repl_instance_name);
-    repl_instance.OnFailPing();
-    const auto &repl_instance_uuid = repl_instance.GetMainUUID();
-    MG_ASSERT(repl_instance_uuid.has_value(), "Instance must have uuid set");
-
-    if (!repl_instance.IsAlive() && self->GetMainUUID() == repl_instance_uuid.value()) {
-      spdlog::info("Cluster without main instance, trying automatic failover");
-      self->TryFailover();  // TODO: (andi) Initiate failover
-    }
+    auto &repl_instance = self->FindReplicationInstance(repl_instance_name);
+    std::invoke(repl_instance.GetFailCallback(), self, repl_instance_name);
   };
 }
 
+auto CoordinatorInstance::FindReplicationInstance(std::string_view replication_instance_name) -> ReplicationInstance & {
+  auto repl_instance =
+      std::ranges::find_if(repl_instances_, [replication_instance_name](ReplicationInstance const &instance) {
+        return instance.InstanceName() == replication_instance_name;
+      });
+
+  MG_ASSERT(repl_instance != repl_instances_.end(), "Instance {} not found during callback!",
+            replication_instance_name);
+  return *repl_instance;
+}
+
 auto CoordinatorInstance::ShowInstances() const -> std::vector<InstanceStatus> {
-  auto const coord_instances = raft_state_.GetAllCoordinators();
-
-  auto const stringify_repl_role = [](ReplicationInstance const &instance) -> std::string {
-    if (!instance.IsAlive()) return "unknown";
-    if (instance.IsMain()) return "main";
-    return "replica";
-  };
-
-  auto const repl_instance_to_status = [&stringify_repl_role](ReplicationInstance const &instance) -> InstanceStatus {
-    return {.instance_name = instance.InstanceName(),
-            .coord_socket_address = instance.SocketAddress(),
-            .cluster_role = stringify_repl_role(instance),
-            .is_alive = instance.IsAlive()};
-  };
-
   auto const coord_instance_to_status = [](ptr<srv_config> const &instance) -> InstanceStatus {
     return {.instance_name = "coordinator_" + std::to_string(instance->get_id()),
             .raft_socket_address = instance->get_endpoint(),
             .cluster_role = "coordinator",
-            .is_alive = true};  // TODO: (andi) Get this info from RAFT and test it or when we will move
-                                // CoordinatorState to every instance, we can be smarter about this using our RPC.
+            .health = "unknown"};  // TODO: (andi) Get this info from RAFT and test it or when we will move
   };
+  auto instances_status = utils::fmap(raft_state_.GetAllCoordinators(), coord_instance_to_status);
 
-  auto instances_status = utils::fmap(coord_instance_to_status, coord_instances);
-  {
-    auto lock = std::shared_lock{coord_instance_lock_};
-    std::ranges::transform(repl_instances_, std::back_inserter(instances_status), repl_instance_to_status);
+  if (raft_state_.IsLeader()) {
+    auto const stringify_repl_role = [this](ReplicationInstance const &instance) -> std::string {
+      if (!instance.IsAlive()) return "unknown";
+      if (raft_state_.IsMain(instance.InstanceName())) return "main";
+      return "replica";
+    };
+
+    auto const stringify_repl_health = [](ReplicationInstance const &instance) -> std::string {
+      return instance.IsAlive() ? "up" : "down";
+    };
+
+    auto process_repl_instance_as_leader =
+        [&stringify_repl_role, &stringify_repl_health](ReplicationInstance const &instance) -> InstanceStatus {
+      return {.instance_name = instance.InstanceName(),
+              .coord_socket_address = instance.CoordinatorSocketAddress(),
+              .cluster_role = stringify_repl_role(instance),
+              .health = stringify_repl_health(instance)};
+    };
+
+    {
+      auto lock = std::shared_lock{coord_instance_lock_};
+      std::ranges::transform(repl_instances_, std::back_inserter(instances_status), process_repl_instance_as_leader);
+    }
+  } else {
+    auto const stringify_inst_status = [](ReplicationRole status) -> std::string {
+      return status == ReplicationRole::MAIN ? "main" : "replica";
+    };
+
+    // TODO: (andi) Add capability that followers can also return socket addresses
+    auto process_repl_instance_as_follower = [&stringify_inst_status](auto const &instance) -> InstanceStatus {
+      return {.instance_name = instance.config.instance_name,
+              .cluster_role = stringify_inst_status(instance.status),
+              .health = "unknown"};
+    };
+
+    std::ranges::transform(raft_state_.GetInstances(), std::back_inserter(instances_status),
+                           process_repl_instance_as_follower);
   }
 
   return instances_status;
 }
 
 auto CoordinatorInstance::TryFailover() -> void {
-  auto alive_replicas = repl_instances_ | ranges::views::filter(&ReplicationInstance::IsReplica) |
-                        ranges::views::filter(&ReplicationInstance::IsAlive);
+  auto const is_replica = [this](ReplicationInstance const &instance) { return IsReplica(instance.InstanceName()); };
+
+  auto alive_replicas =
+      repl_instances_ | ranges::views::filter(is_replica) | ranges::views::filter(&ReplicationInstance::IsAlive);
 
   if (ranges::empty(alive_replicas)) {
     spdlog::warn("Failover failed since all replicas are down!");
     return;
   }
 
-  // TODO: Smarter choice
-  auto new_main = ranges::begin(alive_replicas);
+  if (!raft_state_.RequestLeadership()) {
+    spdlog::error("Failover failed since the instance is not the leader!");
+    return;
+  }
+
+  auto const get_ts = [](ReplicationInstance &replica) { return replica.GetClient().SendGetInstanceTimestampsRpc(); };
+
+  auto maybe_instance_db_histories = alive_replicas | ranges::views::transform(get_ts) | ranges::to<std::vector>();
+
+  auto const ts_has_error = [](auto const &res) -> bool { return res.HasError(); };
+
+  if (std::ranges::any_of(maybe_instance_db_histories, ts_has_error)) {
+    spdlog::error("Aborting failover as at least one instance didn't provide per database history.");
+    return;
+  }
+
+  auto transform_to_pairs = ranges::views::transform([](auto const &zipped) {
+    auto &[replica, res] = zipped;
+    return std::make_pair(replica.InstanceName(), res.GetValue());
+  });
+
+  auto instance_db_histories =
+      ranges::views::zip(alive_replicas, maybe_instance_db_histories) | transform_to_pairs | ranges::to<std::vector>();
+
+  auto [most_up_to_date_instance, latest_epoch, latest_commit_timestamp] =
+      ChooseMostUpToDateInstance(instance_db_histories);
+
+  spdlog::trace("The most up to date instance is {} with epoch {} and {} latest commit timestamp",
+                most_up_to_date_instance, latest_epoch, latest_commit_timestamp);  // NOLINT
+
+  auto *new_main = &FindReplicationInstance(most_up_to_date_instance);
 
   new_main->PauseFrequentCheck();
   utils::OnScopeExit scope_exit{[&new_main] { new_main->ResumeFrequentCheck(); }};
@@ -177,41 +191,56 @@ auto CoordinatorInstance::TryFailover() -> void {
   };
 
   auto const new_main_uuid = utils::UUID{};
+
+  auto const failed_to_swap = [&new_main_uuid](ReplicationInstance &instance) {
+    return !instance.SendSwapAndUpdateUUID(new_main_uuid);
+  };
+
   // If for some replicas swap fails, for others on successful ping we will revert back on next change
   // or we will do failover first again and then it will be consistent again
-  for (auto &other_replica_instance : alive_replicas | ranges::views::filter(is_not_new_main)) {
-    if (!other_replica_instance.SendSwapAndUpdateUUID(new_main_uuid)) {
-      spdlog::error(fmt::format("Failed to swap uuid for instance {} which is alive, aborting failover",
-                                other_replica_instance.InstanceName()));
-      return;
-    }
+  if (std::ranges::any_of(alive_replicas | ranges::views::filter(is_not_new_main), failed_to_swap)) {
+    spdlog::error("Failed to swap uuid for all instances");
+    return;
   }
-
   auto repl_clients_info = repl_instances_ | ranges::views::filter(is_not_new_main) |
                            ranges::views::transform(&ReplicationInstance::ReplicationClientInfo) |
                            ranges::to<ReplicationClientsInfo>();
 
-  if (!new_main->PromoteToMain(new_main_uuid, std::move(repl_clients_info), main_succ_cb_, main_fail_cb_)) {
+  if (!new_main->PromoteToMain(new_main_uuid, std::move(repl_clients_info), &CoordinatorInstance::MainSuccessCallback,
+                               &CoordinatorInstance::MainFailCallback)) {
     spdlog::warn("Failover failed since promoting replica to main failed!");
     return;
   }
-  // TODO: (andi) This should be replicated across all coordinator instances with Raft log
-  SetMainUUID(new_main_uuid);
+
+  if (!raft_state_.AppendUpdateUUIDLog(new_main_uuid)) {
+    return;
+  }
+
+  auto const new_main_instance_name = new_main->InstanceName();
+
+  if (!raft_state_.AppendSetInstanceAsMainLog(new_main_instance_name)) {
+    return;
+  }
+
   spdlog::info("Failover successful! Instance {} promoted to main.", new_main->InstanceName());
 }
 
-// TODO: (andi) Make sure you cannot put coordinator instance to the main
-auto CoordinatorInstance::SetReplicationInstanceToMain(std::string instance_name)
+auto CoordinatorInstance::SetReplicationInstanceToMain(std::string_view instance_name)
     -> SetInstanceToMainCoordinatorStatus {
   auto lock = std::lock_guard{coord_instance_lock_};
 
-  if (std::ranges::any_of(repl_instances_, &ReplicationInstance::IsMain)) {
+  if (raft_state_.MainExists()) {
     return SetInstanceToMainCoordinatorStatus::MAIN_ALREADY_EXISTS;
   }
 
+  if (!raft_state_.RequestLeadership()) {
+    return SetInstanceToMainCoordinatorStatus::NOT_LEADER;
+  }
+
   auto const is_new_main = [&instance_name](ReplicationInstance const &instance) {
     return instance.InstanceName() == instance_name;
   };
+
   auto new_main = std::ranges::find_if(repl_instances_, is_new_main);
 
   if (new_main == repl_instances_.end()) {
@@ -229,85 +258,93 @@ auto CoordinatorInstance::SetReplicationInstanceToMain(std::string instance_name
 
   auto const new_main_uuid = utils::UUID{};
 
-  for (auto &other_instance : repl_instances_ | ranges::views::filter(is_not_new_main)) {
-    if (!other_instance.SendSwapAndUpdateUUID(new_main_uuid)) {
-      spdlog::error(
-          fmt::format("Failed to swap uuid for instance {}, aborting failover", other_instance.InstanceName()));
-      return SetInstanceToMainCoordinatorStatus::SWAP_UUID_FAILED;
-    }
+  auto const failed_to_swap = [&new_main_uuid](ReplicationInstance &instance) {
+    return !instance.SendSwapAndUpdateUUID(new_main_uuid);
+  };
+
+  if (std::ranges::any_of(repl_instances_ | ranges::views::filter(is_not_new_main), failed_to_swap)) {
+    spdlog::error("Failed to swap uuid for all instances");
+    return SetInstanceToMainCoordinatorStatus::SWAP_UUID_FAILED;
   }
 
-  ReplicationClientsInfo repl_clients_info;
-  repl_clients_info.reserve(repl_instances_.size() - 1);
-  std::ranges::transform(repl_instances_ | ranges::views::filter(is_not_new_main),
-                         std::back_inserter(repl_clients_info), &ReplicationInstance::ReplicationClientInfo);
+  auto repl_clients_info = repl_instances_ | ranges::views::filter(is_not_new_main) |
+                           ranges::views::transform(&ReplicationInstance::ReplicationClientInfo) |
+                           ranges::to<ReplicationClientsInfo>();
 
-  if (!new_main->PromoteToMain(new_main_uuid, std::move(repl_clients_info), main_succ_cb_, main_fail_cb_)) {
+  if (!new_main->PromoteToMain(new_main_uuid, std::move(repl_clients_info), &CoordinatorInstance::MainSuccessCallback,
+                               &CoordinatorInstance::MainFailCallback)) {
     return SetInstanceToMainCoordinatorStatus::COULD_NOT_PROMOTE_TO_MAIN;
   }
 
-  // TODO: (andi) This should be replicated across all coordinator instances with Raft log
-  SetMainUUID(new_main_uuid);
-  spdlog::info("Instance {} promoted to main", instance_name);
+  if (!raft_state_.AppendUpdateUUIDLog(new_main_uuid)) {
+    return SetInstanceToMainCoordinatorStatus::RAFT_LOG_ERROR;
+  }
+
+  if (!raft_state_.AppendSetInstanceAsMainLog(instance_name)) {
+    return SetInstanceToMainCoordinatorStatus::RAFT_LOG_ERROR;
+  }
+
+  spdlog::info("Instance {} promoted to main on leader", instance_name);
   return SetInstanceToMainCoordinatorStatus::SUCCESS;
 }
 
-auto CoordinatorInstance::RegisterReplicationInstance(CoordinatorClientConfig config)
+auto CoordinatorInstance::RegisterReplicationInstance(CoordinatorClientConfig const &config)
     -> RegisterInstanceCoordinatorStatus {
   auto lock = std::lock_guard{coord_instance_lock_};
 
-  auto instance_name = config.instance_name;
-
-  auto const name_matches = [&instance_name](ReplicationInstance const &instance) {
-    return instance.InstanceName() == instance_name;
-  };
-
-  if (std::ranges::any_of(repl_instances_, name_matches)) {
+  if (std::ranges::any_of(repl_instances_, [instance_name = config.instance_name](ReplicationInstance const &instance) {
+        return instance.InstanceName() == instance_name;
+      })) {
     return RegisterInstanceCoordinatorStatus::NAME_EXISTS;
   }
 
-  auto const socket_address_matches = [&config](ReplicationInstance const &instance) {
-    return instance.SocketAddress() == config.SocketAddress();
-  };
+  if (std::ranges::any_of(repl_instances_, [&config](ReplicationInstance const &instance) {
+        return instance.CoordinatorSocketAddress() == config.CoordinatorSocketAddress();
+      })) {
+    return RegisterInstanceCoordinatorStatus::COORD_ENDPOINT_EXISTS;
+  }
 
-  if (std::ranges::any_of(repl_instances_, socket_address_matches)) {
-    return RegisterInstanceCoordinatorStatus::ENDPOINT_EXISTS;
+  if (std::ranges::any_of(repl_instances_, [&config](ReplicationInstance const &instance) {
+        return instance.ReplicationSocketAddress() == config.ReplicationSocketAddress();
+      })) {
+    return RegisterInstanceCoordinatorStatus::REPL_ENDPOINT_EXISTS;
   }
 
   if (!raft_state_.RequestLeadership()) {
     return RegisterInstanceCoordinatorStatus::NOT_LEADER;
   }
 
-  auto const res = raft_state_.AppendRegisterReplicationInstance(instance_name);
-  if (!res->get_accepted()) {
-    spdlog::error(
-        "Failed to accept request for registering instance {}. Most likely the reason is that the instance is not "
-        "the "
-        "leader.",
-        config.instance_name);
-    return RegisterInstanceCoordinatorStatus::RAFT_COULD_NOT_ACCEPT;
-  }
+  auto const undo_action_ = [this]() { repl_instances_.pop_back(); };
 
-  spdlog::info("Request for registering instance {} accepted", instance_name);
-  try {
-    repl_instances_.emplace_back(this, std::move(config), replica_succ_cb_, replica_fail_cb_);
-  } catch (CoordinatorRegisterInstanceException const &) {
+  auto *new_instance = &repl_instances_.emplace_back(this, config, client_succ_cb_, client_fail_cb_,
+                                                     &CoordinatorInstance::ReplicaSuccessCallback,
+                                                     &CoordinatorInstance::ReplicaFailCallback);
+
+  if (!new_instance->SendDemoteToReplicaRpc()) {
+    spdlog::error("Failed to send demote to replica rpc for instance {}", config.instance_name);
+    undo_action_();
     return RegisterInstanceCoordinatorStatus::RPC_FAILED;
   }
 
-  if (res->get_result_code() != nuraft::cmd_result_code::OK) {
-    spdlog::error("Failed to register instance {} with error code {}", instance_name, res->get_result_code());
-    return RegisterInstanceCoordinatorStatus::RAFT_COULD_NOT_APPEND;
+  if (!raft_state_.AppendRegisterReplicationInstanceLog(config)) {
+    undo_action_();
+    return RegisterInstanceCoordinatorStatus::RAFT_LOG_ERROR;
   }
 
-  spdlog::info("Instance {} registered", instance_name);
+  new_instance->StartFrequentCheck();
+
+  spdlog::info("Instance {} registered", config.instance_name);
   return RegisterInstanceCoordinatorStatus::SUCCESS;
 }
 
-auto CoordinatorInstance::UnregisterReplicationInstance(std::string instance_name)
+auto CoordinatorInstance::UnregisterReplicationInstance(std::string_view instance_name)
     -> UnregisterInstanceCoordinatorStatus {
   auto lock = std::lock_guard{coord_instance_lock_};
 
+  if (!raft_state_.RequestLeadership()) {
+    return UnregisterInstanceCoordinatorStatus::NOT_LEADER;
+  }
+
   auto const name_matches = [&instance_name](ReplicationInstance const &instance) {
     return instance.InstanceName() == instance_name;
   };
@@ -317,31 +354,208 @@ auto CoordinatorInstance::UnregisterReplicationInstance(std::string instance_nam
     return UnregisterInstanceCoordinatorStatus::NO_INSTANCE_WITH_NAME;
   }
 
-  if (inst_to_remove->IsMain() && inst_to_remove->IsAlive()) {
+  auto const is_main = [this](ReplicationInstance const &instance) {
+    return IsMain(instance.InstanceName()) && instance.GetMainUUID() == raft_state_.GetUUID() && instance.IsAlive();
+  };
+
+  if (is_main(*inst_to_remove)) {
     return UnregisterInstanceCoordinatorStatus::IS_MAIN;
   }
 
   inst_to_remove->StopFrequentCheck();
-  auto curr_main = std::ranges::find_if(repl_instances_, &ReplicationInstance::IsMain);
-  MG_ASSERT(curr_main != repl_instances_.end(), "There must be a main instance when unregistering a replica");
-  if (!curr_main->SendUnregisterReplicaRpc(instance_name)) {
-    inst_to_remove->StartFrequentCheck();
-    return UnregisterInstanceCoordinatorStatus::RPC_FAILED;
+
+  auto curr_main = std::ranges::find_if(repl_instances_, is_main);
+
+  if (curr_main != repl_instances_.end() && curr_main->IsAlive()) {
+    if (!curr_main->SendUnregisterReplicaRpc(instance_name)) {
+      inst_to_remove->StartFrequentCheck();
+      return UnregisterInstanceCoordinatorStatus::RPC_FAILED;
+    }
   }
+
   std::erase_if(repl_instances_, name_matches);
 
+  if (!raft_state_.AppendUnregisterReplicationInstanceLog(instance_name)) {
+    return UnregisterInstanceCoordinatorStatus::RAFT_LOG_ERROR;
+  }
+
   return UnregisterInstanceCoordinatorStatus::SUCCESS;
 }
 
-auto CoordinatorInstance::AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string raft_address)
-    -> void {
-  raft_state_.AddCoordinatorInstance(raft_server_id, raft_port, std::move(raft_address));
+auto CoordinatorInstance::AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port,
+                                                 std::string_view raft_address) -> void {
+  raft_state_.AddCoordinatorInstance(raft_server_id, raft_port, raft_address);
 }
 
-auto CoordinatorInstance::GetMainUUID() const -> utils::UUID { return main_uuid_; }
+void CoordinatorInstance::MainFailCallback(std::string_view repl_instance_name) {
+  spdlog::trace("Instance {} performing main fail callback", repl_instance_name);
+  auto &repl_instance = FindReplicationInstance(repl_instance_name);
+  repl_instance.OnFailPing();
+  const auto &repl_instance_uuid = repl_instance.GetMainUUID();
+  MG_ASSERT(repl_instance_uuid.has_value(), "Replication instance must have uuid set");
 
-// TODO: (andi) Add to the RAFT log.
-auto CoordinatorInstance::SetMainUUID(utils::UUID new_uuid) -> void { main_uuid_ = new_uuid; }
+  // NOLINTNEXTLINE
+  if (!repl_instance.IsAlive() && raft_state_.GetUUID() == repl_instance_uuid.value()) {
+    spdlog::info("Cluster without main instance, trying automatic failover");
+    TryFailover();
+  }
+}
+
+void CoordinatorInstance::MainSuccessCallback(std::string_view repl_instance_name) {
+  spdlog::trace("Instance {} performing main successful callback", repl_instance_name);
+  auto &repl_instance = FindReplicationInstance(repl_instance_name);
+
+  if (repl_instance.IsAlive()) {
+    repl_instance.OnSuccessPing();
+    return;
+  }
+
+  const auto &repl_instance_uuid = repl_instance.GetMainUUID();
+  MG_ASSERT(repl_instance_uuid.has_value(), "Instance must have uuid set.");
+
+  // NOLINTNEXTLINE
+  if (raft_state_.GetUUID() == repl_instance_uuid.value()) {
+    if (!repl_instance.EnableWritingOnMain()) {
+      spdlog::error("Failed to enable writing on main instance {}", repl_instance_name);
+      return;
+    }
+
+    repl_instance.OnSuccessPing();
+    return;
+  }
+
+  if (!raft_state_.RequestLeadership()) {
+    spdlog::error("Demoting main instance {} to replica failed since the instance is not the leader!",
+                  repl_instance_name);
+    return;
+  }
+
+  if (repl_instance.DemoteToReplica(&CoordinatorInstance::ReplicaSuccessCallback,
+                                    &CoordinatorInstance::ReplicaFailCallback)) {
+    repl_instance.OnSuccessPing();
+    spdlog::info("Instance {} demoted to replica", repl_instance_name);
+  } else {
+    spdlog::error("Instance {} failed to become replica", repl_instance_name);
+    return;
+  }
+
+  if (!repl_instance.SendSwapAndUpdateUUID(raft_state_.GetUUID())) {
+    spdlog::error("Failed to swap uuid for demoted main instance {}", repl_instance_name);
+    return;
+  }
+
+  if (!raft_state_.AppendSetInstanceAsReplicaLog(repl_instance_name)) {
+    return;
+  }
+}
+
+void CoordinatorInstance::ReplicaSuccessCallback(std::string_view repl_instance_name) {
+  spdlog::trace("Instance {} performing replica successful callback", repl_instance_name);
+  auto &repl_instance = FindReplicationInstance(repl_instance_name);
+
+  if (!IsReplica(repl_instance_name)) {
+    spdlog::error("Aborting replica callback since instance {} is not replica anymore", repl_instance_name);
+    return;
+  }
+  // We need to get replicas UUID from time to time to ensure replica is listening to correct main
+  // and that it didn't go down for less time than we could notice
+  // We need to get id of main replica is listening to
+  // and swap if necessary
+  if (!repl_instance.EnsureReplicaHasCorrectMainUUID(raft_state_.GetUUID())) {
+    spdlog::error("Failed to swap uuid for replica instance {} which is alive", repl_instance.InstanceName());
+    return;
+  }
+
+  repl_instance.OnSuccessPing();
+}
+
+void CoordinatorInstance::ReplicaFailCallback(std::string_view repl_instance_name) {
+  spdlog::trace("Instance {} performing replica failure callback", repl_instance_name);
+  auto &repl_instance = FindReplicationInstance(repl_instance_name);
+
+  if (!IsReplica(repl_instance_name)) {
+    spdlog::error("Aborting replica fail callback since instance {} is not replica anymore", repl_instance_name);
+    return;
+  }
+
+  repl_instance.OnFailPing();
+}
+
+auto CoordinatorInstance::ChooseMostUpToDateInstance(std::span<InstanceNameDbHistories> instance_database_histories)
+    -> NewMainRes {
+  std::optional<NewMainRes> new_main_res;
+  std::for_each(
+      instance_database_histories.begin(), instance_database_histories.end(),
+      [&new_main_res](const InstanceNameDbHistories &instance_res_pair) {
+        const auto &[instance_name, instance_db_histories] = instance_res_pair;
+
+        // Find default db for instance and its history
+        auto default_db_history_data = std::ranges::find_if(
+            instance_db_histories, [default_db = memgraph::dbms::kDefaultDB](
+                                       const replication_coordination_glue::DatabaseHistory &db_timestamps) {
+              return db_timestamps.name == default_db;
+            });
+
+        std::ranges::for_each(
+            instance_db_histories,
+            [&instance_name = instance_name](const replication_coordination_glue::DatabaseHistory &db_history) {
+              spdlog::debug("Instance {}: name {}, default db {}", instance_name, db_history.name,
+                            memgraph::dbms::kDefaultDB);
+            });
+
+        MG_ASSERT(default_db_history_data != instance_db_histories.end(), "No history for instance");
+
+        const auto &instance_default_db_history = default_db_history_data->history;
+
+        std::ranges::for_each(instance_default_db_history | ranges::views::reverse,
+                              [&instance_name = instance_name](const auto &epoch_history_it) {
+                                spdlog::debug("Instance {}: epoch {}, last_commit_timestamp: {}", instance_name,
+                                              std::get<0>(epoch_history_it), std::get<1>(epoch_history_it));
+                              });
+
+        // get latest epoch
+        // get latest timestamp
+
+        if (!new_main_res) {
+          const auto &[epoch, timestamp] = *instance_default_db_history.crbegin();
+          new_main_res = std::make_optional<NewMainRes>({instance_name, epoch, timestamp});
+          spdlog::debug("Currently the most up to date instance is {} with epoch {} and {} latest commit timestamp",
+                        instance_name, epoch, timestamp);
+          return;
+        }
+
+        bool found_same_point{false};
+        std::string last_most_up_to_date_epoch{new_main_res->latest_epoch};
+        for (auto [epoch, timestamp] : ranges::reverse_view(instance_default_db_history)) {
+          if (new_main_res->latest_commit_timestamp < timestamp) {
+            new_main_res = std::make_optional<NewMainRes>({instance_name, epoch, timestamp});
+            spdlog::trace("Found the new most up to date instance {} with epoch {} and {} latest commit timestamp",
+                          instance_name, epoch, timestamp);
+          }
+
+          // we found point at which they were same
+          if (epoch == last_most_up_to_date_epoch) {
+            found_same_point = true;
+            break;
+          }
+        }
+
+        if (!found_same_point) {
+          spdlog::error("Didn't find same history epoch {} for instance {} and instance {}", last_most_up_to_date_epoch,
+                        new_main_res->most_up_to_date_instance, instance_name);
+        }
+      });
+
+  return std::move(*new_main_res);
+}
+
+auto CoordinatorInstance::IsMain(std::string_view instance_name) const -> bool {
+  return raft_state_.IsMain(instance_name);
+}
+
+auto CoordinatorInstance::IsReplica(std::string_view instance_name) const -> bool {
+  return raft_state_.IsReplica(instance_name);
+}
 
 }  // namespace memgraph::coordination
 #endif
diff --git a/src/coordination/coordinator_log_store.cpp b/src/coordination/coordinator_log_store.cpp
index 37126b747..d5e134492 100644
--- a/src/coordination/coordinator_log_store.cpp
+++ b/src/coordination/coordinator_log_store.cpp
@@ -62,34 +62,33 @@ ptr<log_entry> CoordinatorLogStore::last_entry() const {
 
 uint64_t CoordinatorLogStore::append(ptr<log_entry> &entry) {
   ptr<log_entry> clone = MakeClone(entry);
-  uint64_t next_slot{0};
-  {
-    auto lock = std::lock_guard{logs_lock_};
-    next_slot = start_idx_ + logs_.size() - 1;
-    logs_[next_slot] = clone;
-  }
+
+  auto lock = std::lock_guard{logs_lock_};
+  uint64_t next_slot = start_idx_ + logs_.size() - 1;
+  logs_[next_slot] = clone;
+
   return next_slot;
 }
 
+// TODO: (andi) I think this is used for resolving conflicts inside NuRaft, check...
+// different compared to in_memory_log_store.cxx
 void CoordinatorLogStore::write_at(uint64_t index, ptr<log_entry> &entry) {
   ptr<log_entry> clone = MakeClone(entry);
 
   // Discard all logs equal to or greater than `index.
-  {
-    auto lock = std::lock_guard{logs_lock_};
-    auto itr = logs_.lower_bound(index);
-    while (itr != logs_.end()) {
-      itr = logs_.erase(itr);
-    }
-    logs_[index] = clone;
+  auto lock = std::lock_guard{logs_lock_};
+  auto itr = logs_.lower_bound(index);
+  while (itr != logs_.end()) {
+    itr = logs_.erase(itr);
   }
+  logs_[index] = clone;
 }
 
 ptr<std::vector<ptr<log_entry>>> CoordinatorLogStore::log_entries(uint64_t start, uint64_t end) {
   auto ret = cs_new<std::vector<ptr<log_entry>>>();
   ret->resize(end - start);
 
-  for (uint64_t i = start, curr_index = 0; i < end; ++i, ++curr_index) {
+  for (uint64_t i = start, curr_index = 0; i < end; i++, curr_index++) {
     ptr<log_entry> src = nullptr;
     {
       auto lock = std::lock_guard{logs_lock_};
@@ -105,21 +104,14 @@ ptr<std::vector<ptr<log_entry>>> CoordinatorLogStore::log_entries(uint64_t start
 }
 
 ptr<log_entry> CoordinatorLogStore::entry_at(uint64_t index) {
-  ptr<log_entry> src = nullptr;
-  {
-    auto lock = std::lock_guard{logs_lock_};
-    src = FindOrDefault_(index);
-  }
+  auto lock = std::lock_guard{logs_lock_};
+  ptr<log_entry> src = FindOrDefault_(index);
   return MakeClone(src);
 }
 
 uint64_t CoordinatorLogStore::term_at(uint64_t index) {
-  uint64_t term = 0;
-  {
-    auto lock = std::lock_guard{logs_lock_};
-    term = FindOrDefault_(index)->get_term();
-  }
-  return term;
+  auto lock = std::lock_guard{logs_lock_};
+  return FindOrDefault_(index)->get_term();
 }
 
 ptr<buffer> CoordinatorLogStore::pack(uint64_t index, int32 cnt) {
diff --git a/src/coordination/coordinator_rpc.cpp b/src/coordination/coordinator_rpc.cpp
index 4115f1979..815693824 100644
--- a/src/coordination/coordinator_rpc.cpp
+++ b/src/coordination/coordinator_rpc.cpp
@@ -76,9 +76,9 @@ void EnableWritingOnMainRes::Load(EnableWritingOnMainRes *self, memgraph::slk::R
   memgraph::slk::Load(self, reader);
 }
 
-void EnableWritingOnMainReq::Save(EnableWritingOnMainReq const &self, memgraph::slk::Builder *builder) {}
+void EnableWritingOnMainReq::Save(EnableWritingOnMainReq const & /*self*/, memgraph::slk::Builder * /*builder*/) {}
 
-void EnableWritingOnMainReq::Load(EnableWritingOnMainReq *self, memgraph::slk::Reader *reader) {}
+void EnableWritingOnMainReq::Load(EnableWritingOnMainReq * /*self*/, memgraph::slk::Reader * /*reader*/) {}
 
 // GetInstanceUUID
 void GetInstanceUUIDReq::Save(const GetInstanceUUIDReq &self, memgraph::slk::Builder *builder) {
@@ -97,6 +97,24 @@ void GetInstanceUUIDRes::Load(GetInstanceUUIDRes *self, memgraph::slk::Reader *r
   memgraph::slk::Load(self, reader);
 }
 
+// GetDatabaseHistoriesRpc
+
+void GetDatabaseHistoriesReq::Save(const GetDatabaseHistoriesReq & /*self*/, memgraph::slk::Builder * /*builder*/) {
+  /* nothing to serialize */
+}
+
+void GetDatabaseHistoriesReq::Load(GetDatabaseHistoriesReq * /*self*/, memgraph::slk::Reader * /*reader*/) {
+  /* nothing to serialize */
+}
+
+void GetDatabaseHistoriesRes::Save(const GetDatabaseHistoriesRes &self, memgraph::slk::Builder *builder) {
+  memgraph::slk::Save(self, builder);
+}
+
+void GetDatabaseHistoriesRes::Load(GetDatabaseHistoriesRes *self, memgraph::slk::Reader *reader) {
+  memgraph::slk::Load(self, reader);
+}
+
 }  // namespace coordination
 
 constexpr utils::TypeInfo coordination::PromoteReplicaToMainReq::kType{utils::TypeId::COORD_FAILOVER_REQ,
@@ -130,6 +148,12 @@ constexpr utils::TypeInfo coordination::GetInstanceUUIDReq::kType{utils::TypeId:
 constexpr utils::TypeInfo coordination::GetInstanceUUIDRes::kType{utils::TypeId::COORD_GET_UUID_RES, "CoordGetUUIDRes",
                                                                   nullptr};
 
+constexpr utils::TypeInfo coordination::GetDatabaseHistoriesReq::kType{utils::TypeId::COORD_GET_INSTANCE_DATABASES_REQ,
+                                                                       "GetInstanceDatabasesReq", nullptr};
+
+constexpr utils::TypeInfo coordination::GetDatabaseHistoriesRes::kType{utils::TypeId::COORD_GET_INSTANCE_DATABASES_RES,
+                                                                       "GetInstanceDatabasesRes", nullptr};
+
 namespace slk {
 
 // PromoteReplicaToMainRpc
@@ -213,6 +237,16 @@ void Load(memgraph::coordination::GetInstanceUUIDRes *self, memgraph::slk::Reade
   memgraph::slk::Load(&self->uuid, reader);
 }
 
+// GetInstanceTimestampsReq
+
+void Save(const memgraph::coordination::GetDatabaseHistoriesRes &self, memgraph::slk::Builder *builder) {
+  memgraph::slk::Save(self.database_histories, builder);
+}
+
+void Load(memgraph::coordination::GetDatabaseHistoriesRes *self, memgraph::slk::Reader *reader) {
+  memgraph::slk::Load(&self->database_histories, reader);
+}
+
 }  // namespace slk
 
 }  // namespace memgraph
diff --git a/src/coordination/coordinator_state.cpp b/src/coordination/coordinator_state.cpp
index 28d6c604e..f429cd5a7 100644
--- a/src/coordination/coordinator_state.cpp
+++ b/src/coordination/coordinator_state.cpp
@@ -41,7 +41,7 @@ CoordinatorState::CoordinatorState() {
   }
 }
 
-auto CoordinatorState::RegisterReplicationInstance(CoordinatorClientConfig config)
+auto CoordinatorState::RegisterReplicationInstance(CoordinatorClientConfig const &config)
     -> RegisterInstanceCoordinatorStatus {
   MG_ASSERT(std::holds_alternative<CoordinatorInstance>(data_),
             "Coordinator cannot register replica since variant holds wrong alternative");
@@ -56,7 +56,8 @@ auto CoordinatorState::RegisterReplicationInstance(CoordinatorClientConfig confi
       data_);
 }
 
-auto CoordinatorState::UnregisterReplicationInstance(std::string instance_name) -> UnregisterInstanceCoordinatorStatus {
+auto CoordinatorState::UnregisterReplicationInstance(std::string_view instance_name)
+    -> UnregisterInstanceCoordinatorStatus {
   MG_ASSERT(std::holds_alternative<CoordinatorInstance>(data_),
             "Coordinator cannot unregister instance since variant holds wrong alternative");
 
@@ -70,7 +71,8 @@ auto CoordinatorState::UnregisterReplicationInstance(std::string instance_name)
       data_);
 }
 
-auto CoordinatorState::SetReplicationInstanceToMain(std::string instance_name) -> SetInstanceToMainCoordinatorStatus {
+auto CoordinatorState::SetReplicationInstanceToMain(std::string_view instance_name)
+    -> SetInstanceToMainCoordinatorStatus {
   MG_ASSERT(std::holds_alternative<CoordinatorInstance>(data_),
             "Coordinator cannot register replica since variant holds wrong alternative");
 
@@ -96,8 +98,8 @@ auto CoordinatorState::GetCoordinatorServer() const -> CoordinatorServer & {
   return *std::get<CoordinatorMainReplicaData>(data_).coordinator_server_;
 }
 
-auto CoordinatorState::AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string raft_address)
-    -> void {
+auto CoordinatorState::AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port,
+                                              std::string_view raft_address) -> void {
   MG_ASSERT(std::holds_alternative<CoordinatorInstance>(data_),
             "Coordinator cannot register replica since variant holds wrong alternative");
   return std::get<CoordinatorInstance>(data_).AddCoordinatorInstance(raft_server_id, raft_port, raft_address);
diff --git a/src/coordination/coordinator_state_machine.cpp b/src/coordination/coordinator_state_machine.cpp
index b939bd304..631c3c4d2 100644
--- a/src/coordination/coordinator_state_machine.cpp
+++ b/src/coordination/coordinator_state_machine.cpp
@@ -12,100 +12,204 @@
 #ifdef MG_ENTERPRISE
 
 #include "nuraft/coordinator_state_machine.hpp"
+#include "utils/logging.hpp"
+
+namespace {
+constexpr int MAX_SNAPSHOTS = 3;
+}  // namespace
 
 namespace memgraph::coordination {
 
-auto CoordinatorStateMachine::EncodeRegisterReplicationInstance(const std::string &name) -> ptr<buffer> {
-  std::string str_log = name + "_replica";
-  ptr<buffer> log = buffer::alloc(sizeof(uint32_t) + str_log.size());
-  buffer_serializer bs(log);
-  bs.put_str(str_log);
-  return log;
+auto CoordinatorStateMachine::FindCurrentMainInstanceName() const -> std::optional<std::string> {
+  return cluster_state_.FindCurrentMainInstanceName();
 }
 
-auto CoordinatorStateMachine::DecodeRegisterReplicationInstance(buffer &data) -> std::string {
+auto CoordinatorStateMachine::MainExists() const -> bool { return cluster_state_.MainExists(); }
+
+auto CoordinatorStateMachine::IsMain(std::string_view instance_name) const -> bool {
+  return cluster_state_.IsMain(instance_name);
+}
+
+auto CoordinatorStateMachine::IsReplica(std::string_view instance_name) const -> bool {
+  return cluster_state_.IsReplica(instance_name);
+}
+
+auto CoordinatorStateMachine::CreateLog(nlohmann::json &&log) -> ptr<buffer> {
+  auto const log_dump = log.dump();
+  ptr<buffer> log_buf = buffer::alloc(sizeof(uint32_t) + log_dump.size());
+  buffer_serializer bs(log_buf);
+  bs.put_str(log_dump);
+  return log_buf;
+}
+
+auto CoordinatorStateMachine::SerializeRegisterInstance(CoordinatorClientConfig const &config) -> ptr<buffer> {
+  return CreateLog({{"action", RaftLogAction::REGISTER_REPLICATION_INSTANCE}, {"info", config}});
+}
+
+auto CoordinatorStateMachine::SerializeUnregisterInstance(std::string_view instance_name) -> ptr<buffer> {
+  return CreateLog({{"action", RaftLogAction::UNREGISTER_REPLICATION_INSTANCE}, {"info", instance_name}});
+}
+
+auto CoordinatorStateMachine::SerializeSetInstanceAsMain(std::string_view instance_name) -> ptr<buffer> {
+  return CreateLog({{"action", RaftLogAction::SET_INSTANCE_AS_MAIN}, {"info", instance_name}});
+}
+
+auto CoordinatorStateMachine::SerializeSetInstanceAsReplica(std::string_view instance_name) -> ptr<buffer> {
+  return CreateLog({{"action", RaftLogAction::SET_INSTANCE_AS_REPLICA}, {"info", instance_name}});
+}
+
+auto CoordinatorStateMachine::SerializeUpdateUUID(utils::UUID const &uuid) -> ptr<buffer> {
+  return CreateLog({{"action", RaftLogAction::UPDATE_UUID}, {"info", uuid}});
+}
+
+auto CoordinatorStateMachine::DecodeLog(buffer &data) -> std::pair<TRaftLog, RaftLogAction> {
   buffer_serializer bs(data);
-  return bs.get_str();
+  auto const json = nlohmann::json::parse(bs.get_str());
+
+  auto const action = json["action"].get<RaftLogAction>();
+  auto const &info = json["info"];
+
+  switch (action) {
+    case RaftLogAction::REGISTER_REPLICATION_INSTANCE:
+      return {info.get<CoordinatorClientConfig>(), action};
+    case RaftLogAction::UPDATE_UUID:
+      return {info.get<utils::UUID>(), action};
+    case RaftLogAction::UNREGISTER_REPLICATION_INSTANCE:
+    case RaftLogAction::SET_INSTANCE_AS_MAIN:
+      [[fallthrough]];
+    case RaftLogAction::SET_INSTANCE_AS_REPLICA:
+      return {info.get<std::string>(), action};
+  }
+  throw std::runtime_error("Unknown action");
 }
 
-auto CoordinatorStateMachine::pre_commit(ulong const log_idx, buffer &data) -> ptr<buffer> {
-  buffer_serializer bs(data);
-  std::string str = bs.get_str();
-
-  spdlog::info("pre_commit {} : {}", log_idx, str);
-  return nullptr;
-}
+auto CoordinatorStateMachine::pre_commit(ulong const /*log_idx*/, buffer & /*data*/) -> ptr<buffer> { return nullptr; }
 
 auto CoordinatorStateMachine::commit(ulong const log_idx, buffer &data) -> ptr<buffer> {
-  buffer_serializer bs(data);
-  std::string str = bs.get_str();
-
-  spdlog::info("commit {} : {}", log_idx, str);
-
+  spdlog::debug("Commit: log_idx={}, data.size()={}", log_idx, data.size());
+  auto const [parsed_data, log_action] = DecodeLog(data);
+  cluster_state_.DoAction(parsed_data, log_action);
   last_committed_idx_ = log_idx;
-  return nullptr;
+
+  // Return raft log number
+  ptr<buffer> ret = buffer::alloc(sizeof(log_idx));
+  buffer_serializer bs_ret(ret);
+  bs_ret.put_u64(log_idx);
+  return ret;
 }
 
 auto CoordinatorStateMachine::commit_config(ulong const log_idx, ptr<cluster_config> & /*new_conf*/) -> void {
   last_committed_idx_ = log_idx;
+  spdlog::debug("Commit config: log_idx={}", log_idx);
 }
 
 auto CoordinatorStateMachine::rollback(ulong const log_idx, buffer &data) -> void {
-  buffer_serializer bs(data);
-  std::string str = bs.get_str();
-
-  spdlog::info("rollback {} : {}", log_idx, str);
+  // NOTE: Nothing since we don't do anything in pre_commit
+  spdlog::debug("Rollback: log_idx={}, data.size()={}", log_idx, data.size());
 }
 
-auto CoordinatorStateMachine::read_logical_snp_obj(snapshot & /*snapshot*/, void *& /*user_snp_ctx*/, ulong /*obj_id*/,
+auto CoordinatorStateMachine::read_logical_snp_obj(snapshot &snapshot, void *& /*user_snp_ctx*/, ulong obj_id,
                                                    ptr<buffer> &data_out, bool &is_last_obj) -> int {
-  // Put dummy data.
-  data_out = buffer::alloc(sizeof(int32));
-  buffer_serializer bs(data_out);
-  bs.put_i32(0);
+  spdlog::debug("read logical snapshot object, obj_id: {}", obj_id);
+
+  ptr<SnapshotCtx> ctx = nullptr;
+  {
+    auto ll = std::lock_guard{snapshots_lock_};
+    auto entry = snapshots_.find(snapshot.get_last_log_idx());
+    if (entry == snapshots_.end()) {
+      data_out = nullptr;
+      is_last_obj = true;
+      return 0;
+    }
+    ctx = entry->second;
+  }
+
+  if (obj_id == 0) {
+    // Object ID == 0: first object, put dummy data.
+    data_out = buffer::alloc(sizeof(int32));
+    buffer_serializer bs(data_out);
+    bs.put_i32(0);
+    is_last_obj = false;
+  } else {
+    // Object ID > 0: second object, put actual value.
+    ctx->cluster_state_.Serialize(data_out);
+  }
 
-  is_last_obj = true;
   return 0;
 }
 
-auto CoordinatorStateMachine::save_logical_snp_obj(snapshot &s, ulong &obj_id, buffer & /*data*/, bool /*is_first_obj*/,
-                                                   bool /*is_last_obj*/) -> void {
-  spdlog::info("save snapshot {} term {} object ID", s.get_last_log_idx(), s.get_last_log_term(), obj_id);
-  // Request next object.
-  obj_id++;
+auto CoordinatorStateMachine::save_logical_snp_obj(snapshot &snapshot, ulong &obj_id, buffer &data, bool is_first_obj,
+                                                   bool is_last_obj) -> void {
+  spdlog::debug("save logical snapshot object, obj_id: {}, is_first_obj: {}, is_last_obj: {}", obj_id, is_first_obj,
+                is_last_obj);
+
+  if (obj_id == 0) {
+    ptr<buffer> snp_buf = snapshot.serialize();
+    auto ss = snapshot::deserialize(*snp_buf);
+    create_snapshot_internal(ss);
+  } else {
+    auto cluster_state = CoordinatorClusterState::Deserialize(data);
+
+    auto ll = std::lock_guard{snapshots_lock_};
+    auto entry = snapshots_.find(snapshot.get_last_log_idx());
+    DMG_ASSERT(entry != snapshots_.end());
+    entry->second->cluster_state_ = cluster_state;
+  }
 }
 
 auto CoordinatorStateMachine::apply_snapshot(snapshot &s) -> bool {
-  spdlog::info("apply snapshot {} term {}", s.get_last_log_idx(), s.get_last_log_term());
-  {
-    auto lock = std::lock_guard{last_snapshot_lock_};
-    ptr<buffer> snp_buf = s.serialize();
-    last_snapshot_ = snapshot::deserialize(*snp_buf);
-  }
+  auto ll = std::lock_guard{snapshots_lock_};
+  spdlog::debug("apply snapshot, last_log_idx: {}", s.get_last_log_idx());
+
+  auto entry = snapshots_.find(s.get_last_log_idx());
+  if (entry == snapshots_.end()) return false;
+
+  cluster_state_ = entry->second->cluster_state_;
   return true;
 }
 
 auto CoordinatorStateMachine::free_user_snp_ctx(void *&user_snp_ctx) -> void {}
 
 auto CoordinatorStateMachine::last_snapshot() -> ptr<snapshot> {
-  auto lock = std::lock_guard{last_snapshot_lock_};
-  return last_snapshot_;
+  auto ll = std::lock_guard{snapshots_lock_};
+  spdlog::debug("last_snapshot");
+  auto entry = snapshots_.rbegin();
+  if (entry == snapshots_.rend()) return nullptr;
+
+  ptr<SnapshotCtx> ctx = entry->second;
+  return ctx->snapshot_;
 }
 
 auto CoordinatorStateMachine::last_commit_index() -> ulong { return last_committed_idx_; }
 
 auto CoordinatorStateMachine::create_snapshot(snapshot &s, async_result<bool>::handler_type &when_done) -> void {
-  spdlog::info("create snapshot {} term {}", s.get_last_log_idx(), s.get_last_log_term());
-  // Clone snapshot from `s`.
-  {
-    auto lock = std::lock_guard{last_snapshot_lock_};
-    ptr<buffer> snp_buf = s.serialize();
-    last_snapshot_ = snapshot::deserialize(*snp_buf);
-  }
+  spdlog::debug("create_snapshot, last_log_idx: {}", s.get_last_log_idx());
+  ptr<buffer> snp_buf = s.serialize();
+  ptr<snapshot> ss = snapshot::deserialize(*snp_buf);
+  create_snapshot_internal(ss);
+
   ptr<std::exception> except(nullptr);
   bool ret = true;
   when_done(ret, except);
 }
 
+auto CoordinatorStateMachine::create_snapshot_internal(ptr<snapshot> snapshot) -> void {
+  auto ll = std::lock_guard{snapshots_lock_};
+  spdlog::debug("create_snapshot_internal, last_log_idx: {}", snapshot->get_last_log_idx());
+
+  auto ctx = cs_new<SnapshotCtx>(snapshot, cluster_state_);
+  snapshots_[snapshot->get_last_log_idx()] = ctx;
+
+  while (snapshots_.size() > MAX_SNAPSHOTS) {
+    snapshots_.erase(snapshots_.begin());
+  }
+}
+
+auto CoordinatorStateMachine::GetInstances() const -> std::vector<InstanceState> {
+  return cluster_state_.GetInstances();
+}
+
+auto CoordinatorStateMachine::GetUUID() const -> utils::UUID { return cluster_state_.GetUUID(); }
+
 }  // namespace memgraph::coordination
 #endif
diff --git a/src/coordination/include/coordination/coordinator_client.hpp b/src/coordination/include/coordination/coordinator_client.hpp
index 5e10af89d..5d4795f81 100644
--- a/src/coordination/include/coordination/coordinator_client.hpp
+++ b/src/coordination/include/coordination/coordinator_client.hpp
@@ -14,6 +14,7 @@
 #ifdef MG_ENTERPRISE
 
 #include "coordination/coordinator_config.hpp"
+#include "replication_coordination_glue/common.hpp"
 #include "rpc/client.hpp"
 #include "rpc_errors.hpp"
 #include "utils/result.hpp"
@@ -23,13 +24,13 @@
 namespace memgraph::coordination {
 
 class CoordinatorInstance;
-using HealthCheckCallback = std::function<void(CoordinatorInstance *, std::string_view)>;
+using HealthCheckClientCallback = std::function<void(CoordinatorInstance *, std::string_view)>;
 using ReplicationClientsInfo = std::vector<ReplClientInfo>;
 
 class CoordinatorClient {
  public:
   explicit CoordinatorClient(CoordinatorInstance *coord_instance, CoordinatorClientConfig config,
-                             HealthCheckCallback succ_cb, HealthCheckCallback fail_cb);
+                             HealthCheckClientCallback succ_cb, HealthCheckClientCallback fail_cb);
 
   ~CoordinatorClient() = default;
 
@@ -45,16 +46,17 @@ class CoordinatorClient {
   void ResumeFrequentCheck();
 
   auto InstanceName() const -> std::string;
-  auto SocketAddress() const -> std::string;
+  auto CoordinatorSocketAddress() const -> std::string;
+  auto ReplicationSocketAddress() const -> std::string;
 
   [[nodiscard]] auto DemoteToReplica() const -> bool;
 
-  auto SendPromoteReplicaToMainRpc(const utils::UUID &uuid, ReplicationClientsInfo replication_clients_info) const
+  auto SendPromoteReplicaToMainRpc(utils::UUID const &uuid, ReplicationClientsInfo replication_clients_info) const
       -> bool;
 
-  auto SendSwapMainUUIDRpc(const utils::UUID &uuid) const -> bool;
+  auto SendSwapMainUUIDRpc(utils::UUID const &uuid) const -> bool;
 
-  auto SendUnregisterReplicaRpc(std::string const &instance_name) const -> bool;
+  auto SendUnregisterReplicaRpc(std::string_view instance_name) const -> bool;
 
   auto SendEnableWritingOnMainRpc() const -> bool;
 
@@ -62,7 +64,8 @@ class CoordinatorClient {
 
   auto ReplicationClientInfo() const -> ReplClientInfo;
 
-  auto SetCallbacks(HealthCheckCallback succ_cb, HealthCheckCallback fail_cb) -> void;
+  auto SendGetInstanceTimestampsRpc() const
+      -> utils::BasicResult<GetInstanceUUIDError, replication_coordination_glue::DatabaseHistories>;
 
   auto RpcClient() -> rpc::Client & { return rpc_client_; }
 
@@ -82,8 +85,8 @@ class CoordinatorClient {
 
   CoordinatorClientConfig config_;
   CoordinatorInstance *coord_instance_;
-  HealthCheckCallback succ_cb_;
-  HealthCheckCallback fail_cb_;
+  HealthCheckClientCallback succ_cb_;
+  HealthCheckClientCallback fail_cb_;
 };
 
 }  // namespace memgraph::coordination
diff --git a/src/coordination/include/coordination/coordinator_config.hpp b/src/coordination/include/coordination/coordinator_config.hpp
index df7a5f94f..127a365eb 100644
--- a/src/coordination/include/coordination/coordinator_config.hpp
+++ b/src/coordination/include/coordination/coordinator_config.hpp
@@ -14,12 +14,16 @@
 #ifdef MG_ENTERPRISE
 
 #include "replication_coordination_glue/mode.hpp"
+#include "utils/string.hpp"
 
 #include <chrono>
 #include <cstdint>
 #include <optional>
 #include <string>
 
+#include <fmt/format.h>
+#include "json/json.hpp"
+
 namespace memgraph::coordination {
 
 inline constexpr auto *kDefaultReplicationServerIp = "0.0.0.0";
@@ -32,7 +36,11 @@ struct CoordinatorClientConfig {
   std::chrono::seconds instance_down_timeout_sec{5};
   std::chrono::seconds instance_get_uuid_frequency_sec{10};
 
-  auto SocketAddress() const -> std::string { return ip_address + ":" + std::to_string(port); }
+  auto CoordinatorSocketAddress() const -> std::string { return fmt::format("{}:{}", ip_address, port); }
+  auto ReplicationSocketAddress() const -> std::string {
+    return fmt::format("{}:{}", replication_client_info.replication_ip_address,
+                       replication_client_info.replication_port);
+  }
 
   struct ReplicationClientInfo {
     std::string instance_name;
@@ -75,5 +83,11 @@ struct CoordinatorServerConfig {
   friend bool operator==(CoordinatorServerConfig const &, CoordinatorServerConfig const &) = default;
 };
 
+void to_json(nlohmann::json &j, CoordinatorClientConfig const &config);
+void from_json(nlohmann::json const &j, CoordinatorClientConfig &config);
+
+void to_json(nlohmann::json &j, ReplClientInfo const &config);
+void from_json(nlohmann::json const &j, ReplClientInfo &config);
+
 }  // namespace memgraph::coordination
 #endif
diff --git a/src/coordination/include/coordination/coordinator_exceptions.hpp b/src/coordination/include/coordination/coordinator_exceptions.hpp
index 59a2e89d8..7a967f80b 100644
--- a/src/coordination/include/coordination/coordinator_exceptions.hpp
+++ b/src/coordination/include/coordination/coordinator_exceptions.hpp
@@ -83,5 +83,16 @@ class RaftCouldNotParseFlagsException final : public utils::BasicException {
   SPECIALIZE_GET_EXCEPTION_NAME(RaftCouldNotParseFlagsException)
 };
 
+class InvalidRaftLogActionException final : public utils::BasicException {
+ public:
+  explicit InvalidRaftLogActionException(std::string_view what) noexcept : BasicException(what) {}
+
+  template <class... Args>
+  explicit InvalidRaftLogActionException(fmt::format_string<Args...> fmt, Args &&...args) noexcept
+      : InvalidRaftLogActionException(fmt::format(fmt, std::forward<Args>(args)...)) {}
+
+  SPECIALIZE_GET_EXCEPTION_NAME(InvalidRaftLogActionException)
+};
+
 }  // namespace memgraph::coordination
 #endif
diff --git a/src/coordination/include/coordination/coordinator_handlers.hpp b/src/coordination/include/coordination/coordinator_handlers.hpp
index b9ed4b519..18aecc9cf 100644
--- a/src/coordination/include/coordination/coordinator_handlers.hpp
+++ b/src/coordination/include/coordination/coordinator_handlers.hpp
@@ -41,6 +41,9 @@ class CoordinatorHandlers {
 
   static void GetInstanceUUIDHandler(replication::ReplicationHandler &replication_handler, slk::Reader *req_reader,
                                      slk::Builder *res_builder);
+
+  static void GetDatabaseHistoriesHandler(replication::ReplicationHandler &replication_handler, slk::Reader *req_reader,
+                                          slk::Builder *res_builder);
 };
 
 }  // namespace memgraph::dbms
diff --git a/src/coordination/include/coordination/coordinator_instance.hpp b/src/coordination/include/coordination/coordinator_instance.hpp
index 15b377ed9..10549f468 100644
--- a/src/coordination/include/coordination/coordinator_instance.hpp
+++ b/src/coordination/include/coordination/coordinator_instance.hpp
@@ -18,6 +18,7 @@
 #include "coordination/raft_state.hpp"
 #include "coordination/register_main_replica_coordinator_status.hpp"
 #include "coordination/replication_instance.hpp"
+#include "utils/resource_lock.hpp"
 #include "utils/rw_lock.hpp"
 #include "utils/thread_pool.hpp"
 
@@ -25,33 +26,54 @@
 
 namespace memgraph::coordination {
 
+struct NewMainRes {
+  std::string most_up_to_date_instance;
+  std::string latest_epoch;
+  uint64_t latest_commit_timestamp;
+};
+using InstanceNameDbHistories = std::pair<std::string, replication_coordination_glue::DatabaseHistories>;
+
 class CoordinatorInstance {
  public:
   CoordinatorInstance();
 
-  [[nodiscard]] auto RegisterReplicationInstance(CoordinatorClientConfig config) -> RegisterInstanceCoordinatorStatus;
-  [[nodiscard]] auto UnregisterReplicationInstance(std::string instance_name) -> UnregisterInstanceCoordinatorStatus;
+  [[nodiscard]] auto RegisterReplicationInstance(CoordinatorClientConfig const &config)
+      -> RegisterInstanceCoordinatorStatus;
+  [[nodiscard]] auto UnregisterReplicationInstance(std::string_view instance_name)
+      -> UnregisterInstanceCoordinatorStatus;
 
-  [[nodiscard]] auto SetReplicationInstanceToMain(std::string instance_name) -> SetInstanceToMainCoordinatorStatus;
+  [[nodiscard]] auto SetReplicationInstanceToMain(std::string_view instance_name) -> SetInstanceToMainCoordinatorStatus;
 
   auto ShowInstances() const -> std::vector<InstanceStatus>;
 
   auto TryFailover() -> void;
 
-  auto AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string raft_address) -> void;
+  auto AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string_view raft_address) -> void;
 
-  auto GetMainUUID() const -> utils::UUID;
-
-  auto SetMainUUID(utils::UUID new_uuid) -> void;
+  static auto ChooseMostUpToDateInstance(std::span<InstanceNameDbHistories> histories) -> NewMainRes;
 
  private:
-  HealthCheckCallback main_succ_cb_, main_fail_cb_, replica_succ_cb_, replica_fail_cb_;
+  HealthCheckClientCallback client_succ_cb_, client_fail_cb_;
 
-  // NOTE: Must be std::list because we rely on pointer stability
+  auto OnRaftCommitCallback(TRaftLog const &log_entry, RaftLogAction log_action) -> void;
+
+  auto FindReplicationInstance(std::string_view replication_instance_name) -> ReplicationInstance &;
+
+  void MainFailCallback(std::string_view);
+
+  void MainSuccessCallback(std::string_view);
+
+  void ReplicaSuccessCallback(std::string_view);
+
+  void ReplicaFailCallback(std::string_view);
+
+  auto IsMain(std::string_view instance_name) const -> bool;
+  auto IsReplica(std::string_view instance_name) const -> bool;
+
+  // NOTE: Must be std::list because we rely on pointer stability.
+  // Leader and followers should both have same view on repl_instances_
   std::list<ReplicationInstance> repl_instances_;
-  mutable utils::RWLock coord_instance_lock_{utils::RWLock::Priority::READ};
-
-  utils::UUID main_uuid_;
+  mutable utils::ResourceLock coord_instance_lock_{};
 
   RaftState raft_state_;
 };
diff --git a/src/coordination/include/coordination/coordinator_rpc.hpp b/src/coordination/include/coordination/coordinator_rpc.hpp
index 1578b4577..d799b2955 100644
--- a/src/coordination/include/coordination/coordinator_rpc.hpp
+++ b/src/coordination/include/coordination/coordinator_rpc.hpp
@@ -15,6 +15,7 @@
 #ifdef MG_ENTERPRISE
 
 #include "coordination/coordinator_config.hpp"
+#include "replication_coordination_glue/common.hpp"
 #include "rpc/messages.hpp"
 #include "slk/serialization.hpp"
 
@@ -89,7 +90,7 @@ struct UnregisterReplicaReq {
   static void Load(UnregisterReplicaReq *self, memgraph::slk::Reader *reader);
   static void Save(UnregisterReplicaReq const &self, memgraph::slk::Builder *builder);
 
-  explicit UnregisterReplicaReq(std::string instance_name) : instance_name(std::move(instance_name)) {}
+  explicit UnregisterReplicaReq(std::string_view inst_name) : instance_name(inst_name) {}
 
   UnregisterReplicaReq() = default;
 
@@ -161,6 +162,32 @@ struct GetInstanceUUIDRes {
 
 using GetInstanceUUIDRpc = rpc::RequestResponse<GetInstanceUUIDReq, GetInstanceUUIDRes>;
 
+struct GetDatabaseHistoriesReq {
+  static const utils::TypeInfo kType;
+  static const utils::TypeInfo &GetTypeInfo() { return kType; }
+
+  static void Load(GetDatabaseHistoriesReq *self, memgraph::slk::Reader *reader);
+  static void Save(const GetDatabaseHistoriesReq &self, memgraph::slk::Builder *builder);
+
+  GetDatabaseHistoriesReq() = default;
+};
+
+struct GetDatabaseHistoriesRes {
+  static const utils::TypeInfo kType;
+  static const utils::TypeInfo &GetTypeInfo() { return kType; }
+
+  static void Load(GetDatabaseHistoriesRes *self, memgraph::slk::Reader *reader);
+  static void Save(const GetDatabaseHistoriesRes &self, memgraph::slk::Builder *builder);
+
+  explicit GetDatabaseHistoriesRes(const replication_coordination_glue::DatabaseHistories &database_histories)
+      : database_histories(database_histories) {}
+  GetDatabaseHistoriesRes() = default;
+
+  replication_coordination_glue::DatabaseHistories database_histories;
+};
+
+using GetDatabaseHistoriesRpc = rpc::RequestResponse<GetDatabaseHistoriesReq, GetDatabaseHistoriesRes>;
+
 }  // namespace memgraph::coordination
 
 // SLK serialization declarations
@@ -183,15 +210,21 @@ void Save(const memgraph::coordination::GetInstanceUUIDReq &self, memgraph::slk:
 void Load(memgraph::coordination::GetInstanceUUIDReq *self, memgraph::slk::Reader *reader);
 void Save(const memgraph::coordination::GetInstanceUUIDRes &self, memgraph::slk::Builder *builder);
 void Load(memgraph::coordination::GetInstanceUUIDRes *self, memgraph::slk::Reader *reader);
+
 // UnregisterReplicaRpc
 void Save(memgraph::coordination::UnregisterReplicaRes const &self, memgraph::slk::Builder *builder);
 void Load(memgraph::coordination::UnregisterReplicaRes *self, memgraph::slk::Reader *reader);
 void Save(memgraph::coordination::UnregisterReplicaReq const &self, memgraph::slk::Builder *builder);
 void Load(memgraph::coordination::UnregisterReplicaReq *self, memgraph::slk::Reader *reader);
 
+// EnableWritingOnMainRpc
 void Save(memgraph::coordination::EnableWritingOnMainRes const &self, memgraph::slk::Builder *builder);
 void Load(memgraph::coordination::EnableWritingOnMainRes *self, memgraph::slk::Reader *reader);
 
+// GetDatabaseHistoriesRpc
+void Save(const memgraph::coordination::GetDatabaseHistoriesRes &self, memgraph::slk::Builder *builder);
+void Load(memgraph::coordination::GetDatabaseHistoriesRes *self, memgraph::slk::Reader *reader);
+
 }  // namespace memgraph::slk
 
 #endif
diff --git a/src/coordination/include/coordination/coordinator_slk.hpp b/src/coordination/include/coordination/coordinator_slk.hpp
index 49834be41..ee393b7b6 100644
--- a/src/coordination/include/coordination/coordinator_slk.hpp
+++ b/src/coordination/include/coordination/coordinator_slk.hpp
@@ -14,6 +14,7 @@
 #ifdef MG_ENTERPRISE
 
 #include "coordination/coordinator_config.hpp"
+#include "replication_coordination_glue/common.hpp"
 #include "slk/serialization.hpp"
 #include "slk/streams.hpp"
 
@@ -34,5 +35,18 @@ inline void Load(ReplicationClientInfo *obj, Reader *reader) {
   Load(&obj->replication_ip_address, reader);
   Load(&obj->replication_port, reader);
 }
+
+inline void Save(const replication_coordination_glue::DatabaseHistory &obj, Builder *builder) {
+  Save(obj.db_uuid, builder);
+  Save(obj.history, builder);
+  Save(obj.name, builder);
+}
+
+inline void Load(replication_coordination_glue::DatabaseHistory *obj, Reader *reader) {
+  Load(&obj->db_uuid, reader);
+  Load(&obj->history, reader);
+  Load(&obj->name, reader);
+}
+
 }  // namespace memgraph::slk
 #endif
diff --git a/src/coordination/include/coordination/coordinator_state.hpp b/src/coordination/include/coordination/coordinator_state.hpp
index 256af66f9..400c36940 100644
--- a/src/coordination/include/coordination/coordinator_state.hpp
+++ b/src/coordination/include/coordination/coordinator_state.hpp
@@ -33,14 +33,16 @@ class CoordinatorState {
   CoordinatorState(CoordinatorState &&) noexcept = delete;
   CoordinatorState &operator=(CoordinatorState &&) noexcept = delete;
 
-  [[nodiscard]] auto RegisterReplicationInstance(CoordinatorClientConfig config) -> RegisterInstanceCoordinatorStatus;
-  [[nodiscard]] auto UnregisterReplicationInstance(std::string instance_name) -> UnregisterInstanceCoordinatorStatus;
+  [[nodiscard]] auto RegisterReplicationInstance(CoordinatorClientConfig const &config)
+      -> RegisterInstanceCoordinatorStatus;
+  [[nodiscard]] auto UnregisterReplicationInstance(std::string_view instance_name)
+      -> UnregisterInstanceCoordinatorStatus;
 
-  [[nodiscard]] auto SetReplicationInstanceToMain(std::string instance_name) -> SetInstanceToMainCoordinatorStatus;
+  [[nodiscard]] auto SetReplicationInstanceToMain(std::string_view instance_name) -> SetInstanceToMainCoordinatorStatus;
 
   auto ShowInstances() const -> std::vector<InstanceStatus>;
 
-  auto AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string raft_address) -> void;
+  auto AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string_view raft_address) -> void;
 
   // NOTE: The client code must check that the server exists before calling this method.
   auto GetCoordinatorServer() const -> CoordinatorServer &;
diff --git a/src/coordination/include/coordination/instance_status.hpp b/src/coordination/include/coordination/instance_status.hpp
index 492410061..da6fd8828 100644
--- a/src/coordination/include/coordination/instance_status.hpp
+++ b/src/coordination/include/coordination/instance_status.hpp
@@ -26,7 +26,7 @@ struct InstanceStatus {
   std::string raft_socket_address;
   std::string coord_socket_address;
   std::string cluster_role;
-  bool is_alive;
+  std::string health;
 };
 
 }  // namespace memgraph::coordination
diff --git a/src/coordination/include/coordination/raft_state.hpp b/src/coordination/include/coordination/raft_state.hpp
index b6ef06008..34da3e2a6 100644
--- a/src/coordination/include/coordination/raft_state.hpp
+++ b/src/coordination/include/coordination/raft_state.hpp
@@ -14,11 +14,17 @@
 #ifdef MG_ENTERPRISE
 
 #include <flags/replication.hpp>
+#include "io/network/endpoint.hpp"
+#include "nuraft/coordinator_state_machine.hpp"
+#include "nuraft/coordinator_state_manager.hpp"
 
 #include <libnuraft/nuraft.hxx>
 
 namespace memgraph::coordination {
 
+class CoordinatorInstance;
+struct CoordinatorClientConfig;
+
 using BecomeLeaderCb = std::function<void()>;
 using BecomeFollowerCb = std::function<void()>;
 
@@ -47,26 +53,38 @@ class RaftState {
   RaftState &operator=(RaftState &&other) noexcept = default;
   ~RaftState();
 
-  static auto MakeRaftState(BecomeLeaderCb become_leader_cb, BecomeFollowerCb become_follower_cb) -> RaftState;
+  static auto MakeRaftState(BecomeLeaderCb &&become_leader_cb, BecomeFollowerCb &&become_follower_cb) -> RaftState;
 
   auto InstanceName() const -> std::string;
   auto RaftSocketAddress() const -> std::string;
 
-  auto AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string raft_address) -> void;
+  auto AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string_view raft_address) -> void;
   auto GetAllCoordinators() const -> std::vector<ptr<srv_config>>;
 
   auto RequestLeadership() -> bool;
   auto IsLeader() const -> bool;
 
-  auto AppendRegisterReplicationInstance(std::string const &instance) -> ptr<raft_result>;
+  auto FindCurrentMainInstanceName() const -> std::optional<std::string>;
+  auto MainExists() const -> bool;
+  auto IsMain(std::string_view instance_name) const -> bool;
+  auto IsReplica(std::string_view instance_name) const -> bool;
 
-  // TODO: (andi) I think variables below can be abstracted
+  auto AppendRegisterReplicationInstanceLog(CoordinatorClientConfig const &config) -> bool;
+  auto AppendUnregisterReplicationInstanceLog(std::string_view instance_name) -> bool;
+  auto AppendSetInstanceAsMainLog(std::string_view instance_name) -> bool;
+  auto AppendSetInstanceAsReplicaLog(std::string_view instance_name) -> bool;
+  auto AppendUpdateUUIDLog(utils::UUID const &uuid) -> bool;
+
+  auto GetInstances() const -> std::vector<InstanceState>;
+  auto GetUUID() const -> utils::UUID;
+
+ private:
+  // TODO: (andi) I think variables below can be abstracted/clean them.
+  io::network::Endpoint raft_endpoint_;
   uint32_t raft_server_id_;
-  uint32_t raft_port_;
-  std::string raft_address_;
 
-  ptr<state_machine> state_machine_;
-  ptr<state_mgr> state_manager_;
+  ptr<CoordinatorStateMachine> state_machine_;
+  ptr<CoordinatorStateManager> state_manager_;
   ptr<raft_server> raft_server_;
   ptr<logger> logger_;
   raft_launcher launcher_;
diff --git a/src/coordination/include/coordination/register_main_replica_coordinator_status.hpp b/src/coordination/include/coordination/register_main_replica_coordinator_status.hpp
index 3aa7e3ca1..13b58ff9f 100644
--- a/src/coordination/include/coordination/register_main_replica_coordinator_status.hpp
+++ b/src/coordination/include/coordination/register_main_replica_coordinator_status.hpp
@@ -19,12 +19,12 @@ namespace memgraph::coordination {
 
 enum class RegisterInstanceCoordinatorStatus : uint8_t {
   NAME_EXISTS,
-  ENDPOINT_EXISTS,
+  COORD_ENDPOINT_EXISTS,
+  REPL_ENDPOINT_EXISTS,
   NOT_COORDINATOR,
-  RPC_FAILED,
   NOT_LEADER,
-  RAFT_COULD_NOT_ACCEPT,
-  RAFT_COULD_NOT_APPEND,
+  RPC_FAILED,
+  RAFT_LOG_ERROR,
   SUCCESS
 };
 
@@ -32,8 +32,9 @@ enum class UnregisterInstanceCoordinatorStatus : uint8_t {
   NO_INSTANCE_WITH_NAME,
   IS_MAIN,
   NOT_COORDINATOR,
-  NOT_LEADER,
   RPC_FAILED,
+  NOT_LEADER,
+  RAFT_LOG_ERROR,
   SUCCESS,
 };
 
@@ -41,9 +42,11 @@ enum class SetInstanceToMainCoordinatorStatus : uint8_t {
   NO_INSTANCE_WITH_NAME,
   MAIN_ALREADY_EXISTS,
   NOT_COORDINATOR,
-  SUCCESS,
+  NOT_LEADER,
+  RAFT_LOG_ERROR,
   COULD_NOT_PROMOTE_TO_MAIN,
-  SWAP_UUID_FAILED
+  SWAP_UUID_FAILED,
+  SUCCESS,
 };
 
 }  // namespace memgraph::coordination
diff --git a/src/coordination/include/coordination/replication_instance.hpp b/src/coordination/include/coordination/replication_instance.hpp
index 8001d0905..7b5d73b81 100644
--- a/src/coordination/include/coordination/replication_instance.hpp
+++ b/src/coordination/include/coordination/replication_instance.hpp
@@ -17,18 +17,24 @@
 #include "coordination/coordinator_exceptions.hpp"
 #include "replication_coordination_glue/role.hpp"
 
-#include <libnuraft/nuraft.hxx>
+#include "utils/resource_lock.hpp"
 #include "utils/result.hpp"
 #include "utils/uuid.hpp"
 
+#include <libnuraft/nuraft.hxx>
+
 namespace memgraph::coordination {
 
 class CoordinatorInstance;
+class ReplicationInstance;
+
+using HealthCheckInstanceCallback = void (CoordinatorInstance::*)(std::string_view);
 
 class ReplicationInstance {
  public:
-  ReplicationInstance(CoordinatorInstance *peer, CoordinatorClientConfig config, HealthCheckCallback succ_cb,
-                      HealthCheckCallback fail_cb);
+  ReplicationInstance(CoordinatorInstance *peer, CoordinatorClientConfig config, HealthCheckClientCallback succ_cb,
+                      HealthCheckClientCallback fail_cb, HealthCheckInstanceCallback succ_instance_cb,
+                      HealthCheckInstanceCallback fail_instance_cb);
 
   ReplicationInstance(ReplicationInstance const &other) = delete;
   ReplicationInstance &operator=(ReplicationInstance const &other) = delete;
@@ -45,14 +51,16 @@ class ReplicationInstance {
   auto IsAlive() const -> bool;
 
   auto InstanceName() const -> std::string;
-  auto SocketAddress() const -> std::string;
+  auto CoordinatorSocketAddress() const -> std::string;
+  auto ReplicationSocketAddress() const -> std::string;
 
-  auto IsReplica() const -> bool;
-  auto IsMain() const -> bool;
+  auto PromoteToMain(utils::UUID const &uuid, ReplicationClientsInfo repl_clients_info,
+                     HealthCheckInstanceCallback main_succ_cb, HealthCheckInstanceCallback main_fail_cb) -> bool;
 
-  auto PromoteToMain(utils::UUID uuid, ReplicationClientsInfo repl_clients_info, HealthCheckCallback main_succ_cb,
-                     HealthCheckCallback main_fail_cb) -> bool;
-  auto DemoteToReplica(HealthCheckCallback replica_succ_cb, HealthCheckCallback replica_fail_cb) -> bool;
+  auto SendDemoteToReplicaRpc() -> bool;
+
+  auto DemoteToReplica(HealthCheckInstanceCallback replica_succ_cb, HealthCheckInstanceCallback replica_fail_cb)
+      -> bool;
 
   auto StartFrequentCheck() -> void;
   auto StopFrequentCheck() -> void;
@@ -63,9 +71,8 @@ class ReplicationInstance {
 
   auto EnsureReplicaHasCorrectMainUUID(utils::UUID const &curr_main_uuid) -> bool;
 
-  auto SendSwapAndUpdateUUID(const utils::UUID &new_main_uuid) -> bool;
-  auto SendUnregisterReplicaRpc(std::string const &instance_name) -> bool;
-
+  auto SendSwapAndUpdateUUID(utils::UUID const &new_main_uuid) -> bool;
+  auto SendUnregisterReplicaRpc(std::string_view instance_name) -> bool;
 
   auto SendGetInstanceUUID() -> utils::BasicResult<coordination::GetInstanceUUIDError, std::optional<utils::UUID>>;
   auto GetClient() -> CoordinatorClient &;
@@ -74,11 +81,13 @@ class ReplicationInstance {
 
   auto SetNewMainUUID(utils::UUID const &main_uuid) -> void;
   auto ResetMainUUID() -> void;
-  auto GetMainUUID() const -> const std::optional<utils::UUID> &;
+  auto GetMainUUID() const -> std::optional<utils::UUID> const &;
+
+  auto GetSuccessCallback() -> HealthCheckInstanceCallback &;
+  auto GetFailCallback() -> HealthCheckInstanceCallback &;
 
  private:
   CoordinatorClient client_;
-  replication_coordination_glue::ReplicationRole replication_role_;
   std::chrono::system_clock::time_point last_response_time_{};
   bool is_alive_{false};
   std::chrono::system_clock::time_point last_check_of_uuid_{};
@@ -90,8 +99,12 @@ class ReplicationInstance {
   // so we need to send swap uuid again
   std::optional<utils::UUID> main_uuid_;
 
+  HealthCheckInstanceCallback succ_cb_;
+  HealthCheckInstanceCallback fail_cb_;
+
   friend bool operator==(ReplicationInstance const &first, ReplicationInstance const &second) {
-    return first.client_ == second.client_ && first.replication_role_ == second.replication_role_;
+    return first.client_ == second.client_ && first.last_response_time_ == second.last_response_time_ &&
+           first.is_alive_ == second.is_alive_ && first.main_uuid_ == second.main_uuid_;
   }
 };
 
diff --git a/src/coordination/include/coordination/rpc_errors.hpp b/src/coordination/include/coordination/rpc_errors.hpp
index f6bfbf3e0..3829d430a 100644
--- a/src/coordination/include/coordination/rpc_errors.hpp
+++ b/src/coordination/include/coordination/rpc_errors.hpp
@@ -11,4 +11,5 @@
 
 namespace memgraph::coordination {
 enum class GetInstanceUUIDError { NO_RESPONSE, RPC_EXCEPTION };
+enum class GetInstanceTimestampsError { NO_RESPONSE, RPC_EXCEPTION };
 }  // namespace memgraph::coordination
diff --git a/src/coordination/include/nuraft/coordinator_cluster_state.hpp b/src/coordination/include/nuraft/coordinator_cluster_state.hpp
new file mode 100644
index 000000000..11d539a14
--- /dev/null
+++ b/src/coordination/include/nuraft/coordinator_cluster_state.hpp
@@ -0,0 +1,92 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#ifdef MG_ENTERPRISE
+
+#include "coordination/coordinator_config.hpp"
+#include "nuraft/raft_log_action.hpp"
+#include "replication_coordination_glue/role.hpp"
+#include "utils/resource_lock.hpp"
+#include "utils/uuid.hpp"
+
+#include <libnuraft/nuraft.hxx>
+#include <range/v3/view.hpp>
+#include "json/json.hpp"
+
+#include <map>
+#include <numeric>
+#include <string>
+#include <variant>
+
+namespace memgraph::coordination {
+
+using replication_coordination_glue::ReplicationRole;
+
+struct InstanceState {
+  CoordinatorClientConfig config;
+  ReplicationRole status;
+
+  friend auto operator==(InstanceState const &lhs, InstanceState const &rhs) -> bool {
+    return lhs.config == rhs.config && lhs.status == rhs.status;
+  }
+};
+
+void to_json(nlohmann::json &j, InstanceState const &instance_state);
+void from_json(nlohmann::json const &j, InstanceState &instance_state);
+
+using TRaftLog = std::variant<CoordinatorClientConfig, std::string, utils::UUID>;
+
+using nuraft::buffer;
+using nuraft::buffer_serializer;
+using nuraft::ptr;
+
+class CoordinatorClusterState {
+ public:
+  CoordinatorClusterState() = default;
+  explicit CoordinatorClusterState(std::map<std::string, InstanceState, std::less<>> instances);
+
+  CoordinatorClusterState(CoordinatorClusterState const &);
+  CoordinatorClusterState &operator=(CoordinatorClusterState const &);
+
+  CoordinatorClusterState(CoordinatorClusterState &&other) noexcept;
+  CoordinatorClusterState &operator=(CoordinatorClusterState &&other) noexcept;
+  ~CoordinatorClusterState() = default;
+
+  auto FindCurrentMainInstanceName() const -> std::optional<std::string>;
+
+  auto MainExists() const -> bool;
+
+  auto IsMain(std::string_view instance_name) const -> bool;
+
+  auto IsReplica(std::string_view instance_name) const -> bool;
+
+  auto InsertInstance(std::string instance_name, InstanceState instance_state) -> void;
+
+  auto DoAction(TRaftLog log_entry, RaftLogAction log_action) -> void;
+
+  auto Serialize(ptr<buffer> &data) -> void;
+
+  static auto Deserialize(buffer &data) -> CoordinatorClusterState;
+
+  auto GetInstances() const -> std::vector<InstanceState>;
+
+  auto GetUUID() const -> utils::UUID;
+
+ private:
+  std::map<std::string, InstanceState, std::less<>> instances_{};
+  utils::UUID uuid_{};
+  mutable utils::ResourceLock log_lock_{};
+};
+
+}  // namespace memgraph::coordination
+#endif
diff --git a/src/coordination/include/nuraft/coordinator_state_machine.hpp b/src/coordination/include/nuraft/coordinator_state_machine.hpp
index 5b5f37b48..836ac17a6 100644
--- a/src/coordination/include/nuraft/coordinator_state_machine.hpp
+++ b/src/coordination/include/nuraft/coordinator_state_machine.hpp
@@ -13,9 +13,15 @@
 
 #ifdef MG_ENTERPRISE
 
+#include "coordination/coordinator_config.hpp"
+#include "nuraft/coordinator_cluster_state.hpp"
+#include "nuraft/raft_log_action.hpp"
+
 #include <spdlog/spdlog.h>
 #include <libnuraft/nuraft.hxx>
 
+#include <variant>
+
 namespace memgraph::coordination {
 
 using nuraft::async_result;
@@ -36,9 +42,19 @@ class CoordinatorStateMachine : public state_machine {
   CoordinatorStateMachine &operator=(CoordinatorStateMachine &&) = delete;
   ~CoordinatorStateMachine() override {}
 
-  static auto EncodeRegisterReplicationInstance(const std::string &name) -> ptr<buffer>;
+  auto FindCurrentMainInstanceName() const -> std::optional<std::string>;
+  auto MainExists() const -> bool;
+  auto IsMain(std::string_view instance_name) const -> bool;
+  auto IsReplica(std::string_view instance_name) const -> bool;
 
-  static auto DecodeRegisterReplicationInstance(buffer &data) -> std::string;
+  static auto CreateLog(nlohmann::json &&log) -> ptr<buffer>;
+  static auto SerializeRegisterInstance(CoordinatorClientConfig const &config) -> ptr<buffer>;
+  static auto SerializeUnregisterInstance(std::string_view instance_name) -> ptr<buffer>;
+  static auto SerializeSetInstanceAsMain(std::string_view instance_name) -> ptr<buffer>;
+  static auto SerializeSetInstanceAsReplica(std::string_view instance_name) -> ptr<buffer>;
+  static auto SerializeUpdateUUID(utils::UUID const &uuid) -> ptr<buffer>;
+
+  static auto DecodeLog(buffer &data) -> std::pair<TRaftLog, RaftLogAction>;
 
   auto pre_commit(ulong log_idx, buffer &data) -> ptr<buffer> override;
 
@@ -64,11 +80,27 @@ class CoordinatorStateMachine : public state_machine {
 
   auto create_snapshot(snapshot &s, async_result<bool>::handler_type &when_done) -> void override;
 
+  auto GetInstances() const -> std::vector<InstanceState>;
+  auto GetUUID() const -> utils::UUID;
+
  private:
+  struct SnapshotCtx {
+    SnapshotCtx(ptr<snapshot> &snapshot, CoordinatorClusterState const &cluster_state)
+        : snapshot_(snapshot), cluster_state_(cluster_state) {}
+
+    ptr<snapshot> snapshot_;
+    CoordinatorClusterState cluster_state_;
+  };
+
+  auto create_snapshot_internal(ptr<snapshot> snapshot) -> void;
+
+  CoordinatorClusterState cluster_state_;
   std::atomic<uint64_t> last_committed_idx_{0};
 
-  ptr<snapshot> last_snapshot_;
+  std::map<uint64_t, ptr<SnapshotCtx>> snapshots_;
+  std::mutex snapshots_lock_;
 
+  ptr<snapshot> last_snapshot_;
   std::mutex last_snapshot_lock_;
 };
 
diff --git a/src/coordination/include/nuraft/raft_log_action.hpp b/src/coordination/include/nuraft/raft_log_action.hpp
new file mode 100644
index 000000000..3f1b26dfa
--- /dev/null
+++ b/src/coordination/include/nuraft/raft_log_action.hpp
@@ -0,0 +1,42 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#ifdef MG_ENTERPRISE
+
+#include "coordination/coordinator_exceptions.hpp"
+
+#include <cstdint>
+#include <string>
+
+#include "json/json.hpp"
+
+namespace memgraph::coordination {
+
+enum class RaftLogAction : uint8_t {
+  REGISTER_REPLICATION_INSTANCE,
+  UNREGISTER_REPLICATION_INSTANCE,
+  SET_INSTANCE_AS_MAIN,
+  SET_INSTANCE_AS_REPLICA,
+  UPDATE_UUID
+};
+
+NLOHMANN_JSON_SERIALIZE_ENUM(RaftLogAction, {
+                                                {RaftLogAction::REGISTER_REPLICATION_INSTANCE, "register"},
+                                                {RaftLogAction::UNREGISTER_REPLICATION_INSTANCE, "unregister"},
+                                                {RaftLogAction::SET_INSTANCE_AS_MAIN, "promote"},
+                                                {RaftLogAction::SET_INSTANCE_AS_REPLICA, "demote"},
+                                                {RaftLogAction::UPDATE_UUID, "update_uuid"},
+                                            })
+
+}  // namespace memgraph::coordination
+#endif
diff --git a/src/coordination/raft_state.cpp b/src/coordination/raft_state.cpp
index d171a6b3d..38acfd85e 100644
--- a/src/coordination/raft_state.cpp
+++ b/src/coordination/raft_state.cpp
@@ -10,12 +10,11 @@
 // licenses/APL.txt.
 
 #ifdef MG_ENTERPRISE
+#include <chrono>
 
-#include "coordination/raft_state.hpp"
-
+#include "coordination/coordinator_config.hpp"
 #include "coordination/coordinator_exceptions.hpp"
-#include "nuraft/coordinator_state_machine.hpp"
-#include "nuraft/coordinator_state_manager.hpp"
+#include "coordination/raft_state.hpp"
 #include "utils/counter.hpp"
 
 namespace memgraph::coordination {
@@ -33,31 +32,35 @@ using raft_result = cmd_result<ptr<buffer>>;
 
 RaftState::RaftState(BecomeLeaderCb become_leader_cb, BecomeFollowerCb become_follower_cb, uint32_t raft_server_id,
                      uint32_t raft_port, std::string raft_address)
-    : raft_server_id_(raft_server_id),
-      raft_port_(raft_port),
-      raft_address_(std::move(raft_address)),
+    : raft_endpoint_(raft_address, raft_port),
+      raft_server_id_(raft_server_id),
       state_machine_(cs_new<CoordinatorStateMachine>()),
-      state_manager_(
-          cs_new<CoordinatorStateManager>(raft_server_id_, raft_address_ + ":" + std::to_string(raft_port_))),
+      state_manager_(cs_new<CoordinatorStateManager>(raft_server_id_, raft_endpoint_.SocketAddress())),
       logger_(nullptr),
       become_leader_cb_(std::move(become_leader_cb)),
       become_follower_cb_(std::move(become_follower_cb)) {}
 
 auto RaftState::InitRaftServer() -> void {
   asio_service::options asio_opts;
-  asio_opts.thread_pool_size_ = 1;  // TODO: (andi) Improve this
+  asio_opts.thread_pool_size_ = 1;
 
   raft_params params;
   params.heart_beat_interval_ = 100;
   params.election_timeout_lower_bound_ = 200;
   params.election_timeout_upper_bound_ = 400;
-  // 5 logs are preserved before the last snapshot
   params.reserved_log_items_ = 5;
-  // Create snapshot for every 5 log appends
   params.snapshot_distance_ = 5;
   params.client_req_timeout_ = 3000;
   params.return_method_ = raft_params::blocking;
 
+  // If the leader doesn't receive any response from quorum nodes
+  // in 200ms, it will step down.
+  // This allows us to achieve strong consistency even if network partition
+  // happens between the current leader and followers.
+  // The value must be <= election_timeout_lower_bound_ so that cluster can never
+  // have multiple leaders.
+  params.leadership_expiry_ = 200;
+
   raft_server::init_options init_opts;
   init_opts.raft_callback_ = [this](cb_func::Type event_type, cb_func::Param *param) -> nuraft::CbReturnCode {
     if (event_type == cb_func::BecomeLeader) {
@@ -72,11 +75,11 @@ auto RaftState::InitRaftServer() -> void {
 
   raft_launcher launcher;
 
-  raft_server_ = launcher.init(state_machine_, state_manager_, logger_, static_cast<int>(raft_port_), asio_opts, params,
-                               init_opts);
+  raft_server_ =
+      launcher.init(state_machine_, state_manager_, logger_, raft_endpoint_.port, asio_opts, params, init_opts);
 
   if (!raft_server_) {
-    throw RaftServerStartException("Failed to launch raft server on {}:{}", raft_address_, raft_port_);
+    throw RaftServerStartException("Failed to launch raft server on {}", raft_endpoint_.SocketAddress());
   }
 
   auto maybe_stop = utils::ResettableCounter<20>();
@@ -87,38 +90,61 @@ auto RaftState::InitRaftServer() -> void {
     std::this_thread::sleep_for(std::chrono::milliseconds(250));
   } while (!maybe_stop());
 
-  throw RaftServerStartException("Failed to initialize raft server on {}:{}", raft_address_, raft_port_);
+  throw RaftServerStartException("Failed to initialize raft server on {}", raft_endpoint_.SocketAddress());
 }
 
-auto RaftState::MakeRaftState(BecomeLeaderCb become_leader_cb, BecomeFollowerCb become_follower_cb) -> RaftState {
-  uint32_t raft_server_id{0};
-  uint32_t raft_port{0};
-  try {
-    raft_server_id = FLAGS_raft_server_id;
-    raft_port = FLAGS_raft_server_port;
-  } catch (std::exception const &e) {
-    throw RaftCouldNotParseFlagsException("Failed to parse flags: {}", e.what());
-  }
+auto RaftState::MakeRaftState(BecomeLeaderCb &&become_leader_cb, BecomeFollowerCb &&become_follower_cb) -> RaftState {
+  uint32_t raft_server_id = FLAGS_raft_server_id;
+  uint32_t raft_port = FLAGS_raft_server_port;
 
   auto raft_state =
       RaftState(std::move(become_leader_cb), std::move(become_follower_cb), raft_server_id, raft_port, "127.0.0.1");
+
   raft_state.InitRaftServer();
   return raft_state;
 }
 
 RaftState::~RaftState() { launcher_.shutdown(); }
 
-auto RaftState::InstanceName() const -> std::string { return "coordinator_" + std::to_string(raft_server_id_); }
+auto RaftState::InstanceName() const -> std::string {
+  return fmt::format("coordinator_{}", std::to_string(raft_server_id_));
+}
 
-auto RaftState::RaftSocketAddress() const -> std::string { return raft_address_ + ":" + std::to_string(raft_port_); }
+auto RaftState::RaftSocketAddress() const -> std::string { return raft_endpoint_.SocketAddress(); }
 
-auto RaftState::AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string raft_address) -> void {
-  auto const endpoint = raft_address + ":" + std::to_string(raft_port);
+auto RaftState::AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string_view raft_address)
+    -> void {
+  auto const endpoint = fmt::format("{}:{}", raft_address, raft_port);
   srv_config const srv_config_to_add(static_cast<int>(raft_server_id), endpoint);
-  if (!raft_server_->add_srv(srv_config_to_add)->get_accepted()) {
-    throw RaftAddServerException("Failed to add server {} to the cluster", endpoint);
+
+  auto cmd_result = raft_server_->add_srv(srv_config_to_add);
+
+  if (cmd_result->get_result_code() == nuraft::cmd_result_code::OK) {
+    spdlog::info("Request to add server {} to the cluster accepted", endpoint);
+  } else {
+    throw RaftAddServerException("Failed to accept request to add server {} to the cluster with error code {}",
+                                 endpoint, cmd_result->get_result_code());
+  }
+
+  // Waiting for server to join
+  constexpr int max_tries{10};
+  auto maybe_stop = utils::ResettableCounter<max_tries>();
+  constexpr int waiting_period{200};
+  bool added{false};
+  while (!maybe_stop()) {
+    std::this_thread::sleep_for(std::chrono::milliseconds(waiting_period));
+    const auto server_config = raft_server_->get_srv_config(static_cast<nuraft::int32>(raft_server_id));
+    if (server_config) {
+      spdlog::trace("Server with id {} added to cluster", raft_server_id);
+      added = true;
+      break;
+    }
+  }
+
+  if (!added) {
+    throw RaftAddServerException("Failed to add server {} to the cluster in {}ms", endpoint,
+                                 max_tries * waiting_period);
   }
-  spdlog::info("Request to add server {} to the cluster accepted", endpoint);
 }
 
 auto RaftState::GetAllCoordinators() const -> std::vector<ptr<srv_config>> {
@@ -131,10 +157,123 @@ auto RaftState::IsLeader() const -> bool { return raft_server_->is_leader(); }
 
 auto RaftState::RequestLeadership() -> bool { return raft_server_->is_leader() || raft_server_->request_leadership(); }
 
-auto RaftState::AppendRegisterReplicationInstance(std::string const &instance) -> ptr<raft_result> {
-  auto new_log = CoordinatorStateMachine::EncodeRegisterReplicationInstance(instance);
-  return raft_server_->append_entries({new_log});
+auto RaftState::AppendRegisterReplicationInstanceLog(CoordinatorClientConfig const &config) -> bool {
+  auto new_log = CoordinatorStateMachine::SerializeRegisterInstance(config);
+  auto const res = raft_server_->append_entries({new_log});
+
+  if (!res->get_accepted()) {
+    spdlog::error(
+        "Failed to accept request for registering instance {}. Most likely the reason is that the instance is not "
+        "the "
+        "leader.",
+        config.instance_name);
+    return false;
+  }
+
+  spdlog::info("Request for registering instance {} accepted", config.instance_name);
+
+  if (res->get_result_code() != nuraft::cmd_result_code::OK) {
+    spdlog::error("Failed to register instance {} with error code {}", config.instance_name, res->get_result_code());
+    return false;
+  }
+
+  return true;
 }
 
+auto RaftState::AppendUnregisterReplicationInstanceLog(std::string_view instance_name) -> bool {
+  auto new_log = CoordinatorStateMachine::SerializeUnregisterInstance(instance_name);
+  auto const res = raft_server_->append_entries({new_log});
+  if (!res->get_accepted()) {
+    spdlog::error(
+        "Failed to accept request for unregistering instance {}. Most likely the reason is that the instance is not "
+        "the leader.",
+        instance_name);
+    return false;
+  }
+
+  spdlog::info("Request for unregistering instance {} accepted", instance_name);
+
+  if (res->get_result_code() != nuraft::cmd_result_code::OK) {
+    spdlog::error("Failed to unregister instance {} with error code {}", instance_name, res->get_result_code());
+    return false;
+  }
+  return true;
+}
+
+auto RaftState::AppendSetInstanceAsMainLog(std::string_view instance_name) -> bool {
+  auto new_log = CoordinatorStateMachine::SerializeSetInstanceAsMain(instance_name);
+  auto const res = raft_server_->append_entries({new_log});
+  if (!res->get_accepted()) {
+    spdlog::error(
+        "Failed to accept request for promoting instance {}. Most likely the reason is that the instance is not "
+        "the leader.",
+        instance_name);
+    return false;
+  }
+
+  spdlog::info("Request for promoting instance {} accepted", instance_name);
+
+  if (res->get_result_code() != nuraft::cmd_result_code::OK) {
+    spdlog::error("Failed to promote instance {} with error code {}", instance_name, res->get_result_code());
+    return false;
+  }
+  return true;
+}
+
+auto RaftState::AppendSetInstanceAsReplicaLog(std::string_view instance_name) -> bool {
+  auto new_log = CoordinatorStateMachine::SerializeSetInstanceAsReplica(instance_name);
+  auto const res = raft_server_->append_entries({new_log});
+  if (!res->get_accepted()) {
+    spdlog::error(
+        "Failed to accept request for demoting instance {}. Most likely the reason is that the instance is not "
+        "the leader.",
+        instance_name);
+    return false;
+  }
+  spdlog::info("Request for demoting instance {} accepted", instance_name);
+
+  if (res->get_result_code() != nuraft::cmd_result_code::OK) {
+    spdlog::error("Failed to promote instance {} with error code {}", instance_name, res->get_result_code());
+    return false;
+  }
+
+  return true;
+}
+
+auto RaftState::AppendUpdateUUIDLog(utils::UUID const &uuid) -> bool {
+  auto new_log = CoordinatorStateMachine::SerializeUpdateUUID(uuid);
+  auto const res = raft_server_->append_entries({new_log});
+  if (!res->get_accepted()) {
+    spdlog::error(
+        "Failed to accept request for updating UUID. Most likely the reason is that the instance is not "
+        "the leader.");
+    return false;
+  }
+  spdlog::info("Request for updating UUID accepted");
+
+  if (res->get_result_code() != nuraft::cmd_result_code::OK) {
+    spdlog::error("Failed to update UUID with error code {}", res->get_result_code());
+    return false;
+  }
+
+  return true;
+}
+
+auto RaftState::FindCurrentMainInstanceName() const -> std::optional<std::string> {
+  return state_machine_->FindCurrentMainInstanceName();
+}
+
+auto RaftState::MainExists() const -> bool { return state_machine_->MainExists(); }
+
+auto RaftState::IsMain(std::string_view instance_name) const -> bool { return state_machine_->IsMain(instance_name); }
+
+auto RaftState::IsReplica(std::string_view instance_name) const -> bool {
+  return state_machine_->IsReplica(instance_name);
+}
+
+auto RaftState::GetInstances() const -> std::vector<InstanceState> { return state_machine_->GetInstances(); }
+
+auto RaftState::GetUUID() const -> utils::UUID { return state_machine_->GetUUID(); }
+
 }  // namespace memgraph::coordination
 #endif
diff --git a/src/coordination/replication_instance.cpp b/src/coordination/replication_instance.cpp
index 0d16db648..ca7572ea7 100644
--- a/src/coordination/replication_instance.cpp
+++ b/src/coordination/replication_instance.cpp
@@ -13,21 +13,20 @@
 
 #include "coordination/replication_instance.hpp"
 
+#include <utility>
+
 #include "replication_coordination_glue/handler.hpp"
 #include "utils/result.hpp"
 
 namespace memgraph::coordination {
 
 ReplicationInstance::ReplicationInstance(CoordinatorInstance *peer, CoordinatorClientConfig config,
-                                         HealthCheckCallback succ_cb, HealthCheckCallback fail_cb)
+                                         HealthCheckClientCallback succ_cb, HealthCheckClientCallback fail_cb,
+                                         HealthCheckInstanceCallback succ_instance_cb,
+                                         HealthCheckInstanceCallback fail_instance_cb)
     : client_(peer, std::move(config), std::move(succ_cb), std::move(fail_cb)),
-      replication_role_(replication_coordination_glue::ReplicationRole::REPLICA) {
-  if (!client_.DemoteToReplica()) {
-    throw CoordinatorRegisterInstanceException("Failed to demote instance {} to replica", client_.InstanceName());
-  }
-
-  client_.StartFrequentCheck();
-}
+      succ_cb_(succ_instance_cb),
+      fail_cb_(fail_instance_cb) {}
 
 auto ReplicationInstance::OnSuccessPing() -> void {
   last_response_time_ = std::chrono::system_clock::now();
@@ -46,37 +45,34 @@ auto ReplicationInstance::IsReadyForUUIDPing() -> bool {
 }
 
 auto ReplicationInstance::InstanceName() const -> std::string { return client_.InstanceName(); }
-auto ReplicationInstance::SocketAddress() const -> std::string { return client_.SocketAddress(); }
+auto ReplicationInstance::CoordinatorSocketAddress() const -> std::string { return client_.CoordinatorSocketAddress(); }
+auto ReplicationInstance::ReplicationSocketAddress() const -> std::string { return client_.ReplicationSocketAddress(); }
 auto ReplicationInstance::IsAlive() const -> bool { return is_alive_; }
 
-auto ReplicationInstance::IsReplica() const -> bool {
-  return replication_role_ == replication_coordination_glue::ReplicationRole::REPLICA;
-}
-auto ReplicationInstance::IsMain() const -> bool {
-  return replication_role_ == replication_coordination_glue::ReplicationRole::MAIN;
-}
-
-auto ReplicationInstance::PromoteToMain(utils::UUID new_uuid, ReplicationClientsInfo repl_clients_info,
-                                        HealthCheckCallback main_succ_cb, HealthCheckCallback main_fail_cb) -> bool {
+auto ReplicationInstance::PromoteToMain(utils::UUID const &new_uuid, ReplicationClientsInfo repl_clients_info,
+                                        HealthCheckInstanceCallback main_succ_cb,
+                                        HealthCheckInstanceCallback main_fail_cb) -> bool {
   if (!client_.SendPromoteReplicaToMainRpc(new_uuid, std::move(repl_clients_info))) {
     return false;
   }
 
-  replication_role_ = replication_coordination_glue::ReplicationRole::MAIN;
   main_uuid_ = new_uuid;
-  client_.SetCallbacks(std::move(main_succ_cb), std::move(main_fail_cb));
+  succ_cb_ = main_succ_cb;
+  fail_cb_ = main_fail_cb;
 
   return true;
 }
 
-auto ReplicationInstance::DemoteToReplica(HealthCheckCallback replica_succ_cb, HealthCheckCallback replica_fail_cb)
-    -> bool {
+auto ReplicationInstance::SendDemoteToReplicaRpc() -> bool { return client_.DemoteToReplica(); }
+
+auto ReplicationInstance::DemoteToReplica(HealthCheckInstanceCallback replica_succ_cb,
+                                          HealthCheckInstanceCallback replica_fail_cb) -> bool {
   if (!client_.DemoteToReplica()) {
     return false;
   }
 
-  replication_role_ = replication_coordination_glue::ReplicationRole::REPLICA;
-  client_.SetCallbacks(std::move(replica_succ_cb), std::move(replica_fail_cb));
+  succ_cb_ = replica_succ_cb;
+  fail_cb_ = replica_fail_cb;
 
   return true;
 }
@@ -90,10 +86,12 @@ auto ReplicationInstance::ReplicationClientInfo() const -> CoordinatorClientConf
   return client_.ReplicationClientInfo();
 }
 
+auto ReplicationInstance::GetSuccessCallback() -> HealthCheckInstanceCallback & { return succ_cb_; }
+auto ReplicationInstance::GetFailCallback() -> HealthCheckInstanceCallback & { return fail_cb_; }
+
 auto ReplicationInstance::GetClient() -> CoordinatorClient & { return client_; }
 
 auto ReplicationInstance::SetNewMainUUID(utils::UUID const &main_uuid) -> void { main_uuid_ = main_uuid; }
-auto ReplicationInstance::ResetMainUUID() -> void { main_uuid_ = std::nullopt; }
 auto ReplicationInstance::GetMainUUID() const -> std::optional<utils::UUID> const & { return main_uuid_; }
 
 auto ReplicationInstance::EnsureReplicaHasCorrectMainUUID(utils::UUID const &curr_main_uuid) -> bool {
@@ -106,6 +104,7 @@ auto ReplicationInstance::EnsureReplicaHasCorrectMainUUID(utils::UUID const &cur
   }
   UpdateReplicaLastResponseUUID();
 
+  // NOLINTNEXTLINE
   if (res.GetValue().has_value() && res.GetValue().value() == curr_main_uuid) {
     return true;
   }
@@ -113,7 +112,7 @@ auto ReplicationInstance::EnsureReplicaHasCorrectMainUUID(utils::UUID const &cur
   return SendSwapAndUpdateUUID(curr_main_uuid);
 }
 
-auto ReplicationInstance::SendSwapAndUpdateUUID(const utils::UUID &new_main_uuid) -> bool {
+auto ReplicationInstance::SendSwapAndUpdateUUID(utils::UUID const &new_main_uuid) -> bool {
   if (!replication_coordination_glue::SendSwapMainUUIDRpc(client_.RpcClient(), new_main_uuid)) {
     return false;
   }
@@ -121,7 +120,7 @@ auto ReplicationInstance::SendSwapAndUpdateUUID(const utils::UUID &new_main_uuid
   return true;
 }
 
-auto ReplicationInstance::SendUnregisterReplicaRpc(std::string const &instance_name) -> bool {
+auto ReplicationInstance::SendUnregisterReplicaRpc(std::string_view instance_name) -> bool {
   return client_.SendUnregisterReplicaRpc(instance_name);
 }
 
diff --git a/src/dbms/coordinator_handler.cpp b/src/dbms/coordinator_handler.cpp
index f8e14e2a0..292d50d3d 100644
--- a/src/dbms/coordinator_handler.cpp
+++ b/src/dbms/coordinator_handler.cpp
@@ -20,28 +20,28 @@ namespace memgraph::dbms {
 CoordinatorHandler::CoordinatorHandler(coordination::CoordinatorState &coordinator_state)
     : coordinator_state_(coordinator_state) {}
 
-auto CoordinatorHandler::RegisterReplicationInstance(memgraph::coordination::CoordinatorClientConfig config)
+auto CoordinatorHandler::RegisterReplicationInstance(coordination::CoordinatorClientConfig const &config)
     -> coordination::RegisterInstanceCoordinatorStatus {
   return coordinator_state_.RegisterReplicationInstance(config);
 }
 
-auto CoordinatorHandler::UnregisterReplicationInstance(std::string instance_name)
+auto CoordinatorHandler::UnregisterReplicationInstance(std::string_view instance_name)
     -> coordination::UnregisterInstanceCoordinatorStatus {
-  return coordinator_state_.UnregisterReplicationInstance(std::move(instance_name));
+  return coordinator_state_.UnregisterReplicationInstance(instance_name);
 }
 
-auto CoordinatorHandler::SetReplicationInstanceToMain(std::string instance_name)
+auto CoordinatorHandler::SetReplicationInstanceToMain(std::string_view instance_name)
     -> coordination::SetInstanceToMainCoordinatorStatus {
-  return coordinator_state_.SetReplicationInstanceToMain(std::move(instance_name));
+  return coordinator_state_.SetReplicationInstanceToMain(instance_name);
 }
 
 auto CoordinatorHandler::ShowInstances() const -> std::vector<coordination::InstanceStatus> {
   return coordinator_state_.ShowInstances();
 }
 
-auto CoordinatorHandler::AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string raft_address)
-    -> void {
-  coordinator_state_.AddCoordinatorInstance(raft_server_id, raft_port, std::move(raft_address));
+auto CoordinatorHandler::AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port,
+                                                std::string_view raft_address) -> void {
+  coordinator_state_.AddCoordinatorInstance(raft_server_id, raft_port, raft_address);
 }
 
 }  // namespace memgraph::dbms
diff --git a/src/dbms/coordinator_handler.hpp b/src/dbms/coordinator_handler.hpp
index d06e70676..1c456134d 100644
--- a/src/dbms/coordinator_handler.hpp
+++ b/src/dbms/coordinator_handler.hpp
@@ -30,16 +30,17 @@ class CoordinatorHandler {
 
   // TODO: (andi) When moving coordinator state on same instances, rename from RegisterReplicationInstance to
   // RegisterInstance
-  auto RegisterReplicationInstance(coordination::CoordinatorClientConfig config)
+  auto RegisterReplicationInstance(coordination::CoordinatorClientConfig const &config)
       -> coordination::RegisterInstanceCoordinatorStatus;
 
-  auto UnregisterReplicationInstance(std::string instance_name) -> coordination::UnregisterInstanceCoordinatorStatus;
+  auto UnregisterReplicationInstance(std::string_view instance_name)
+      -> coordination::UnregisterInstanceCoordinatorStatus;
 
-  auto SetReplicationInstanceToMain(std::string instance_name) -> coordination::SetInstanceToMainCoordinatorStatus;
+  auto SetReplicationInstanceToMain(std::string_view instance_name) -> coordination::SetInstanceToMainCoordinatorStatus;
 
   auto ShowInstances() const -> std::vector<coordination::InstanceStatus>;
 
-  auto AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string raft_address) -> void;
+  auto AddCoordinatorInstance(uint32_t raft_server_id, uint32_t raft_port, std::string_view raft_address) -> void;
 
  private:
   coordination::CoordinatorState &coordinator_state_;
diff --git a/src/dbms/database_handler.hpp b/src/dbms/database_handler.hpp
index de5f813ba..cae54088e 100644
--- a/src/dbms/database_handler.hpp
+++ b/src/dbms/database_handler.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/src/dbms/dbms_handler.cpp b/src/dbms/dbms_handler.cpp
index 1c38106db..16927d7e2 100644
--- a/src/dbms/dbms_handler.cpp
+++ b/src/dbms/dbms_handler.cpp
@@ -185,6 +185,16 @@ DbmsHandler::DbmsHandler(storage::Config config, replication::ReplicationState &
   auto directories = std::set{std::string{kDefaultDB}};
 
   // Recover previous databases
+  if (flags::AreExperimentsEnabled(flags::Experiments::SYSTEM_REPLICATION) && !recovery_on_startup) {
+    // This will result in dropping databases on SystemRecoveryHandler
+    // for MT case, and for single DB case we might not even set replication as commit timestamp is checked
+    spdlog::warn(
+        "Data recovery on startup not set, this will result in dropping database in case of multi-tenancy enabled.");
+  }
+
+  // TODO: Problem is if user doesn't set this up "database" name won't be recovered
+  // but if storage-recover-on-startup is true storage will be recovered which is an issue
+  spdlog::info("Data recovery on startup set to {}", recovery_on_startup);
   if (recovery_on_startup) {
     auto it = durability_->begin(std::string(kDBPrefix));
     auto end = durability_->end(std::string(kDBPrefix));
@@ -410,9 +420,10 @@ void DbmsHandler::UpdateDurability(const storage::Config &config, std::optional<
   if (!durability_) return;
   // Save database in a list of active databases
   const auto &key = Durability::GenKey(config.salient.name);
-  if (rel_dir == std::nullopt)
+  if (rel_dir == std::nullopt) {
     rel_dir =
         std::filesystem::relative(config.durability.storage_directory, default_config_.durability.storage_directory);
+  }
   const auto &val = Durability::GenVal(config.salient.uuid, *rel_dir);
   durability_->Put(key, val);
 }
diff --git a/src/dbms/dbms_handler.hpp b/src/dbms/dbms_handler.hpp
index 7b1d45335..b0bbd5758 100644
--- a/src/dbms/dbms_handler.hpp
+++ b/src/dbms/dbms_handler.hpp
@@ -155,6 +155,8 @@ class DbmsHandler {
     spdlog::debug("Trying to create db '{}' on replica which already exists.", config.name);
 
     auto db = Get_(config.name);
+    spdlog::debug("Aligning database with name {} which has UUID {}, where config UUID is {}", config.name,
+                  std::string(db->uuid()), std::string(config.uuid));
     if (db->uuid() == config.uuid) {  // Same db
       return db;
     }
@@ -163,18 +165,22 @@ class DbmsHandler {
 
     // TODO: Fix this hack
     if (config.name == kDefaultDB) {
+      spdlog::debug("Last commit timestamp for DB {} is {}", kDefaultDB,
+                    db->storage()->repl_storage_state_.last_commit_timestamp_);
+      // This seems correct, if database made progress
       if (db->storage()->repl_storage_state_.last_commit_timestamp_ != storage::kTimestampInitialId) {
         spdlog::debug("Default storage is not clean, cannot update UUID...");
         return NewError::GENERIC;  // Update error
       }
-      spdlog::debug("Update default db's UUID");
+      spdlog::debug("Updated default db's UUID");
       // Default db cannot be deleted and remade, have to just update the UUID
       db->storage()->config_.salient.uuid = config.uuid;
       UpdateDurability(db->storage()->config_, ".");
       return db;
     }
 
-    spdlog::debug("Drop database and recreate with the correct UUID");
+    spdlog::debug("Dropping database {} with UUID: {} and recreating with the correct UUID: {}", config.name,
+                  std::string(db->uuid()), std::string(config.uuid));
     // Defer drop
     (void)Delete_(db->name());
     // Second attempt
@@ -266,10 +272,6 @@ class DbmsHandler {
   bool IsMain() const { return repl_state_.IsMain(); }
   bool IsReplica() const { return repl_state_.IsReplica(); }
 
-#ifdef MG_ENTERPRISE
-  // coordination::CoordinatorState &CoordinatorState() { return coordinator_state_; }
-#endif
-
   /**
    * @brief Return all active databases.
    *
diff --git a/src/dbms/inmemory/replication_handlers.cpp b/src/dbms/inmemory/replication_handlers.cpp
index 3fc174d3c..3e4a31884 100644
--- a/src/dbms/inmemory/replication_handlers.cpp
+++ b/src/dbms/inmemory/replication_handlers.cpp
@@ -19,7 +19,6 @@
 #include "storage/v2/durability/durability.hpp"
 #include "storage/v2/durability/snapshot.hpp"
 #include "storage/v2/durability/version.hpp"
-#include "storage/v2/fmt.hpp"
 #include "storage/v2/indices/label_index_stats.hpp"
 #include "storage/v2/inmemory/storage.hpp"
 #include "storage/v2/inmemory/unique_constraints.hpp"
@@ -119,9 +118,14 @@ void InMemoryReplicationHandlers::Register(dbms::DbmsHandler *dbms_handler, repl
       });
   server.rpc_server_.Register<replication_coordination_glue::SwapMainUUIDRpc>(
       [&data, dbms_handler](auto *req_reader, auto *res_builder) {
-        spdlog::debug("Received SwapMainUUIDHandler");
+        spdlog::debug("Received SwapMainUUIDRpc");
         InMemoryReplicationHandlers::SwapMainUUIDHandler(dbms_handler, data, req_reader, res_builder);
       });
+  server.rpc_server_.Register<storage::replication::ForceResetStorageRpc>(
+      [&data, dbms_handler](auto *req_reader, auto *res_builder) {
+        spdlog::debug("Received ForceResetStorageRpc");
+        InMemoryReplicationHandlers::ForceResetStorageHandler(dbms_handler, data.uuid_, req_reader, res_builder);
+      });
 }
 
 void InMemoryReplicationHandlers::SwapMainUUIDHandler(dbms::DbmsHandler *dbms_handler,
@@ -135,7 +139,7 @@ void InMemoryReplicationHandlers::SwapMainUUIDHandler(dbms::DbmsHandler *dbms_ha
 
   replication_coordination_glue::SwapMainUUIDReq req;
   slk::Load(&req, req_reader);
-  spdlog::info(fmt::format("Set replica data UUID  to main uuid {}", std::string(req.uuid)));
+  spdlog::info("Set replica data UUID  to main uuid {}", std::string(req.uuid));
   dbms_handler->ReplicationState().TryPersistRoleReplica(role_replica_data.config, req.uuid);
   role_replica_data.uuid_ = req.uuid;
 
@@ -330,6 +334,78 @@ void InMemoryReplicationHandlers::SnapshotHandler(dbms::DbmsHandler *dbms_handle
   spdlog::debug("Replication recovery from snapshot finished!");
 }
 
+void InMemoryReplicationHandlers::ForceResetStorageHandler(dbms::DbmsHandler *dbms_handler,
+                                                           const std::optional<utils::UUID> &current_main_uuid,
+                                                           slk::Reader *req_reader, slk::Builder *res_builder) {
+  storage::replication::ForceResetStorageReq req;
+  slk::Load(&req, req_reader);
+  auto db_acc = GetDatabaseAccessor(dbms_handler, req.db_uuid);
+  if (!db_acc) {
+    storage::replication::ForceResetStorageRes res{false, 0};
+    slk::Save(res, res_builder);
+    return;
+  }
+  if (!current_main_uuid.has_value() || req.main_uuid != current_main_uuid) [[unlikely]] {
+    LogWrongMain(current_main_uuid, req.main_uuid, storage::replication::SnapshotReq::kType.name);
+    storage::replication::ForceResetStorageRes res{false, 0};
+    slk::Save(res, res_builder);
+    return;
+  }
+
+  storage::replication::Decoder decoder(req_reader);
+
+  auto *storage = static_cast<storage::InMemoryStorage *>(db_acc->get()->storage());
+
+  auto storage_guard = std::unique_lock{storage->main_lock_};
+
+  // Clear the database
+  storage->vertices_.clear();
+  storage->edges_.clear();
+  storage->commit_log_.reset();
+  storage->commit_log_.emplace();
+
+  storage->constraints_.existence_constraints_ = std::make_unique<storage::ExistenceConstraints>();
+  storage->constraints_.unique_constraints_ = std::make_unique<storage::InMemoryUniqueConstraints>();
+  storage->indices_.label_index_ = std::make_unique<storage::InMemoryLabelIndex>();
+  storage->indices_.label_property_index_ = std::make_unique<storage::InMemoryLabelPropertyIndex>();
+
+  // Fine since we will force push when reading from WAL just random epoch with 0 timestamp, as it should be if it
+  // acted as MAIN before
+  storage->repl_storage_state_.epoch_.SetEpoch(std::string(utils::UUID{}));
+  storage->repl_storage_state_.last_commit_timestamp_ = 0;
+
+  storage->repl_storage_state_.history.clear();
+  storage->vertex_id_ = 0;
+  storage->edge_id_ = 0;
+  storage->timestamp_ = storage::kTimestampInitialId;
+
+  storage->CollectGarbage<true>(std::move(storage_guard), false);
+  storage->vertices_.run_gc();
+  storage->edges_.run_gc();
+
+  storage::replication::ForceResetStorageRes res{true, storage->repl_storage_state_.last_commit_timestamp_.load()};
+  slk::Save(res, res_builder);
+
+  spdlog::trace("Deleting old snapshot files.");
+  // Delete other durability files
+  auto snapshot_files = storage::durability::GetSnapshotFiles(storage->recovery_.snapshot_directory_, storage->uuid_);
+  for (const auto &[path, uuid, _] : snapshot_files) {
+    spdlog::trace("Deleting snapshot file {}", path);
+    storage->file_retainer_.DeleteFile(path);
+  }
+
+  spdlog::trace("Deleting old WAL files.");
+  auto wal_files = storage::durability::GetWalFiles(storage->recovery_.wal_directory_, storage->uuid_);
+  if (wal_files) {
+    for (const auto &wal_file : *wal_files) {
+      spdlog::trace("Deleting WAL file {}", wal_file.path);
+      storage->file_retainer_.DeleteFile(wal_file.path);
+    }
+
+    storage->wal_file_.reset();
+  }
+}
+
 void InMemoryReplicationHandlers::WalFilesHandler(dbms::DbmsHandler *dbms_handler,
                                                   const std::optional<utils::UUID> &current_main_uuid,
                                                   slk::Reader *req_reader, slk::Builder *res_builder) {
@@ -764,6 +840,20 @@ uint64_t InMemoryReplicationHandlers::ReadAndApplyDelta(storage::InMemoryStorage
         transaction->DeleteLabelPropertyIndexStats(storage->NameToLabel(info.label));
         break;
       }
+      case WalDeltaData::Type::EDGE_INDEX_CREATE: {
+        spdlog::trace("       Create edge index on :{}", delta.operation_edge_type.edge_type);
+        auto *transaction = get_transaction(timestamp, kUniqueAccess);
+        if (transaction->CreateIndex(storage->NameToEdgeType(delta.operation_label.label)).HasError())
+          throw utils::BasicException("Invalid transaction! Please raise an issue, {}:{}", __FILE__, __LINE__);
+        break;
+      }
+      case WalDeltaData::Type::EDGE_INDEX_DROP: {
+        spdlog::trace("       Drop edge index on :{}", delta.operation_edge_type.edge_type);
+        auto *transaction = get_transaction(timestamp, kUniqueAccess);
+        if (transaction->DropIndex(storage->NameToEdgeType(delta.operation_label.label)).HasError())
+          throw utils::BasicException("Invalid transaction! Please raise an issue, {}:{}", __FILE__, __LINE__);
+        break;
+      }
       case WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE: {
         spdlog::trace("       Create existence constraint on :{} ({})", delta.operation_label_property.label,
                       delta.operation_label_property.property);
diff --git a/src/dbms/inmemory/replication_handlers.hpp b/src/dbms/inmemory/replication_handlers.hpp
index 4406b8338..aaa2d0755 100644
--- a/src/dbms/inmemory/replication_handlers.hpp
+++ b/src/dbms/inmemory/replication_handlers.hpp
@@ -48,6 +48,9 @@ class InMemoryReplicationHandlers {
 
   static void SwapMainUUIDHandler(dbms::DbmsHandler *dbms_handler, replication::RoleReplicaData &role_replica_data,
                                   slk::Reader *req_reader, slk::Builder *res_builder);
+  static void ForceResetStorageHandler(dbms::DbmsHandler *dbms_handler,
+                                       const std::optional<utils::UUID> &current_main_uuid, slk::Reader *req_reader,
+                                       slk::Builder *res_builder);
 
   static void LoadWal(storage::InMemoryStorage *storage, storage::replication::Decoder *decoder);
 
diff --git a/src/glue/communication.hpp b/src/glue/communication.hpp
index 737f32db2..a448b05fc 100644
--- a/src/glue/communication.hpp
+++ b/src/glue/communication.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/src/io/network/endpoint.cpp b/src/io/network/endpoint.cpp
index 44123db6b..6ed4a6753 100644
--- a/src/io/network/endpoint.cpp
+++ b/src/io/network/endpoint.cpp
@@ -22,113 +22,15 @@
 #include "utils/message.hpp"
 #include "utils/string.hpp"
 
+namespace {
+constexpr std::string_view delimiter = ":";
+}  // namespace
+
 namespace memgraph::io::network {
 
-Endpoint::IpFamily Endpoint::GetIpFamily(const std::string &address) {
-  in_addr addr4;
-  in6_addr addr6;
-  int ipv4_result = inet_pton(AF_INET, address.c_str(), &addr4);
-  int ipv6_result = inet_pton(AF_INET6, address.c_str(), &addr6);
-  if (ipv4_result == 1) {
-    return IpFamily::IP4;
-  } else if (ipv6_result == 1) {
-    return IpFamily::IP6;
-  } else {
-    return IpFamily::NONE;
-  }
-}
-
-std::optional<std::pair<std::string, uint16_t>> Endpoint::ParseSocketOrIpAddress(
-    const std::string &address, const std::optional<uint16_t> default_port) {
-  /// expected address format:
-  ///   - "ip_address:port_number"
-  ///   - "ip_address"
-  /// We parse the address first. If it's an IP address, a default port must
-  // be given, or we return nullopt. If it's a socket address, we try to parse
-  // it into an ip address and a port number; even if a default port is given,
-  // it won't be used, as we expect that it is given in the address string.
-  const std::string delimiter = ":";
-  std::string ip_address;
-
-  std::vector<std::string> parts = utils::Split(address, delimiter);
-  if (parts.size() == 1) {
-    if (default_port) {
-      if (GetIpFamily(address) == IpFamily::NONE) {
-        return std::nullopt;
-      }
-      return std::pair{address, *default_port};
-    }
-  } else if (parts.size() == 2) {
-    ip_address = std::move(parts[0]);
-    if (GetIpFamily(ip_address) == IpFamily::NONE) {
-      return std::nullopt;
-    }
-    int64_t int_port{0};
-    try {
-      int_port = utils::ParseInt(parts[1]);
-    } catch (utils::BasicException &e) {
-      spdlog::error(utils::MessageWithLink("Invalid port number {}.", parts[1], "https://memgr.ph/ports"));
-      return std::nullopt;
-    }
-    if (int_port < 0) {
-      spdlog::error(utils::MessageWithLink("Invalid port number {}. The port number must be a positive integer.",
-                                           int_port, "https://memgr.ph/ports"));
-      return std::nullopt;
-    }
-    if (int_port > std::numeric_limits<uint16_t>::max()) {
-      spdlog::error(utils::MessageWithLink("Invalid port number. The port number exceedes the maximum possible size.",
-                                           "https://memgr.ph/ports"));
-      return std::nullopt;
-    }
-
-    return std::pair{ip_address, static_cast<uint16_t>(int_port)};
-  }
-
-  return std::nullopt;
-}
-
-std::optional<std::pair<std::string, uint16_t>> Endpoint::ParseHostname(
-    const std::string &address, const std::optional<uint16_t> default_port = {}) {
-  const std::string delimiter = ":";
-  std::string ip_address;
-  std::vector<std::string> parts = utils::Split(address, delimiter);
-  if (parts.size() == 1) {
-    if (default_port) {
-      if (!IsResolvableAddress(address, *default_port)) {
-        return std::nullopt;
-      }
-      return std::pair{address, *default_port};
-    }
-  } else if (parts.size() == 2) {
-    int64_t int_port{0};
-    auto hostname = std::move(parts[0]);
-    try {
-      int_port = utils::ParseInt(parts[1]);
-    } catch (utils::BasicException &e) {
-      spdlog::error(utils::MessageWithLink("Invalid port number {}.", parts[1], "https://memgr.ph/ports"));
-      return std::nullopt;
-    }
-    if (int_port < 0) {
-      spdlog::error(utils::MessageWithLink("Invalid port number {}. The port number must be a positive integer.",
-                                           int_port, "https://memgr.ph/ports"));
-      return std::nullopt;
-    }
-    if (int_port > std::numeric_limits<uint16_t>::max()) {
-      spdlog::error(utils::MessageWithLink("Invalid port number. The port number exceedes the maximum possible size.",
-                                           "https://memgr.ph/ports"));
-      return std::nullopt;
-    }
-    if (IsResolvableAddress(hostname, static_cast<uint16_t>(int_port))) {
-      return std::pair{hostname, static_cast<u_int16_t>(int_port)};
-    }
-  }
-  return std::nullopt;
-}
-
-std::string Endpoint::SocketAddress() const {
-  auto ip_address = address.empty() ? "EMPTY" : address;
-  return ip_address + ":" + std::to_string(port);
-}
+// NOLINTNEXTLINE
+Endpoint::Endpoint(needs_resolving_t, std::string hostname, uint16_t port)
+    : address(std::move(hostname)), port(port), family{GetIpFamily(address)} {}
 
 Endpoint::Endpoint(std::string ip_address, uint16_t port) : address(std::move(ip_address)), port(port) {
   IpFamily ip_family = GetIpFamily(address);
@@ -138,9 +40,23 @@ Endpoint::Endpoint(std::string ip_address, uint16_t port) : address(std::move(ip
   family = ip_family;
 }
 
-// NOLINTNEXTLINE
-Endpoint::Endpoint(needs_resolving_t, std::string hostname, uint16_t port)
-    : address(std::move(hostname)), port(port), family{GetIpFamily(address)} {}
+std::string Endpoint::SocketAddress() const { return fmt::format("{}:{}", address, port); }
+
+Endpoint::IpFamily Endpoint::GetIpFamily(std::string_view address) {
+  // Ensure null-terminated
+  auto const tmp = std::string(address);
+  in_addr addr4;
+  in6_addr addr6;
+  int ipv4_result = inet_pton(AF_INET, tmp.c_str(), &addr4);
+  int ipv6_result = inet_pton(AF_INET6, tmp.c_str(), &addr6);
+  if (ipv4_result == 1) {
+    return IpFamily::IP4;
+  }
+  if (ipv6_result == 1) {
+    return IpFamily::IP6;
+  }
+  return IpFamily::NONE;
+}
 
 std::ostream &operator<<(std::ostream &os, const Endpoint &endpoint) {
   // no need to cover the IpFamily::NONE case, as you can't even construct an
@@ -153,35 +69,73 @@ std::ostream &operator<<(std::ostream &os, const Endpoint &endpoint) {
   return os << endpoint.address << ":" << endpoint.port;
 }
 
-bool Endpoint::IsResolvableAddress(const std::string &address, uint16_t port) {
+// NOTE: Intentional copy to ensure null-terminated string
+bool Endpoint::IsResolvableAddress(std::string_view address, uint16_t port) {
   addrinfo hints{
       .ai_flags = AI_PASSIVE,
       .ai_family = AF_UNSPEC,     // IPv4 and IPv6
       .ai_socktype = SOCK_STREAM  // TCP socket
   };
   addrinfo *info = nullptr;
-  auto status = getaddrinfo(address.c_str(), std::to_string(port).c_str(), &hints, &info);
+  auto status = getaddrinfo(std::string(address).c_str(), std::to_string(port).c_str(), &hints, &info);
   if (info) freeaddrinfo(info);
   return status == 0;
 }
 
-std::optional<std::pair<std::string, uint16_t>> Endpoint::ParseSocketOrAddress(
-    const std::string &address, const std::optional<uint16_t> default_port) {
-  const std::string delimiter = ":";
-  std::vector<std::string> parts = utils::Split(address, delimiter);
-  if (parts.size() == 1) {
-    if (GetIpFamily(address) == IpFamily::NONE) {
-      return ParseHostname(address, default_port);
-    }
-    return ParseSocketOrIpAddress(address, default_port);
+std::optional<ParsedAddress> Endpoint::ParseSocketOrAddress(std::string_view address,
+                                                            std::optional<uint16_t> default_port) {
+  auto const parts = utils::SplitView(address, delimiter);
+
+  if (parts.size() > 2) {
+    return std::nullopt;
   }
-  if (parts.size() == 2) {
-    if (GetIpFamily(parts[0]) == IpFamily::NONE) {
-      return ParseHostname(address, default_port);
+
+  auto const port = [default_port, &parts]() -> std::optional<uint16_t> {
+    if (parts.size() == 2) {
+      return static_cast<uint16_t>(utils::ParseInt(parts[1]));
     }
-    return ParseSocketOrIpAddress(address, default_port);
+    return default_port;
+  }();
+
+  if (!ValidatePort(port)) {
+    return std::nullopt;
   }
-  return std::nullopt;
+
+  auto const addr = [address, &parts]() {
+    if (parts.size() == 2) {
+      return parts[0];
+    }
+    return address;
+  }();
+
+  if (GetIpFamily(addr) == IpFamily::NONE) {
+    if (IsResolvableAddress(addr, *port)) {  // NOLINT
+      return std::pair{addr, *port};         // NOLINT
+    }
+    return std::nullopt;
+  }
+
+  return std::pair{addr, *port};  // NOLINT
+}
+
+auto Endpoint::ValidatePort(std::optional<uint16_t> port) -> bool {
+  if (!port) {
+    return false;
+  }
+
+  if (port < 0) {
+    spdlog::error(utils::MessageWithLink("Invalid port number {}. The port number must be a positive integer.", *port,
+                                         "https://memgr.ph/ports"));
+    return false;
+  }
+
+  if (port > std::numeric_limits<uint16_t>::max()) {
+    spdlog::error(utils::MessageWithLink("Invalid port number. The port number exceedes the maximum possible size.",
+                                         "https://memgr.ph/ports"));
+    return false;
+  }
+
+  return true;
 }
 
 }  // namespace memgraph::io::network
diff --git a/src/io/network/endpoint.hpp b/src/io/network/endpoint.hpp
index 16d70e080..f46d28ace 100644
--- a/src/io/network/endpoint.hpp
+++ b/src/io/network/endpoint.hpp
@@ -19,11 +19,8 @@
 
 namespace memgraph::io::network {
 
-/**
- * This class represents a network endpoint that is used in Socket.
- * It is used when connecting to an address and to get the current
- * connection address.
- */
+using ParsedAddress = std::pair<std::string_view, uint16_t>;
+
 struct Endpoint {
   static const struct needs_resolving_t {
   } needs_resolving;
@@ -31,59 +28,35 @@ struct Endpoint {
   Endpoint() = default;
   Endpoint(std::string ip_address, uint16_t port);
   Endpoint(needs_resolving_t, std::string hostname, uint16_t port);
+
   Endpoint(Endpoint const &) = default;
   Endpoint(Endpoint &&) noexcept = default;
+
   Endpoint &operator=(Endpoint const &) = default;
   Endpoint &operator=(Endpoint &&) noexcept = default;
+
   ~Endpoint() = default;
 
   enum class IpFamily : std::uint8_t { NONE, IP4, IP6 };
 
-  std::string SocketAddress() const;
+  static std::optional<ParsedAddress> ParseSocketOrAddress(std::string_view address,
+                                                           std::optional<uint16_t> default_port = {});
 
-  bool operator==(const Endpoint &other) const = default;
-  friend std::ostream &operator<<(std::ostream &os, const Endpoint &endpoint);
+  std::string SocketAddress() const;
 
   std::string address;
   uint16_t port{0};
   IpFamily family{IpFamily::NONE};
 
-  static std::optional<std::pair<std::string, uint16_t>> ParseSocketOrAddress(const std::string &address,
-                                                                              std::optional<uint16_t> default_port);
+  bool operator==(const Endpoint &other) const = default;
+  friend std::ostream &operator<<(std::ostream &os, const Endpoint &endpoint);
 
-  /**
-   * Tries to parse the given string as either a socket address or ip address.
-   * Expected address format:
-   *   - "ip_address:port_number"
-   *   - "ip_address"
-   * We parse the address first. If it's an IP address, a default port must
-   * be given, or we return nullopt. If it's a socket address, we try to parse
-   * it into an ip address and a port number; even if a default port is given,
-   * it won't be used, as we expect that it is given in the address string.
-   */
-  static std::optional<std::pair<std::string, uint16_t>> ParseSocketOrIpAddress(
-      const std::string &address, std::optional<uint16_t> default_port = {});
+ private:
+  static IpFamily GetIpFamily(std::string_view address);
 
-  /**
-   * Tries to parse given string as either socket address or hostname.
-   * Expected address format:
-   *    - "hostname:port_number"
-   *    - "hostname"
-   * After we parse hostname and port we try to resolve the hostname into an ip_address.
-   */
-  static std::optional<std::pair<std::string, uint16_t>> ParseHostname(const std::string &address,
-                                                                       std::optional<uint16_t> default_port);
+  static bool IsResolvableAddress(std::string_view address, uint16_t port);
 
-  static IpFamily GetIpFamily(const std::string &address);
-
-  static bool IsResolvableAddress(const std::string &address, uint16_t port);
-
-  /**
-   * Tries to resolve hostname to its corresponding IP address.
-   * Given a DNS hostname, this function performs resolution and returns
-   * the IP address associated with the hostname.
-   */
-  static std::string ResolveHostnameIntoIpAddress(const std::string &address, uint16_t port);
+  static auto ValidatePort(std::optional<uint16_t> port) -> bool;
 };
 
 }  // namespace memgraph::io::network
diff --git a/src/memgraph.cpp b/src/memgraph.cpp
index 34d64f434..d896bcc4c 100644
--- a/src/memgraph.cpp
+++ b/src/memgraph.cpp
@@ -334,7 +334,8 @@ int main(int argc, char **argv) {
       .salient.items = {.properties_on_edges = FLAGS_storage_properties_on_edges,
                         .enable_schema_metadata = FLAGS_storage_enable_schema_metadata},
       .salient.storage_mode = memgraph::flags::ParseStorageMode()};
-
+  spdlog::info("config recover on startup {}, flags {} {}", db_config.durability.recover_on_startup,
+               FLAGS_storage_recover_on_startup, FLAGS_data_recovery_on_startup);
   memgraph::utils::Scheduler jemalloc_purge_scheduler;
   jemalloc_purge_scheduler.Run("Jemalloc purge", std::chrono::seconds(FLAGS_storage_gc_cycle_sec),
                                [] { memgraph::memory::PurgeUnusedMemory(); });
diff --git a/src/memory/global_memory_control.cpp b/src/memory/global_memory_control.cpp
index bcf12bd2c..6073f9d9a 100644
--- a/src/memory/global_memory_control.cpp
+++ b/src/memory/global_memory_control.cpp
@@ -122,11 +122,11 @@ static bool my_commit(extent_hooks_t *extent_hooks, void *addr, size_t size, siz
 
   [[maybe_unused]] auto blocker = memgraph::utils::MemoryTracker::OutOfMemoryExceptionBlocker{};
   if (GetQueriesMemoryControl().IsThreadTracked()) [[unlikely]] {
-    bool ok = GetQueriesMemoryControl().TrackAllocOnCurrentThread(length);
+    [[maybe_unused]] bool ok = GetQueriesMemoryControl().TrackAllocOnCurrentThread(length);
     DMG_ASSERT(ok);
   }
 
-  auto ok = memgraph::utils::total_memory_tracker.Alloc(static_cast<int64_t>(length));
+  [[maybe_unused]] auto ok = memgraph::utils::total_memory_tracker.Alloc(static_cast<int64_t>(length));
   DMG_ASSERT(ok);
 
   return false;
diff --git a/src/mg_import_csv.cpp b/src/mg_import_csv.cpp
index 2d77c2db2..75a7c3bbb 100644
--- a/src/mg_import_csv.cpp
+++ b/src/mg_import_csv.cpp
@@ -416,7 +416,7 @@ memgraph::storage::PropertyValue StringToValue(const std::string &str, const std
 std::string GetIdSpace(const std::string &type) {
   // The format of this field is as follows:
   // [START_|END_]ID[(<id_space>)]
-  std::regex format(R"(^(START_|END_)?ID(\(([^\(\)]+)\))?$)", std::regex::extended);
+  static std::regex format(R"(^(START_|END_)?ID(\(([^\(\)]+)\))?$)", std::regex::extended);
   std::smatch res;
   if (!std::regex_match(type, res, format))
     throw LoadException(
diff --git a/src/query/db_accessor.hpp b/src/query/db_accessor.hpp
index e10102ee5..915ea9936 100644
--- a/src/query/db_accessor.hpp
+++ b/src/query/db_accessor.hpp
@@ -371,6 +371,62 @@ class VerticesIterable final {
   }
 };
 
+class EdgesIterable final {
+  std::variant<storage::EdgesIterable, std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                                          utils::Allocator<EdgeAccessor>> *>
+      iterable_;
+
+ public:
+  class Iterator final {
+    std::variant<storage::EdgesIterable::Iterator,
+                 std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                    utils::Allocator<EdgeAccessor>>::iterator>
+        it_;
+
+   public:
+    explicit Iterator(storage::EdgesIterable::Iterator it) : it_(std::move(it)) {}
+    explicit Iterator(std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                         utils::Allocator<EdgeAccessor>>::iterator it)
+        : it_(it) {}
+
+    EdgeAccessor operator*() const {
+      return std::visit([](auto &it_) { return EdgeAccessor(*it_); }, it_);
+    }
+
+    Iterator &operator++() {
+      std::visit([](auto &it_) { ++it_; }, it_);
+      return *this;
+    }
+
+    bool operator==(const Iterator &other) const { return it_ == other.it_; }
+
+    bool operator!=(const Iterator &other) const { return !(other == *this); }
+  };
+
+  explicit EdgesIterable(storage::EdgesIterable iterable) : iterable_(std::move(iterable)) {}
+  explicit EdgesIterable(std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                            utils::Allocator<EdgeAccessor>> *edges)
+      : iterable_(edges) {}
+
+  Iterator begin() {
+    return std::visit(
+        memgraph::utils::Overloaded{
+            [](storage::EdgesIterable &iterable_) { return Iterator(iterable_.begin()); },
+            [](std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                  utils::Allocator<EdgeAccessor>> *iterable_) { return Iterator(iterable_->begin()); }},
+        iterable_);
+  }
+
+  Iterator end() {
+    return std::visit(
+        memgraph::utils::Overloaded{
+            [](storage::EdgesIterable &iterable_) { return Iterator(iterable_.end()); },
+            [](std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                  utils::Allocator<EdgeAccessor>> *iterable_) { return Iterator(iterable_->end()); }},
+        iterable_);
+  }
+};
+
 class DbAccessor final {
   storage::Storage::Accessor *accessor_;
 
@@ -416,6 +472,10 @@ class DbAccessor final {
     return VerticesIterable(accessor_->Vertices(label, property, lower, upper, view));
   }
 
+  EdgesIterable Edges(storage::View view, storage::EdgeTypeId edge_type) {
+    return EdgesIterable(accessor_->Edges(edge_type, view));
+  }
+
   VertexAccessor InsertVertex() { return VertexAccessor(accessor_->CreateVertex()); }
 
   storage::Result<EdgeAccessor> InsertEdge(VertexAccessor *from, VertexAccessor *to,
@@ -572,6 +632,8 @@ class DbAccessor final {
     return accessor_->LabelPropertyIndexExists(label, prop);
   }
 
+  bool EdgeTypeIndexExists(storage::EdgeTypeId edge_type) const { return accessor_->EdgeTypeIndexExists(edge_type); }
+
   std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId &label) const {
     return accessor_->GetIndexStats(label);
   }
@@ -638,6 +700,10 @@ class DbAccessor final {
     return accessor_->CreateIndex(label, property);
   }
 
+  utils::BasicResult<storage::StorageIndexDefinitionError, void> CreateIndex(storage::EdgeTypeId edge_type) {
+    return accessor_->CreateIndex(edge_type);
+  }
+
   utils::BasicResult<storage::StorageIndexDefinitionError, void> DropIndex(storage::LabelId label) {
     return accessor_->DropIndex(label);
   }
@@ -647,6 +713,10 @@ class DbAccessor final {
     return accessor_->DropIndex(label, property);
   }
 
+  utils::BasicResult<storage::StorageIndexDefinitionError, void> DropIndex(storage::EdgeTypeId edge_type) {
+    return accessor_->DropIndex(edge_type);
+  }
+
   utils::BasicResult<storage::StorageExistenceConstraintDefinitionError, void> CreateExistenceConstraint(
       storage::LabelId label, storage::PropertyId property) {
     return accessor_->CreateExistenceConstraint(label, property);
diff --git a/src/query/dump.cpp b/src/query/dump.cpp
index 2925023fb..f1dd08c8d 100644
--- a/src/query/dump.cpp
+++ b/src/query/dump.cpp
@@ -242,6 +242,10 @@ void DumpLabelIndex(std::ostream *os, query::DbAccessor *dba, const storage::Lab
   *os << "CREATE INDEX ON :" << EscapeName(dba->LabelToName(label)) << ";";
 }
 
+void DumpEdgeTypeIndex(std::ostream *os, query::DbAccessor *dba, const storage::EdgeTypeId edge_type) {
+  *os << "CREATE EDGE INDEX ON :" << EscapeName(dba->EdgeTypeToName(edge_type)) << ";";
+}
+
 void DumpLabelPropertyIndex(std::ostream *os, query::DbAccessor *dba, storage::LabelId label,
                             storage::PropertyId property) {
   *os << "CREATE INDEX ON :" << EscapeName(dba->LabelToName(label)) << "(" << EscapeName(dba->PropertyToName(property))
@@ -297,7 +301,9 @@ PullPlanDump::PullPlanDump(DbAccessor *dba, dbms::DatabaseAccess db_acc)
                    // Internal index cleanup
                    CreateInternalIndexCleanupPullChunk(),
                    // Dump all triggers
-                   CreateTriggersPullChunk()} {}
+                   CreateTriggersPullChunk(),
+                   // Dump all edge-type indices
+                   CreateEdgeTypeIndicesPullChunk()} {}
 
 bool PullPlanDump::Pull(AnyStream *stream, std::optional<int> n) {
   // Iterate all functions that stream some results.
@@ -352,6 +358,33 @@ PullPlanDump::PullChunk PullPlanDump::CreateLabelIndicesPullChunk() {
   };
 }
 
+PullPlanDump::PullChunk PullPlanDump::CreateEdgeTypeIndicesPullChunk() {
+  // Dump all label indices
+  return [this, global_index = 0U](AnyStream *stream, std::optional<int> n) mutable -> std::optional<size_t> {
+    // Delay the construction of indices vectors
+    if (!indices_info_) {
+      indices_info_.emplace(dba_->ListAllIndices());
+    }
+    const auto &edge_type = indices_info_->edge_type;
+
+    size_t local_counter = 0;
+    while (global_index < edge_type.size() && (!n || local_counter < *n)) {
+      std::ostringstream os;
+      DumpEdgeTypeIndex(&os, dba_, edge_type[global_index]);
+      stream->Result({TypedValue(os.str())});
+
+      ++global_index;
+      ++local_counter;
+    }
+
+    if (global_index == edge_type.size()) {
+      return local_counter;
+    }
+
+    return std::nullopt;
+  };
+}
+
 PullPlanDump::PullChunk PullPlanDump::CreateLabelPropertyIndicesPullChunk() {
   return [this, global_index = 0U](AnyStream *stream, std::optional<int> n) mutable -> std::optional<size_t> {
     // Delay the construction of indices vectors
diff --git a/src/query/dump.hpp b/src/query/dump.hpp
index a9d68d45c..05bd42967 100644
--- a/src/query/dump.hpp
+++ b/src/query/dump.hpp
@@ -63,5 +63,6 @@ struct PullPlanDump {
   PullChunk CreateDropInternalIndexPullChunk();
   PullChunk CreateInternalIndexCleanupPullChunk();
   PullChunk CreateTriggersPullChunk();
+  PullChunk CreateEdgeTypeIndicesPullChunk();
 };
 }  // namespace memgraph::query
diff --git a/src/query/frontend/ast/ast.cpp b/src/query/frontend/ast/ast.cpp
index 2c6a9ea8d..6847c32e6 100644
--- a/src/query/frontend/ast/ast.cpp
+++ b/src/query/frontend/ast/ast.cpp
@@ -186,6 +186,9 @@ constexpr utils::TypeInfo query::ProfileQuery::kType{utils::TypeId::AST_PROFILE_
 
 constexpr utils::TypeInfo query::IndexQuery::kType{utils::TypeId::AST_INDEX_QUERY, "IndexQuery", &query::Query::kType};
 
+constexpr utils::TypeInfo query::EdgeIndexQuery::kType{utils::TypeId::AST_EDGE_INDEX_QUERY, "EdgeIndexQuery",
+                                                       &query::Query::kType};
+
 constexpr utils::TypeInfo query::Create::kType{utils::TypeId::AST_CREATE, "Create", &query::Clause::kType};
 
 constexpr utils::TypeInfo query::CallProcedure::kType{utils::TypeId::AST_CALL_PROCEDURE, "CallProcedure",
diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp
index 65f1b58ef..afa417788 100644
--- a/src/query/frontend/ast/ast.hpp
+++ b/src/query/frontend/ast/ast.hpp
@@ -21,6 +21,7 @@
 #include "query/interpret/awesome_memgraph_functions.hpp"
 #include "query/typed_value.hpp"
 #include "storage/v2/property_value.hpp"
+#include "utils/exceptions.hpp"
 #include "utils/typeinfo.hpp"
 
 namespace memgraph::query {
@@ -2223,6 +2224,34 @@ class IndexQuery : public memgraph::query::Query {
   friend class AstStorage;
 };
 
+class EdgeIndexQuery : public memgraph::query::Query {
+ public:
+  static const utils::TypeInfo kType;
+  const utils::TypeInfo &GetTypeInfo() const override { return kType; }
+
+  enum class Action { CREATE, DROP };
+
+  EdgeIndexQuery() = default;
+
+  DEFVISITABLE(QueryVisitor<void>);
+
+  memgraph::query::EdgeIndexQuery::Action action_;
+  memgraph::query::EdgeTypeIx edge_type_;
+
+  EdgeIndexQuery *Clone(AstStorage *storage) const override {
+    EdgeIndexQuery *object = storage->Create<EdgeIndexQuery>();
+    object->action_ = action_;
+    object->edge_type_ = storage->GetEdgeTypeIx(edge_type_.name);
+    return object;
+  }
+
+ protected:
+  EdgeIndexQuery(Action action, EdgeTypeIx edge_type) : action_(action), edge_type_(edge_type) {}
+
+ private:
+  friend class AstStorage;
+};
+
 class Create : public memgraph::query::Clause {
  public:
   static const utils::TypeInfo kType;
@@ -3604,7 +3633,7 @@ class PatternComprehension : public memgraph::query::Expression {
   bool Accept(HierarchicalTreeVisitor &visitor) override {
     if (visitor.PreVisit(*this)) {
       if (variable_) {
-        variable_->Accept(visitor);
+        throw utils::NotYetImplemented("Variable in pattern comprehension.");
       }
       pattern_->Accept(visitor);
       if (filter_) {
@@ -3633,7 +3662,8 @@ class PatternComprehension : public memgraph::query::Expression {
   int32_t symbol_pos_{-1};
 
   PatternComprehension *Clone(AstStorage *storage) const override {
-    PatternComprehension *object = storage->Create<PatternComprehension>();
+    auto *object = storage->Create<PatternComprehension>();
+    object->variable_ = variable_ ? variable_->Clone(storage) : nullptr;
     object->pattern_ = pattern_ ? pattern_->Clone(storage) : nullptr;
     object->filter_ = filter_ ? filter_->Clone(storage) : nullptr;
     object->resultExpr_ = resultExpr_ ? resultExpr_->Clone(storage) : nullptr;
@@ -3643,7 +3673,8 @@ class PatternComprehension : public memgraph::query::Expression {
   }
 
  protected:
-  PatternComprehension(Identifier *variable, Pattern *pattern) : variable_(variable), pattern_(pattern) {}
+  PatternComprehension(Identifier *variable, Pattern *pattern, Where *filter, Expression *resultExpr)
+      : variable_(variable), pattern_(pattern), filter_(filter), resultExpr_(resultExpr) {}
 
  private:
   friend class AstStorage;
diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp
index 9904bbbee..e1c302695 100644
--- a/src/query/frontend/ast/ast_visitor.hpp
+++ b/src/query/frontend/ast/ast_visitor.hpp
@@ -82,6 +82,7 @@ class AuthQuery;
 class ExplainQuery;
 class ProfileQuery;
 class IndexQuery;
+class EdgeIndexQuery;
 class DatabaseInfoQuery;
 class SystemInfoQuery;
 class ConstraintQuery;
@@ -144,11 +145,11 @@ class ExpressionVisitor
 
 template <class TResult>
 class QueryVisitor
-    : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery, DatabaseInfoQuery,
-                            SystemInfoQuery, ConstraintQuery, DumpQuery, ReplicationQuery, LockPathQuery,
-                            FreeMemoryQuery, TriggerQuery, IsolationLevelQuery, CreateSnapshotQuery, StreamQuery,
-                            SettingQuery, VersionQuery, ShowConfigQuery, TransactionQueueQuery, StorageModeQuery,
-                            AnalyzeGraphQuery, MultiDatabaseQuery, ShowDatabasesQuery, EdgeImportModeQuery,
-                            CoordinatorQuery, DropGraphQuery> {};
+    : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, EdgeIndexQuery, AuthQuery,
+                            DatabaseInfoQuery, SystemInfoQuery, ConstraintQuery, DumpQuery, ReplicationQuery,
+                            LockPathQuery, FreeMemoryQuery, TriggerQuery, IsolationLevelQuery, CreateSnapshotQuery,
+                            StreamQuery, SettingQuery, VersionQuery, ShowConfigQuery, TransactionQueueQuery,
+                            StorageModeQuery, AnalyzeGraphQuery, MultiDatabaseQuery, ShowDatabasesQuery,
+                            EdgeImportModeQuery, CoordinatorQuery, DropGraphQuery> {};
 
 }  // namespace memgraph::query
diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp
index e8425a8ed..2f9199482 100644
--- a/src/query/frontend/ast/cypher_main_visitor.cpp
+++ b/src/query/frontend/ast/cypher_main_visitor.cpp
@@ -265,6 +265,27 @@ antlrcpp::Any CypherMainVisitor::visitDropIndex(MemgraphCypher::DropIndexContext
   return index_query;
 }
 
+antlrcpp::Any CypherMainVisitor::visitEdgeIndexQuery(MemgraphCypher::EdgeIndexQueryContext *ctx) {
+  MG_ASSERT(ctx->children.size() == 1, "EdgeIndexQuery should have exactly one child!");
+  auto *index_query = std::any_cast<EdgeIndexQuery *>(ctx->children[0]->accept(this));
+  query_ = index_query;
+  return index_query;
+}
+
+antlrcpp::Any CypherMainVisitor::visitCreateEdgeIndex(MemgraphCypher::CreateEdgeIndexContext *ctx) {
+  auto *index_query = storage_->Create<EdgeIndexQuery>();
+  index_query->action_ = EdgeIndexQuery::Action::CREATE;
+  index_query->edge_type_ = AddEdgeType(std::any_cast<std::string>(ctx->labelName()->accept(this)));
+  return index_query;
+}
+
+antlrcpp::Any CypherMainVisitor::visitDropEdgeIndex(MemgraphCypher::DropEdgeIndexContext *ctx) {
+  auto *index_query = storage_->Create<EdgeIndexQuery>();
+  index_query->action_ = EdgeIndexQuery::Action::DROP;
+  index_query->edge_type_ = AddEdgeType(std::any_cast<std::string>(ctx->labelName()->accept(this)));
+  return index_query;
+}
+
 antlrcpp::Any CypherMainVisitor::visitAuthQuery(MemgraphCypher::AuthQueryContext *ctx) {
   MG_ASSERT(ctx->children.size() == 1, "AuthQuery should have exactly one child!");
   auto *auth_query = std::any_cast<AuthQuery *>(ctx->children[0]->accept(this));
diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp
index d627c0d08..ed776d74c 100644
--- a/src/query/frontend/ast/cypher_main_visitor.hpp
+++ b/src/query/frontend/ast/cypher_main_visitor.hpp
@@ -148,6 +148,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
    */
   antlrcpp::Any visitIndexQuery(MemgraphCypher::IndexQueryContext *ctx) override;
 
+  /**
+   * @return IndexQuery*
+   */
+  antlrcpp::Any visitEdgeIndexQuery(MemgraphCypher::EdgeIndexQueryContext *ctx) override;
+
   /**
    * @return ExplainQuery*
    */
@@ -504,6 +509,16 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
    */
   antlrcpp::Any visitDropIndex(MemgraphCypher::DropIndexContext *ctx) override;
 
+  /**
+   * @return EdgeIndexQuery*
+   */
+  antlrcpp::Any visitCreateEdgeIndex(MemgraphCypher::CreateEdgeIndexContext *ctx) override;
+
+  /**
+   * @return DropEdgeIndex*
+   */
+  antlrcpp::Any visitDropEdgeIndex(MemgraphCypher::DropEdgeIndexContext *ctx) override;
+
   /**
    * @return AuthQuery*
    */
diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
index 769f75b6c..8cad56fb6 100644
--- a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
+++ b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
@@ -133,6 +133,7 @@ symbolicName : UnescapedSymbolicName
 
 query : cypherQuery
       | indexQuery
+      | edgeIndexQuery
       | explainQuery
       | profileQuery
       | databaseInfoQuery
@@ -529,4 +530,10 @@ showDatabases : SHOW DATABASES ;
 
 edgeImportModeQuery : EDGE IMPORT MODE ( ACTIVE | INACTIVE ) ;
 
+createEdgeIndex : CREATE EDGE INDEX ON ':' labelName ;
+
+dropEdgeIndex : DROP EDGE INDEX ON ':' labelName ;
+
+edgeIndexQuery : createEdgeIndex | dropEdgeIndex ;
+
 dropGraphQuery : DROP GRAPH ;
diff --git a/src/query/frontend/semantic/required_privileges.cpp b/src/query/frontend/semantic/required_privileges.cpp
index c015240ab..06a840f81 100644
--- a/src/query/frontend/semantic/required_privileges.cpp
+++ b/src/query/frontend/semantic/required_privileges.cpp
@@ -27,6 +27,8 @@ class PrivilegeExtractor : public QueryVisitor<void>, public HierarchicalTreeVis
 
   void Visit(IndexQuery & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::INDEX); }
 
+  void Visit(EdgeIndexQuery & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::INDEX); }
+
   void Visit(AnalyzeGraphQuery & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::INDEX); }
 
   void Visit(AuthQuery & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::AUTH); }
diff --git a/src/query/frontend/semantic/symbol.hpp b/src/query/frontend/semantic/symbol.hpp
index 77557b6fe..1a5aa2756 100644
--- a/src/query/frontend/semantic/symbol.hpp
+++ b/src/query/frontend/semantic/symbol.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -53,6 +53,8 @@ class Symbol {
   bool user_declared() const { return user_declared_; }
   int token_position() const { return token_position_; }
 
+  bool IsSymbolAnonym() const { return name_.substr(0U, 4U) == "anon"; }
+
   std::string name_;
   int64_t position_;
   bool user_declared_{true};
diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp
index e8ef3cba5..2cfbee584 100644
--- a/src/query/frontend/semantic/symbol_generator.cpp
+++ b/src/query/frontend/semantic/symbol_generator.cpp
@@ -721,6 +721,32 @@ bool SymbolGenerator::PostVisit(EdgeAtom &) {
   return true;
 }
 
+bool SymbolGenerator::PreVisit(PatternComprehension &pc) {
+  auto &scope = scopes_.back();
+
+  if (scope.in_set_property) {
+    throw utils::NotYetImplemented("Pattern Comprehension cannot be used within SET clause.!");
+  }
+
+  if (scope.in_with) {
+    throw utils::NotYetImplemented("Pattern Comprehension cannot be used within WITH!");
+  }
+
+  if (scope.in_reduce) {
+    throw utils::NotYetImplemented("Pattern Comprehension cannot be used within REDUCE!");
+  }
+
+  if (scope.num_if_operators) {
+    throw utils::NotYetImplemented("IF operator cannot be used with Pattern Comprehension!");
+  }
+
+  const auto &symbol = CreateAnonymousSymbol();
+  pc.MapTo(symbol);
+  return true;
+}
+
+bool SymbolGenerator::PostVisit(PatternComprehension & /*pc*/) { return true; }
+
 void SymbolGenerator::VisitWithIdentifiers(Expression *expr, const std::vector<Identifier *> &identifiers) {
   auto &scope = scopes_.back();
   std::vector<std::pair<std::optional<Symbol>, Identifier *>> prev_symbols;
diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp
index f9e6468f6..e5b46fbfe 100644
--- a/src/query/frontend/semantic/symbol_generator.hpp
+++ b/src/query/frontend/semantic/symbol_generator.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -97,6 +97,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
   bool PostVisit(NodeAtom &) override;
   bool PreVisit(EdgeAtom &) override;
   bool PostVisit(EdgeAtom &) override;
+  bool PreVisit(PatternComprehension &) override;
+  bool PostVisit(PatternComprehension &) override;
 
  private:
   // Scope stores the state of where we are when visiting the AST and a map of
diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp
index 3aebade6e..8db13c26f 100644
--- a/src/query/interpreter.cpp
+++ b/src/query/interpreter.cpp
@@ -355,7 +355,7 @@ class ReplQueryHandler {
       const auto replication_config =
           replication::ReplicationClientConfig{.name = name,
                                                .mode = repl_mode,
-                                               .ip_address = ip,
+                                               .ip_address = std::string(ip),
                                                .port = port,
                                                .replica_check_frequency = replica_check_frequency,
                                                .ssl = std::nullopt};
@@ -410,7 +410,7 @@ class CoordQueryHandler final : public query::CoordinatorQueryHandler {
 
       : coordinator_handler_(coordinator_state) {}
 
-  void UnregisterInstance(std::string const &instance_name) override {
+  void UnregisterInstance(std::string_view instance_name) override {
     auto status = coordinator_handler_.UnregisterReplicationInstance(instance_name);
     switch (status) {
       using enum memgraph::coordination::UnregisterInstanceCoordinatorStatus;
@@ -423,6 +423,8 @@ class CoordQueryHandler final : public query::CoordinatorQueryHandler {
         throw QueryRuntimeException("UNREGISTER INSTANCE query can only be run on a coordinator!");
       case NOT_LEADER:
         throw QueryRuntimeException("Couldn't unregister replica instance since coordinator is not a leader!");
+      case RAFT_LOG_ERROR:
+        throw QueryRuntimeException("Couldn't unregister replica instance since raft server couldn't append the log!");
       case RPC_FAILED:
         throw QueryRuntimeException(
             "Couldn't unregister replica instance because current main instance couldn't unregister replica!");
@@ -431,20 +433,18 @@ class CoordQueryHandler final : public query::CoordinatorQueryHandler {
     }
   }
 
-  void RegisterReplicationInstance(std::string const &coordinator_socket_address,
-                                   std::string const &replication_socket_address,
+  void RegisterReplicationInstance(std::string_view coordinator_socket_address,
+                                   std::string_view replication_socket_address,
                                    std::chrono::seconds const &instance_check_frequency,
                                    std::chrono::seconds const &instance_down_timeout,
                                    std::chrono::seconds const &instance_get_uuid_frequency,
-                                   std::string const &instance_name, CoordinatorQuery::SyncMode sync_mode) override {
-    const auto maybe_replication_ip_port =
-        io::network::Endpoint::ParseSocketOrAddress(replication_socket_address, std::nullopt);
+                                   std::string_view instance_name, CoordinatorQuery::SyncMode sync_mode) override {
+    const auto maybe_replication_ip_port = io::network::Endpoint::ParseSocketOrAddress(replication_socket_address);
     if (!maybe_replication_ip_port) {
       throw QueryRuntimeException("Invalid replication socket address!");
     }
 
-    const auto maybe_coordinator_ip_port =
-        io::network::Endpoint::ParseSocketOrAddress(coordinator_socket_address, std::nullopt);
+    const auto maybe_coordinator_ip_port = io::network::Endpoint::ParseSocketOrAddress(coordinator_socket_address);
     if (!maybe_replication_ip_port) {
       throw QueryRuntimeException("Invalid replication socket address!");
     }
@@ -452,14 +452,14 @@ class CoordQueryHandler final : public query::CoordinatorQueryHandler {
     const auto [replication_ip, replication_port] = *maybe_replication_ip_port;
     const auto [coordinator_server_ip, coordinator_server_port] = *maybe_coordinator_ip_port;
     const auto repl_config = coordination::CoordinatorClientConfig::ReplicationClientInfo{
-        .instance_name = instance_name,
+        .instance_name = std::string(instance_name),
         .replication_mode = convertFromCoordinatorToReplicationMode(sync_mode),
-        .replication_ip_address = replication_ip,
+        .replication_ip_address = std::string(replication_ip),
         .replication_port = replication_port};
 
     auto coordinator_client_config =
-        coordination::CoordinatorClientConfig{.instance_name = instance_name,
-                                              .ip_address = coordinator_server_ip,
+        coordination::CoordinatorClientConfig{.instance_name = std::string(instance_name),
+                                              .ip_address = std::string(coordinator_server_ip),
                                               .port = coordinator_server_port,
                                               .instance_health_check_frequency_sec = instance_check_frequency,
                                               .instance_down_timeout_sec = instance_down_timeout,
@@ -472,18 +472,17 @@ class CoordQueryHandler final : public query::CoordinatorQueryHandler {
       using enum memgraph::coordination::RegisterInstanceCoordinatorStatus;
       case NAME_EXISTS:
         throw QueryRuntimeException("Couldn't register replica instance since instance with such name already exists!");
-      case ENDPOINT_EXISTS:
+      case COORD_ENDPOINT_EXISTS:
         throw QueryRuntimeException(
-            "Couldn't register replica instance since instance with such endpoint already exists!");
+            "Couldn't register replica instance since instance with such coordinator endpoint already exists!");
+      case REPL_ENDPOINT_EXISTS:
+        throw QueryRuntimeException(
+            "Couldn't register replica instance since instance with such replication endpoint already exists!");
       case NOT_COORDINATOR:
         throw QueryRuntimeException("REGISTER INSTANCE query can only be run on a coordinator!");
       case NOT_LEADER:
         throw QueryRuntimeException("Couldn't register replica instance since coordinator is not a leader!");
-      case RAFT_COULD_NOT_ACCEPT:
-        throw QueryRuntimeException(
-            "Couldn't register replica instance since raft server couldn't accept the log! Most likely the raft "
-            "instance is not a leader!");
-      case RAFT_COULD_NOT_APPEND:
+      case RAFT_LOG_ERROR:
         throw QueryRuntimeException("Couldn't register replica instance since raft server couldn't append the log!");
       case RPC_FAILED:
         throw QueryRuntimeException(
@@ -494,19 +493,19 @@ class CoordQueryHandler final : public query::CoordinatorQueryHandler {
     }
   }
 
-  auto AddCoordinatorInstance(uint32_t raft_server_id, std::string const &raft_socket_address) -> void override {
-    auto const maybe_ip_and_port = io::network::Endpoint::ParseSocketOrIpAddress(raft_socket_address);
+  auto AddCoordinatorInstance(uint32_t raft_server_id, std::string_view raft_socket_address) -> void override {
+    auto const maybe_ip_and_port = io::network::Endpoint::ParseSocketOrAddress(raft_socket_address);
     if (maybe_ip_and_port) {
       auto const [ip, port] = *maybe_ip_and_port;
-      spdlog::info("Adding instance {} with raft socket address {}:{}.", raft_server_id, port, ip);
+      spdlog::info("Adding instance {} with raft socket address {}:{}.", raft_server_id, ip, port);
       coordinator_handler_.AddCoordinatorInstance(raft_server_id, port, ip);
     } else {
       spdlog::error("Invalid raft socket address {}.", raft_socket_address);
     }
   }
 
-  void SetReplicationInstanceToMain(const std::string &instance_name) override {
-    auto status = coordinator_handler_.SetReplicationInstanceToMain(instance_name);
+  void SetReplicationInstanceToMain(std::string_view instance_name) override {
+    auto const status = coordinator_handler_.SetReplicationInstanceToMain(instance_name);
     switch (status) {
       using enum memgraph::coordination::SetInstanceToMainCoordinatorStatus;
       case NO_INSTANCE_WITH_NAME:
@@ -515,6 +514,10 @@ class CoordQueryHandler final : public query::CoordinatorQueryHandler {
         throw QueryRuntimeException("Couldn't set instance to main since there is already a main instance in cluster!");
       case NOT_COORDINATOR:
         throw QueryRuntimeException("SET INSTANCE TO MAIN query can only be run on a coordinator!");
+      case NOT_LEADER:
+        throw QueryRuntimeException("Couldn't set instance to main since coordinator is not a leader!");
+      case RAFT_LOG_ERROR:
+        throw QueryRuntimeException("Couldn't promote instance since raft server couldn't append the log!");
       case COULD_NOT_PROMOTE_TO_MAIN:
         throw QueryRuntimeException(
             "Couldn't set replica instance to main! Check coordinator and replica for more logs");
@@ -1209,7 +1212,7 @@ Callback HandleCoordinatorQuery(CoordinatorQuery *coordinator_query, const Param
       };
 
       notifications->emplace_back(
-          SeverityLevel::INFO, NotificationCode::REGISTER_COORDINATOR_SERVER,
+          SeverityLevel::INFO, NotificationCode::REGISTER_REPLICATION_INSTANCE,
           fmt::format("Coordinator has registered coordinator server on {} for instance {}.",
                       coordinator_socket_address_tv.ValueString(), coordinator_query->instance_name_));
       return callback;
@@ -1251,17 +1254,16 @@ Callback HandleCoordinatorQuery(CoordinatorQuery *coordinator_query, const Param
         throw QueryRuntimeException("Only coordinator can run SHOW INSTANCES.");
       }
 
-      callback.header = {"name", "raft_socket_address", "coordinator_socket_address", "alive", "role"};
+      callback.header = {"name", "raft_socket_address", "coordinator_socket_address", "health", "role"};
       callback.fn = [handler = CoordQueryHandler{*coordinator_state},
                      replica_nfields = callback.header.size()]() mutable {
         auto const instances = handler.ShowInstances();
         auto const converter = [](const auto &status) -> std::vector<TypedValue> {
           return {TypedValue{status.instance_name}, TypedValue{status.raft_socket_address},
-                  TypedValue{status.coord_socket_address}, TypedValue{status.is_alive},
-                  TypedValue{status.cluster_role}};
+                  TypedValue{status.coord_socket_address}, TypedValue{status.health}, TypedValue{status.cluster_role}};
         };
 
-        return utils::fmap(converter, instances);
+        return utils::fmap(instances, converter);
       };
       return callback;
     }
@@ -2677,6 +2679,75 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
       RWType::W};
 }
 
+PreparedQuery PrepareEdgeIndexQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
+                                    std::vector<Notification> *notifications, CurrentDB &current_db) {
+  if (in_explicit_transaction) {
+    throw IndexInMulticommandTxException();
+  }
+
+  auto *index_query = utils::Downcast<EdgeIndexQuery>(parsed_query.query);
+  std::function<void(Notification &)> handler;
+
+  MG_ASSERT(current_db.db_acc_, "Index query expects a current DB");
+  auto &db_acc = *current_db.db_acc_;
+
+  MG_ASSERT(current_db.db_transactional_accessor_, "Index query expects a current DB transaction");
+  auto *dba = &*current_db.execution_db_accessor_;
+
+  auto invalidate_plan_cache = [plan_cache = db_acc->plan_cache()] {
+    plan_cache->WithLock([&](auto &cache) { cache.reset(); });
+  };
+
+  auto *storage = db_acc->storage();
+  auto edge_type = storage->NameToEdgeType(index_query->edge_type_.name);
+
+  Notification index_notification(SeverityLevel::INFO);
+  switch (index_query->action_) {
+    case EdgeIndexQuery::Action::CREATE: {
+      index_notification.code = NotificationCode::CREATE_INDEX;
+      index_notification.title = fmt::format("Created index on edge-type {}.", index_query->edge_type_.name);
+
+      handler = [dba, edge_type, label_name = index_query->edge_type_.name,
+                 invalidate_plan_cache = std::move(invalidate_plan_cache)](Notification &index_notification) {
+        auto maybe_index_error = dba->CreateIndex(edge_type);
+        utils::OnScopeExit invalidator(invalidate_plan_cache);
+
+        if (maybe_index_error.HasError()) {
+          index_notification.code = NotificationCode::EXISTENT_INDEX;
+          index_notification.title = fmt::format("Index on edge-type {} already exists.", label_name);
+        }
+      };
+      break;
+    }
+    case EdgeIndexQuery::Action::DROP: {
+      index_notification.code = NotificationCode::DROP_INDEX;
+      index_notification.title = fmt::format("Dropped index on edge-type {}.", index_query->edge_type_.name);
+      handler = [dba, edge_type, label_name = index_query->edge_type_.name,
+                 invalidate_plan_cache = std::move(invalidate_plan_cache)](Notification &index_notification) {
+        auto maybe_index_error = dba->DropIndex(edge_type);
+        utils::OnScopeExit invalidator(invalidate_plan_cache);
+
+        if (maybe_index_error.HasError()) {
+          index_notification.code = NotificationCode::NONEXISTENT_INDEX;
+          index_notification.title = fmt::format("Index on edge-type {} doesn't exist.", label_name);
+        }
+      };
+      break;
+    }
+  }
+
+  return PreparedQuery{
+      {},
+      std::move(parsed_query.required_privileges),
+      [handler = std::move(handler), notifications, index_notification = std::move(index_notification)](
+          AnyStream * /*stream*/, std::optional<int> /*unused*/) mutable {
+        handler(index_notification);
+        notifications->push_back(index_notification);
+        return QueryHandlerResult::COMMIT;
+      },
+      RWType::W};
+}
+
 PreparedQuery PrepareAuthQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
                                InterpreterContext *interpreter_context, Interpreter &interpreter) {
   if (in_explicit_transaction) {
@@ -3196,6 +3267,21 @@ Callback DropGraph(memgraph::dbms::DatabaseAccess &db) {
       throw utils::BasicException("Drop graph can not be used without IN_MEMORY_ANALYTICAL storage mode!");
     }
     storage->DropGraph();
+
+    auto trigger_store = db->trigger_store();
+    if (trigger_store->HasTriggers()) {
+      std::vector<std::string> trigger_names;
+      for (auto const &trigger_info : trigger_store->GetTriggerInfo()) {
+        trigger_names.push_back(trigger_info.name);
+      }
+      for (auto const &trigger_name : trigger_names) {
+        trigger_store->DropTrigger(trigger_name);
+      }
+    }
+
+    auto streams = db->streams();
+    streams->DropAll();
+
     return std::vector<std::vector<TypedValue>>();
   };
 
@@ -3515,6 +3601,7 @@ PreparedQuery PrepareDatabaseInfoQuery(ParsedQuery parsed_query, bool in_explici
         auto *storage = database->storage();
         const std::string_view label_index_mark{"label"};
         const std::string_view label_property_index_mark{"label+property"};
+        const std::string_view edge_type_index_mark{"edge-type"};
         auto info = dba->ListAllIndices();
         auto storage_acc = database->Access();
         std::vector<std::vector<TypedValue>> results;
@@ -3529,6 +3616,10 @@ PreparedQuery PrepareDatabaseInfoQuery(ParsedQuery parsed_query, bool in_explici
                TypedValue(storage->PropertyToName(item.second)),
                TypedValue(static_cast<int>(storage_acc->ApproximateVertexCount(item.first, item.second)))});
         }
+        for (const auto &item : info.edge_type) {
+          results.push_back({TypedValue(edge_type_index_mark), TypedValue(storage->EdgeTypeToName(item)), TypedValue(),
+                             TypedValue(static_cast<int>(storage_acc->ApproximateEdgeCount(item)))});
+        }
         std::sort(results.begin(), results.end(), [&label_index_mark](const auto &record_1, const auto &record_2) {
           const auto type_1 = record_1[0].ValueString();
           const auto type_2 = record_2[0].ValueString();
@@ -4315,13 +4406,14 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
         utils::Downcast<CypherQuery>(parsed_query.query) || utils::Downcast<ExplainQuery>(parsed_query.query) ||
         utils::Downcast<ProfileQuery>(parsed_query.query) || utils::Downcast<DumpQuery>(parsed_query.query) ||
         utils::Downcast<TriggerQuery>(parsed_query.query) || utils::Downcast<AnalyzeGraphQuery>(parsed_query.query) ||
-        utils::Downcast<IndexQuery>(parsed_query.query) || utils::Downcast<DatabaseInfoQuery>(parsed_query.query) ||
-        utils::Downcast<ConstraintQuery>(parsed_query.query);
+        utils::Downcast<IndexQuery>(parsed_query.query) || utils::Downcast<EdgeIndexQuery>(parsed_query.query) ||
+        utils::Downcast<DatabaseInfoQuery>(parsed_query.query) || utils::Downcast<ConstraintQuery>(parsed_query.query);
 
     if (!in_explicit_transaction_ && requires_db_transaction) {
       // TODO: ATM only a single database, will change when we have multiple database transactions
       bool could_commit = utils::Downcast<CypherQuery>(parsed_query.query) != nullptr;
       bool unique = utils::Downcast<IndexQuery>(parsed_query.query) != nullptr ||
+                    utils::Downcast<EdgeIndexQuery>(parsed_query.query) != nullptr ||
                     utils::Downcast<ConstraintQuery>(parsed_query.query) != nullptr ||
                     upper_case_query.find(kSchemaAssert) != std::string::npos;
       SetupDatabaseTransaction(could_commit, unique);
@@ -4358,6 +4450,9 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
     } else if (utils::Downcast<IndexQuery>(parsed_query.query)) {
       prepared_query = PrepareIndexQuery(std::move(parsed_query), in_explicit_transaction_,
                                          &query_execution->notifications, current_db_);
+    } else if (utils::Downcast<EdgeIndexQuery>(parsed_query.query)) {
+      prepared_query = PrepareEdgeIndexQuery(std::move(parsed_query), in_explicit_transaction_,
+                                             &query_execution->notifications, current_db_);
     } else if (utils::Downcast<AnalyzeGraphQuery>(parsed_query.query)) {
       prepared_query = PrepareAnalyzeGraphQuery(std::move(parsed_query), in_explicit_transaction_, current_db_);
     } else if (utils::Downcast<AuthQuery>(parsed_query.query)) {
diff --git a/src/query/interpreter.hpp b/src/query/interpreter.hpp
index 01a443d6d..f18bd6721 100644
--- a/src/query/interpreter.hpp
+++ b/src/query/interpreter.hpp
@@ -95,25 +95,24 @@ class CoordinatorQueryHandler {
   };
 
   /// @throw QueryRuntimeException if an error ocurred.
-  virtual void RegisterReplicationInstance(std::string const &coordinator_socket_address,
-                                           std::string const &replication_socket_address,
+  virtual void RegisterReplicationInstance(std::string_view coordinator_socket_address,
+                                           std::string_view replication_socket_address,
                                            std::chrono::seconds const &instance_health_check_frequency,
                                            std::chrono::seconds const &instance_down_timeout,
                                            std::chrono::seconds const &instance_get_uuid_frequency,
-                                           std::string const &instance_name, CoordinatorQuery::SyncMode sync_mode) = 0;
+                                           std::string_view instance_name, CoordinatorQuery::SyncMode sync_mode) = 0;
 
   /// @throw QueryRuntimeException if an error ocurred.
-  virtual void UnregisterInstance(std::string const &instance_name) = 0;
+  virtual void UnregisterInstance(std::string_view instance_name) = 0;
 
   /// @throw QueryRuntimeException if an error ocurred.
-  virtual void SetReplicationInstanceToMain(const std::string &instance_name) = 0;
+  virtual void SetReplicationInstanceToMain(std::string_view instance_name) = 0;
 
   /// @throw QueryRuntimeException if an error ocurred.
   virtual std::vector<coordination::InstanceStatus> ShowInstances() const = 0;
 
   /// @throw QueryRuntimeException if an error ocurred.
-  virtual auto AddCoordinatorInstance(uint32_t raft_server_id, std::string const &coordinator_socket_address)
-      -> void = 0;
+  virtual auto AddCoordinatorInstance(uint32_t raft_server_id, std::string_view coordinator_socket_address) -> void = 0;
 };
 #endif
 
diff --git a/src/query/metadata.cpp b/src/query/metadata.cpp
index e339aad57..af3b8d15f 100644
--- a/src/query/metadata.cpp
+++ b/src/query/metadata.cpp
@@ -67,8 +67,8 @@ constexpr std::string_view GetCodeString(const NotificationCode code) {
     case NotificationCode::REGISTER_REPLICA:
       return "RegisterReplica"sv;
 #ifdef MG_ENTERPRISE
-    case NotificationCode::REGISTER_COORDINATOR_SERVER:
-      return "RegisterCoordinatorServer"sv;
+    case NotificationCode::REGISTER_REPLICATION_INSTANCE:
+      return "RegisterReplicationInstance"sv;
     case NotificationCode::ADD_COORDINATOR_INSTANCE:
       return "AddCoordinatorInstance"sv;
     case NotificationCode::UNREGISTER_INSTANCE:
diff --git a/src/query/metadata.hpp b/src/query/metadata.hpp
index dd8c2db07..fba672f4b 100644
--- a/src/query/metadata.hpp
+++ b/src/query/metadata.hpp
@@ -43,7 +43,7 @@ enum class NotificationCode : uint8_t {
   REPLICA_PORT_WARNING,
   REGISTER_REPLICA,
 #ifdef MG_ENTERPRISE
-  REGISTER_COORDINATOR_SERVER,  // TODO: (andi) What is this?
+  REGISTER_REPLICATION_INSTANCE,
   ADD_COORDINATOR_INSTANCE,
   UNREGISTER_INSTANCE,
 #endif
diff --git a/src/query/plan/hint_provider.hpp b/src/query/plan/hint_provider.hpp
index 74dde2f46..3c8510561 100644
--- a/src/query/plan/hint_provider.hpp
+++ b/src/query/plan/hint_provider.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -114,6 +114,9 @@ class PlanHintsProvider final : public HierarchicalLogicalOperatorVisitor {
   bool PreVisit(ScanAllById & /*unused*/) override { return true; }
   bool PostVisit(ScanAllById & /*unused*/) override { return true; }
 
+  bool PreVisit(ScanAllByEdgeType & /*unused*/) override { return true; }
+  bool PostVisit(ScanAllByEdgeType & /*unused*/) override { return true; }
+
   bool PreVisit(ConstructNamedPath & /*unused*/) override { return true; }
   bool PostVisit(ConstructNamedPath & /*unused*/) override { return true; }
 
@@ -206,6 +209,14 @@ class PlanHintsProvider final : public HierarchicalLogicalOperatorVisitor {
 
   bool PostVisit(IndexedJoin & /*unused*/) override { return true; }
 
+  bool PreVisit(RollUpApply &op) override {
+    op.input()->Accept(*this);
+    op.list_collection_branch_->Accept(*this);
+    return false;
+  }
+
+  bool PostVisit(RollUpApply & /*unused*/) override { return true; }
+
  private:
   const SymbolTable &symbol_table_;
   std::vector<std::string> hints_;
diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp
index 75b531261..7cd506050 100644
--- a/src/query/plan/operator.cpp
+++ b/src/query/plan/operator.cpp
@@ -105,6 +105,7 @@ extern const Event ScanAllByLabelPropertyRangeOperator;
 extern const Event ScanAllByLabelPropertyValueOperator;
 extern const Event ScanAllByLabelPropertyOperator;
 extern const Event ScanAllByIdOperator;
+extern const Event ScanAllByEdgeTypeOperator;
 extern const Event ExpandOperator;
 extern const Event ExpandVariableOperator;
 extern const Event ConstructNamedPathOperator;
@@ -517,6 +518,60 @@ class ScanAllCursor : public Cursor {
   const char *op_name_;
 };
 
+template <typename TEdgesFun>
+class ScanAllByEdgeTypeCursor : public Cursor {
+ public:
+  explicit ScanAllByEdgeTypeCursor(const ScanAllByEdgeType &self, Symbol output_symbol, UniqueCursorPtr input_cursor,
+                                   storage::View view, TEdgesFun get_edges, const char *op_name)
+      : self_(self),
+        output_symbol_(std::move(output_symbol)),
+        input_cursor_(std::move(input_cursor)),
+        view_(view),
+        get_edges_(std::move(get_edges)),
+        op_name_(op_name) {}
+
+  bool Pull(Frame &frame, ExecutionContext &context) override {
+    OOMExceptionEnabler oom_exception;
+    SCOPED_PROFILE_OP_BY_REF(self_);
+
+    AbortCheck(context);
+
+    while (!vertices_ || vertices_it_.value() == vertices_end_it_.value()) {
+      if (!input_cursor_->Pull(frame, context)) return false;
+      auto next_vertices = get_edges_(frame, context);
+      if (!next_vertices) continue;
+
+      vertices_.emplace(std::move(next_vertices.value()));
+      vertices_it_.emplace(vertices_.value().begin());
+      vertices_end_it_.emplace(vertices_.value().end());
+    }
+
+    frame[output_symbol_] = *vertices_it_.value();
+    ++vertices_it_.value();
+    return true;
+  }
+
+  void Shutdown() override { input_cursor_->Shutdown(); }
+
+  void Reset() override {
+    input_cursor_->Reset();
+    vertices_ = std::nullopt;
+    vertices_it_ = std::nullopt;
+    vertices_end_it_ = std::nullopt;
+  }
+
+ private:
+  const ScanAllByEdgeType &self_;
+  const Symbol output_symbol_;
+  const UniqueCursorPtr input_cursor_;
+  storage::View view_;
+  TEdgesFun get_edges_;
+  std::optional<typename std::result_of<TEdgesFun(Frame &, ExecutionContext &)>::type::value_type> vertices_;
+  std::optional<decltype(vertices_.value().begin())> vertices_it_;
+  std::optional<decltype(vertices_.value().end())> vertices_end_it_;
+  const char *op_name_;
+};
+
 ScanAll::ScanAll(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol, storage::View view)
     : input_(input ? input : std::make_shared<Once>()), output_symbol_(std::move(output_symbol)), view_(view) {}
 
@@ -556,6 +611,33 @@ UniqueCursorPtr ScanAllByLabel::MakeCursor(utils::MemoryResource *mem) const {
                                                                 view_, std::move(vertices), "ScanAllByLabel");
 }
 
+ScanAllByEdgeType::ScanAllByEdgeType(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol,
+                                     storage::EdgeTypeId edge_type, storage::View view)
+    : input_(input ? input : std::make_shared<Once>()),
+      output_symbol_(std::move(output_symbol)),
+      view_(view),
+      edge_type_(edge_type) {}
+
+ACCEPT_WITH_INPUT(ScanAllByEdgeType)
+
+UniqueCursorPtr ScanAllByEdgeType::MakeCursor(utils::MemoryResource *mem) const {
+  memgraph::metrics::IncrementCounter(memgraph::metrics::ScanAllByEdgeTypeOperator);
+
+  auto edges = [this](Frame &, ExecutionContext &context) {
+    auto *db = context.db_accessor;
+    return std::make_optional(db->Edges(view_, edge_type_));
+  };
+
+  return MakeUniqueCursorPtr<ScanAllByEdgeTypeCursor<decltype(edges)>>(
+      mem, *this, output_symbol_, input_->MakeCursor(mem), view_, std::move(edges), "ScanAllByEdgeType");
+}
+
+std::vector<Symbol> ScanAllByEdgeType::ModifiedSymbols(const SymbolTable &table) const {
+  auto symbols = input_->ModifiedSymbols(table);
+  symbols.emplace_back(output_symbol_);
+  return symbols;
+}
+
 // TODO(buda): Implement ScanAllByLabelProperty operator to iterate over
 // vertices that have the label and some value for the given property.
 
@@ -5624,4 +5706,25 @@ UniqueCursorPtr HashJoin::MakeCursor(utils::MemoryResource *mem) const {
   return MakeUniqueCursorPtr<HashJoinCursor>(mem, *this, mem);
 }
 
+RollUpApply::RollUpApply(const std::shared_ptr<LogicalOperator> &input,
+                         std::shared_ptr<LogicalOperator> &&second_branch)
+    : input_(input), list_collection_branch_(second_branch) {}
+
+std::vector<Symbol> RollUpApply::OutputSymbols(const SymbolTable & /*symbol_table*/) const {
+  std::vector<Symbol> symbols;
+  return symbols;
+}
+
+std::vector<Symbol> RollUpApply::ModifiedSymbols(const SymbolTable &table) const { return OutputSymbols(table); }
+
+bool RollUpApply::Accept(HierarchicalLogicalOperatorVisitor &visitor) {
+  if (visitor.PreVisit(*this)) {
+    if (!input_ || !list_collection_branch_) {
+      throw utils::NotYetImplemented("One of the branches in pattern comprehension is null! Please contact support.");
+    }
+    input_->Accept(visitor) && list_collection_branch_->Accept(visitor);
+  }
+  return visitor.PostVisit(*this);
+}
+
 }  // namespace memgraph::query::plan
diff --git a/src/query/plan/operator.hpp b/src/query/plan/operator.hpp
index 516ef2e38..6563c2bb0 100644
--- a/src/query/plan/operator.hpp
+++ b/src/query/plan/operator.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -99,6 +99,7 @@ class ScanAllByLabelPropertyRange;
 class ScanAllByLabelPropertyValue;
 class ScanAllByLabelProperty;
 class ScanAllById;
+class ScanAllByEdgeType;
 class Expand;
 class ExpandVariable;
 class ConstructNamedPath;
@@ -130,14 +131,15 @@ class EvaluatePatternFilter;
 class Apply;
 class IndexedJoin;
 class HashJoin;
+class RollUpApply;
 
 using LogicalOperatorCompositeVisitor =
     utils::CompositeVisitor<Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel, ScanAllByLabelPropertyRange,
-                            ScanAllByLabelPropertyValue, ScanAllByLabelProperty, ScanAllById, Expand, ExpandVariable,
-                            ConstructNamedPath, Filter, Produce, Delete, SetProperty, SetProperties, SetLabels,
-                            RemoveProperty, RemoveLabels, EdgeUniquenessFilter, Accumulate, Aggregate, Skip, Limit,
-                            OrderBy, Merge, Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv,
-                            Foreach, EmptyResult, EvaluatePatternFilter, Apply, IndexedJoin, HashJoin>;
+                            ScanAllByLabelPropertyValue, ScanAllByLabelProperty, ScanAllById, ScanAllByEdgeType, Expand,
+                            ExpandVariable, ConstructNamedPath, Filter, Produce, Delete, SetProperty, SetProperties,
+                            SetLabels, RemoveProperty, RemoveLabels, EdgeUniquenessFilter, Accumulate, Aggregate, Skip,
+                            Limit, OrderBy, Merge, Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv,
+                            Foreach, EmptyResult, EvaluatePatternFilter, Apply, IndexedJoin, HashJoin, RollUpApply>;
 
 using LogicalOperatorLeafVisitor = utils::LeafVisitor<Once>;
 
@@ -591,6 +593,42 @@ class ScanAllByLabel : public memgraph::query::plan::ScanAll {
   }
 };
 
+class ScanAllByEdgeType : public memgraph::query::plan::LogicalOperator {
+ public:
+  static const utils::TypeInfo kType;
+  const utils::TypeInfo &GetTypeInfo() const override { return kType; }
+
+  ScanAllByEdgeType() = default;
+  ScanAllByEdgeType(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol, storage::EdgeTypeId edge_type,
+                    storage::View view = storage::View::OLD);
+  bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
+  UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override;
+  std::vector<Symbol> ModifiedSymbols(const SymbolTable &) const override;
+
+  bool HasSingleInput() const override { return true; }
+  std::shared_ptr<LogicalOperator> input() const override { return input_; }
+  void set_input(std::shared_ptr<LogicalOperator> input) override { input_ = input; }
+
+  std::string ToString() const override {
+    return fmt::format("ScanAllByEdgeType ({} :{})", output_symbol_.name(), dba_->EdgeTypeToName(edge_type_));
+  }
+
+  std::shared_ptr<memgraph::query::plan::LogicalOperator> input_;
+  Symbol output_symbol_;
+  storage::View view_;
+
+  storage::EdgeTypeId edge_type_;
+
+  std::unique_ptr<LogicalOperator> Clone(AstStorage *storage) const override {
+    auto object = std::make_unique<ScanAllByEdgeType>();
+    object->input_ = input_ ? input_->Clone(storage) : nullptr;
+    object->output_symbol_ = output_symbol_;
+    object->view_ = view_;
+    object->edge_type_ = edge_type_;
+    return object;
+  }
+};
+
 /// Behaves like @c ScanAll, but produces only vertices with given label and
 /// property value which is inside a range (inclusive or exlusive).
 ///
@@ -2634,5 +2672,38 @@ class HashJoin : public memgraph::query::plan::LogicalOperator {
   }
 };
 
+/// RollUpApply operator is used to execute an expression which takes as input a pattern,
+/// and returns a list with content from the matched pattern
+/// It's used for a pattern expression or pattern comprehension in a query.
+class RollUpApply : public memgraph::query::plan::LogicalOperator {
+ public:
+  static const utils::TypeInfo kType;
+  const utils::TypeInfo &GetTypeInfo() const override { return kType; }
+
+  RollUpApply() = default;
+  RollUpApply(const std::shared_ptr<LogicalOperator> &input, std::shared_ptr<LogicalOperator> &&second_branch);
+
+  bool HasSingleInput() const override { return false; }
+  std::shared_ptr<LogicalOperator> input() const override { return input_; }
+  void set_input(std::shared_ptr<LogicalOperator> input) override { input_ = input; }
+
+  bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
+  UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override {
+    throw utils::NotYetImplemented("Execution of Pattern comprehension is currently unsupported.");
+  }
+  std::vector<Symbol> OutputSymbols(const SymbolTable &) const override;
+  std::vector<Symbol> ModifiedSymbols(const SymbolTable &) const override;
+
+  std::unique_ptr<LogicalOperator> Clone(AstStorage *storage) const override {
+    auto object = std::make_unique<RollUpApply>();
+    object->input_ = input_ ? input_->Clone(storage) : nullptr;
+    object->list_collection_branch_ = list_collection_branch_ ? list_collection_branch_->Clone(storage) : nullptr;
+    return object;
+  }
+
+  std::shared_ptr<memgraph::query::plan::LogicalOperator> input_;
+  std::shared_ptr<memgraph::query::plan::LogicalOperator> list_collection_branch_;
+};
+
 }  // namespace plan
 }  // namespace memgraph::query
diff --git a/src/query/plan/operator_type_info.cpp b/src/query/plan/operator_type_info.cpp
index 3b3ffe14e..6b0a28313 100644
--- a/src/query/plan/operator_type_info.cpp
+++ b/src/query/plan/operator_type_info.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -49,6 +49,8 @@ constexpr utils::TypeInfo query::plan::ScanAllByLabelProperty::kType{
 
 constexpr utils::TypeInfo query::plan::ScanAllById::kType{utils::TypeId::SCAN_ALL_BY_ID, "ScanAllById",
                                                           &query::plan::ScanAll::kType};
+constexpr utils::TypeInfo query::plan::ScanAllByEdgeType::kType{utils::TypeId::SCAN_ALL_BY_EDGE_TYPE,
+                                                                "ScanAllByEdgeType", &query::plan::ScanAll::kType};
 
 constexpr utils::TypeInfo query::plan::ExpandCommon::kType{utils::TypeId::EXPAND_COMMON, "ExpandCommon", nullptr};
 
@@ -154,4 +156,7 @@ constexpr utils::TypeInfo query::plan::IndexedJoin::kType{utils::TypeId::INDEXED
 
 constexpr utils::TypeInfo query::plan::HashJoin::kType{utils::TypeId::HASH_JOIN, "HashJoin",
                                                        &query::plan::LogicalOperator::kType};
+
+constexpr utils::TypeInfo query::plan::RollUpApply::kType{utils::TypeId::ROLLUP_APPLY, "RollUpApply",
+                                                          &query::plan::LogicalOperator::kType};
 }  // namespace memgraph
diff --git a/src/query/plan/planner.hpp b/src/query/plan/planner.hpp
index e8ca80e39..3136e7271 100644
--- a/src/query/plan/planner.hpp
+++ b/src/query/plan/planner.hpp
@@ -23,6 +23,7 @@
 #include "query/plan/operator.hpp"
 #include "query/plan/preprocess.hpp"
 #include "query/plan/pretty_print.hpp"
+#include "query/plan/rewrite/edge_type_index_lookup.hpp"
 #include "query/plan/rewrite/index_lookup.hpp"
 #include "query/plan/rewrite/join.hpp"
 #include "query/plan/rule_based_planner.hpp"
@@ -54,8 +55,11 @@ class PostProcessor final {
   std::unique_ptr<LogicalOperator> Rewrite(std::unique_ptr<LogicalOperator> plan, TPlanningContext *context) {
     auto index_lookup_plan =
         RewriteWithIndexLookup(std::move(plan), context->symbol_table, context->ast_storage, context->db, index_hints_);
-    return RewriteWithJoinRewriter(std::move(index_lookup_plan), context->symbol_table, context->ast_storage,
-                                   context->db);
+    auto join_plan =
+        RewriteWithJoinRewriter(std::move(index_lookup_plan), context->symbol_table, context->ast_storage, context->db);
+    auto edge_index_plan = RewriteWithEdgeTypeIndexRewriter(std::move(join_plan), context->symbol_table,
+                                                            context->ast_storage, context->db);
+    return edge_index_plan;
   }
 
   template <class TVertexCounts>
diff --git a/src/query/plan/preprocess.cpp b/src/query/plan/preprocess.cpp
index c3bfdf462..ca605a46a 100644
--- a/src/query/plan/preprocess.cpp
+++ b/src/query/plan/preprocess.cpp
@@ -632,20 +632,20 @@ void AddMatching(const Match &match, SymbolTable &symbol_table, AstStorage &stor
 
   // If there are any pattern filters, we add those as well
   for (auto &filter : matching.filters) {
-    PatternFilterVisitor visitor(symbol_table, storage);
+    PatternVisitor visitor(symbol_table, storage);
 
     filter.expression->Accept(visitor);
-    filter.matchings = visitor.getMatchings();
+    filter.matchings = visitor.getFilterMatchings();
   }
 }
 
-PatternFilterVisitor::PatternFilterVisitor(SymbolTable &symbol_table, AstStorage &storage)
+PatternVisitor::PatternVisitor(SymbolTable &symbol_table, AstStorage &storage)
     : symbol_table_(symbol_table), storage_(storage) {}
-PatternFilterVisitor::PatternFilterVisitor(const PatternFilterVisitor &) = default;
-PatternFilterVisitor::PatternFilterVisitor(PatternFilterVisitor &&) noexcept = default;
-PatternFilterVisitor::~PatternFilterVisitor() = default;
+PatternVisitor::PatternVisitor(const PatternVisitor &) = default;
+PatternVisitor::PatternVisitor(PatternVisitor &&) noexcept = default;
+PatternVisitor::~PatternVisitor() = default;
 
-void PatternFilterVisitor::Visit(Exists &op) {
+void PatternVisitor::Visit(Exists &op) {
   std::vector<Pattern *> patterns;
   patterns.push_back(op.pattern_);
 
@@ -655,10 +655,14 @@ void PatternFilterVisitor::Visit(Exists &op) {
   filter_matching.type = PatternFilterType::EXISTS;
   filter_matching.symbol = std::make_optional<Symbol>(symbol_table_.at(op));
 
-  matchings_.push_back(std::move(filter_matching));
+  filter_matchings_.push_back(std::move(filter_matching));
 }
 
-std::vector<FilterMatching> PatternFilterVisitor::getMatchings() { return matchings_; }
+std::vector<FilterMatching> PatternVisitor::getFilterMatchings() { return filter_matchings_; }
+
+std::vector<PatternComprehensionMatching> PatternVisitor::getPatternComprehensionMatchings() {
+  return pattern_comprehension_matchings_;
+}
 
 static void ParseForeach(query::Foreach &foreach, SingleQueryPart &query_part, AstStorage &storage,
                          SymbolTable &symbol_table) {
@@ -672,6 +676,30 @@ static void ParseForeach(query::Foreach &foreach, SingleQueryPart &query_part, A
   }
 }
 
+static void ParseReturn(query::Return &ret, AstStorage &storage, SymbolTable &symbol_table,
+                        std::unordered_map<std::string, PatternComprehensionMatching> &matchings) {
+  PatternVisitor visitor(symbol_table, storage);
+
+  for (auto *expr : ret.body_.named_expressions) {
+    expr->Accept(visitor);
+    auto pattern_comprehension_matchings = visitor.getPatternComprehensionMatchings();
+    for (auto &matching : pattern_comprehension_matchings) {
+      matchings.emplace(expr->name_, matching);
+    }
+  }
+}
+
+void PatternVisitor::Visit(NamedExpression &op) { op.expression_->Accept(*this); }
+
+void PatternVisitor::Visit(PatternComprehension &op) {
+  PatternComprehensionMatching matching;
+  AddMatching({op.pattern_}, op.filter_, symbol_table_, storage_, matching);
+  matching.result_expr = storage_.Create<NamedExpression>(symbol_table_.at(op).name(), op.resultExpr_);
+  matching.result_expr->MapTo(symbol_table_.at(op));
+
+  pattern_comprehension_matchings_.push_back(std::move(matching));
+}
+
 // Converts a Query to multiple QueryParts. In the process new Ast nodes may be
 // created, e.g. filter expressions.
 std::vector<SingleQueryPart> CollectSingleQueryParts(SymbolTable &symbol_table, AstStorage &storage,
@@ -703,7 +731,8 @@ std::vector<SingleQueryPart> CollectSingleQueryParts(SymbolTable &symbol_table,
         // This query part is done, continue with a new one.
         query_parts.emplace_back(SingleQueryPart{});
         query_part = &query_parts.back();
-      } else if (utils::IsSubtype(*clause, Return::kType)) {
+      } else if (auto *ret = utils::Downcast<Return>(clause)) {
+        ParseReturn(*ret, storage, symbol_table, query_part->pattern_comprehension_matchings);
         return query_parts;
       }
     }
diff --git a/src/query/plan/preprocess.hpp b/src/query/plan/preprocess.hpp
index 01b10ebaf..5d4e2e8d2 100644
--- a/src/query/plan/preprocess.hpp
+++ b/src/query/plan/preprocess.hpp
@@ -153,19 +153,20 @@ struct Expansion {
   ExpansionGroupId expansion_group_id = ExpansionGroupId();
 };
 
+struct PatternComprehensionMatching;
 struct FilterMatching;
 
 enum class PatternFilterType { EXISTS };
 
-/// Collects matchings from filters that include patterns
-class PatternFilterVisitor : public ExpressionVisitor<void> {
+/// Collects matchings that include patterns
+class PatternVisitor : public ExpressionVisitor<void> {
  public:
-  explicit PatternFilterVisitor(SymbolTable &symbol_table, AstStorage &storage);
-  PatternFilterVisitor(const PatternFilterVisitor &);
-  PatternFilterVisitor &operator=(const PatternFilterVisitor &) = delete;
-  PatternFilterVisitor(PatternFilterVisitor &&) noexcept;
-  PatternFilterVisitor &operator=(PatternFilterVisitor &&) noexcept = delete;
-  ~PatternFilterVisitor() override;
+  explicit PatternVisitor(SymbolTable &symbol_table, AstStorage &storage);
+  PatternVisitor(const PatternVisitor &);
+  PatternVisitor &operator=(const PatternVisitor &) = delete;
+  PatternVisitor(PatternVisitor &&) noexcept;
+  PatternVisitor &operator=(PatternVisitor &&) noexcept = delete;
+  ~PatternVisitor() override;
 
   using ExpressionVisitor<void>::Visit;
 
@@ -233,18 +234,22 @@ class PatternFilterVisitor : public ExpressionVisitor<void> {
   void Visit(PropertyLookup &op) override{};
   void Visit(AllPropertiesLookup &op) override{};
   void Visit(ParameterLookup &op) override{};
-  void Visit(NamedExpression &op) override{};
   void Visit(RegexMatch &op) override{};
-  void Visit(PatternComprehension &op) override{};
+  void Visit(NamedExpression &op) override;
+  void Visit(PatternComprehension &op) override;
 
-  std::vector<FilterMatching> getMatchings();
+  std::vector<FilterMatching> getFilterMatchings();
+  std::vector<PatternComprehensionMatching> getPatternComprehensionMatchings();
 
   SymbolTable &symbol_table_;
   AstStorage &storage_;
 
  private:
   /// Collection of matchings in the filter expression being analyzed.
-  std::vector<FilterMatching> matchings_;
+  std::vector<FilterMatching> filter_matchings_;
+
+  /// Collection of matchings in the pattern comprehension being analyzed.
+  std::vector<PatternComprehensionMatching> pattern_comprehension_matchings_;
 };
 
 /// Stores the symbols and expression used to filter a property.
@@ -495,6 +500,11 @@ inline auto Filters::IdFilters(const Symbol &symbol) const -> std::vector<Filter
   return filters;
 }
 
+struct PatternComprehensionMatching : Matching {
+  /// Pattern comprehension result named expression
+  NamedExpression *result_expr = nullptr;
+};
+
 /// @brief Represents a read (+ write) part of a query. Parts are split on
 /// `WITH` clauses.
 ///
@@ -537,6 +547,14 @@ struct SingleQueryPart {
   /// in the `remaining_clauses` but rather in the `Foreach` itself and are guranteed
   /// to be processed in the same order by the semantics of the `RuleBasedPlanner`.
   std::vector<Matching> merge_matching{};
+
+  /// @brief @c NamedExpression name to @c PatternComprehensionMatching for each pattern comprehension.
+  ///
+  /// Storing the normalized pattern of a @c PatternComprehension does not preclude storing the
+  /// @c PatternComprehension clause itself inside `remaining_clauses`. The reason is that we
+  /// need to have access to other parts of the clause, such as pattern, filter clauses.
+  std::unordered_map<std::string, PatternComprehensionMatching> pattern_comprehension_matchings{};
+
   /// @brief All the remaining clauses (without @c Match).
   std::vector<Clause *> remaining_clauses{};
   /// The subqueries vector are all the subqueries in this query part ordered in a list by
diff --git a/src/query/plan/pretty_print.cpp b/src/query/plan/pretty_print.cpp
index a2df9422c..eeb0c15b5 100644
--- a/src/query/plan/pretty_print.cpp
+++ b/src/query/plan/pretty_print.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -76,6 +76,13 @@ bool PlanPrinter::PreVisit(ScanAllById &op) {
   return true;
 }
 
+bool PlanPrinter::PreVisit(query::plan::ScanAllByEdgeType &op) {
+  op.dba_ = dba_;
+  WithPrintLn([&op](auto &out) { out << "* " << op.ToString(); });
+  op.dba_ = nullptr;
+  return true;
+}
+
 bool PlanPrinter::PreVisit(query::plan::Expand &op) {
   op.dba_ = dba_;
   WithPrintLn([&op](auto &out) { out << "* " << op.ToString(); });
@@ -143,6 +150,13 @@ bool PlanPrinter::PreVisit(query::plan::Union &op) {
   return false;
 }
 
+bool PlanPrinter::PreVisit(query::plan::RollUpApply &op) {
+  WithPrintLn([&op](auto &out) { out << "* " << op.ToString(); });
+  Branch(*op.list_collection_branch_);
+  op.input_->Accept(*this);
+  return false;
+}
+
 bool PlanPrinter::PreVisit(query::plan::CallProcedure &op) {
   WithPrintLn([&op](auto &out) { out << "* " << op.ToString(); });
   return true;
@@ -457,6 +471,19 @@ bool PlanToJsonVisitor::PreVisit(ScanAllById &op) {
   return false;
 }
 
+bool PlanToJsonVisitor::PreVisit(ScanAllByEdgeType &op) {
+  json self;
+  self["name"] = "ScanAllByEdgeType";
+  self["edge_type"] = ToJson(op.edge_type_, *dba_);
+  self["output_symbol"] = ToJson(op.output_symbol_);
+
+  op.input_->Accept(*this);
+  self["input"] = PopOutput();
+
+  output_ = std::move(self);
+  return false;
+}
+
 bool PlanToJsonVisitor::PreVisit(CreateNode &op) {
   json self;
   self["name"] = "CreateNode";
diff --git a/src/query/plan/pretty_print.hpp b/src/query/plan/pretty_print.hpp
index 645fe17a5..d62ae6bf2 100644
--- a/src/query/plan/pretty_print.hpp
+++ b/src/query/plan/pretty_print.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -67,6 +67,7 @@ class PlanPrinter : public virtual HierarchicalLogicalOperatorVisitor {
   bool PreVisit(ScanAllByLabelPropertyRange &) override;
   bool PreVisit(ScanAllByLabelProperty &) override;
   bool PreVisit(ScanAllById &) override;
+  bool PreVisit(ScanAllByEdgeType &) override;
 
   bool PreVisit(Expand &) override;
   bool PreVisit(ExpandVariable &) override;
@@ -91,6 +92,7 @@ class PlanPrinter : public virtual HierarchicalLogicalOperatorVisitor {
   bool PreVisit(OrderBy &) override;
   bool PreVisit(Distinct &) override;
   bool PreVisit(Union &) override;
+  bool PreVisit(RollUpApply &) override;
 
   bool PreVisit(Unwind &) override;
   bool PreVisit(CallProcedure &) override;
@@ -203,6 +205,7 @@ class PlanToJsonVisitor : public virtual HierarchicalLogicalOperatorVisitor {
   bool PreVisit(ScanAllByLabelPropertyValue &) override;
   bool PreVisit(ScanAllByLabelProperty &) override;
   bool PreVisit(ScanAllById &) override;
+  bool PreVisit(ScanAllByEdgeType &) override;
 
   bool PreVisit(EmptyResult &) override;
   bool PreVisit(Produce &) override;
diff --git a/src/query/plan/rewrite/edge_type_index_lookup.hpp b/src/query/plan/rewrite/edge_type_index_lookup.hpp
new file mode 100644
index 000000000..ed8666513
--- /dev/null
+++ b/src/query/plan/rewrite/edge_type_index_lookup.hpp
@@ -0,0 +1,534 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+/// @file
+/// This file provides a plan rewriter which replaces `ScanAll` and `Expand`
+/// operations with `ScanAllByEdgeType` if possible. The public entrypoint is
+/// `RewriteWithEdgeTypeIndexRewriter`.
+
+#pragma once
+
+#include <algorithm>
+#include <memory>
+#include <optional>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include <gflags/gflags.h>
+
+#include "query/plan/operator.hpp"
+#include "query/plan/preprocess.hpp"
+#include "query/plan/rewrite/index_lookup.hpp"
+#include "utils/algorithm.hpp"
+
+namespace memgraph::query::plan {
+
+namespace impl {
+
+template <class TDbAccessor>
+class EdgeTypeIndexRewriter final : public HierarchicalLogicalOperatorVisitor {
+ public:
+  EdgeTypeIndexRewriter(SymbolTable *symbol_table, AstStorage *ast_storage, TDbAccessor *db)
+      : symbol_table_(symbol_table), ast_storage_(ast_storage), db_(db) {}
+
+  using HierarchicalLogicalOperatorVisitor::PostVisit;
+  using HierarchicalLogicalOperatorVisitor::PreVisit;
+  using HierarchicalLogicalOperatorVisitor::Visit;
+
+  bool Visit(Once &) override { return true; }
+
+  bool PreVisit(Filter &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(Filter & /*op*/) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAll &op) override {
+    prev_ops_.push_back(&op);
+
+    if (op.input()->GetTypeInfo() == Once::kType) {
+      const bool is_node_anon = op.output_symbol_.IsSymbolAnonym();
+      once_under_scanall_ = is_node_anon;
+    }
+
+    return true;
+  }
+
+  bool PostVisit(ScanAll &op) override {
+    prev_ops_.pop_back();
+
+    if (EdgeTypeIndexingPossible()) {
+      SetOnParent(op.input());
+    }
+
+    return true;
+  }
+
+  bool PreVisit(Expand &op) override {
+    prev_ops_.push_back(&op);
+
+    if (op.input()->GetTypeInfo() == ScanAll::kType) {
+      const bool only_one_edge_type = (op.common_.edge_types.size() == 1U);
+      const bool expansion_is_named = !(op.common_.edge_symbol.IsSymbolAnonym());
+      const bool expdanded_node_not_named = op.common_.node_symbol.IsSymbolAnonym();
+
+      edge_type_index_exist = only_one_edge_type ? db_->EdgeTypeIndexExists(op.common_.edge_types.front()) : false;
+
+      scanall_under_expand_ = only_one_edge_type && expansion_is_named && expdanded_node_not_named;
+    }
+
+    return true;
+  }
+
+  bool PostVisit(Expand &op) override {
+    prev_ops_.pop_back();
+
+    if (EdgeTypeIndexingPossible()) {
+      auto indexed_scan = GenEdgeTypeScan(op);
+      SetOnParent(std::move(indexed_scan));
+    }
+
+    return true;
+  }
+
+  bool PreVisit(ExpandVariable &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(ExpandVariable &expand) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Merge &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.merge_match_);
+    return false;
+  }
+
+  bool PostVisit(Merge &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Optional &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.optional_);
+    return false;
+  }
+
+  bool PostVisit(Optional &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Cartesian &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(Cartesian &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(IndexedJoin &op) override {
+    prev_ops_.push_back(&op);
+    RewriteBranch(&op.main_branch_);
+    RewriteBranch(&op.sub_branch_);
+    return false;
+  }
+
+  bool PostVisit(IndexedJoin &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(HashJoin &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(HashJoin &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Union &op) override {
+    prev_ops_.push_back(&op);
+    RewriteBranch(&op.left_op_);
+    RewriteBranch(&op.right_op_);
+    return false;
+  }
+
+  bool PostVisit(Union &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(CreateNode &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(CreateNode &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(CreateExpand &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(CreateExpand &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByLabel &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByLabel &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByLabelPropertyRange &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByLabelPropertyRange &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByLabelPropertyValue &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByLabelPropertyValue &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByLabelProperty &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByLabelProperty &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllById &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllById &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByEdgeType &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByEdgeType &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ConstructNamedPath &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ConstructNamedPath &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Produce &op) override {
+    prev_ops_.push_back(&op);
+
+    if (op.input()->GetTypeInfo() == Expand::kType) {
+      expand_under_produce_ = true;
+    }
+
+    return true;
+  }
+  bool PostVisit(Produce &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(EmptyResult &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(EmptyResult &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Delete &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Delete &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(SetProperty &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(SetProperty &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(SetProperties &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(SetProperties &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(SetLabels &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(SetLabels &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(RemoveProperty &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(RemoveProperty &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(RemoveLabels &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(RemoveLabels &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(EdgeUniquenessFilter &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(EdgeUniquenessFilter &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Accumulate &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Accumulate &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Aggregate &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Aggregate &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Skip &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Skip &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Limit &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Limit &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(OrderBy &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(OrderBy &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Unwind &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Unwind &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Distinct &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Distinct &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(CallProcedure &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(CallProcedure &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Foreach &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.update_clauses_);
+    return false;
+  }
+
+  bool PostVisit(Foreach &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(EvaluatePatternFilter &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(EvaluatePatternFilter & /*op*/) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Apply &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.subquery_);
+    return false;
+  }
+
+  bool PostVisit(Apply & /*op*/) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(LoadCsv &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(LoadCsv & /*op*/) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  std::shared_ptr<LogicalOperator> new_root_;
+
+ private:
+  SymbolTable *symbol_table_;
+  AstStorage *ast_storage_;
+  TDbAccessor *db_;
+  // Collected filters, pending for examination if they can be used for advanced
+  // lookup operations (by index, node ID, ...).
+  Filters filters_;
+  // Expressions which no longer need a plain Filter operator.
+  std::unordered_set<Expression *> filter_exprs_for_removal_;
+  std::vector<LogicalOperator *> prev_ops_;
+  std::unordered_set<Symbol> cartesian_symbols_;
+
+  bool EdgeTypeIndexingPossible() const {
+    return expand_under_produce_ && scanall_under_expand_ && once_under_scanall_ && edge_type_index_exist;
+  }
+  bool expand_under_produce_ = false;
+  bool scanall_under_expand_ = false;
+  bool once_under_scanall_ = false;
+  bool edge_type_index_exist = false;
+
+  bool DefaultPreVisit() override {
+    throw utils::NotYetImplemented("Operator not yet covered by EdgeTypeIndexRewriter");
+  }
+
+  std::unique_ptr<ScanAllByEdgeType> GenEdgeTypeScan(const Expand &expand) {
+    const auto &input = expand.input();
+    const auto &output_symbol = expand.common_.edge_symbol;
+    const auto &view = expand.view_;
+
+    // Extract edge_type from symbol
+    auto edge_type = expand.common_.edge_types.front();
+    return std::make_unique<ScanAllByEdgeType>(input, output_symbol, edge_type, view);
+  }
+
+  void SetOnParent(const std::shared_ptr<LogicalOperator> &input) {
+    MG_ASSERT(input);
+    if (prev_ops_.empty()) {
+      MG_ASSERT(!new_root_);
+      new_root_ = input;
+      return;
+    }
+    prev_ops_.back()->set_input(input);
+  }
+
+  void RewriteBranch(std::shared_ptr<LogicalOperator> *branch) {
+    EdgeTypeIndexRewriter<TDbAccessor> rewriter(symbol_table_, ast_storage_, db_);
+    (*branch)->Accept(rewriter);
+    if (rewriter.new_root_) {
+      *branch = rewriter.new_root_;
+    }
+  }
+};
+
+}  // namespace impl
+
+template <class TDbAccessor>
+std::unique_ptr<LogicalOperator> RewriteWithEdgeTypeIndexRewriter(std::unique_ptr<LogicalOperator> root_op,
+                                                                  SymbolTable *symbol_table, AstStorage *ast_storage,
+                                                                  TDbAccessor *db) {
+  impl::EdgeTypeIndexRewriter<TDbAccessor> rewriter(symbol_table, ast_storage, db);
+  root_op->Accept(rewriter);
+  return root_op;
+}
+
+}  // namespace memgraph::query::plan
diff --git a/src/query/plan/rewrite/index_lookup.hpp b/src/query/plan/rewrite/index_lookup.hpp
index 09c6e2014..90c222b42 100644
--- a/src/query/plan/rewrite/index_lookup.hpp
+++ b/src/query/plan/rewrite/index_lookup.hpp
@@ -595,6 +595,18 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
     return true;
   }
 
+  bool PreVisit(RollUpApply &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.list_collection_branch_);
+    return false;
+  }
+
+  bool PostVisit(RollUpApply &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
   std::shared_ptr<LogicalOperator> new_root_;
 
  private:
diff --git a/src/query/plan/rewrite/join.hpp b/src/query/plan/rewrite/join.hpp
index e346ded45..9ef6c6aec 100644
--- a/src/query/plan/rewrite/join.hpp
+++ b/src/query/plan/rewrite/join.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -455,6 +455,18 @@ class JoinRewriter final : public HierarchicalLogicalOperatorVisitor {
     return true;
   }
 
+  bool PreVisit(RollUpApply &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.list_collection_branch_);
+    return false;
+  }
+
+  bool PostVisit(RollUpApply &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
   std::shared_ptr<LogicalOperator> new_root_;
 
  private:
diff --git a/src/query/plan/rule_based_planner.cpp b/src/query/plan/rule_based_planner.cpp
index bf5e66158..54b5c3834 100644
--- a/src/query/plan/rule_based_planner.cpp
+++ b/src/query/plan/rule_based_planner.cpp
@@ -14,9 +14,12 @@
 #include <algorithm>
 #include <functional>
 #include <limits>
+#include <memory>
 #include <stack>
 #include <unordered_set>
 
+#include "query/frontend/ast/ast.hpp"
+#include "query/plan/operator.hpp"
 #include "query/plan/preprocess.hpp"
 #include "utils/algorithm.hpp"
 #include "utils/exceptions.hpp"
@@ -40,7 +43,8 @@ namespace {
 class ReturnBodyContext : public HierarchicalTreeVisitor {
  public:
   ReturnBodyContext(const ReturnBody &body, SymbolTable &symbol_table, const std::unordered_set<Symbol> &bound_symbols,
-                    AstStorage &storage, Where *where = nullptr)
+                    AstStorage &storage, std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pc_ops,
+                    Where *where = nullptr)
       : body_(body), symbol_table_(symbol_table), bound_symbols_(bound_symbols), storage_(storage), where_(where) {
     // Collect symbols from named expressions.
     output_symbols_.reserve(body_.named_expressions.size());
@@ -53,6 +57,14 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
       output_symbols_.emplace_back(symbol_table_.at(*named_expr));
       named_expr->Accept(*this);
       named_expressions_.emplace_back(named_expr);
+      if (pattern_comprehension_) {
+        if (auto it = pc_ops.find(named_expr->name_); it != pc_ops.end()) {
+          pattern_comprehension_op_ = std::move(it->second);
+          pc_ops.erase(it);
+        } else {
+          throw utils::NotYetImplemented("Operation on top of pattern comprehension");
+        }
+      }
     }
     // Collect symbols used in group by expressions.
     if (!aggregations_.empty()) {
@@ -386,8 +398,20 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
     return true;
   }
 
-  bool PostVisit(PatternComprehension & /*unused*/) override {
-    throw utils::NotYetImplemented("Planner can not handle pattern comprehension.");
+  bool PreVisit(PatternComprehension & /*unused*/) override {
+    pattern_compression_aggregations_start_index_ = has_aggregation_.size();
+    return true;
+  }
+
+  bool PostVisit(PatternComprehension &pattern_comprehension) override {
+    bool has_aggr = false;
+    for (auto i = has_aggregation_.size(); i > pattern_compression_aggregations_start_index_; --i) {
+      has_aggr |= has_aggregation_.back();
+      has_aggregation_.pop_back();
+    }
+    has_aggregation_.emplace_back(has_aggr);
+    pattern_comprehension_ = &pattern_comprehension;
+    return true;
   }
 
   // Creates NamedExpression with an Identifier for each user declared symbol.
@@ -444,6 +468,10 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
   // named_expressions.
   const auto &output_symbols() const { return output_symbols_; }
 
+  const auto *pattern_comprehension() const { return pattern_comprehension_; }
+
+  std::shared_ptr<LogicalOperator> pattern_comprehension_op() const { return pattern_comprehension_op_; }
+
  private:
   const ReturnBody &body_;
   SymbolTable &symbol_table_;
@@ -465,10 +493,13 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
   //                      group by it.
   std::list<bool> has_aggregation_;
   std::vector<NamedExpression *> named_expressions_;
+  PatternComprehension *pattern_comprehension_ = nullptr;
+  std::shared_ptr<LogicalOperator> pattern_comprehension_op_;
+  size_t pattern_compression_aggregations_start_index_ = 0;
 };
 
 std::unique_ptr<LogicalOperator> GenReturnBody(std::unique_ptr<LogicalOperator> input_op, bool advance_command,
-                                               const ReturnBodyContext &body, bool accumulate = false) {
+                                               const ReturnBodyContext &body, bool accumulate) {
   std::vector<Symbol> used_symbols(body.used_symbols().begin(), body.used_symbols().end());
   auto last_op = std::move(input_op);
   if (accumulate) {
@@ -482,6 +513,11 @@ std::unique_ptr<LogicalOperator> GenReturnBody(std::unique_ptr<LogicalOperator>
     std::vector<Symbol> remember(body.group_by_used_symbols().begin(), body.group_by_used_symbols().end());
     last_op = std::make_unique<Aggregate>(std::move(last_op), body.aggregations(), body.group_by(), remember);
   }
+
+  if (body.pattern_comprehension()) {
+    last_op = std::make_unique<RollUpApply>(std::move(last_op), body.pattern_comprehension_op());
+  }
+
   last_op = std::make_unique<Produce>(std::move(last_op), body.named_expressions());
   // Distinct in ReturnBody only makes Produce values unique, so plan after it.
   if (body.distinct()) {
@@ -506,6 +542,7 @@ std::unique_ptr<LogicalOperator> GenReturnBody(std::unique_ptr<LogicalOperator>
     last_op = std::make_unique<Filter>(std::move(last_op), std::vector<std::shared_ptr<LogicalOperator>>{},
                                        body.where()->expression_);
   }
+
   return last_op;
 }
 
@@ -543,8 +580,9 @@ Expression *ExtractFilters(const std::unordered_set<Symbol> &bound_symbols, Filt
   return filter_expr;
 }
 
-std::unordered_set<Symbol> GetSubqueryBoundSymbols(const std::vector<SingleQueryPart> &single_query_parts,
-                                                   SymbolTable &symbol_table, AstStorage &storage) {
+std::unordered_set<Symbol> GetSubqueryBoundSymbols(
+    const std::vector<SingleQueryPart> &single_query_parts, SymbolTable &symbol_table, AstStorage &storage,
+    std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pc_ops) {
   const auto &query = single_query_parts[0];
 
   if (!query.matching.expansions.empty() || query.remaining_clauses.empty()) {
@@ -552,7 +590,7 @@ std::unordered_set<Symbol> GetSubqueryBoundSymbols(const std::vector<SingleQuery
   }
 
   if (std::unordered_set<Symbol> bound_symbols; auto *with = utils::Downcast<query::With>(query.remaining_clauses[0])) {
-    auto input_op = impl::GenWith(*with, nullptr, symbol_table, false, bound_symbols, storage);
+    auto input_op = impl::GenWith(*with, nullptr, symbol_table, false, bound_symbols, storage, pc_ops);
     return bound_symbols;
   }
 
@@ -583,7 +621,8 @@ std::unique_ptr<LogicalOperator> GenNamedPaths(std::unique_ptr<LogicalOperator>
 
 std::unique_ptr<LogicalOperator> GenReturn(Return &ret, std::unique_ptr<LogicalOperator> input_op,
                                            SymbolTable &symbol_table, bool is_write,
-                                           const std::unordered_set<Symbol> &bound_symbols, AstStorage &storage) {
+                                           const std::unordered_set<Symbol> &bound_symbols, AstStorage &storage,
+                                           std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pc_ops) {
   // Similar to WITH clause, but we want to accumulate when the query writes to
   // the database. This way we handle the case when we want to return
   // expressions with the latest updated results. For example, `MATCH (n) -- ()
@@ -592,13 +631,14 @@ std::unique_ptr<LogicalOperator> GenReturn(Return &ret, std::unique_ptr<LogicalO
   // final result of 'k' increments.
   bool accumulate = is_write;
   bool advance_command = false;
-  ReturnBodyContext body(ret.body_, symbol_table, bound_symbols, storage);
+  ReturnBodyContext body(ret.body_, symbol_table, bound_symbols, storage, pc_ops);
   return GenReturnBody(std::move(input_op), advance_command, body, accumulate);
 }
 
 std::unique_ptr<LogicalOperator> GenWith(With &with, std::unique_ptr<LogicalOperator> input_op,
                                          SymbolTable &symbol_table, bool is_write,
-                                         std::unordered_set<Symbol> &bound_symbols, AstStorage &storage) {
+                                         std::unordered_set<Symbol> &bound_symbols, AstStorage &storage,
+                                         std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pc_ops) {
   // WITH clause is Accumulate/Aggregate (advance_command) + Produce and
   // optional Filter. In case of update and aggregation, we want to accumulate
   // first, so that when aggregating, we get the latest results. Similar to
@@ -606,7 +646,7 @@ std::unique_ptr<LogicalOperator> GenWith(With &with, std::unique_ptr<LogicalOper
   bool accumulate = is_write;
   // No need to advance the command if we only performed reads.
   bool advance_command = is_write;
-  ReturnBodyContext body(with.body_, symbol_table, bound_symbols, storage, with.where_);
+  ReturnBodyContext body(with.body_, symbol_table, bound_symbols, storage, pc_ops, with.where_);
   auto last_op = GenReturnBody(std::move(input_op), advance_command, body, accumulate);
   // Reset bound symbols, so that only those in WITH are exposed.
   bound_symbols.clear();
diff --git a/src/query/plan/rule_based_planner.hpp b/src/query/plan/rule_based_planner.hpp
index 7fba3b623..27f46e764 100644
--- a/src/query/plan/rule_based_planner.hpp
+++ b/src/query/plan/rule_based_planner.hpp
@@ -21,6 +21,7 @@
 #include "query/frontend/ast/ast_visitor.hpp"
 #include "query/plan/operator.hpp"
 #include "query/plan/preprocess.hpp"
+#include "utils/exceptions.hpp"
 #include "utils/logging.hpp"
 #include "utils/typeinfo.hpp"
 
@@ -87,8 +88,9 @@ bool HasBoundFilterSymbols(const std::unordered_set<Symbol> &bound_symbols, cons
 
 // Returns the set of symbols for the subquery that are actually referenced from the outer scope and
 // used in the subquery.
-std::unordered_set<Symbol> GetSubqueryBoundSymbols(const std::vector<SingleQueryPart> &single_query_parts,
-                                                   SymbolTable &symbol_table, AstStorage &storage);
+std::unordered_set<Symbol> GetSubqueryBoundSymbols(
+    const std::vector<SingleQueryPart> &single_query_parts, SymbolTable &symbol_table, AstStorage &storage,
+    std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pc_ops);
 
 Symbol GetSymbol(NodeAtom *atom, const SymbolTable &symbol_table);
 Symbol GetSymbol(EdgeAtom *atom, const SymbolTable &symbol_table);
@@ -142,11 +144,13 @@ std::unique_ptr<LogicalOperator> GenNamedPaths(std::unique_ptr<LogicalOperator>
 
 std::unique_ptr<LogicalOperator> GenReturn(Return &ret, std::unique_ptr<LogicalOperator> input_op,
                                            SymbolTable &symbol_table, bool is_write,
-                                           const std::unordered_set<Symbol> &bound_symbols, AstStorage &storage);
+                                           const std::unordered_set<Symbol> &bound_symbols, AstStorage &storage,
+                                           std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pc_ops);
 
 std::unique_ptr<LogicalOperator> GenWith(With &with, std::unique_ptr<LogicalOperator> input_op,
                                          SymbolTable &symbol_table, bool is_write,
-                                         std::unordered_set<Symbol> &bound_symbols, AstStorage &storage);
+                                         std::unordered_set<Symbol> &bound_symbols, AstStorage &storage,
+                                         std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pc_ops);
 
 std::unique_ptr<LogicalOperator> GenUnion(const CypherUnion &cypher_union, std::shared_ptr<LogicalOperator> left_op,
                                           std::shared_ptr<LogicalOperator> right_op, SymbolTable &symbol_table);
@@ -190,11 +194,24 @@ class RuleBasedPlanner {
         uint64_t merge_id = 0;
         uint64_t subquery_id = 0;
 
+        std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pattern_comprehension_ops;
+
+        if (single_query_part.pattern_comprehension_matchings.size() > 1) {
+          throw utils::NotYetImplemented("Multiple pattern comprehensions.");
+        }
+        for (const auto &matching : single_query_part.pattern_comprehension_matchings) {
+          std::unique_ptr<LogicalOperator> new_input;
+          MatchContext match_ctx{matching.second, *context.symbol_table, context.bound_symbols};
+          new_input = PlanMatching(match_ctx, std::move(new_input));
+          new_input = std::make_unique<Produce>(std::move(new_input), std::vector{matching.second.result_expr});
+          pattern_comprehension_ops.emplace(matching.first, std::move(new_input));
+        }
+
         for (const auto &clause : single_query_part.remaining_clauses) {
           MG_ASSERT(!utils::IsSubtype(*clause, Match::kType), "Unexpected Match in remaining clauses");
           if (auto *ret = utils::Downcast<Return>(clause)) {
             input_op = impl::GenReturn(*ret, std::move(input_op), *context.symbol_table, context.is_write_query,
-                                       context.bound_symbols, *context.ast_storage);
+                                       context.bound_symbols, *context.ast_storage, pattern_comprehension_ops);
           } else if (auto *merge = utils::Downcast<query::Merge>(clause)) {
             input_op = GenMerge(*merge, std::move(input_op), single_query_part.merge_matching[merge_id++]);
             // Treat MERGE clause as write, because we do not know if it will
@@ -202,7 +219,7 @@ class RuleBasedPlanner {
             context.is_write_query = true;
           } else if (auto *with = utils::Downcast<query::With>(clause)) {
             input_op = impl::GenWith(*with, std::move(input_op), *context.symbol_table, context.is_write_query,
-                                     context.bound_symbols, *context.ast_storage);
+                                     context.bound_symbols, *context.ast_storage, pattern_comprehension_ops);
             // WITH clause advances the command, so reset the flag.
             context.is_write_query = false;
           } else if (auto op = HandleWriteClause(clause, input_op, *context.symbol_table, context.bound_symbols)) {
@@ -241,7 +258,7 @@ class RuleBasedPlanner {
                                            single_query_part, merge_id);
           } else if (auto *call_sub = utils::Downcast<query::CallSubquery>(clause)) {
             input_op = HandleSubquery(std::move(input_op), single_query_part.subqueries[subquery_id++],
-                                      *context.symbol_table, *context_->ast_storage);
+                                      *context.symbol_table, *context_->ast_storage, pattern_comprehension_ops);
           } else {
             throw utils::NotYetImplemented("clause '{}' conversion to operator(s)", clause->GetTypeInfo().name);
           }
@@ -860,15 +877,15 @@ class RuleBasedPlanner {
                                            symbol);
   }
 
-  std::unique_ptr<LogicalOperator> HandleSubquery(std::unique_ptr<LogicalOperator> last_op,
-                                                  std::shared_ptr<QueryParts> subquery, SymbolTable &symbol_table,
-                                                  AstStorage &storage) {
+  std::unique_ptr<LogicalOperator> HandleSubquery(
+      std::unique_ptr<LogicalOperator> last_op, std::shared_ptr<QueryParts> subquery, SymbolTable &symbol_table,
+      AstStorage &storage, std::unordered_map<std::string, std::shared_ptr<LogicalOperator>> pc_ops) {
     std::unordered_set<Symbol> outer_scope_bound_symbols;
     outer_scope_bound_symbols.insert(std::make_move_iterator(context_->bound_symbols.begin()),
                                      std::make_move_iterator(context_->bound_symbols.end()));
 
     context_->bound_symbols =
-        impl::GetSubqueryBoundSymbols(subquery->query_parts[0].single_query_parts, symbol_table, storage);
+        impl::GetSubqueryBoundSymbols(subquery->query_parts[0].single_query_parts, symbol_table, storage, pc_ops);
 
     auto subquery_op = Plan(*subquery);
 
diff --git a/src/query/plan/vertex_count_cache.hpp b/src/query/plan/vertex_count_cache.hpp
index 4cfb2486b..69e002c0a 100644
--- a/src/query/plan/vertex_count_cache.hpp
+++ b/src/query/plan/vertex_count_cache.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -78,6 +78,8 @@ class VertexCountCache {
     return db_->LabelPropertyIndexExists(label, property);
   }
 
+  bool EdgeTypeIndexExists(storage::EdgeTypeId edge_type) { return db_->EdgeTypeIndexExists(edge_type); }
+
   std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId &label) const {
     return db_->GetIndexStats(label);
   }
diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp
index 647f3e14d..d6ce3c7b7 100644
--- a/src/query/procedure/mg_procedure_impl.cpp
+++ b/src/query/procedure/mg_procedure_impl.cpp
@@ -3798,7 +3798,7 @@ void PrintFuncSignature(const mgp_func &func, std::ostream &stream) {
 
 bool IsValidIdentifierName(const char *name) {
   if (!name) return false;
-  std::regex regex("[_[:alpha:]][_[:alnum:]]*");
+  static std::regex regex("[_[:alpha:]][_[:alnum:]]*");
   return std::regex_match(name, regex);
 }
 
diff --git a/src/query/procedure/module.hpp b/src/query/procedure/module.hpp
index 41cda0ca6..f5027dafa 100644
--- a/src/query/procedure/module.hpp
+++ b/src/query/procedure/module.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/src/query/procedure/py_module.hpp b/src/query/procedure/py_module.hpp
index 9cb22fe2c..fe93b5c51 100644
--- a/src/query/procedure/py_module.hpp
+++ b/src/query/procedure/py_module.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/src/replication_coordination_glue/CMakeLists.txt b/src/replication_coordination_glue/CMakeLists.txt
index f81aed4ba..f452e1c1f 100644
--- a/src/replication_coordination_glue/CMakeLists.txt
+++ b/src/replication_coordination_glue/CMakeLists.txt
@@ -7,6 +7,7 @@ target_sources(mg-repl_coord_glue
         mode.hpp
         role.hpp
         handler.hpp
+        common.hpp
 
         PRIVATE
         messages.cpp
diff --git a/src/replication_coordination_glue/common.hpp b/src/replication_coordination_glue/common.hpp
new file mode 100644
index 000000000..439e5cae8
--- /dev/null
+++ b/src/replication_coordination_glue/common.hpp
@@ -0,0 +1,32 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include "rpc/client.hpp"
+#include "utils/uuid.hpp"
+
+#include <deque>
+#include "messages.hpp"
+#include "rpc/messages.hpp"
+#include "utils/uuid.hpp"
+
+namespace memgraph::replication_coordination_glue {
+
+struct DatabaseHistory {
+  memgraph::utils::UUID db_uuid;
+  std::vector<std::pair<std::string, uint64_t>> history;
+  std::string name;
+};
+
+using DatabaseHistories = std::vector<DatabaseHistory>;
+
+}  // namespace memgraph::replication_coordination_glue
diff --git a/src/replication_coordination_glue/mode.hpp b/src/replication_coordination_glue/mode.hpp
index d0b415733..4ca98b3a0 100644
--- a/src/replication_coordination_glue/mode.hpp
+++ b/src/replication_coordination_glue/mode.hpp
@@ -12,7 +12,19 @@
 #pragma once
 
 #include <cstdint>
+#include <map>
+#include <stdexcept>
+#include <string>
+
+#include "json/json.hpp"
 
 namespace memgraph::replication_coordination_glue {
+
 enum class ReplicationMode : std::uint8_t { SYNC, ASYNC };
+
+NLOHMANN_JSON_SERIALIZE_ENUM(ReplicationMode, {
+                                                  {ReplicationMode::SYNC, "sync"},
+                                                  {ReplicationMode::ASYNC, "async"},
+                                              })
+
 }  // namespace memgraph::replication_coordination_glue
diff --git a/src/replication_coordination_glue/role.hpp b/src/replication_coordination_glue/role.hpp
index d472cb454..3fbf522ba 100644
--- a/src/replication_coordination_glue/role.hpp
+++ b/src/replication_coordination_glue/role.hpp
@@ -12,8 +12,14 @@
 #pragma once
 
 #include <cstdint>
+
+#include "json/json.hpp"
+
 namespace memgraph::replication_coordination_glue {
 
 // TODO: figure out a way of ensuring that usage of this type is never uninitialed/defaulted incorrectly to MAIN
 enum class ReplicationRole : uint8_t { MAIN, REPLICA };
+
+NLOHMANN_JSON_SERIALIZE_ENUM(ReplicationRole, {{ReplicationRole::MAIN, "main"}, {ReplicationRole::REPLICA, "replica"}})
+
 }  // namespace memgraph::replication_coordination_glue
diff --git a/src/replication_handler/include/replication_handler/replication_handler.hpp b/src/replication_handler/include/replication_handler/replication_handler.hpp
index b110e6015..e1da19bfa 100644
--- a/src/replication_handler/include/replication_handler/replication_handler.hpp
+++ b/src/replication_handler/include/replication_handler/replication_handler.hpp
@@ -14,6 +14,7 @@
 #include "dbms/dbms_handler.hpp"
 #include "flags/experimental.hpp"
 #include "replication/include/replication/state.hpp"
+#include "replication_coordination_glue/common.hpp"
 #include "replication_handler/system_replication.hpp"
 #include "replication_handler/system_rpc.hpp"
 #include "utils/result.hpp"
@@ -149,6 +150,8 @@ struct ReplicationHandler : public memgraph::query::ReplicationQueryHandler {
 
   auto GetReplicaUUID() -> std::optional<utils::UUID>;
 
+  auto GetDatabasesHistories() -> replication_coordination_glue::DatabaseHistories;
+
  private:
   template <bool SendSwapUUID>
   auto RegisterReplica_(const memgraph::replication::ReplicationClientConfig &config)
@@ -207,8 +210,13 @@ struct ReplicationHandler : public memgraph::query::ReplicationQueryHandler {
             auto client = std::make_unique<storage::ReplicationStorageClient>(*instance_client_ptr, main_uuid);
             client->Start(storage, std::move(db_acc));
             bool const success = std::invoke([state = client->State()]() {
+              // We force sync replicas in other situation
               if (state == storage::replication::ReplicaState::DIVERGED_FROM_MAIN) {
+#ifdef MG_ENTERPRISE
+                return FLAGS_coordinator_server_port != 0;
+#else
                 return false;
+#endif
               }
               return true;
             });
diff --git a/src/replication_handler/replication_handler.cpp b/src/replication_handler/replication_handler.cpp
index 5f807779d..34ccdfc99 100644
--- a/src/replication_handler/replication_handler.cpp
+++ b/src/replication_handler/replication_handler.cpp
@@ -14,6 +14,7 @@
 #include "dbms/dbms_handler.hpp"
 #include "replication/replication_client.hpp"
 #include "replication_handler/system_replication.hpp"
+#include "utils/functional.hpp"
 
 namespace memgraph::replication {
 
@@ -265,8 +266,24 @@ auto ReplicationHandler::GetRole() const -> replication_coordination_glue::Repli
   return repl_state_.GetRole();
 }
 
+auto ReplicationHandler::GetDatabasesHistories() -> replication_coordination_glue::DatabaseHistories {
+  replication_coordination_glue::DatabaseHistories results;
+  dbms_handler_.ForEach([&results](memgraph::dbms::DatabaseAccess db_acc) {
+    auto &repl_storage_state = db_acc->storage()->repl_storage_state_;
+
+    std::vector<std::pair<std::string, uint64_t>> history = utils::fmap(repl_storage_state.history);
+
+    history.emplace_back(std::string(repl_storage_state.epoch_.id()), repl_storage_state.last_commit_timestamp_.load());
+    replication_coordination_glue::DatabaseHistory repl{
+        .db_uuid = utils::UUID{db_acc->storage()->uuid()}, .history = history, .name = std::string(db_acc->name())};
+    results.emplace_back(repl);
+  });
+
+  return results;
+}
+
 auto ReplicationHandler::GetReplicaUUID() -> std::optional<utils::UUID> {
-  MG_ASSERT(repl_state_.IsReplica());
+  MG_ASSERT(repl_state_.IsReplica(), "Instance is not replica");
   return std::get<RoleReplicaData>(repl_state_.ReplicationData()).uuid_;
 }
 
diff --git a/src/storage/v2/CMakeLists.txt b/src/storage/v2/CMakeLists.txt
index 150a02cc7..ec5108d63 100644
--- a/src/storage/v2/CMakeLists.txt
+++ b/src/storage/v2/CMakeLists.txt
@@ -21,8 +21,10 @@ add_library(mg-storage-v2 STATIC
         storage.cpp
         indices/indices.cpp
         all_vertices_iterable.cpp
+        edges_iterable.cpp
         vertices_iterable.cpp
         inmemory/storage.cpp
+        inmemory/edge_type_index.cpp
         inmemory/label_index.cpp
         inmemory/label_property_index.cpp
         inmemory/unique_constraints.cpp
@@ -30,6 +32,7 @@ add_library(mg-storage-v2 STATIC
         disk/edge_import_mode_cache.cpp
         disk/storage.cpp
         disk/rocksdb_storage.cpp
+        disk/edge_type_index.cpp
         disk/label_index.cpp
         disk/label_property_index.cpp
         disk/unique_constraints.cpp
diff --git a/src/storage/v2/delta.hpp b/src/storage/v2/delta.hpp
index 9c70bdc4c..60d589dcd 100644
--- a/src/storage/v2/delta.hpp
+++ b/src/storage/v2/delta.hpp
@@ -123,6 +123,26 @@ inline bool operator==(const PreviousPtr::Pointer &a, const PreviousPtr::Pointer
 
 inline bool operator!=(const PreviousPtr::Pointer &a, const PreviousPtr::Pointer &b) { return !(a == b); }
 
+struct opt_str {
+  opt_str(std::optional<std::string> const &other) : str_{other ? new_cstr(*other) : nullptr} {}
+
+  ~opt_str() { delete[] str_; }
+
+  auto as_opt_str() const -> std::optional<std::string> {
+    if (!str_) return std::nullopt;
+    return std::optional<std::string>{std::in_place, str_};
+  }
+
+ private:
+  static auto new_cstr(std::string const &str) -> char const * {
+    auto *mem = new char[str.length() + 1];
+    strcpy(mem, str.c_str());
+    return mem;
+  }
+
+  char const *str_ = nullptr;
+};
+
 struct Delta {
   enum class Action : std::uint8_t {
     /// Use for Vertex and Edge
@@ -160,7 +180,7 @@ struct Delta {
   // Because of this object was created in past txs, we create timestamp by ourselves inside instead of having it from
   // current tx. This timestamp we got from RocksDB timestamp stored in key.
   Delta(DeleteDeserializedObjectTag /*tag*/, uint64_t ts, std::optional<std::string> old_disk_key)
-      : timestamp(new std::atomic<uint64_t>(ts)), command_id(0), old_disk_key{.value = std::move(old_disk_key)} {}
+      : timestamp(new std::atomic<uint64_t>(ts)), command_id(0), old_disk_key{.value = old_disk_key} {}
 
   Delta(DeleteObjectTag /*tag*/, std::atomic<uint64_t> *timestamp, uint64_t command_id)
       : timestamp(timestamp), command_id(command_id), action(Action::DELETE_OBJECT) {}
@@ -222,7 +242,7 @@ struct Delta {
       case Action::REMOVE_OUT_EDGE:
         break;
       case Action::DELETE_DESERIALIZED_OBJECT:
-        old_disk_key.value.reset();
+        std::destroy_at(&old_disk_key.value);
         delete timestamp;
         timestamp = nullptr;
         break;
@@ -242,7 +262,7 @@ struct Delta {
     Action action;
     struct {
       Action action = Action::DELETE_DESERIALIZED_OBJECT;
-      std::optional<std::string> value;
+      opt_str value;
     } old_disk_key;
     struct {
       Action action;
diff --git a/src/storage/v2/disk/edge_type_index.cpp b/src/storage/v2/disk/edge_type_index.cpp
new file mode 100644
index 000000000..d11eb6caf
--- /dev/null
+++ b/src/storage/v2/disk/edge_type_index.cpp
@@ -0,0 +1,49 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include "edge_type_index.hpp"
+
+#include "utils/exceptions.hpp"
+
+namespace memgraph::storage {
+
+bool DiskEdgeTypeIndex::DropIndex(EdgeTypeId /*edge_type*/) {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return true;
+}
+
+bool DiskEdgeTypeIndex::IndexExists(EdgeTypeId /*edge_type*/) const {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return false;
+}
+
+std::vector<EdgeTypeId> DiskEdgeTypeIndex::ListIndices() const {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return {};
+}
+
+uint64_t DiskEdgeTypeIndex::ApproximateEdgeCount(EdgeTypeId /*edge_type*/) const {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return 0U;
+}
+
+void DiskEdgeTypeIndex::UpdateOnEdgeCreation(Vertex * /*from*/, Vertex * /*to*/, EdgeRef /*edge_ref*/,
+                                             EdgeTypeId /*edge_type*/, const Transaction & /*tx*/) {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
+void DiskEdgeTypeIndex::UpdateOnEdgeModification(Vertex * /*old_from*/, Vertex * /*old_to*/, Vertex * /*new_from*/,
+                                                 Vertex * /*new_to*/, EdgeRef /*edge_ref*/, EdgeTypeId /*edge_type*/,
+                                                 const Transaction & /*tx*/) {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/disk/edge_type_index.hpp b/src/storage/v2/disk/edge_type_index.hpp
new file mode 100644
index 000000000..fe79b2690
--- /dev/null
+++ b/src/storage/v2/disk/edge_type_index.hpp
@@ -0,0 +1,35 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include "storage/v2/indices/edge_type_index.hpp"
+
+namespace memgraph::storage {
+
+class DiskEdgeTypeIndex : public storage::EdgeTypeIndex {
+ public:
+  bool DropIndex(EdgeTypeId edge_type) override;
+
+  bool IndexExists(EdgeTypeId edge_type) const override;
+
+  std::vector<EdgeTypeId> ListIndices() const override;
+
+  uint64_t ApproximateEdgeCount(EdgeTypeId edge_type) const override;
+
+  void UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                            const Transaction &tx) override;
+
+  void UpdateOnEdgeModification(Vertex *old_from, Vertex *old_to, Vertex *new_from, Vertex *new_to, EdgeRef edge_ref,
+                                EdgeTypeId edge_type, const Transaction &tx) override;
+};
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/disk/storage.cpp b/src/storage/v2/disk/storage.cpp
index f9cd2ac13..21ae7755e 100644
--- a/src/storage/v2/disk/storage.cpp
+++ b/src/storage/v2/disk/storage.cpp
@@ -41,6 +41,7 @@
 #include "storage/v2/edge_accessor.hpp"
 #include "storage/v2/edge_import_mode.hpp"
 #include "storage/v2/edge_ref.hpp"
+#include "storage/v2/edges_iterable.hpp"
 #include "storage/v2/id_types.hpp"
 #include "storage/v2/modified_edge.hpp"
 #include "storage/v2/mvcc.hpp"
@@ -807,11 +808,21 @@ void DiskStorage::LoadVerticesFromDiskLabelPropertyIndexForIntervalSearch(
   }
 }
 
+EdgesIterable DiskStorage::DiskAccessor::Edges(EdgeTypeId /*edge_type*/, View /*view*/) {
+  throw utils::NotYetImplemented(
+      "Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
 uint64_t DiskStorage::DiskAccessor::ApproximateVertexCount() const {
   auto *disk_storage = static_cast<DiskStorage *>(storage_);
   return disk_storage->vertex_count_.load(std::memory_order_acquire);
 }
 
+uint64_t DiskStorage::DiskAccessor::ApproximateEdgeCount(EdgeTypeId /*edge_type*/) const {
+  spdlog::info("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return 0U;
+}
+
 uint64_t DiskStorage::GetDiskSpaceUsage() const {
   uint64_t main_disk_storage_size = utils::GetDirDiskUsage(config_.disk.main_storage_directory);
   uint64_t index_disk_storage_size = utils::GetDirDiskUsage(config_.disk.label_index_directory) +
@@ -1629,6 +1640,9 @@ utils::BasicResult<StorageManipulationError, void> DiskStorage::DiskAccessor::Co
             return StorageManipulationError{PersistenceError{}};
           }
         } break;
+        case MetadataDelta::Action::EDGE_INDEX_CREATE: {
+          throw utils::NotYetImplemented("Edge-type indexing is not yet implemented on on-disk storage mode.");
+        }
         case MetadataDelta::Action::LABEL_INDEX_DROP: {
           if (!disk_storage->durable_metadata_.PersistLabelIndexDeletion(md_delta.label)) {
             return StorageManipulationError{PersistenceError{}};
@@ -1641,6 +1655,9 @@ utils::BasicResult<StorageManipulationError, void> DiskStorage::DiskAccessor::Co
             return StorageManipulationError{PersistenceError{}};
           }
         } break;
+        case MetadataDelta::Action::EDGE_INDEX_DROP: {
+          throw utils::NotYetImplemented("Edge-type indexing is not yet implemented on on-disk storage mode.");
+        }
         case MetadataDelta::Action::LABEL_INDEX_STATS_SET: {
           throw utils::NotYetImplemented("SetIndexStats(stats) is not implemented for DiskStorage.");
         } break;
@@ -1917,6 +1934,11 @@ utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor:
   return {};
 }
 
+utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor::CreateIndex(EdgeTypeId /*edge_type*/) {
+  throw utils::NotYetImplemented(
+      "Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
 utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor::DropIndex(LabelId label) {
   MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!");
   auto *on_disk = static_cast<DiskStorage *>(storage_);
@@ -1945,6 +1967,11 @@ utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor:
   return {};
 }
 
+utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor::DropIndex(EdgeTypeId /*edge_type*/) {
+  throw utils::NotYetImplemented(
+      "Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
 utils::BasicResult<StorageExistenceConstraintDefinitionError, void>
 DiskStorage::DiskAccessor::CreateExistenceConstraint(LabelId label, PropertyId property) {
   MG_ASSERT(unique_guard_.owns_lock(), "Create existence constraint requires a unique access to the storage!");
@@ -2053,6 +2080,12 @@ std::unique_ptr<Storage::Accessor> DiskStorage::UniqueAccess(
   return std::unique_ptr<DiskAccessor>(
       new DiskAccessor{Storage::Accessor::unique_access, this, isolation_level, storage_mode_});
 }
+
+bool DiskStorage::DiskAccessor::EdgeTypeIndexExists(EdgeTypeId /*edge_type*/) const {
+  spdlog::info("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return false;
+}
+
 IndicesInfo DiskStorage::DiskAccessor::ListAllIndices() const {
   auto *on_disk = static_cast<DiskStorage *>(storage_);
   auto *disk_label_index = static_cast<DiskLabelIndex *>(on_disk->indices_.label_index_.get());
diff --git a/src/storage/v2/disk/storage.hpp b/src/storage/v2/disk/storage.hpp
index ea2e6714e..f7815ae58 100644
--- a/src/storage/v2/disk/storage.hpp
+++ b/src/storage/v2/disk/storage.hpp
@@ -72,6 +72,8 @@ class DiskStorage final : public Storage {
                               const std::optional<utils::Bound<PropertyValue>> &lower_bound,
                               const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) override;
 
+    EdgesIterable Edges(EdgeTypeId edge_type, View view) override;
+
     uint64_t ApproximateVertexCount() const override;
 
     uint64_t ApproximateVertexCount(LabelId /*label*/) const override { return 10; }
@@ -89,6 +91,8 @@ class DiskStorage final : public Storage {
       return 10;
     }
 
+    uint64_t ApproximateEdgeCount(EdgeTypeId edge_type) const override;
+
     std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId & /*label*/) const override {
       return {};
     }
@@ -140,6 +144,8 @@ class DiskStorage final : public Storage {
       return disk_storage->indices_.label_property_index_->IndexExists(label, property);
     }
 
+    bool EdgeTypeIndexExists(EdgeTypeId edge_type) const override;
+
     IndicesInfo ListAllIndices() const override;
 
     ConstraintsInfo ListAllConstraints() const override;
@@ -158,10 +164,14 @@ class DiskStorage final : public Storage {
 
     utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(LabelId label, PropertyId property) override;
 
+    utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(EdgeTypeId edge_type) override;
+
     utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label) override;
 
     utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label, PropertyId property) override;
 
+    utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(EdgeTypeId edge_type) override;
+
     utils::BasicResult<StorageExistenceConstraintDefinitionError, void> CreateExistenceConstraint(
         LabelId label, PropertyId property) override;
 
@@ -312,7 +322,7 @@ class DiskStorage final : public Storage {
   StorageInfo GetBaseInfo() override;
   StorageInfo GetInfo(memgraph::replication_coordination_glue::ReplicationRole replication_role) override;
 
-  void FreeMemory(std::unique_lock<utils::ResourceLock> /*lock*/) override {}
+  void FreeMemory(std::unique_lock<utils::ResourceLock> /*lock*/, bool /*periodic*/) override {}
 
   void PrepareForNewEpoch() override { throw utils::BasicException("Disk storage mode does not support replication."); }
 
diff --git a/src/storage/v2/durability/durability.cpp b/src/storage/v2/durability/durability.cpp
index 92c4d11e8..fbbedbee5 100644
--- a/src/storage/v2/durability/durability.cpp
+++ b/src/storage/v2/durability/durability.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -31,6 +31,7 @@
 #include "storage/v2/durability/paths.hpp"
 #include "storage/v2/durability/snapshot.hpp"
 #include "storage/v2/durability/wal.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
 #include "storage/v2/inmemory/label_index.hpp"
 #include "storage/v2/inmemory/label_property_index.hpp"
 #include "storage/v2/inmemory/unique_constraints.hpp"
@@ -118,6 +119,8 @@ std::optional<std::vector<WalDurabilityInfo>> GetWalFiles(const std::filesystem:
     if (!item.is_regular_file()) continue;
     try {
       auto info = ReadWalInfo(item.path());
+      spdlog::trace("Getting wal file with following info: uuid: {}, epoch id: {}, from timestamp {}, to_timestamp {} ",
+                    info.uuid, info.epoch_id, info.from_timestamp, info.to_timestamp);
       if ((uuid.empty() || info.uuid == uuid) && (!current_seq_num || info.seq_num < *current_seq_num)) {
         wal_files.emplace_back(info.seq_num, info.from_timestamp, info.to_timestamp, std::move(info.uuid),
                                std::move(info.epoch_id), item.path());
@@ -197,9 +200,18 @@ void RecoverIndicesAndStats(const RecoveredIndicesAndConstraints::IndicesMetadat
   }
   spdlog::info("Label+property indices statistics are recreated.");
 
-  spdlog::info("Indices are recreated.");
+  // Recover edge-type indices.
+  spdlog::info("Recreating {} edge-type indices from metadata.", indices_metadata.edge.size());
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(indices->edge_type_index_.get());
+  for (const auto &item : indices_metadata.edge) {
+    if (!mem_edge_type_index->CreateIndex(item, vertices->access())) {
+      throw RecoveryFailure("The edge-type index must be created here!");
+    }
+    spdlog::info("Index on :{} is recreated from metadata", name_id_mapper->IdToName(item.AsUint()));
+  }
+  spdlog::info("Edge-type indices are recreated.");
 
-  spdlog::info("Recreating constraints from metadata.");
+  spdlog::info("Indices are recreated.");
 }
 
 void RecoverExistenceConstraints(const RecoveredIndicesAndConstraints::ConstraintsMetadata &constraints_metadata,
@@ -410,22 +422,17 @@ std::optional<RecoveryInfo> Recovery::RecoverData(std::string *uuid, Replication
     std::optional<uint64_t> previous_seq_num;
     auto last_loaded_timestamp = snapshot_timestamp;
     spdlog::info("Trying to load WAL files.");
+
+    if (last_loaded_timestamp) {
+      epoch_history->emplace_back(repl_storage_state.epoch_.id(), *last_loaded_timestamp);
+    }
+
     for (auto &wal_file : wal_files) {
       if (previous_seq_num && (wal_file.seq_num - *previous_seq_num) > 1) {
         LOG_FATAL("You are missing a WAL file with the sequence number {}!", *previous_seq_num + 1);
       }
       previous_seq_num = wal_file.seq_num;
 
-      if (wal_file.epoch_id != repl_storage_state.epoch_.id()) {
-        // This way we skip WALs finalized only because of role change.
-        // We can also set the last timestamp to 0 if last loaded timestamp
-        // is nullopt as this can only happen if the WAL file with seq = 0
-        // does not contain any deltas and we didn't find any snapshots.
-        if (last_loaded_timestamp) {
-          epoch_history->emplace_back(wal_file.epoch_id, *last_loaded_timestamp);
-        }
-        repl_storage_state.epoch_.SetEpoch(std::move(wal_file.epoch_id));
-      }
       try {
         auto info = LoadWal(wal_file.path, &indices_constraints, last_loaded_timestamp, vertices, edges, name_id_mapper,
                             edge_count, config.salient.items);
@@ -434,13 +441,24 @@ std::optional<RecoveryInfo> Recovery::RecoverData(std::string *uuid, Replication
         recovery_info.next_timestamp = std::max(recovery_info.next_timestamp, info.next_timestamp);
 
         recovery_info.last_commit_timestamp = info.last_commit_timestamp;
+
+        if (recovery_info.next_timestamp != 0) {
+          last_loaded_timestamp.emplace(recovery_info.next_timestamp - 1);
+        }
+
+        auto last_loaded_timestamp_value = last_loaded_timestamp.value_or(0);
+        if (epoch_history->empty() || epoch_history->back().first != wal_file.epoch_id) {
+          // no history or new epoch, add it
+          epoch_history->emplace_back(wal_file.epoch_id, last_loaded_timestamp_value);
+          repl_storage_state.epoch_.SetEpoch(wal_file.epoch_id);
+        } else if (epoch_history->back().second < last_loaded_timestamp_value) {
+          // existing epoch, update with newer timestamp
+          epoch_history->back().second = last_loaded_timestamp_value;
+        }
+
       } catch (const RecoveryFailure &e) {
         LOG_FATAL("Couldn't recover WAL deltas from {} because of: {}", wal_file.path, e.what());
       }
-
-      if (recovery_info.next_timestamp != 0) {
-        last_loaded_timestamp.emplace(recovery_info.next_timestamp - 1);
-      }
     }
     // The sequence number needs to be recovered even though `LoadWal` didn't
     // load any deltas from that file.
@@ -456,7 +474,12 @@ std::optional<RecoveryInfo> Recovery::RecoverData(std::string *uuid, Replication
 
   memgraph::metrics::Measure(memgraph::metrics::SnapshotRecoveryLatency_us,
                              std::chrono::duration_cast<std::chrono::microseconds>(timer.Elapsed()).count());
+  spdlog::trace("Set epoch id: {}  with commit timestamp {}", std::string(repl_storage_state.epoch_.id()),
+                repl_storage_state.last_commit_timestamp_);
 
+  std::for_each(repl_storage_state.history.begin(), repl_storage_state.history.end(), [](auto &history) {
+    spdlog::trace("epoch id: {}  with commit timestamp {}", std::string(history.first), history.second);
+  });
   return recovery_info;
 }
 
diff --git a/src/storage/v2/durability/marker.hpp b/src/storage/v2/durability/marker.hpp
index 8f00d435d..ac0cc074d 100644
--- a/src/storage/v2/durability/marker.hpp
+++ b/src/storage/v2/durability/marker.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -37,6 +37,8 @@ enum class Marker : uint8_t {
   SECTION_CONSTRAINTS = 0x25,
   SECTION_DELTA = 0x26,
   SECTION_EPOCH_HISTORY = 0x27,
+  SECTION_EDGE_INDICES = 0x28,
+
   SECTION_OFFSETS = 0x42,
 
   DELTA_VERTEX_CREATE = 0x50,
@@ -60,6 +62,8 @@ enum class Marker : uint8_t {
   DELTA_LABEL_INDEX_STATS_CLEAR = 0x62,
   DELTA_LABEL_PROPERTY_INDEX_STATS_SET = 0x63,
   DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR = 0x64,
+  DELTA_EDGE_TYPE_INDEX_CREATE = 0x65,
+  DELTA_EDGE_TYPE_INDEX_DROP = 0x66,
 
   VALUE_FALSE = 0x00,
   VALUE_TRUE = 0xff,
@@ -85,6 +89,7 @@ static const Marker kMarkersAll[] = {
     Marker::SECTION_CONSTRAINTS,
     Marker::SECTION_DELTA,
     Marker::SECTION_EPOCH_HISTORY,
+    Marker::SECTION_EDGE_INDICES,
     Marker::SECTION_OFFSETS,
     Marker::DELTA_VERTEX_CREATE,
     Marker::DELTA_VERTEX_DELETE,
@@ -103,6 +108,8 @@ static const Marker kMarkersAll[] = {
     Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR,
     Marker::DELTA_LABEL_PROPERTY_INDEX_CREATE,
     Marker::DELTA_LABEL_PROPERTY_INDEX_DROP,
+    Marker::DELTA_EDGE_TYPE_INDEX_CREATE,
+    Marker::DELTA_EDGE_TYPE_INDEX_DROP,
     Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE,
     Marker::DELTA_EXISTENCE_CONSTRAINT_DROP,
     Marker::DELTA_UNIQUE_CONSTRAINT_CREATE,
diff --git a/src/storage/v2/durability/metadata.hpp b/src/storage/v2/durability/metadata.hpp
index 42e24e723..c8ee27b2f 100644
--- a/src/storage/v2/durability/metadata.hpp
+++ b/src/storage/v2/durability/metadata.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -43,6 +43,7 @@ struct RecoveredIndicesAndConstraints {
     std::vector<std::pair<LabelId, PropertyId>> label_property;
     std::vector<std::pair<LabelId, LabelIndexStats>> label_stats;
     std::vector<std::pair<LabelId, std::pair<PropertyId, LabelPropertyIndexStats>>> label_property_stats;
+    std::vector<EdgeTypeId> edge;
   } indices;
 
   struct ConstraintsMetadata {
diff --git a/src/storage/v2/durability/serialization.cpp b/src/storage/v2/durability/serialization.cpp
index 6b13d9d00..28ba64943 100644
--- a/src/storage/v2/durability/serialization.cpp
+++ b/src/storage/v2/durability/serialization.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -332,6 +332,7 @@ std::optional<PropertyValue> Decoder::ReadPropertyValue() {
     case Marker::SECTION_CONSTRAINTS:
     case Marker::SECTION_DELTA:
     case Marker::SECTION_EPOCH_HISTORY:
+    case Marker::SECTION_EDGE_INDICES:
     case Marker::SECTION_OFFSETS:
     case Marker::DELTA_VERTEX_CREATE:
     case Marker::DELTA_VERTEX_DELETE:
@@ -350,6 +351,8 @@ std::optional<PropertyValue> Decoder::ReadPropertyValue() {
     case Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR:
     case Marker::DELTA_LABEL_PROPERTY_INDEX_CREATE:
     case Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
+    case Marker::DELTA_EDGE_TYPE_INDEX_CREATE:
+    case Marker::DELTA_EDGE_TYPE_INDEX_DROP:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
     case Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
@@ -435,6 +438,7 @@ bool Decoder::SkipPropertyValue() {
     case Marker::SECTION_CONSTRAINTS:
     case Marker::SECTION_DELTA:
     case Marker::SECTION_EPOCH_HISTORY:
+    case Marker::SECTION_EDGE_INDICES:
     case Marker::SECTION_OFFSETS:
     case Marker::DELTA_VERTEX_CREATE:
     case Marker::DELTA_VERTEX_DELETE:
@@ -453,6 +457,8 @@ bool Decoder::SkipPropertyValue() {
     case Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR:
     case Marker::DELTA_LABEL_PROPERTY_INDEX_CREATE:
     case Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
+    case Marker::DELTA_EDGE_TYPE_INDEX_CREATE:
+    case Marker::DELTA_EDGE_TYPE_INDEX_DROP:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
     case Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
diff --git a/src/storage/v2/durability/snapshot.cpp b/src/storage/v2/durability/snapshot.cpp
index eee099870..5fea3dfa5 100644
--- a/src/storage/v2/durability/snapshot.cpp
+++ b/src/storage/v2/durability/snapshot.cpp
@@ -153,6 +153,11 @@ SnapshotInfo ReadSnapshotInfo(const std::filesystem::path &path) {
     info.offset_edges = read_offset();
     info.offset_vertices = read_offset();
     info.offset_indices = read_offset();
+    if (*version >= 17) {
+      info.offset_edge_indices = read_offset();
+    } else {
+      info.offset_edge_indices = 0U;
+    }
     info.offset_constraints = read_offset();
     info.offset_mapper = read_offset();
     info.offset_epoch_history = read_offset();
@@ -1379,10 +1384,11 @@ RecoveredSnapshot LoadSnapshotVersion15(const std::filesystem::path &path, utils
   return {info, recovery_info, std::move(indices_constraints)};
 }
 
-RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipList<Vertex> *vertices,
-                               utils::SkipList<Edge> *edges,
-                               std::deque<std::pair<std::string, uint64_t>> *epoch_history,
-                               NameIdMapper *name_id_mapper, std::atomic<uint64_t> *edge_count, const Config &config) {
+RecoveredSnapshot LoadSnapshotVersion16(const std::filesystem::path &path, utils::SkipList<Vertex> *vertices,
+                                        utils::SkipList<Edge> *edges,
+                                        std::deque<std::pair<std::string, uint64_t>> *epoch_history,
+                                        NameIdMapper *name_id_mapper, std::atomic<uint64_t> *edge_count,
+                                        const Config &config) {
   RecoveryInfo recovery_info;
   RecoveredIndicesAndConstraints indices_constraints;
 
@@ -1391,13 +1397,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
   if (!version) throw RecoveryFailure("Couldn't read snapshot magic and/or version!");
 
   if (!IsVersionSupported(*version)) throw RecoveryFailure(fmt::format("Invalid snapshot version {}", *version));
-  if (*version == 14U) {
-    return LoadSnapshotVersion14(path, vertices, edges, epoch_history, name_id_mapper, edge_count,
-                                 config.salient.items);
-  }
-  if (*version == 15U) {
-    return LoadSnapshotVersion15(path, vertices, edges, epoch_history, name_id_mapper, edge_count, config);
-  }
+  if (*version != 16U) throw RecoveryFailure(fmt::format("Expected snapshot version is 16, but got {}", *version));
 
   // Cleanup of loaded data in case of failure.
   bool success = false;
@@ -1727,6 +1727,380 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
   return {info, recovery_info, std::move(indices_constraints)};
 }
 
+RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipList<Vertex> *vertices,
+                               utils::SkipList<Edge> *edges,
+                               std::deque<std::pair<std::string, uint64_t>> *epoch_history,
+                               NameIdMapper *name_id_mapper, std::atomic<uint64_t> *edge_count, const Config &config) {
+  RecoveryInfo recovery_info;
+  RecoveredIndicesAndConstraints indices_constraints;
+
+  Decoder snapshot;
+  const auto version = snapshot.Initialize(path, kSnapshotMagic);
+  if (!version) throw RecoveryFailure("Couldn't read snapshot magic and/or version!");
+
+  if (!IsVersionSupported(*version)) throw RecoveryFailure(fmt::format("Invalid snapshot version {}", *version));
+  if (*version == 14U) {
+    return LoadSnapshotVersion14(path, vertices, edges, epoch_history, name_id_mapper, edge_count,
+                                 config.salient.items);
+  }
+  if (*version == 15U) {
+    return LoadSnapshotVersion15(path, vertices, edges, epoch_history, name_id_mapper, edge_count, config);
+  }
+  if (*version == 16U) {
+    return LoadSnapshotVersion16(path, vertices, edges, epoch_history, name_id_mapper, edge_count, config);
+  }
+
+  // Cleanup of loaded data in case of failure.
+  bool success = false;
+  utils::OnScopeExit cleanup([&] {
+    if (!success) {
+      edges->clear();
+      vertices->clear();
+      epoch_history->clear();
+    }
+  });
+
+  // Read snapshot info.
+  const auto info = ReadSnapshotInfo(path);
+  spdlog::info("Recovering {} vertices and {} edges.", info.vertices_count, info.edges_count);
+  // Check for edges.
+  bool snapshot_has_edges = info.offset_edges != 0;
+
+  // Recover mapper.
+  std::unordered_map<uint64_t, uint64_t> snapshot_id_map;
+  {
+    spdlog::info("Recovering mapper metadata.");
+    if (!snapshot.SetPosition(info.offset_mapper)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    auto marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_MAPPER) throw RecoveryFailure("Failed to read section mapper!");
+
+    auto size = snapshot.ReadUint();
+    if (!size) throw RecoveryFailure("Failed to read name-id mapper size!");
+
+    for (uint64_t i = 0; i < *size; ++i) {
+      auto id = snapshot.ReadUint();
+      if (!id) throw RecoveryFailure("Failed to read id for name-id mapper!");
+      auto name = snapshot.ReadString();
+      if (!name) throw RecoveryFailure("Failed to read name for name-id mapper!");
+      auto my_id = name_id_mapper->NameToId(*name);
+      snapshot_id_map.emplace(*id, my_id);
+      SPDLOG_TRACE("Mapping \"{}\"from snapshot id {} to actual id {}.", *name, *id, my_id);
+    }
+  }
+  auto get_label_from_id = [&snapshot_id_map](uint64_t label_id) {
+    auto it = snapshot_id_map.find(label_id);
+    if (it == snapshot_id_map.end()) throw RecoveryFailure("Couldn't find label id in snapshot_id_map!");
+    return LabelId::FromUint(it->second);
+  };
+  auto get_property_from_id = [&snapshot_id_map](uint64_t property_id) {
+    auto it = snapshot_id_map.find(property_id);
+    if (it == snapshot_id_map.end()) throw RecoveryFailure("Couldn't find property id in snapshot_id_map!");
+    return PropertyId::FromUint(it->second);
+  };
+  auto get_edge_type_from_id = [&snapshot_id_map](uint64_t edge_type_id) {
+    auto it = snapshot_id_map.find(edge_type_id);
+    if (it == snapshot_id_map.end()) throw RecoveryFailure("Couldn't find edge type id in snapshot_id_map!");
+    return EdgeTypeId::FromUint(it->second);
+  };
+
+  // Reset current edge count.
+  edge_count->store(0, std::memory_order_release);
+
+  {
+    spdlog::info("Recovering edges.");
+    // Recover edges.
+    if (snapshot_has_edges) {
+      // We don't need to check whether we store properties on edge or not, because `LoadPartialEdges` will always
+      // iterate over the edges in the snapshot (if they exist) and the current configuration of properties on edge only
+      // affect what it does:
+      // 1. If properties are allowed on edges, then it loads the edges.
+      // 2. If properties are not allowed on edges, then it checks that none of the edges have any properties.
+      if (!snapshot.SetPosition(info.offset_edge_batches)) {
+        throw RecoveryFailure("Couldn't read data from snapshot!");
+      }
+      const auto edge_batches = ReadBatchInfos(snapshot);
+
+      RecoverOnMultipleThreads(
+          config.durability.recovery_thread_count,
+          [path, edges, items = config.salient.items, &get_property_from_id](const size_t /*batch_index*/,
+                                                                             const BatchInfo &batch) {
+            LoadPartialEdges(path, *edges, batch.offset, batch.count, items, get_property_from_id);
+          },
+          edge_batches);
+    }
+    spdlog::info("Edges are recovered.");
+
+    // Recover vertices (labels and properties).
+    spdlog::info("Recovering vertices.", info.vertices_count);
+    uint64_t last_vertex_gid{0};
+
+    if (!snapshot.SetPosition(info.offset_vertex_batches)) {
+      throw RecoveryFailure("Couldn't read data from snapshot!");
+    }
+
+    const auto vertex_batches = ReadBatchInfos(snapshot);
+    RecoverOnMultipleThreads(
+        config.durability.recovery_thread_count,
+        [path, vertices, &vertex_batches, &get_label_from_id, &get_property_from_id, &last_vertex_gid](
+            const size_t batch_index, const BatchInfo &batch) {
+          const auto last_vertex_gid_in_batch =
+              LoadPartialVertices(path, *vertices, batch.offset, batch.count, get_label_from_id, get_property_from_id);
+          if (batch_index == vertex_batches.size() - 1) {
+            last_vertex_gid = last_vertex_gid_in_batch;
+          }
+        },
+        vertex_batches);
+
+    spdlog::info("Vertices are recovered.");
+
+    // Recover vertices (in/out edges).
+    spdlog::info("Recover connectivity.");
+    recovery_info.vertex_batches.reserve(vertex_batches.size());
+    for (const auto batch : vertex_batches) {
+      recovery_info.vertex_batches.emplace_back(Gid::FromUint(0), batch.count);
+    }
+    std::atomic<uint64_t> highest_edge_gid{0};
+
+    RecoverOnMultipleThreads(
+        config.durability.recovery_thread_count,
+        [path, vertices, edges, edge_count, items = config.salient.items, snapshot_has_edges, &get_edge_type_from_id,
+         &highest_edge_gid, &recovery_info](const size_t batch_index, const BatchInfo &batch) {
+          const auto result = LoadPartialConnectivity(path, *vertices, *edges, batch.offset, batch.count, items,
+                                                      snapshot_has_edges, get_edge_type_from_id);
+          edge_count->fetch_add(result.edge_count);
+          auto known_highest_edge_gid = highest_edge_gid.load();
+          while (known_highest_edge_gid < result.highest_edge_id) {
+            highest_edge_gid.compare_exchange_weak(known_highest_edge_gid, result.highest_edge_id);
+          }
+          recovery_info.vertex_batches[batch_index].first = result.first_vertex_gid;
+        },
+        vertex_batches);
+
+    spdlog::info("Connectivity is recovered.");
+
+    // Set initial values for edge/vertex ID generators.
+    recovery_info.next_edge_id = highest_edge_gid + 1;
+    recovery_info.next_vertex_id = last_vertex_gid + 1;
+  }
+
+  // Recover indices.
+  {
+    spdlog::info("Recovering metadata of indices.");
+    if (!snapshot.SetPosition(info.offset_indices)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    auto marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_INDICES) throw RecoveryFailure("Couldn't read section indices!");
+
+    // Recover label indices.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of label indices");
+      spdlog::info("Recovering metadata of {} label indices.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label of label index!");
+        AddRecoveredIndexConstraint(&indices_constraints.indices.label, get_label_from_id(*label),
+                                    "The label index already exists!");
+        SPDLOG_TRACE("Recovered metadata of label index for :{}", name_id_mapper->IdToName(snapshot_id_map.at(*label)));
+      }
+      spdlog::info("Metadata of label indices are recovered.");
+    }
+
+    // Recover label indices statistics.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of entries for label index statistics!");
+      spdlog::info("Recovering metadata of {} label indices statistics.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        const auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label while recovering label index statistics!");
+        const auto count = snapshot.ReadUint();
+        if (!count) throw RecoveryFailure("Couldn't read count for label index statistics!");
+        const auto avg_degree = snapshot.ReadDouble();
+        if (!avg_degree) throw RecoveryFailure("Couldn't read average degree for label index statistics");
+        const auto label_id = get_label_from_id(*label);
+        indices_constraints.indices.label_stats.emplace_back(label_id, LabelIndexStats{*count, *avg_degree});
+        SPDLOG_TRACE("Recovered metadata of label index statistics for :{}",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)));
+      }
+      spdlog::info("Metadata of label indices are recovered.");
+    }
+
+    // Recover label+property indices.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't recover the number of label property indices!");
+      spdlog::info("Recovering metadata of {} label+property indices.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label for label property index!");
+        auto property = snapshot.ReadUint();
+        if (!property) throw RecoveryFailure("Couldn't read property for label property index");
+        AddRecoveredIndexConstraint(&indices_constraints.indices.label_property,
+                                    {get_label_from_id(*label), get_property_from_id(*property)},
+                                    "The label+property index already exists!");
+        SPDLOG_TRACE("Recovered metadata of label+property index for :{}({})",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)),
+                     name_id_mapper->IdToName(snapshot_id_map.at(*property)));
+      }
+      spdlog::info("Metadata of label+property indices are recovered.");
+    }
+
+    // Recover label+property indices statistics.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't recover the number of entries for label property statistics!");
+      spdlog::info("Recovering metadata of {} label+property indices statistics.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        const auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label for label property index statistics!");
+        const auto property = snapshot.ReadUint();
+        if (!property) throw RecoveryFailure("Couldn't read property for label property index statistics!");
+        const auto count = snapshot.ReadUint();
+        if (!count) throw RecoveryFailure("Couldn't read count for label property index statistics!!");
+        const auto distinct_values_count = snapshot.ReadUint();
+        if (!distinct_values_count)
+          throw RecoveryFailure("Couldn't read distinct values count for label property index statistics!");
+        const auto statistic = snapshot.ReadDouble();
+        if (!statistic) throw RecoveryFailure("Couldn't read statistics value for label-property index statistics!");
+        const auto avg_group_size = snapshot.ReadDouble();
+        if (!avg_group_size)
+          throw RecoveryFailure("Couldn't read average group size for label property index statistics!");
+        const auto avg_degree = snapshot.ReadDouble();
+        if (!avg_degree) throw RecoveryFailure("Couldn't read average degree for label property index statistics!");
+        const auto label_id = get_label_from_id(*label);
+        const auto property_id = get_property_from_id(*property);
+        indices_constraints.indices.label_property_stats.emplace_back(
+            label_id, std::make_pair(property_id, LabelPropertyIndexStats{*count, *distinct_values_count, *statistic,
+                                                                          *avg_group_size, *avg_degree}));
+        SPDLOG_TRACE("Recovered metadata of label+property index statistics for :{}({})",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)),
+                     name_id_mapper->IdToName(snapshot_id_map.at(*property)));
+      }
+      spdlog::info("Metadata of label+property indices are recovered.");
+    }
+
+    // Recover edge-type indices.
+    spdlog::info("Recovering metadata of indices.");
+    if (!snapshot.SetPosition(info.offset_edge_indices)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_EDGE_INDICES)
+      throw RecoveryFailure("Couldn't read section edge-indices!");
+
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of edge-type indices");
+      spdlog::info("Recovering metadata of {} edge-type indices.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto edge_type = snapshot.ReadUint();
+        if (!edge_type) throw RecoveryFailure("Couldn't read edge-type of edge-type index!");
+        AddRecoveredIndexConstraint(&indices_constraints.indices.edge, get_edge_type_from_id(*edge_type),
+                                    "The edge-type index already exists!");
+        SPDLOG_TRACE("Recovered metadata of edge-type index for :{}",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*edge_type)));
+      }
+      spdlog::info("Metadata of edge-type indices are recovered.");
+    }
+
+    spdlog::info("Metadata of indices are recovered.");
+  }
+
+  // Recover constraints.
+  {
+    spdlog::info("Recovering metadata of constraints.");
+    if (!snapshot.SetPosition(info.offset_constraints)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    auto marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_CONSTRAINTS)
+      throw RecoveryFailure("Couldn't read section constraints marker!");
+
+    // Recover existence constraints.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of existence constraints!");
+      spdlog::info("Recovering metadata of {} existence constraints.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label of existence constraints!");
+        auto property = snapshot.ReadUint();
+        if (!property) throw RecoveryFailure("Couldn't read property of existence constraints!");
+        AddRecoveredIndexConstraint(&indices_constraints.constraints.existence,
+                                    {get_label_from_id(*label), get_property_from_id(*property)},
+                                    "The existence constraint already exists!");
+        SPDLOG_TRACE("Recovered metadata of existence constraint for :{}({})",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)),
+                     name_id_mapper->IdToName(snapshot_id_map.at(*property)));
+      }
+      spdlog::info("Metadata of existence constraints are recovered.");
+    }
+
+    // Recover unique constraints.
+    // Snapshot version should be checked since unique constraints were
+    // implemented in later versions of snapshot.
+    if (*version >= kUniqueConstraintVersion) {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of unique constraints!");
+      spdlog::info("Recovering metadata of {} unique constraints.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label of unique constraints!");
+        auto properties_count = snapshot.ReadUint();
+        if (!properties_count) throw RecoveryFailure("Couldn't read the number of properties in unique constraint!");
+        std::set<PropertyId> properties;
+        for (uint64_t j = 0; j < *properties_count; ++j) {
+          auto property = snapshot.ReadUint();
+          if (!property) throw RecoveryFailure("Couldn't read property of unique constraint!");
+          properties.insert(get_property_from_id(*property));
+        }
+        AddRecoveredIndexConstraint(&indices_constraints.constraints.unique, {get_label_from_id(*label), properties},
+                                    "The unique constraint already exists!");
+        SPDLOG_TRACE("Recovered metadata of unique constraints for :{}",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)));
+      }
+      spdlog::info("Metadata of unique constraints are recovered.");
+    }
+    spdlog::info("Metadata of constraints are recovered.");
+  }
+
+  spdlog::info("Recovering metadata.");
+  // Recover epoch history
+  {
+    if (!snapshot.SetPosition(info.offset_epoch_history)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    const auto marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_EPOCH_HISTORY)
+      throw RecoveryFailure("Couldn't read section epoch history marker!");
+
+    const auto history_size = snapshot.ReadUint();
+    if (!history_size) {
+      throw RecoveryFailure("Couldn't read history size!");
+    }
+
+    for (int i = 0; i < *history_size; ++i) {
+      auto maybe_epoch_id = snapshot.ReadString();
+      if (!maybe_epoch_id) {
+        throw RecoveryFailure("Couldn't read maybe epoch id!");
+      }
+      const auto maybe_last_commit_timestamp = snapshot.ReadUint();
+      if (!maybe_last_commit_timestamp) {
+        throw RecoveryFailure("Couldn't read maybe last commit timestamp!");
+      }
+      epoch_history->emplace_back(std::move(*maybe_epoch_id), *maybe_last_commit_timestamp);
+    }
+  }
+
+  spdlog::info("Metadata recovered.");
+  // Recover timestamp.
+  recovery_info.next_timestamp = info.start_timestamp + 1;
+
+  // Set success flag (to disable cleanup).
+  success = true;
+
+  return {info, recovery_info, std::move(indices_constraints)};
+}
+
 using OldSnapshotFiles = std::vector<std::pair<uint64_t, std::filesystem::path>>;
 void EnsureNecessaryWalFilesExist(const std::filesystem::path &wal_directory, const std::string &uuid,
                                   OldSnapshotFiles old_snapshot_files, Transaction *transaction,
@@ -1835,6 +2209,7 @@ void CreateSnapshot(Storage *storage, Transaction *transaction, const std::files
   uint64_t offset_edges = 0;
   uint64_t offset_vertices = 0;
   uint64_t offset_indices = 0;
+  uint64_t offset_edge_indices = 0;
   uint64_t offset_constraints = 0;
   uint64_t offset_mapper = 0;
   uint64_t offset_metadata = 0;
@@ -1847,6 +2222,7 @@ void CreateSnapshot(Storage *storage, Transaction *transaction, const std::files
     snapshot.WriteUint(offset_edges);
     snapshot.WriteUint(offset_vertices);
     snapshot.WriteUint(offset_indices);
+    snapshot.WriteUint(offset_edge_indices);
     snapshot.WriteUint(offset_constraints);
     snapshot.WriteUint(offset_mapper);
     snapshot.WriteUint(offset_epoch_history);
@@ -2106,6 +2482,17 @@ void CreateSnapshot(Storage *storage, Transaction *transaction, const std::files
         snapshot.SetPosition(last_pos);
       }
     }
+
+    // Write edge-type indices.
+    offset_edge_indices = snapshot.GetPosition();
+    snapshot.WriteMarker(Marker::SECTION_EDGE_INDICES);
+    {
+      auto edge_type = storage->indices_.edge_type_index_->ListIndices();
+      snapshot.WriteUint(edge_type.size());
+      for (const auto &item : edge_type) {
+        write_mapping(item);
+      }
+    }
   }
 
   // Write constraints.
@@ -2196,6 +2583,7 @@ void CreateSnapshot(Storage *storage, Transaction *transaction, const std::files
     snapshot.WriteUint(offset_edges);
     snapshot.WriteUint(offset_vertices);
     snapshot.WriteUint(offset_indices);
+    snapshot.WriteUint(offset_edge_indices);
     snapshot.WriteUint(offset_constraints);
     snapshot.WriteUint(offset_mapper);
     snapshot.WriteUint(offset_epoch_history);
diff --git a/src/storage/v2/durability/snapshot.hpp b/src/storage/v2/durability/snapshot.hpp
index 4c1aee1ce..b8c224b3f 100644
--- a/src/storage/v2/durability/snapshot.hpp
+++ b/src/storage/v2/durability/snapshot.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -34,6 +34,7 @@ struct SnapshotInfo {
   uint64_t offset_edges;
   uint64_t offset_vertices;
   uint64_t offset_indices;
+  uint64_t offset_edge_indices;
   uint64_t offset_constraints;
   uint64_t offset_mapper;
   uint64_t offset_epoch_history;
diff --git a/src/storage/v2/durability/storage_global_operation.hpp b/src/storage/v2/durability/storage_global_operation.hpp
index a4f1b043a..7dd635e9d 100644
--- a/src/storage/v2/durability/storage_global_operation.hpp
+++ b/src/storage/v2/durability/storage_global_operation.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -23,6 +23,8 @@ enum class StorageMetadataOperation {
   LABEL_PROPERTY_INDEX_DROP,
   LABEL_PROPERTY_INDEX_STATS_SET,
   LABEL_PROPERTY_INDEX_STATS_CLEAR,
+  EDGE_TYPE_INDEX_CREATE,
+  EDGE_TYPE_INDEX_DROP,
   EXISTENCE_CONSTRAINT_CREATE,
   EXISTENCE_CONSTRAINT_DROP,
   UNIQUE_CONSTRAINT_CREATE,
diff --git a/src/storage/v2/durability/version.hpp b/src/storage/v2/durability/version.hpp
index 25eb30904..58ca0364a 100644
--- a/src/storage/v2/durability/version.hpp
+++ b/src/storage/v2/durability/version.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -20,7 +20,7 @@ namespace memgraph::storage::durability {
 // The current version of snapshot and WAL encoding / decoding.
 // IMPORTANT: Please bump this version for every snapshot and/or WAL format
 // change!!!
-const uint64_t kVersion{16};
+const uint64_t kVersion{17};
 
 const uint64_t kOldestSupportedVersion{14};
 const uint64_t kUniqueConstraintVersion{13};
diff --git a/src/storage/v2/durability/wal.cpp b/src/storage/v2/durability/wal.cpp
index 52e916052..5c40ab1c5 100644
--- a/src/storage/v2/durability/wal.cpp
+++ b/src/storage/v2/durability/wal.cpp
@@ -95,6 +95,10 @@ Marker OperationToMarker(StorageMetadataOperation operation) {
       return Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_SET;
     case StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_CLEAR:
       return Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR;
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+      return Marker::DELTA_EDGE_TYPE_INDEX_CREATE;
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_DROP:
+      return Marker::DELTA_EDGE_TYPE_INDEX_DROP;
     case StorageMetadataOperation::EXISTENCE_CONSTRAINT_CREATE:
       return Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE;
     case StorageMetadataOperation::EXISTENCE_CONSTRAINT_DROP:
@@ -172,6 +176,10 @@ WalDeltaData::Type MarkerToWalDeltaDataType(Marker marker) {
       return WalDeltaData::Type::LABEL_PROPERTY_INDEX_STATS_SET;
     case Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR:
       return WalDeltaData::Type::LABEL_PROPERTY_INDEX_STATS_CLEAR;
+    case Marker::DELTA_EDGE_TYPE_INDEX_CREATE:
+      return WalDeltaData::Type::EDGE_INDEX_CREATE;
+    case Marker::DELTA_EDGE_TYPE_INDEX_DROP:
+      return WalDeltaData::Type::EDGE_INDEX_DROP;
     case Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
       return WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE;
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
@@ -198,6 +206,7 @@ WalDeltaData::Type MarkerToWalDeltaDataType(Marker marker) {
     case Marker::SECTION_CONSTRAINTS:
     case Marker::SECTION_DELTA:
     case Marker::SECTION_EPOCH_HISTORY:
+    case Marker::SECTION_EDGE_INDICES:
     case Marker::SECTION_OFFSETS:
     case Marker::VALUE_FALSE:
     case Marker::VALUE_TRUE:
@@ -280,6 +289,7 @@ WalDeltaData ReadSkipWalDeltaData(BaseDecoder *decoder) {
     }
     case WalDeltaData::Type::TRANSACTION_END:
       break;
+    // NOLINTNEXTLINE(bugprone-branch-clone)
     case WalDeltaData::Type::LABEL_INDEX_CREATE:
     case WalDeltaData::Type::LABEL_INDEX_DROP:
     case WalDeltaData::Type::LABEL_INDEX_STATS_CLEAR:
@@ -295,6 +305,17 @@ WalDeltaData ReadSkipWalDeltaData(BaseDecoder *decoder) {
       }
       break;
     }
+    case WalDeltaData::Type::EDGE_INDEX_CREATE:
+    case WalDeltaData::Type::EDGE_INDEX_DROP: {
+      if constexpr (read_data) {
+        auto edge_type = decoder->ReadString();
+        if (!edge_type) throw RecoveryFailure("Invalid WAL data!");
+        delta.operation_edge_type.edge_type = std::move(*edge_type);
+      } else {
+        if (!decoder->SkipString()) throw RecoveryFailure("Invalid WAL data!");
+      }
+      break;
+    }
     case WalDeltaData::Type::LABEL_INDEX_STATS_SET: {
       if constexpr (read_data) {
         auto label = decoder->ReadString();
@@ -522,6 +543,9 @@ bool operator==(const WalDeltaData &a, const WalDeltaData &b) {
     case WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP:
       return a.operation_label_properties.label == b.operation_label_properties.label &&
              a.operation_label_properties.properties == b.operation_label_properties.properties;
+    case WalDeltaData::Type::EDGE_INDEX_CREATE:
+    case WalDeltaData::Type::EDGE_INDEX_DROP:
+      return a.operation_edge_type.edge_type == b.operation_edge_type.edge_type;
   }
 }
 bool operator!=(const WalDeltaData &a, const WalDeltaData &b) { return !(a == b); }
@@ -703,6 +727,37 @@ void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Storage
       }
       break;
     }
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_DROP: {
+      MG_ASSERT(false, "Invalid function  call!");
+    }
+  }
+}
+
+void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, StorageMetadataOperation operation,
+                     EdgeTypeId edge_type, uint64_t timestamp) {
+  encoder->WriteMarker(Marker::SECTION_DELTA);
+  encoder->WriteUint(timestamp);
+  switch (operation) {
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_DROP: {
+      encoder->WriteMarker(OperationToMarker(operation));
+      encoder->WriteString(name_id_mapper->IdToName(edge_type.AsUint()));
+      break;
+    }
+    case StorageMetadataOperation::LABEL_INDEX_CREATE:
+    case StorageMetadataOperation::LABEL_INDEX_DROP:
+    case StorageMetadataOperation::LABEL_INDEX_STATS_CLEAR:
+    case StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_CLEAR:
+    case StorageMetadataOperation::LABEL_INDEX_STATS_SET:
+    case StorageMetadataOperation::LABEL_PROPERTY_INDEX_CREATE:
+    case StorageMetadataOperation::LABEL_PROPERTY_INDEX_DROP:
+    case StorageMetadataOperation::EXISTENCE_CONSTRAINT_CREATE:
+    case StorageMetadataOperation::EXISTENCE_CONSTRAINT_DROP:
+    case StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_SET:
+    case StorageMetadataOperation::UNIQUE_CONSTRAINT_CREATE:
+    case StorageMetadataOperation::UNIQUE_CONSTRAINT_DROP:
+      MG_ASSERT(false, "Invalid function call!");
   }
 }
 
@@ -887,6 +942,18 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
                                          "The label index doesn't exist!");
           break;
         }
+        case WalDeltaData::Type::EDGE_INDEX_CREATE: {
+          auto edge_type_id = EdgeTypeId::FromUint(name_id_mapper->NameToId(delta.operation_edge_type.edge_type));
+          AddRecoveredIndexConstraint(&indices_constraints->indices.edge, edge_type_id,
+                                      "The edge-type index already exists!");
+          break;
+        }
+        case WalDeltaData::Type::EDGE_INDEX_DROP: {
+          auto edge_type_id = EdgeTypeId::FromUint(name_id_mapper->NameToId(delta.operation_edge_type.edge_type));
+          RemoveRecoveredIndexConstraint(&indices_constraints->indices.edge, edge_type_id,
+                                         "The edge-type index doesn't exist!");
+          break;
+        }
         case WalDeltaData::Type::LABEL_INDEX_STATS_SET: {
           auto label_id = LabelId::FromUint(name_id_mapper->NameToId(delta.operation_label_stats.label));
           LabelIndexStats stats{};
@@ -1088,6 +1155,11 @@ void WalFile::AppendOperation(StorageMetadataOperation operation, LabelId label,
   UpdateStats(timestamp);
 }
 
+void WalFile::AppendOperation(StorageMetadataOperation operation, EdgeTypeId edge_type, uint64_t timestamp) {
+  EncodeOperation(&wal_, name_id_mapper_, operation, edge_type, timestamp);
+  UpdateStats(timestamp);
+}
+
 void WalFile::Sync() { wal_.Sync(); }
 
 uint64_t WalFile::GetSize() { return wal_.GetSize(); }
diff --git a/src/storage/v2/durability/wal.hpp b/src/storage/v2/durability/wal.hpp
index 20d88b040..516487e0d 100644
--- a/src/storage/v2/durability/wal.hpp
+++ b/src/storage/v2/durability/wal.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -67,6 +67,8 @@ struct WalDeltaData {
     LABEL_PROPERTY_INDEX_DROP,
     LABEL_PROPERTY_INDEX_STATS_SET,
     LABEL_PROPERTY_INDEX_STATS_CLEAR,
+    EDGE_INDEX_CREATE,
+    EDGE_INDEX_DROP,
     EXISTENCE_CONSTRAINT_CREATE,
     EXISTENCE_CONSTRAINT_DROP,
     UNIQUE_CONSTRAINT_CREATE,
@@ -111,6 +113,10 @@ struct WalDeltaData {
     std::set<std::string, std::less<>> properties;
   } operation_label_properties;
 
+  struct {
+    std::string edge_type;
+  } operation_edge_type;
+
   struct {
     std::string label;
     std::string stats;
@@ -155,6 +161,8 @@ constexpr bool IsWalDeltaDataTypeTransactionEndVersion15(const WalDeltaData::Typ
     case WalDeltaData::Type::LABEL_PROPERTY_INDEX_DROP:
     case WalDeltaData::Type::LABEL_PROPERTY_INDEX_STATS_SET:
     case WalDeltaData::Type::LABEL_PROPERTY_INDEX_STATS_CLEAR:
+    case WalDeltaData::Type::EDGE_INDEX_CREATE:
+    case WalDeltaData::Type::EDGE_INDEX_DROP:
     case WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE:
     case WalDeltaData::Type::EXISTENCE_CONSTRAINT_DROP:
     case WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE:
@@ -164,7 +172,7 @@ constexpr bool IsWalDeltaDataTypeTransactionEndVersion15(const WalDeltaData::Typ
 }
 
 constexpr bool IsWalDeltaDataTypeTransactionEnd(const WalDeltaData::Type type, const uint64_t version = kVersion) {
-  if (version < 16U) {
+  if (version < 17U) {
     return IsWalDeltaDataTypeTransactionEndVersion15(type);
   }
   // All deltas are now handled in a transactional scope
@@ -208,6 +216,9 @@ void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Storage
                      LabelId label, const std::set<PropertyId> &properties, const LabelIndexStats &stats,
                      const LabelPropertyIndexStats &property_stats, uint64_t timestamp);
 
+void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, StorageMetadataOperation operation,
+                     EdgeTypeId edge_type, uint64_t timestamp);
+
 /// Function used to load the WAL data into the storage.
 /// @throw RecoveryFailure
 RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConstraints *indices_constraints,
@@ -240,6 +251,8 @@ class WalFile {
   void AppendOperation(StorageMetadataOperation operation, LabelId label, const std::set<PropertyId> &properties,
                        const LabelIndexStats &stats, const LabelPropertyIndexStats &property_stats, uint64_t timestamp);
 
+  void AppendOperation(StorageMetadataOperation operation, EdgeTypeId edge_type, uint64_t timestamp);
+
   void Sync();
 
   uint64_t GetSize();
diff --git a/src/storage/v2/edges_iterable.cpp b/src/storage/v2/edges_iterable.cpp
new file mode 100644
index 000000000..6acae34e3
--- /dev/null
+++ b/src/storage/v2/edges_iterable.cpp
@@ -0,0 +1,149 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include "storage/v2/edges_iterable.hpp"
+
+namespace memgraph::storage {
+
+EdgesIterable::EdgesIterable(InMemoryEdgeTypeIndex::Iterable edges) : type_(Type::BY_EDGE_TYPE_IN_MEMORY) {
+  new (&in_memory_edges_by_edge_type_) InMemoryEdgeTypeIndex::Iterable(std::move(edges));
+}
+
+EdgesIterable::EdgesIterable(EdgesIterable &&other) noexcept : type_(other.type_) {
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          InMemoryEdgeTypeIndex::Iterable(std::move(other.in_memory_edges_by_edge_type_));
+      break;
+  }
+}
+
+EdgesIterable &EdgesIterable::operator=(EdgesIterable &&other) noexcept {
+  Destroy();
+  type_ = other.type_;
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          InMemoryEdgeTypeIndex::Iterable(std::move(other.in_memory_edges_by_edge_type_));
+      break;
+  }
+  return *this;
+}
+
+EdgesIterable::~EdgesIterable() { Destroy(); }
+
+void EdgesIterable::Destroy() noexcept {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      in_memory_edges_by_edge_type_.InMemoryEdgeTypeIndex::Iterable::~Iterable();
+      break;
+  }
+}
+
+EdgesIterable::Iterator EdgesIterable::begin() {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      return Iterator(in_memory_edges_by_edge_type_.begin());
+  }
+}
+
+EdgesIterable::Iterator EdgesIterable::end() {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      return Iterator(in_memory_edges_by_edge_type_.end());
+  }
+}
+
+EdgesIterable::Iterator::Iterator(InMemoryEdgeTypeIndex::Iterable::Iterator it) : type_(Type::BY_EDGE_TYPE_IN_MEMORY) {
+  // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
+  new (&in_memory_edges_by_edge_type_) InMemoryEdgeTypeIndex::Iterable::Iterator(std::move(it));
+}
+
+EdgesIterable::Iterator::Iterator(const EdgesIterable::Iterator &other) : type_(other.type_) {
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          InMemoryEdgeTypeIndex::Iterable::Iterator(other.in_memory_edges_by_edge_type_);
+      break;
+  }
+}
+
+// NOLINTNEXTLINE(cert-oop54-cpp)
+EdgesIterable::Iterator &EdgesIterable::Iterator::operator=(const EdgesIterable::Iterator &other) {
+  Destroy();
+  type_ = other.type_;
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          InMemoryEdgeTypeIndex::Iterable::Iterator(other.in_memory_edges_by_edge_type_);
+      break;
+  }
+  return *this;
+}
+
+EdgesIterable::Iterator::Iterator(EdgesIterable::Iterator &&other) noexcept : type_(other.type_) {
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
+          InMemoryEdgeTypeIndex::Iterable::Iterator(std::move(other.in_memory_edges_by_edge_type_));
+      break;
+  }
+}
+
+EdgesIterable::Iterator &EdgesIterable::Iterator::operator=(EdgesIterable::Iterator &&other) noexcept {
+  Destroy();
+  type_ = other.type_;
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
+          InMemoryEdgeTypeIndex::Iterable::Iterator(std::move(other.in_memory_edges_by_edge_type_));
+      break;
+  }
+  return *this;
+}
+
+EdgesIterable::Iterator::~Iterator() { Destroy(); }
+
+void EdgesIterable::Iterator::Destroy() noexcept {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      in_memory_edges_by_edge_type_.InMemoryEdgeTypeIndex::Iterable::Iterator::~Iterator();
+      break;
+  }
+}
+
+EdgeAccessor const &EdgesIterable::Iterator::operator*() const {
+  switch (type_) {
+    ;
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      return *in_memory_edges_by_edge_type_;
+  }
+}
+
+EdgesIterable::Iterator &EdgesIterable::Iterator::operator++() {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      ++in_memory_edges_by_edge_type_;
+      break;
+  }
+  return *this;
+}
+
+bool EdgesIterable::Iterator::operator==(const Iterator &other) const {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      return in_memory_edges_by_edge_type_ == other.in_memory_edges_by_edge_type_;
+  }
+}
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/edges_iterable.hpp b/src/storage/v2/edges_iterable.hpp
new file mode 100644
index 000000000..9c9326705
--- /dev/null
+++ b/src/storage/v2/edges_iterable.hpp
@@ -0,0 +1,73 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include "storage/v2/all_vertices_iterable.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
+
+namespace memgraph::storage {
+
+class InMemoryEdgeTypeIndex;
+
+class EdgesIterable final {
+  enum class Type { BY_EDGE_TYPE_IN_MEMORY };
+
+  Type type_;
+  union {
+    InMemoryEdgeTypeIndex::Iterable in_memory_edges_by_edge_type_;
+  };
+
+  void Destroy() noexcept;
+
+ public:
+  explicit EdgesIterable(InMemoryEdgeTypeIndex::Iterable);
+
+  EdgesIterable(const EdgesIterable &) = delete;
+  EdgesIterable &operator=(const EdgesIterable &) = delete;
+
+  EdgesIterable(EdgesIterable &&) noexcept;
+  EdgesIterable &operator=(EdgesIterable &&) noexcept;
+
+  ~EdgesIterable();
+
+  class Iterator final {
+    Type type_;
+    union {
+      InMemoryEdgeTypeIndex::Iterable::Iterator in_memory_edges_by_edge_type_;
+    };
+
+    void Destroy() noexcept;
+
+   public:
+    explicit Iterator(InMemoryEdgeTypeIndex::Iterable::Iterator);
+
+    Iterator(const Iterator &);
+    Iterator &operator=(const Iterator &);
+
+    Iterator(Iterator &&) noexcept;
+    Iterator &operator=(Iterator &&) noexcept;
+
+    ~Iterator();
+
+    EdgeAccessor const &operator*() const;
+
+    Iterator &operator++();
+
+    bool operator==(const Iterator &other) const;
+    bool operator!=(const Iterator &other) const { return !(*this == other); }
+  };
+
+  Iterator begin();
+  Iterator end();
+};
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/indices/edge_type_index.hpp b/src/storage/v2/indices/edge_type_index.hpp
new file mode 100644
index 000000000..788ccb225
--- /dev/null
+++ b/src/storage/v2/indices/edge_type_index.hpp
@@ -0,0 +1,46 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include <vector>
+
+#include "storage/v2/transaction.hpp"
+
+namespace memgraph::storage {
+
+class EdgeTypeIndex {
+ public:
+  EdgeTypeIndex() = default;
+
+  EdgeTypeIndex(const EdgeTypeIndex &) = delete;
+  EdgeTypeIndex(EdgeTypeIndex &&) = delete;
+  EdgeTypeIndex &operator=(const EdgeTypeIndex &) = delete;
+  EdgeTypeIndex &operator=(EdgeTypeIndex &&) = delete;
+
+  virtual ~EdgeTypeIndex() = default;
+
+  virtual bool DropIndex(EdgeTypeId edge_type) = 0;
+
+  virtual bool IndexExists(EdgeTypeId edge_type) const = 0;
+
+  virtual std::vector<EdgeTypeId> ListIndices() const = 0;
+
+  virtual uint64_t ApproximateEdgeCount(EdgeTypeId edge_type) const = 0;
+
+  virtual void UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                                    const Transaction &tx) = 0;
+
+  virtual void UpdateOnEdgeModification(Vertex *old_from, Vertex *old_to, Vertex *new_from, Vertex *new_to,
+                                        EdgeRef edge_ref, EdgeTypeId edge_type, const Transaction &tx) = 0;
+};
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/indices/indices.cpp b/src/storage/v2/indices/indices.cpp
index 764adccdc..50bd97199 100644
--- a/src/storage/v2/indices/indices.cpp
+++ b/src/storage/v2/indices/indices.cpp
@@ -10,8 +10,10 @@
 // licenses/APL.txt.
 
 #include "storage/v2/indices/indices.hpp"
+#include "storage/v2/disk/edge_type_index.hpp"
 #include "storage/v2/disk/label_index.hpp"
 #include "storage/v2/disk/label_property_index.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
 #include "storage/v2/inmemory/label_index.hpp"
 #include "storage/v2/inmemory/label_property_index.hpp"
 
@@ -35,6 +37,8 @@ void Indices::AbortEntries(LabelId label, std::span<std::pair<PropertyValue, Ver
 void Indices::RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp, std::stop_token token) const {
   static_cast<InMemoryLabelIndex *>(label_index_.get())->RemoveObsoleteEntries(oldest_active_start_timestamp, token);
   static_cast<InMemoryLabelPropertyIndex *>(label_property_index_.get())
+      ->RemoveObsoleteEntries(oldest_active_start_timestamp, token);
+  static_cast<InMemoryEdgeTypeIndex *>(edge_type_index_.get())
       ->RemoveObsoleteEntries(oldest_active_start_timestamp, std::move(token));
 }
 
@@ -58,14 +62,21 @@ void Indices::UpdateOnSetProperty(PropertyId property, const PropertyValue &valu
   label_property_index_->UpdateOnSetProperty(property, value, vertex, tx);
 }
 
+void Indices::UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                                   const Transaction &tx) const {
+  edge_type_index_->UpdateOnEdgeCreation(from, to, edge_ref, edge_type, tx);
+}
+
 Indices::Indices(const Config &config, StorageMode storage_mode) {
   std::invoke([this, config, storage_mode]() {
     if (storage_mode == StorageMode::IN_MEMORY_TRANSACTIONAL || storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
       label_index_ = std::make_unique<InMemoryLabelIndex>();
       label_property_index_ = std::make_unique<InMemoryLabelPropertyIndex>();
+      edge_type_index_ = std::make_unique<InMemoryEdgeTypeIndex>();
     } else {
       label_index_ = std::make_unique<DiskLabelIndex>(config);
       label_property_index_ = std::make_unique<DiskLabelPropertyIndex>(config);
+      edge_type_index_ = std::make_unique<DiskEdgeTypeIndex>();
     }
   });
 }
diff --git a/src/storage/v2/indices/indices.hpp b/src/storage/v2/indices/indices.hpp
index 618d7e070..b658e0d12 100644
--- a/src/storage/v2/indices/indices.hpp
+++ b/src/storage/v2/indices/indices.hpp
@@ -15,6 +15,7 @@
 #include <span>
 
 #include "storage/v2/id_types.hpp"
+#include "storage/v2/indices/edge_type_index.hpp"
 #include "storage/v2/indices/label_index.hpp"
 #include "storage/v2/indices/label_property_index.hpp"
 #include "storage/v2/storage_mode.hpp"
@@ -66,8 +67,12 @@ struct Indices {
   void UpdateOnSetProperty(PropertyId property, const PropertyValue &value, Vertex *vertex,
                            const Transaction &tx) const;
 
+  void UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                            const Transaction &tx) const;
+
   std::unique_ptr<LabelIndex> label_index_;
   std::unique_ptr<LabelPropertyIndex> label_property_index_;
+  std::unique_ptr<EdgeTypeIndex> edge_type_index_;
 };
 
 }  // namespace memgraph::storage
diff --git a/src/storage/v2/inmemory/edge_type_index.cpp b/src/storage/v2/inmemory/edge_type_index.cpp
new file mode 100644
index 000000000..e439628b4
--- /dev/null
+++ b/src/storage/v2/inmemory/edge_type_index.cpp
@@ -0,0 +1,318 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include "storage/v2/inmemory/edge_type_index.hpp"
+
+#include "storage/v2/constraints/constraints.hpp"
+#include "storage/v2/indices/indices_utils.hpp"
+#include "utils/counter.hpp"
+
+namespace {
+
+using Delta = memgraph::storage::Delta;
+using Vertex = memgraph::storage::Vertex;
+using Edge = memgraph::storage::Edge;
+using EdgeRef = memgraph::storage::EdgeRef;
+using EdgeTypeId = memgraph::storage::EdgeTypeId;
+using Transaction = memgraph::storage::Transaction;
+using View = memgraph::storage::View;
+
+bool IsIndexEntryVisible(Edge *edge, const Transaction *transaction, View view) {
+  bool exists = true;
+  bool deleted = true;
+  Delta *delta = nullptr;
+  {
+    auto guard = std::shared_lock{edge->lock};
+    deleted = edge->deleted;
+    delta = edge->delta;
+  }
+  ApplyDeltasForRead(transaction, delta, view, [&](const Delta &delta) {
+    switch (delta.action) {
+      case Delta::Action::ADD_LABEL:
+      case Delta::Action::REMOVE_LABEL:
+      case Delta::Action::SET_PROPERTY:
+      case Delta::Action::ADD_IN_EDGE:
+      case Delta::Action::ADD_OUT_EDGE:
+      case Delta::Action::REMOVE_IN_EDGE:
+      case Delta::Action::REMOVE_OUT_EDGE:
+        break;
+      case Delta::Action::RECREATE_OBJECT: {
+        deleted = false;
+        break;
+      }
+      case Delta::Action::DELETE_DESERIALIZED_OBJECT:
+      case Delta::Action::DELETE_OBJECT: {
+        exists = false;
+        break;
+      }
+    }
+  });
+  return exists && !deleted;
+}
+
+using ReturnType = std::optional<std::tuple<EdgeTypeId, Vertex *, EdgeRef>>;
+ReturnType VertexDeletedConnectedEdges(Vertex *vertex, Edge *edge, const Transaction *transaction, View view) {
+  ReturnType link;
+  Delta *delta = nullptr;
+  {
+    auto guard = std::shared_lock{vertex->lock};
+    delta = vertex->delta;
+  }
+  ApplyDeltasForRead(transaction, delta, view, [&](const Delta &delta) {
+    switch (delta.action) {
+      case Delta::Action::ADD_LABEL:
+      case Delta::Action::REMOVE_LABEL:
+      case Delta::Action::SET_PROPERTY:
+        break;
+      case Delta::Action::ADD_IN_EDGE: {
+        if (edge == delta.vertex_edge.edge.ptr) {
+          link = {delta.vertex_edge.edge_type, delta.vertex_edge.vertex, delta.vertex_edge.edge};
+          auto it = std::find(vertex->in_edges.begin(), vertex->in_edges.end(), link);
+          MG_ASSERT(it == vertex->in_edges.end(), "Invalid database state!");
+          break;
+        }
+      }
+      case Delta::Action::ADD_OUT_EDGE: {
+        if (edge == delta.vertex_edge.edge.ptr) {
+          link = {delta.vertex_edge.edge_type, delta.vertex_edge.vertex, delta.vertex_edge.edge};
+          auto it = std::find(vertex->out_edges.begin(), vertex->out_edges.end(), link);
+          MG_ASSERT(it == vertex->out_edges.end(), "Invalid database state!");
+          break;
+        }
+      }
+      case Delta::Action::REMOVE_IN_EDGE:
+      case Delta::Action::REMOVE_OUT_EDGE:
+      case Delta::Action::RECREATE_OBJECT:
+      case Delta::Action::DELETE_DESERIALIZED_OBJECT:
+      case Delta::Action::DELETE_OBJECT:
+        break;
+    }
+  });
+  return link;
+}
+
+}  // namespace
+
+namespace memgraph::storage {
+
+bool InMemoryEdgeTypeIndex::CreateIndex(EdgeTypeId edge_type, utils::SkipList<Vertex>::Accessor vertices) {
+  auto [it, emplaced] = index_.try_emplace(edge_type);
+  if (!emplaced) {
+    return false;
+  }
+
+  utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
+  try {
+    auto edge_acc = it->second.access();
+    for (auto &from_vertex : vertices) {
+      if (from_vertex.deleted) {
+        continue;
+      }
+
+      for (auto &edge : from_vertex.out_edges) {
+        const auto type = std::get<kEdgeTypeIdPos>(edge);
+        if (type == edge_type) {
+          auto *to_vertex = std::get<kVertexPos>(edge);
+          if (to_vertex->deleted) {
+            continue;
+          }
+          edge_acc.insert({&from_vertex, to_vertex, std::get<kEdgeRefPos>(edge).ptr, 0});
+        }
+      }
+    }
+  } catch (const utils::OutOfMemoryException &) {
+    utils::MemoryTracker::OutOfMemoryExceptionBlocker oom_exception_blocker;
+    index_.erase(it);
+    throw;
+  }
+
+  return true;
+}
+
+bool InMemoryEdgeTypeIndex::DropIndex(EdgeTypeId edge_type) { return index_.erase(edge_type) > 0; }
+
+bool InMemoryEdgeTypeIndex::IndexExists(EdgeTypeId edge_type) const { return index_.find(edge_type) != index_.end(); }
+
+std::vector<EdgeTypeId> InMemoryEdgeTypeIndex::ListIndices() const {
+  std::vector<EdgeTypeId> ret;
+  ret.reserve(index_.size());
+  for (const auto &item : index_) {
+    ret.push_back(item.first);
+  }
+  return ret;
+}
+
+void InMemoryEdgeTypeIndex::RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp, std::stop_token token) {
+  auto maybe_stop = utils::ResettableCounter<2048>();
+
+  for (auto &label_storage : index_) {
+    if (token.stop_requested()) return;
+
+    auto edges_acc = label_storage.second.access();
+    for (auto it = edges_acc.begin(); it != edges_acc.end();) {
+      if (maybe_stop() && token.stop_requested()) return;
+
+      auto next_it = it;
+      ++next_it;
+
+      if (it->timestamp >= oldest_active_start_timestamp) {
+        it = next_it;
+        continue;
+      }
+
+      if (next_it != edges_acc.end() || it->from_vertex->deleted || it->to_vertex->deleted ||
+          !std::ranges::all_of(it->from_vertex->out_edges, [&](const auto &edge) {
+            auto *to_vertex = std::get<InMemoryEdgeTypeIndex::kVertexPos>(edge);
+            return to_vertex != it->to_vertex;
+          })) {
+        edges_acc.remove(*it);
+      }
+
+      it = next_it;
+    }
+  }
+}
+
+uint64_t InMemoryEdgeTypeIndex::ApproximateEdgeCount(EdgeTypeId edge_type) const {
+  if (auto it = index_.find(edge_type); it != index_.end()) {
+    return it->second.size();
+  }
+  return 0;
+}
+
+void InMemoryEdgeTypeIndex::UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                                                 const Transaction &tx) {
+  auto it = index_.find(edge_type);
+  if (it == index_.end()) {
+    return;
+  }
+  auto acc = it->second.access();
+  acc.insert(Entry{from, to, edge_ref.ptr, tx.start_timestamp});
+}
+
+void InMemoryEdgeTypeIndex::UpdateOnEdgeModification(Vertex *old_from, Vertex *old_to, Vertex *new_from, Vertex *new_to,
+                                                     EdgeRef edge_ref, EdgeTypeId edge_type, const Transaction &tx) {
+  auto it = index_.find(edge_type);
+  if (it == index_.end()) {
+    return;
+  }
+  auto acc = it->second.access();
+
+  auto entry_to_update = std::ranges::find_if(acc, [&](const auto &entry) {
+    return entry.from_vertex == old_from && entry.to_vertex == old_to && entry.edge == edge_ref.ptr;
+  });
+
+  acc.remove(Entry{entry_to_update->from_vertex, entry_to_update->to_vertex, entry_to_update->edge,
+                   entry_to_update->timestamp});
+  acc.insert(Entry{new_from, new_to, edge_ref.ptr, tx.start_timestamp});
+}
+
+InMemoryEdgeTypeIndex::Iterable::Iterable(utils::SkipList<Entry>::Accessor index_accessor, EdgeTypeId edge_type,
+                                          View view, Storage *storage, Transaction *transaction)
+    : index_accessor_(std::move(index_accessor)),
+      edge_type_(edge_type),
+      view_(view),
+      storage_(storage),
+      transaction_(transaction) {}
+
+InMemoryEdgeTypeIndex::Iterable::Iterator::Iterator(Iterable *self, utils::SkipList<Entry>::Iterator index_iterator)
+    : self_(self),
+      index_iterator_(index_iterator),
+      current_edge_accessor_(EdgeRef{nullptr}, EdgeTypeId::FromInt(0), nullptr, nullptr, self_->storage_, nullptr),
+      current_edge_(nullptr) {
+  AdvanceUntilValid();
+}
+
+InMemoryEdgeTypeIndex::Iterable::Iterator &InMemoryEdgeTypeIndex::Iterable::Iterator::operator++() {
+  ++index_iterator_;
+  AdvanceUntilValid();
+  return *this;
+}
+
+void InMemoryEdgeTypeIndex::Iterable::Iterator::AdvanceUntilValid() {
+  for (; index_iterator_ != self_->index_accessor_.end(); ++index_iterator_) {
+    auto *from_vertex = index_iterator_->from_vertex;
+    auto *to_vertex = index_iterator_->to_vertex;
+
+    if (!IsIndexEntryVisible(index_iterator_->edge, self_->transaction_, self_->view_) || from_vertex->deleted ||
+        to_vertex->deleted) {
+      continue;
+    }
+
+    const bool edge_was_deleted = index_iterator_->edge->deleted;
+    auto [edge_ref, edge_type, deleted_from_vertex, deleted_to_vertex] = GetEdgeInfo();
+    MG_ASSERT(edge_ref != EdgeRef(nullptr), "Invalid database state!");
+
+    if (edge_was_deleted) {
+      from_vertex = deleted_from_vertex;
+      to_vertex = deleted_to_vertex;
+    }
+
+    auto accessor = EdgeAccessor{edge_ref, edge_type, from_vertex, to_vertex, self_->storage_, self_->transaction_};
+    if (!accessor.IsVisible(self_->view_)) {
+      continue;
+    }
+
+    current_edge_accessor_ = accessor;
+    current_edge_ = edge_ref;
+    break;
+  }
+}
+
+std::tuple<EdgeRef, EdgeTypeId, Vertex *, Vertex *> InMemoryEdgeTypeIndex::Iterable::Iterator::GetEdgeInfo() {
+  auto *from_vertex = index_iterator_->from_vertex;
+  auto *to_vertex = index_iterator_->to_vertex;
+
+  if (index_iterator_->edge->deleted) {
+    const auto missing_in_edge =
+        VertexDeletedConnectedEdges(from_vertex, index_iterator_->edge, self_->transaction_, self_->view_);
+    const auto missing_out_edge =
+        VertexDeletedConnectedEdges(to_vertex, index_iterator_->edge, self_->transaction_, self_->view_);
+    if (missing_in_edge && missing_out_edge &&
+        std::get<kEdgeRefPos>(*missing_in_edge) == std::get<kEdgeRefPos>(*missing_out_edge)) {
+      return std::make_tuple(std::get<kEdgeRefPos>(*missing_in_edge), std::get<kEdgeTypeIdPos>(*missing_in_edge),
+                             to_vertex, from_vertex);
+    }
+  }
+
+  const auto &from_edges = from_vertex->out_edges;
+  const auto &to_edges = to_vertex->in_edges;
+
+  auto it = std::find_if(from_edges.begin(), from_edges.end(), [&](const auto &from_entry) {
+    const auto &from_edge = std::get<kEdgeRefPos>(from_entry);
+    return std::any_of(to_edges.begin(), to_edges.end(), [&](const auto &to_entry) {
+      const auto &to_edge = std::get<kEdgeRefPos>(to_entry);
+      return index_iterator_->edge->gid == from_edge.ptr->gid && from_edge.ptr->gid == to_edge.ptr->gid;
+    });
+  });
+
+  if (it != from_edges.end()) {
+    const auto &from_edge = std::get<kEdgeRefPos>(*it);
+    return std::make_tuple(from_edge, std::get<kEdgeTypeIdPos>(*it), from_vertex, to_vertex);
+  }
+
+  return {EdgeRef(nullptr), EdgeTypeId::FromUint(0U), nullptr, nullptr};
+}
+
+void InMemoryEdgeTypeIndex::RunGC() {
+  for (auto &index_entry : index_) {
+    index_entry.second.run_gc();
+  }
+}
+
+InMemoryEdgeTypeIndex::Iterable InMemoryEdgeTypeIndex::Edges(EdgeTypeId edge_type, View view, Storage *storage,
+                                                             Transaction *transaction) {
+  const auto it = index_.find(edge_type);
+  MG_ASSERT(it != index_.end(), "Index for edge-type {} doesn't exist", edge_type.AsUint());
+  return {it->second.access(), edge_type, view, storage, transaction};
+}
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/inmemory/edge_type_index.hpp b/src/storage/v2/inmemory/edge_type_index.hpp
new file mode 100644
index 000000000..db8f7843f
--- /dev/null
+++ b/src/storage/v2/inmemory/edge_type_index.hpp
@@ -0,0 +1,113 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include <map>
+#include <utility>
+
+#include "storage/v2/constraints/constraints.hpp"
+#include "storage/v2/edge_accessor.hpp"
+#include "storage/v2/id_types.hpp"
+#include "storage/v2/indices/edge_type_index.hpp"
+#include "storage/v2/indices/label_index_stats.hpp"
+#include "utils/rw_lock.hpp"
+#include "utils/synchronized.hpp"
+
+namespace memgraph::storage {
+
+class InMemoryEdgeTypeIndex : public storage::EdgeTypeIndex {
+ private:
+  struct Entry {
+    Vertex *from_vertex;
+    Vertex *to_vertex;
+
+    Edge *edge;
+
+    uint64_t timestamp;
+
+    bool operator<(const Entry &rhs) const { return edge->gid < rhs.edge->gid; }
+    bool operator==(const Entry &rhs) const { return edge->gid == rhs.edge->gid; }
+  };
+
+ public:
+  InMemoryEdgeTypeIndex() = default;
+
+  /// @throw std::bad_alloc
+  bool CreateIndex(EdgeTypeId edge_type, utils::SkipList<Vertex>::Accessor vertices);
+
+  /// Returns false if there was no index to drop
+  bool DropIndex(EdgeTypeId edge_type) override;
+
+  bool IndexExists(EdgeTypeId edge_type) const override;
+
+  std::vector<EdgeTypeId> ListIndices() const override;
+
+  void RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp, std::stop_token token);
+
+  uint64_t ApproximateEdgeCount(EdgeTypeId edge_type) const override;
+
+  void UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                            const Transaction &tx) override;
+
+  void UpdateOnEdgeModification(Vertex *old_from, Vertex *old_to, Vertex *new_from, Vertex *new_to, EdgeRef edge_ref,
+                                EdgeTypeId edge_type, const Transaction &tx) override;
+
+  static constexpr std::size_t kEdgeTypeIdPos = 0U;
+  static constexpr std::size_t kVertexPos = 1U;
+  static constexpr std::size_t kEdgeRefPos = 2U;
+
+  class Iterable {
+   public:
+    Iterable(utils::SkipList<Entry>::Accessor index_accessor, EdgeTypeId edge_type, View view, Storage *storage,
+             Transaction *transaction);
+
+    class Iterator {
+     public:
+      Iterator(Iterable *self, utils::SkipList<Entry>::Iterator index_iterator);
+
+      EdgeAccessor const &operator*() const { return current_edge_accessor_; }
+
+      bool operator==(const Iterator &other) const { return index_iterator_ == other.index_iterator_; }
+      bool operator!=(const Iterator &other) const { return index_iterator_ != other.index_iterator_; }
+
+      Iterator &operator++();
+
+     private:
+      void AdvanceUntilValid();
+      std::tuple<EdgeRef, EdgeTypeId, Vertex *, Vertex *> GetEdgeInfo();
+
+      Iterable *self_;
+      utils::SkipList<Entry>::Iterator index_iterator_;
+      EdgeAccessor current_edge_accessor_;
+      EdgeRef current_edge_{nullptr};
+    };
+
+    Iterator begin() { return {this, index_accessor_.begin()}; }
+    Iterator end() { return {this, index_accessor_.end()}; }
+
+   private:
+    utils::SkipList<Entry>::Accessor index_accessor_;
+    EdgeTypeId edge_type_;
+    View view_;
+    Storage *storage_;
+    Transaction *transaction_;
+  };
+
+  void RunGC();
+
+  Iterable Edges(EdgeTypeId edge_type, View view, Storage *storage, Transaction *transaction);
+
+ private:
+  std::map<EdgeTypeId, utils::SkipList<Entry>> index_;
+};
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/inmemory/label_property_index.cpp b/src/storage/v2/inmemory/label_property_index.cpp
index bccb03dbd..2df2812af 100644
--- a/src/storage/v2/inmemory/label_property_index.cpp
+++ b/src/storage/v2/inmemory/label_property_index.cpp
@@ -144,27 +144,30 @@ void InMemoryLabelPropertyIndex::RemoveObsoleteEntries(uint64_t oldest_active_st
   auto maybe_stop = utils::ResettableCounter<2048>();
 
   for (auto &[label_property, index] : index_) {
+    auto [label_id, prop_id] = label_property;
     // before starting index, check if stop_requested
     if (token.stop_requested()) return;
 
     auto index_acc = index.access();
-    for (auto it = index_acc.begin(); it != index_acc.end();) {
+    auto it = index_acc.begin();
+    auto end_it = index_acc.end();
+    if (it == end_it) continue;
+    while (true) {
       // Hot loop, don't check stop_requested every time
       if (maybe_stop() && token.stop_requested()) return;
 
       auto next_it = it;
       ++next_it;
 
-      if (it->timestamp >= oldest_active_start_timestamp) {
-        it = next_it;
-        continue;
-      }
-
-      if ((next_it != index_acc.end() && it->vertex == next_it->vertex && it->value == next_it->value) ||
-          !AnyVersionHasLabelProperty(*it->vertex, label_property.first, label_property.second, it->value,
-                                      oldest_active_start_timestamp)) {
-        index_acc.remove(*it);
+      bool has_next = next_it != end_it;
+      if (it->timestamp < oldest_active_start_timestamp) {
+        bool redundant_duplicate = has_next && it->vertex == next_it->vertex && it->value == next_it->value;
+        if (redundant_duplicate ||
+            !AnyVersionHasLabelProperty(*it->vertex, label_id, prop_id, it->value, oldest_active_start_timestamp)) {
+          index_acc.remove(*it);
+        }
       }
+      if (!has_next) break;
       it = next_it;
     }
   }
diff --git a/src/storage/v2/inmemory/replication/recovery.cpp b/src/storage/v2/inmemory/replication/recovery.cpp
index 921c1f5c0..5f1182c75 100644
--- a/src/storage/v2/inmemory/replication/recovery.cpp
+++ b/src/storage/v2/inmemory/replication/recovery.cpp
@@ -106,8 +106,8 @@ uint64_t ReplicateCurrentWal(const utils::UUID &main_uuid, const InMemoryStorage
   return response.current_commit_timestamp;
 }
 
-/// This method tries to find the optimal path for recoverying a single replica.
-/// Based on the last commit transfered to replica it tries to update the
+/// This method tries to find the optimal path for recovering a single replica.
+/// Based on the last commit transferred to replica it tries to update the
 /// replica using durability files - WALs and Snapshots. WAL files are much
 /// smaller in size as they contain only the Deltas (changes) made during the
 /// transactions while Snapshots contain all the data. For that reason we prefer
@@ -175,7 +175,7 @@ std::vector<RecoveryStep> GetRecoverySteps(uint64_t replica_commit, utils::FileR
   auto add_snapshot = [&]() {
     if (!latest_snapshot) return;
     const auto lock_success = locker_acc.AddPath(latest_snapshot->path);
-    MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant snapshot path.");
+    MG_ASSERT(!lock_success.HasError(), "Tried to lock a non-existent snapshot path.");
     recovery_steps.emplace_back(std::in_place_type_t<RecoverySnapshot>{}, std::move(latest_snapshot->path));
   };
 
@@ -233,7 +233,7 @@ std::vector<RecoveryStep> GetRecoverySteps(uint64_t replica_commit, utils::FileR
     }
   }
 
-  // In all cases, if we have a current wal file we need to use itW
+  // In all cases, if we have a current wal file we need to use it
   if (current_wal_seq_num) {
     // NOTE: File not handled directly, so no need to lock it
     recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
diff --git a/src/storage/v2/inmemory/storage.cpp b/src/storage/v2/inmemory/storage.cpp
index 45dc7a9c2..bba049a0c 100644
--- a/src/storage/v2/inmemory/storage.cpp
+++ b/src/storage/v2/inmemory/storage.cpp
@@ -20,6 +20,7 @@
 #include "storage/v2/durability/snapshot.hpp"
 #include "storage/v2/edge_direction.hpp"
 #include "storage/v2/id_types.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
 #include "storage/v2/metadata_delta.hpp"
 
 /// REPLICATION ///
@@ -109,6 +110,7 @@ InMemoryStorage::InMemoryStorage(Config config)
       timestamp_ = std::max(timestamp_, info->next_timestamp);
       if (info->last_commit_timestamp) {
         repl_storage_state_.last_commit_timestamp_ = *info->last_commit_timestamp;
+        spdlog::trace("Recovering last commit timestamp {}", *info->last_commit_timestamp);
       }
     }
   } else if (config_.durability.snapshot_wal_mode != Config::Durability::SnapshotWalMode::DISABLED ||
@@ -143,9 +145,7 @@ InMemoryStorage::InMemoryStorage(Config config)
 
   if (config_.gc.type == Config::Gc::Type::PERIODIC) {
     // TODO: move out of storage have one global gc_runner_
-    gc_runner_.Run("Storage GC", config_.gc.interval, [this] {
-      this->FreeMemory(std::unique_lock<utils::ResourceLock>{main_lock_, std::defer_lock});
-    });
+    gc_runner_.Run("Storage GC", config_.gc.interval, [this] { this->FreeMemory({}, true); });
   }
   if (timestamp_ == kTimestampInitialId) {
     commit_log_.emplace();
@@ -351,6 +351,9 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdge(VertexAccesso
         transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
         transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
 
+        // Update indices if they exist.
+        storage_->indices_.UpdateOnEdgeCreation(from_vertex, to_vertex, edge, edge_type, transaction_);
+
         // Increment edge count.
         storage_->edge_count_.fetch_add(1, std::memory_order_acq_rel);
       }};
@@ -554,6 +557,11 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::EdgeSetFrom(EdgeAccessor
         CreateAndLinkDelta(&transaction_, to_vertex, Delta::RemoveInEdgeTag(), edge_type, new_from_vertex, edge_ref);
         to_vertex->in_edges.emplace_back(edge_type, new_from_vertex, edge_ref);
 
+        auto *in_memory = static_cast<InMemoryStorage *>(storage_);
+        auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+        mem_edge_type_index->UpdateOnEdgeModification(old_from_vertex, to_vertex, new_from_vertex, to_vertex, edge_ref,
+                                                      edge_type, transaction_);
+
         transaction_.manyDeltasCache.Invalidate(new_from_vertex, edge_type, EdgeDirection::OUT);
         transaction_.manyDeltasCache.Invalidate(old_from_vertex, edge_type, EdgeDirection::OUT);
         transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
@@ -660,6 +668,11 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::EdgeSetTo(EdgeAccessor *
         CreateAndLinkDelta(&transaction_, new_to_vertex, Delta::RemoveInEdgeTag(), edge_type, from_vertex, edge_ref);
         new_to_vertex->in_edges.emplace_back(edge_type, from_vertex, edge_ref);
 
+        auto *in_memory = static_cast<InMemoryStorage *>(storage_);
+        auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+        mem_edge_type_index->UpdateOnEdgeModification(from_vertex, old_to_vertex, from_vertex, new_to_vertex, edge_ref,
+                                                      edge_type, transaction_);
+
         transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
         transaction_.manyDeltasCache.Invalidate(old_to_vertex, edge_type, EdgeDirection::IN);
         transaction_.manyDeltasCache.Invalidate(new_to_vertex, edge_type, EdgeDirection::IN);
@@ -1265,6 +1278,18 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryA
   return {};
 }
 
+utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::CreateIndex(
+    EdgeTypeId edge_type) {
+  MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!");
+  auto *in_memory = static_cast<InMemoryStorage *>(storage_);
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+  if (!mem_edge_type_index->CreateIndex(edge_type, in_memory->vertices_.access())) {
+    return StorageIndexDefinitionError{IndexDefinitionError{}};
+  }
+  transaction_.md_deltas.emplace_back(MetadataDelta::edge_index_create, edge_type);
+  return {};
+}
+
 utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::DropIndex(LabelId label) {
   MG_ASSERT(unique_guard_.owns_lock(), "Dropping label index requires a unique access to the storage!");
   auto *in_memory = static_cast<InMemoryStorage *>(storage_);
@@ -1293,6 +1318,18 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryA
   return {};
 }
 
+utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::DropIndex(
+    EdgeTypeId edge_type) {
+  MG_ASSERT(unique_guard_.owns_lock(), "Drop index requires a unique access to the storage!");
+  auto *in_memory = static_cast<InMemoryStorage *>(storage_);
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+  if (!mem_edge_type_index->DropIndex(edge_type)) {
+    return StorageIndexDefinitionError{IndexDefinitionError{}};
+  }
+  transaction_.md_deltas.emplace_back(MetadataDelta::edge_index_drop, edge_type);
+  return {};
+}
+
 utils::BasicResult<StorageExistenceConstraintDefinitionError, void>
 InMemoryStorage::InMemoryAccessor::CreateExistenceConstraint(LabelId label, PropertyId property) {
   MG_ASSERT(unique_guard_.owns_lock(), "Creating existence requires a unique access to the storage!");
@@ -1384,6 +1421,11 @@ VerticesIterable InMemoryStorage::InMemoryAccessor::Vertices(
       mem_label_property_index->Vertices(label, property, lower_bound, upper_bound, view, storage_, &transaction_));
 }
 
+EdgesIterable InMemoryStorage::InMemoryAccessor::Edges(EdgeTypeId edge_type, View view) {
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(storage_->indices_.edge_type_index_.get());
+  return EdgesIterable(mem_edge_type_index->Edges(edge_type, view, storage_, &transaction_));
+}
+
 Transaction InMemoryStorage::CreateTransaction(
     IsolationLevel isolation_level, StorageMode storage_mode,
     memgraph::replication_coordination_glue::ReplicationRole replication_role) {
@@ -1425,28 +1467,27 @@ void InMemoryStorage::SetStorageMode(StorageMode new_storage_mode) {
     }
 
     storage_mode_ = new_storage_mode;
-    FreeMemory(std::move(main_guard));
+    FreeMemory(std::move(main_guard), false);
   }
 }
 
-template <bool force>
-void InMemoryStorage::CollectGarbage(std::unique_lock<utils::ResourceLock> main_guard) {
+template <bool aggressive = true>
+void InMemoryStorage::CollectGarbage(std::unique_lock<utils::ResourceLock> main_guard, bool periodic) {
   // NOTE: You do not need to consider cleanup of deleted object that occurred in
   // different storage modes within the same CollectGarbage call. This is because
   // SetStorageMode will ensure CollectGarbage is called before any new transactions
   // with the new storage mode can start.
 
   // SetStorageMode will pass its unique_lock of main_lock_. We will use that lock,
-  // as reacquiring the lock would cause  deadlock. Otherwise, we need to get our own
+  // as reacquiring the lock would cause deadlock. Otherwise, we need to get our own
   // lock.
   if (!main_guard.owns_lock()) {
-    if constexpr (force) {
-      // We take the unique lock on the main storage lock, so we can forcefully clean
-      // everything we can
-      if (!main_lock_.try_lock()) {
-        CollectGarbage<false>();
-        return;
-      }
+    if constexpr (aggressive) {
+      // We tried to be aggressive but we do not already have main lock continue as not aggressive
+      // Perf note: Do not try to get unique lock if it was not already passed in. GC maybe expensive,
+      // do not assume it is fast, unique lock will blocks all new storage transactions.
+      CollectGarbage<false>({}, periodic);
+      return;
     } else {
       // Because the garbage collector iterates through the indices and constraints
       // to clean them up, it must take the main lock for reading to make sure that
@@ -1458,17 +1499,24 @@ void InMemoryStorage::CollectGarbage(std::unique_lock<utils::ResourceLock> main_
   }
 
   utils::OnScopeExit lock_releaser{[&] {
-    if (!main_guard.owns_lock()) {
-      if constexpr (force) {
-        main_lock_.unlock();
-      } else {
-        main_lock_.unlock_shared();
-      }
-    } else {
+    if (main_guard.owns_lock()) {
       main_guard.unlock();
+    } else {
+      main_lock_.unlock_shared();
     }
   }};
 
+  // Only one gc run at a time
+  std::unique_lock<std::mutex> gc_guard(gc_lock_, std::try_to_lock);
+  if (!gc_guard.owns_lock()) {
+    return;
+  }
+
+  // Diagnostic trace
+  spdlog::trace("Storage GC on '{}' started [{}]", name(), periodic ? "periodic" : "forced");
+  auto trace_on_exit = utils::OnScopeExit{
+      [&] { spdlog::trace("Storage GC on '{}' finished [{}]", name(), periodic ? "periodic" : "forced"); }};
+
   // Garbage collection must be performed in two phases. In the first phase,
   // deltas that won't be applied by any transaction anymore are unlinked from
   // the version chains. They cannot be deleted immediately, because there
@@ -1476,27 +1524,29 @@ void InMemoryStorage::CollectGarbage(std::unique_lock<utils::ResourceLock> main_
   // chain traversal. They are instead marked for deletion and will be deleted
   // in the second GC phase in this GC iteration or some of the following
   // ones.
-  std::unique_lock<std::mutex> gc_guard(gc_lock_, std::try_to_lock);
-  if (!gc_guard.owns_lock()) {
-    return;
-  }
 
   uint64_t oldest_active_start_timestamp = commit_log_->OldestActive();
 
-  // Deltas from previous GC runs or from aborts can be cleaned up here
-  garbage_undo_buffers_.WithLock([&](auto &garbage_undo_buffers) {
-    if constexpr (force) {
-      // if force is set to true we can simply delete all the leftover undos because
-      // no transaction is active
-      garbage_undo_buffers.clear();
-    } else {
-      // garbage_undo_buffers is ordered, pop until we can't
-      while (!garbage_undo_buffers.empty() &&
-             garbage_undo_buffers.front().mark_timestamp_ <= oldest_active_start_timestamp) {
-        garbage_undo_buffers.pop_front();
+  {
+    std::unique_lock<utils::SpinLock> guard(engine_lock_);
+    uint64_t mark_timestamp = timestamp_;  // a timestamp no active transaction can currently have
+
+    // Deltas from previous GC runs or from aborts can be cleaned up here
+    garbage_undo_buffers_.WithLock([&](auto &garbage_undo_buffers) {
+      guard.unlock();
+      if (aggressive or mark_timestamp == oldest_active_start_timestamp) {
+        // We know no transaction is active, it is safe to simply delete all the garbage undos
+        // Nothing can be reading them
+        garbage_undo_buffers.clear();
+      } else {
+        // garbage_undo_buffers is ordered, pop until we can't
+        while (!garbage_undo_buffers.empty() &&
+               garbage_undo_buffers.front().mark_timestamp_ <= oldest_active_start_timestamp) {
+          garbage_undo_buffers.pop_front();
+        }
       }
-    }
-  });
+    });
+  }
 
   // We don't move undo buffers of unlinked transactions to garbage_undo_buffers
   // list immediately, because we would have to repeatedly take
@@ -1694,7 +1744,8 @@ void InMemoryStorage::CollectGarbage(std::unique_lock<utils::ResourceLock> main_
     std::unique_lock<utils::SpinLock> guard(engine_lock_);
     uint64_t mark_timestamp = timestamp_;  // a timestamp no active transaction can currently have
 
-    if (force or mark_timestamp == oldest_active_start_timestamp) {
+    if (aggressive or mark_timestamp == oldest_active_start_timestamp) {
+      guard.unlock();
       // if lucky, there are no active transactions, hence nothing looking at the deltas
       // remove them now
       unlinked_undo_buffers.clear();
@@ -1756,8 +1807,8 @@ void InMemoryStorage::CollectGarbage(std::unique_lock<utils::ResourceLock> main_
 }
 
 // tell the linker he can find the CollectGarbage definitions here
-template void InMemoryStorage::CollectGarbage<true>(std::unique_lock<utils::ResourceLock>);
-template void InMemoryStorage::CollectGarbage<false>(std::unique_lock<utils::ResourceLock>);
+template void InMemoryStorage::CollectGarbage<true>(std::unique_lock<utils::ResourceLock> main_guard, bool periodic);
+template void InMemoryStorage::CollectGarbage<false>(std::unique_lock<utils::ResourceLock> main_guard, bool periodic);
 
 StorageInfo InMemoryStorage::GetBaseInfo() {
   StorageInfo info{};
@@ -2009,6 +2060,10 @@ bool InMemoryStorage::AppendToWal(const Transaction &transaction, uint64_t final
         AppendToWalDataDefinition(durability::StorageMetadataOperation::LABEL_INDEX_CREATE, md_delta.label,
                                   final_commit_timestamp);
       } break;
+      case MetadataDelta::Action::EDGE_INDEX_CREATE: {
+        AppendToWalDataDefinition(durability::StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE, md_delta.edge_type,
+                                  final_commit_timestamp);
+      } break;
       case MetadataDelta::Action::LABEL_PROPERTY_INDEX_CREATE: {
         const auto &info = md_delta.label_property;
         AppendToWalDataDefinition(durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_CREATE, info.label,
@@ -2018,6 +2073,10 @@ bool InMemoryStorage::AppendToWal(const Transaction &transaction, uint64_t final
         AppendToWalDataDefinition(durability::StorageMetadataOperation::LABEL_INDEX_DROP, md_delta.label,
                                   final_commit_timestamp);
       } break;
+      case MetadataDelta::Action::EDGE_INDEX_DROP: {
+        AppendToWalDataDefinition(durability::StorageMetadataOperation::EDGE_TYPE_INDEX_DROP, md_delta.edge_type,
+                                  final_commit_timestamp);
+      } break;
       case MetadataDelta::Action::LABEL_PROPERTY_INDEX_DROP: {
         const auto &info = md_delta.label_property;
         AppendToWalDataDefinition(durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_DROP, info.label,
@@ -2083,6 +2142,12 @@ void InMemoryStorage::AppendToWalDataDefinition(durability::StorageMetadataOpera
   repl_storage_state_.AppendOperation(operation, label, properties, stats, property_stats, final_commit_timestamp);
 }
 
+void InMemoryStorage::AppendToWalDataDefinition(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                                                uint64_t final_commit_timestamp) {
+  wal_file_->AppendOperation(operation, edge_type, final_commit_timestamp);
+  repl_storage_state_.AppendOperation(operation, edge_type, final_commit_timestamp);
+}
+
 void InMemoryStorage::AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                                 const std::set<PropertyId> &properties,
                                                 LabelPropertyIndexStats property_stats,
@@ -2108,50 +2173,35 @@ void InMemoryStorage::AppendToWalDataDefinition(durability::StorageMetadataOpera
 
 utils::BasicResult<InMemoryStorage::CreateSnapshotError> InMemoryStorage::CreateSnapshot(
     memgraph::replication_coordination_glue::ReplicationRole replication_role) {
-  if (replication_role == memgraph::replication_coordination_glue::ReplicationRole::REPLICA) {
+  using memgraph::replication_coordination_glue::ReplicationRole;
+  if (replication_role == ReplicationRole::REPLICA) {
     return InMemoryStorage::CreateSnapshotError::DisabledForReplica;
   }
-  auto const &epoch = repl_storage_state_.epoch_;
-  auto snapshot_creator = [this, &epoch]() {
-    utils::Timer timer;
-    auto transaction = CreateTransaction(IsolationLevel::SNAPSHOT_ISOLATION, storage_mode_,
-                                         memgraph::replication_coordination_glue::ReplicationRole::MAIN);
-    durability::CreateSnapshot(this, &transaction, recovery_.snapshot_directory_, recovery_.wal_directory_, &vertices_,
-                               &edges_, uuid_, epoch, repl_storage_state_.history, &file_retainer_);
-    // Finalize snapshot transaction.
-    commit_log_->MarkFinished(transaction.start_timestamp);
-
-    memgraph::metrics::Measure(memgraph::metrics::SnapshotCreationLatency_us,
-                               std::chrono::duration_cast<std::chrono::microseconds>(timer.Elapsed()).count());
-  };
 
   std::lock_guard snapshot_guard(snapshot_lock_);
 
-  auto should_try_shared{true};
-  auto max_num_tries{10};
-  while (max_num_tries) {
-    if (should_try_shared) {
-      std::shared_lock storage_guard(main_lock_);
-      if (storage_mode_ == memgraph::storage::StorageMode::IN_MEMORY_TRANSACTIONAL) {
-        snapshot_creator();
-        return {};
-      }
+  auto accessor = std::invoke([&]() {
+    if (storage_mode_ == StorageMode::IN_MEMORY_ANALYTICAL) {
+      // For analytical no other txn can be in play
+      return UniqueAccess(ReplicationRole::MAIN, IsolationLevel::SNAPSHOT_ISOLATION);
     } else {
-      std::unique_lock main_guard{main_lock_};
-      if (storage_mode_ == memgraph::storage::StorageMode::IN_MEMORY_ANALYTICAL) {
-        snapshot_creator();
-        return {};
-      }
+      return Access(ReplicationRole::MAIN, IsolationLevel::SNAPSHOT_ISOLATION);
     }
-    should_try_shared = !should_try_shared;
-    max_num_tries--;
-  }
+  });
 
-  return CreateSnapshotError::ReachedMaxNumTries;
+  utils::Timer timer;
+  Transaction *transaction = accessor->GetTransaction();
+  auto const &epoch = repl_storage_state_.epoch_;
+  durability::CreateSnapshot(this, transaction, recovery_.snapshot_directory_, recovery_.wal_directory_, &vertices_,
+                             &edges_, uuid_, epoch, repl_storage_state_.history, &file_retainer_);
+
+  memgraph::metrics::Measure(memgraph::metrics::SnapshotCreationLatency_us,
+                             std::chrono::duration_cast<std::chrono::microseconds>(timer.Elapsed()).count());
+  return {};
 }
 
-void InMemoryStorage::FreeMemory(std::unique_lock<utils::ResourceLock> main_guard) {
-  CollectGarbage<true>(std::move(main_guard));
+void InMemoryStorage::FreeMemory(std::unique_lock<utils::ResourceLock> main_guard, bool periodic) {
+  CollectGarbage(std::move(main_guard), periodic);
 
   static_cast<InMemoryLabelIndex *>(indices_.label_index_.get())->RunGC();
   static_cast<InMemoryLabelPropertyIndex *>(indices_.label_property_index_.get())->RunGC();
@@ -2307,7 +2357,8 @@ IndicesInfo InMemoryStorage::InMemoryAccessor::ListAllIndices() const {
   auto *mem_label_index = static_cast<InMemoryLabelIndex *>(in_memory->indices_.label_index_.get());
   auto *mem_label_property_index =
       static_cast<InMemoryLabelPropertyIndex *>(in_memory->indices_.label_property_index_.get());
-  return {mem_label_index->ListIndices(), mem_label_property_index->ListIndices()};
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+  return {mem_label_index->ListIndices(), mem_label_property_index->ListIndices(), mem_edge_type_index->ListIndices()};
 }
 ConstraintsInfo InMemoryStorage::InMemoryAccessor::ListAllConstraints() const {
   const auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
diff --git a/src/storage/v2/inmemory/storage.hpp b/src/storage/v2/inmemory/storage.hpp
index 15d9b4e61..7cf82a16b 100644
--- a/src/storage/v2/inmemory/storage.hpp
+++ b/src/storage/v2/inmemory/storage.hpp
@@ -16,6 +16,7 @@
 #include <memory>
 #include <utility>
 #include "storage/v2/indices/label_index_stats.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
 #include "storage/v2/inmemory/label_index.hpp"
 #include "storage/v2/inmemory/label_property_index.hpp"
 #include "storage/v2/inmemory/replication/recovery.hpp"
@@ -53,6 +54,7 @@ class InMemoryStorage final : public Storage {
                                                     const InMemoryStorage *storage);
   friend class InMemoryLabelIndex;
   friend class InMemoryLabelPropertyIndex;
+  friend class InMemoryEdgeTypeIndex;
 
  public:
   enum class CreateSnapshotError : uint8_t { DisabledForReplica, ReachedMaxNumTries };
@@ -107,6 +109,8 @@ class InMemoryStorage final : public Storage {
                               const std::optional<utils::Bound<PropertyValue>> &lower_bound,
                               const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) override;
 
+    EdgesIterable Edges(EdgeTypeId edge_type, View view) override;
+
     /// Return approximate number of all vertices in the database.
     /// Note that this is always an over-estimate and never an under-estimate.
     uint64_t ApproximateVertexCount() const override {
@@ -145,6 +149,10 @@ class InMemoryStorage final : public Storage {
           label, property, lower, upper);
     }
 
+    uint64_t ApproximateEdgeCount(EdgeTypeId id) const override {
+      return static_cast<InMemoryStorage *>(storage_)->indices_.edge_type_index_->ApproximateEdgeCount(id);
+    }
+
     template <typename TResult, typename TIndex, typename TIndexKey>
     std::optional<TResult> GetIndexStatsForIndex(TIndex *index, TIndexKey &&key) const {
       return index->GetIndexStats(key);
@@ -204,6 +212,10 @@ class InMemoryStorage final : public Storage {
       return static_cast<InMemoryStorage *>(storage_)->indices_.label_property_index_->IndexExists(label, property);
     }
 
+    bool EdgeTypeIndexExists(EdgeTypeId edge_type) const override {
+      return static_cast<InMemoryStorage *>(storage_)->indices_.edge_type_index_->IndexExists(edge_type);
+    }
+
     IndicesInfo ListAllIndices() const override;
 
     ConstraintsInfo ListAllConstraints() const override;
@@ -239,6 +251,14 @@ class InMemoryStorage final : public Storage {
     /// @throw std::bad_alloc
     utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(LabelId label, PropertyId property) override;
 
+    /// Create an index.
+    /// Returns void if the index has been created.
+    /// Returns `StorageIndexDefinitionError` if an error occures. Error can be:
+    /// * `ReplicationError`:  there is at least one SYNC replica that has not confirmed receiving the transaction.
+    /// * `IndexDefinitionError`: the index already exists.
+    /// @throw std::bad_alloc
+    utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(EdgeTypeId edge_type) override;
+
     /// Drop an existing index.
     /// Returns void if the index has been dropped.
     /// Returns `StorageIndexDefinitionError` if an error occures. Error can be:
@@ -253,6 +273,13 @@ class InMemoryStorage final : public Storage {
     /// * `IndexDefinitionError`: the index does not exist.
     utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label, PropertyId property) override;
 
+    /// Drop an existing index.
+    /// Returns void if the index has been dropped.
+    /// Returns `StorageIndexDefinitionError` if an error occures. Error can be:
+    /// * `ReplicationError`:  there is at least one SYNC replica that has not confirmed receiving the transaction.
+    /// * `IndexDefinitionError`: the index does not exist.
+    utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(EdgeTypeId edge_type) override;
+
     /// Returns void if the existence constraint has been created.
     /// Returns `StorageExistenceConstraintDefinitionError` if an error occures. Error can be:
     /// * `ReplicationError`: there is at least one SYNC replica that has not confirmed receiving the transaction.
@@ -334,7 +361,7 @@ class InMemoryStorage final : public Storage {
   std::unique_ptr<Accessor> UniqueAccess(memgraph::replication_coordination_glue::ReplicationRole replication_role,
                                          std::optional<IsolationLevel> override_isolation_level) override;
 
-  void FreeMemory(std::unique_lock<utils::ResourceLock> main_guard) override;
+  void FreeMemory(std::unique_lock<utils::ResourceLock> main_guard, bool periodic) override;
 
   utils::FileRetainer::FileLockerAccessor::ret_type IsPathLocked();
   utils::FileRetainer::FileLockerAccessor::ret_type LockPath();
@@ -367,7 +394,7 @@ class InMemoryStorage final : public Storage {
   /// @throw std::system_error
   /// @throw std::bad_alloc
   template <bool force>
-  void CollectGarbage(std::unique_lock<utils::ResourceLock> main_guard = {});
+  void CollectGarbage(std::unique_lock<utils::ResourceLock> main_guard, bool periodic);
 
   bool InitializeWalFile(memgraph::replication::ReplicationEpoch &epoch);
   void FinalizeWalFile();
@@ -378,20 +405,17 @@ class InMemoryStorage final : public Storage {
   /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   [[nodiscard]] bool AppendToWal(const Transaction &transaction, uint64_t final_commit_timestamp,
                                  DatabaseAccessProtector db_acc);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                  uint64_t final_commit_timestamp);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
+  void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                                 uint64_t final_commit_timestamp);
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                  const std::set<PropertyId> &properties, uint64_t final_commit_timestamp);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label, LabelIndexStats stats,
                                  uint64_t final_commit_timestamp);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                  const std::set<PropertyId> &properties, LabelPropertyIndexStats property_stats,
                                  uint64_t final_commit_timestamp);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                  const std::set<PropertyId> &properties, LabelIndexStats stats,
                                  LabelPropertyIndexStats property_stats, uint64_t final_commit_timestamp);
diff --git a/src/storage/v2/metadata_delta.hpp b/src/storage/v2/metadata_delta.hpp
index 94d806c19..b34966a62 100644
--- a/src/storage/v2/metadata_delta.hpp
+++ b/src/storage/v2/metadata_delta.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -35,6 +35,8 @@ struct MetadataDelta {
     LABEL_PROPERTY_INDEX_DROP,
     LABEL_PROPERTY_INDEX_STATS_SET,
     LABEL_PROPERTY_INDEX_STATS_CLEAR,
+    EDGE_INDEX_CREATE,
+    EDGE_INDEX_DROP,
     EXISTENCE_CONSTRAINT_CREATE,
     EXISTENCE_CONSTRAINT_DROP,
     UNIQUE_CONSTRAINT_CREATE,
@@ -57,6 +59,10 @@ struct MetadataDelta {
   } label_property_index_stats_set;
   static constexpr struct LabelPropertyIndexStatsClear {
   } label_property_index_stats_clear;
+  static constexpr struct EdgeIndexCreate {
+  } edge_index_create;
+  static constexpr struct EdgeIndexDrop {
+  } edge_index_drop;
   static constexpr struct ExistenceConstraintCreate {
   } existence_constraint_create;
   static constexpr struct ExistenceConstraintDrop {
@@ -87,6 +93,11 @@ struct MetadataDelta {
   MetadataDelta(LabelPropertyIndexStatsClear /*tag*/, LabelId label)
       : action(Action::LABEL_PROPERTY_INDEX_STATS_CLEAR), label{label} {}
 
+  MetadataDelta(EdgeIndexCreate /*tag*/, EdgeTypeId edge_type)
+      : action(Action::EDGE_INDEX_CREATE), edge_type(edge_type) {}
+
+  MetadataDelta(EdgeIndexDrop /*tag*/, EdgeTypeId edge_type) : action(Action::EDGE_INDEX_DROP), edge_type(edge_type) {}
+
   MetadataDelta(ExistenceConstraintCreate /*tag*/, LabelId label, PropertyId property)
       : action(Action::EXISTENCE_CONSTRAINT_CREATE), label_property{label, property} {}
 
@@ -114,6 +125,8 @@ struct MetadataDelta {
       case Action::LABEL_PROPERTY_INDEX_DROP:
       case Action::LABEL_PROPERTY_INDEX_STATS_SET:
       case Action::LABEL_PROPERTY_INDEX_STATS_CLEAR:
+      case Action::EDGE_INDEX_CREATE:
+      case Action::EDGE_INDEX_DROP:
       case Action::EXISTENCE_CONSTRAINT_CREATE:
       case Action::EXISTENCE_CONSTRAINT_DROP:
         break;
@@ -129,6 +142,8 @@ struct MetadataDelta {
   union {
     LabelId label;
 
+    EdgeTypeId edge_type;
+
     struct {
       LabelId label;
       PropertyId property;
diff --git a/src/storage/v2/property_store.cpp b/src/storage/v2/property_store.cpp
index e6e4dbbaf..adf3440a2 100644
--- a/src/storage/v2/property_store.cpp
+++ b/src/storage/v2/property_store.cpp
@@ -1051,6 +1051,14 @@ struct SpecificPropertyAndBufferInfo {
   uint64_t all_size;
 };
 
+// Struct used to return info about the property position
+struct SpecificPropertyAndBufferInfoMinimal {
+  uint64_t property_begin;
+  uint64_t property_end;
+
+  auto property_size() const { return property_end - property_begin; }
+};
+
 // Function used to find the position where the property should be in the data
 // buffer. It keeps the properties in the buffer sorted by `PropertyId` and
 // returns the positions in the buffer where the seeked property starts and
@@ -1083,6 +1091,27 @@ SpecificPropertyAndBufferInfo FindSpecificPropertyAndBufferInfo(Reader *reader,
   return {property_begin, property_end, property_end - property_begin, all_begin, all_end, all_end - all_begin};
 }
 
+// Like FindSpecificPropertyAndBufferInfo, but will early exit. No need to find the "all" information
+SpecificPropertyAndBufferInfoMinimal FindSpecificPropertyAndBufferInfoMinimal(Reader *reader, PropertyId property) {
+  uint64_t property_begin = reader->GetPosition();
+  while (true) {
+    switch (HasExpectedProperty(reader, property)) {
+      case ExpectedPropertyStatus::MISSING_DATA:
+        [[fallthrough]];
+      case ExpectedPropertyStatus::GREATER: {
+        return {0, 0};
+      }
+      case ExpectedPropertyStatus::EQUAL: {
+        return {property_begin, reader->GetPosition()};
+      }
+      case ExpectedPropertyStatus::SMALLER: {
+        property_begin = reader->GetPosition();
+        break;
+      }
+    }
+  }
+}
+
 // All data buffers will be allocated to a power of 8 size.
 uint64_t ToPowerOf8(uint64_t size) {
   uint64_t mod = size % 8;
@@ -1254,11 +1283,12 @@ bool PropertyStore::IsPropertyEqual(PropertyId property, const PropertyValue &va
   BufferInfo buffer_info = GetBufferInfo(buffer_);
   Reader reader(buffer_info.data, buffer_info.size);
 
-  auto info = FindSpecificPropertyAndBufferInfo(&reader, property);
-  if (info.property_size == 0) return value.IsNull();
-  Reader prop_reader(buffer_info.data + info.property_begin, info.property_size);
+  auto info = FindSpecificPropertyAndBufferInfoMinimal(&reader, property);
+  auto property_size = info.property_size();
+  if (property_size == 0) return value.IsNull();
+  Reader prop_reader(buffer_info.data + info.property_begin, property_size);
   if (!CompareExpectedProperty(&prop_reader, property, value)) return false;
-  return prop_reader.GetPosition() == info.property_size;
+  return prop_reader.GetPosition() == property_size;
 }
 
 std::map<PropertyId, PropertyValue> PropertyStore::Properties() const {
diff --git a/src/storage/v2/replication/replication_client.cpp b/src/storage/v2/replication/replication_client.cpp
index 16429d11f..3c1081206 100644
--- a/src/storage/v2/replication/replication_client.cpp
+++ b/src/storage/v2/replication/replication_client.cpp
@@ -53,25 +53,60 @@ void ReplicationStorageClient::UpdateReplicaState(Storage *storage, DatabaseAcce
 #endif
 
   std::optional<uint64_t> branching_point;
+  // different epoch id, replica was main
+  // In case there is no epoch transfer, and MAIN doesn't hold all the epochs as it could have been down and miss it
+  // we need then just to check commit timestamp
   if (replica.epoch_id != replStorageState.epoch_.id() && replica.current_commit_timestamp != kTimestampInitialId) {
+    spdlog::trace(
+        "REPLICA: epoch UUID: {} and last_commit_timestamp: {}; MAIN: epoch UUID {} and last_commit_timestamp {}",
+        std::string(replica.epoch_id), replica.current_commit_timestamp, std::string(replStorageState.epoch_.id()),
+        replStorageState.last_commit_timestamp_);
     auto const &history = replStorageState.history;
     const auto epoch_info_iter = std::find_if(history.crbegin(), history.crend(), [&](const auto &main_epoch_info) {
       return main_epoch_info.first == replica.epoch_id;
     });
+    // main didn't have that epoch, but why is here branching point
     if (epoch_info_iter == history.crend()) {
+      spdlog::info("Couldn't find epoch {} in MAIN, setting branching point", std::string(replica.epoch_id));
       branching_point = 0;
-    } else if (epoch_info_iter->second != replica.current_commit_timestamp) {
+    } else if (epoch_info_iter->second < replica.current_commit_timestamp) {
+      spdlog::info("Found epoch {} on MAIN with last_commit_timestamp {}, REPLICA's last_commit_timestamp {}",
+                   std::string(epoch_info_iter->first), epoch_info_iter->second, replica.current_commit_timestamp);
       branching_point = epoch_info_iter->second;
     }
   }
   if (branching_point) {
-    spdlog::error(
-        "You cannot register Replica {} to this Main because at one point "
-        "Replica {} acted as the Main instance. Both the Main and Replica {} "
-        "now hold unique data. Please resolve data conflicts and start the "
-        "replication on a clean instance.",
-        client_.name_, client_.name_, client_.name_);
-    replica_state_.WithLock([](auto &val) { val = replication::ReplicaState::DIVERGED_FROM_MAIN; });
+    auto replica_state = replica_state_.Lock();
+    if (*replica_state == replication::ReplicaState::DIVERGED_FROM_MAIN) {
+      return;
+    }
+    *replica_state = replication::ReplicaState::DIVERGED_FROM_MAIN;
+
+    auto log_error = [client_name = client_.name_]() {
+      spdlog::error(
+          "You cannot register Replica {} to this Main because at one point "
+          "Replica {} acted as the Main instance. Both the Main and Replica {} "
+          "now hold unique data. Please resolve data conflicts and start the "
+          "replication on a clean instance.",
+          client_name, client_name, client_name);
+    };
+#ifdef MG_ENTERPRISE
+    if (!FLAGS_coordinator_server_port) {
+      log_error();
+      return;
+    }
+    client_.thread_pool_.AddTask([storage, gk = std::move(db_acc), this] {
+      const auto [success, timestamp] = this->ForceResetStorage(storage);
+      if (success) {
+        spdlog::info("Successfully reset storage of REPLICA {} to timestamp {}.", client_.name_, timestamp);
+        return;
+      }
+      spdlog::error("You cannot register REPLICA {} to this MAIN because MAIN couldn't reset REPLICA's storage.",
+                    client_.name_);
+    });
+#else
+    log_error();
+#endif
     return;
   }
 
@@ -190,9 +225,6 @@ void ReplicationStorageClient::StartTransactionReplication(const uint64_t curren
   }
 }
 
-//////// AF: you can't finialize transaction replication if you are not replicating
-/////// AF: if there is no stream or it is Defunct than we need to set replica in MAYBE_BEHIND -> is that even used
-/////// AF:
 bool ReplicationStorageClient::FinalizeTransactionReplication(Storage *storage, DatabaseAccessProtector db_acc) {
   // We can only check the state because it guarantees to be only
   // valid during a single transaction replication (if the assumption
@@ -325,6 +357,21 @@ void ReplicationStorageClient::RecoverReplica(uint64_t replica_commit, memgraph:
   }
 }
 
+std::pair<bool, uint64_t> ReplicationStorageClient::ForceResetStorage(memgraph::storage::Storage *storage) {
+  utils::OnScopeExit set_to_maybe_behind{
+      [this]() { replica_state_.WithLock([](auto &state) { state = replication::ReplicaState::MAYBE_BEHIND; }); }};
+  try {
+    auto stream{client_.rpc_client_.Stream<replication::ForceResetStorageRpc>(main_uuid_, storage->uuid())};
+    const auto res = stream.AwaitResponse();
+    return std::pair{res.success, res.current_commit_timestamp};
+  } catch (const rpc::RpcFailedException &) {
+    spdlog::error(
+        utils::MessageWithLink("Couldn't ForceReset data to {}.", client_.name_, "https://memgr.ph/replication"));
+  }
+
+  return {false, 0};
+}
+
 ////// ReplicaStream //////
 ReplicaStream::ReplicaStream(Storage *storage, rpc::Client &rpc_client, const uint64_t current_seq_num,
                              utils::UUID main_uuid)
@@ -360,6 +407,12 @@ void ReplicaStream::AppendOperation(durability::StorageMetadataOperation operati
                   timestamp);
 }
 
+void ReplicaStream::AppendOperation(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                                    uint64_t timestamp) {
+  replication::Encoder encoder(stream_.GetBuilder());
+  EncodeOperation(&encoder, storage_->name_id_mapper_.get(), operation, edge_type, timestamp);
+}
+
 replication::AppendDeltasRes ReplicaStream::Finalize() { return stream_.AwaitResponse(); }
 
 }  // namespace memgraph::storage
diff --git a/src/storage/v2/replication/replication_client.hpp b/src/storage/v2/replication/replication_client.hpp
index 3352bab65..77a9ba40b 100644
--- a/src/storage/v2/replication/replication_client.hpp
+++ b/src/storage/v2/replication/replication_client.hpp
@@ -65,6 +65,9 @@ class ReplicaStream {
                        const std::set<PropertyId> &properties, const LabelIndexStats &stats,
                        const LabelPropertyIndexStats &property_stats, uint64_t timestamp);
 
+  /// @throw rpc::RpcFailedException
+  void AppendOperation(durability::StorageMetadataOperation operation, EdgeTypeId edge_type, uint64_t timestamp);
+
   /// @throw rpc::RpcFailedException
   replication::AppendDeltasRes Finalize();
 
@@ -188,6 +191,13 @@ class ReplicationStorageClient {
    */
   void UpdateReplicaState(Storage *storage, DatabaseAccessProtector db_acc);
 
+  /**
+   * @brief Forcefully reset storage to as it is when started from scratch.
+   *
+   * @param storage pointer to the storage associated with the client
+   */
+  std::pair<bool, uint64_t> ForceResetStorage(Storage *storage);
+
   void LogRpcFailure();
 
   /**
diff --git a/src/storage/v2/replication/replication_storage_state.cpp b/src/storage/v2/replication/replication_storage_state.cpp
index 25cf484c9..b8f3fef62 100644
--- a/src/storage/v2/replication/replication_storage_state.cpp
+++ b/src/storage/v2/replication/replication_storage_state.cpp
@@ -53,6 +53,16 @@ void ReplicationStorageState::AppendOperation(durability::StorageMetadataOperati
   });
 }
 
+void ReplicationStorageState::AppendOperation(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                                              uint64_t final_commit_timestamp) {
+  replication_clients_.WithLock([&](auto &clients) {
+    for (auto &client : clients) {
+      client->IfStreamingTransaction(
+          [&](auto &stream) { stream.AppendOperation(operation, edge_type, final_commit_timestamp); });
+    }
+  });
+}
+
 bool ReplicationStorageState::FinalizeTransaction(uint64_t timestamp, Storage *storage,
                                                   DatabaseAccessProtector db_acc) {
   return replication_clients_.WithLock([=, db_acc = std::move(db_acc)](auto &clients) mutable {
diff --git a/src/storage/v2/replication/replication_storage_state.hpp b/src/storage/v2/replication/replication_storage_state.hpp
index 91cec563c..f99807c13 100644
--- a/src/storage/v2/replication/replication_storage_state.hpp
+++ b/src/storage/v2/replication/replication_storage_state.hpp
@@ -46,6 +46,8 @@ struct ReplicationStorageState {
   void AppendOperation(durability::StorageMetadataOperation operation, LabelId label,
                        const std::set<PropertyId> &properties, const LabelIndexStats &stats,
                        const LabelPropertyIndexStats &property_stats, uint64_t final_commit_timestamp);
+  void AppendOperation(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                       uint64_t final_commit_timestamp);
   bool FinalizeTransaction(uint64_t timestamp, Storage *storage, DatabaseAccessProtector db_acc);
 
   // Getters
diff --git a/src/storage/v2/replication/rpc.cpp b/src/storage/v2/replication/rpc.cpp
index f523bb5d7..71a9ca65c 100644
--- a/src/storage/v2/replication/rpc.cpp
+++ b/src/storage/v2/replication/rpc.cpp
@@ -59,6 +59,19 @@ void TimestampRes::Save(const TimestampRes &self, memgraph::slk::Builder *builde
   memgraph::slk::Save(self, builder);
 }
 void TimestampRes::Load(TimestampRes *self, memgraph::slk::Reader *reader) { memgraph::slk::Load(self, reader); }
+
+void ForceResetStorageReq::Save(const ForceResetStorageReq &self, memgraph::slk::Builder *builder) {
+  memgraph::slk::Save(self, builder);
+}
+void ForceResetStorageReq::Load(ForceResetStorageReq *self, memgraph::slk::Reader *reader) {
+  memgraph::slk::Load(self, reader);
+}
+void ForceResetStorageRes::Save(const ForceResetStorageRes &self, memgraph::slk::Builder *builder) {
+  memgraph::slk::Save(self, builder);
+}
+void ForceResetStorageRes::Load(ForceResetStorageRes *self, memgraph::slk::Reader *reader) {
+  memgraph::slk::Load(self, reader);
+}
 }  // namespace storage::replication
 
 constexpr utils::TypeInfo storage::replication::AppendDeltasReq::kType{utils::TypeId::REP_APPEND_DELTAS_REQ,
@@ -97,6 +110,12 @@ constexpr utils::TypeInfo storage::replication::TimestampReq::kType{utils::TypeI
 constexpr utils::TypeInfo storage::replication::TimestampRes::kType{utils::TypeId::REP_TIMESTAMP_RES, "TimestampRes",
                                                                     nullptr};
 
+constexpr utils::TypeInfo storage::replication::ForceResetStorageReq::kType{utils::TypeId::REP_FORCE_RESET_STORAGE_REQ,
+                                                                            "ForceResetStorageReq", nullptr};
+
+constexpr utils::TypeInfo storage::replication::ForceResetStorageRes::kType{utils::TypeId::REP_FORCE_RESET_STORAGE_RES,
+                                                                            "ForceResetStorageRes", nullptr};
+
 // Autogenerated SLK serialization code
 namespace slk {
 // Serialize code for TimestampRes
@@ -255,6 +274,30 @@ void Load(memgraph::storage::replication::AppendDeltasReq *self, memgraph::slk::
   memgraph::slk::Load(&self->seq_num, reader);
 }
 
+// Serialize code for ForceResetStorageReq
+
+void Save(const memgraph::storage::replication::ForceResetStorageReq &self, memgraph::slk::Builder *builder) {
+  memgraph::slk::Save(self.main_uuid, builder);
+  memgraph::slk::Save(self.db_uuid, builder);
+}
+
+void Load(memgraph::storage::replication::ForceResetStorageReq *self, memgraph::slk::Reader *reader) {
+  memgraph::slk::Load(&self->main_uuid, reader);
+  memgraph::slk::Load(&self->db_uuid, reader);
+}
+
+// Serialize code for ForceResetStorageRes
+
+void Save(const memgraph::storage::replication::ForceResetStorageRes &self, memgraph::slk::Builder *builder) {
+  memgraph::slk::Save(self.success, builder);
+  memgraph::slk::Save(self.current_commit_timestamp, builder);
+}
+
+void Load(memgraph::storage::replication::ForceResetStorageRes *self, memgraph::slk::Reader *reader) {
+  memgraph::slk::Load(&self->success, reader);
+  memgraph::slk::Load(&self->current_commit_timestamp, reader);
+}
+
 // Serialize SalientConfig
 
 void Save(const memgraph::storage::SalientConfig &self, memgraph::slk::Builder *builder) {
diff --git a/src/storage/v2/replication/rpc.hpp b/src/storage/v2/replication/rpc.hpp
index 67f98d0ae..fb19d82f2 100644
--- a/src/storage/v2/replication/rpc.hpp
+++ b/src/storage/v2/replication/rpc.hpp
@@ -210,6 +210,36 @@ struct TimestampRes {
 
 using TimestampRpc = rpc::RequestResponse<TimestampReq, TimestampRes>;
 
+struct ForceResetStorageReq {
+  static const utils::TypeInfo kType;
+  static const utils::TypeInfo &GetTypeInfo() { return kType; }
+
+  static void Load(ForceResetStorageReq *self, memgraph::slk::Reader *reader);
+  static void Save(const ForceResetStorageReq &self, memgraph::slk::Builder *builder);
+  ForceResetStorageReq() = default;
+  explicit ForceResetStorageReq(const utils::UUID &main_uuid, const utils::UUID &db_uuid)
+      : main_uuid{main_uuid}, db_uuid{db_uuid} {}
+
+  utils::UUID main_uuid;
+  utils::UUID db_uuid;
+};
+
+struct ForceResetStorageRes {
+  static const utils::TypeInfo kType;
+  static const utils::TypeInfo &GetTypeInfo() { return kType; }
+
+  static void Load(ForceResetStorageRes *self, memgraph::slk::Reader *reader);
+  static void Save(const ForceResetStorageRes &self, memgraph::slk::Builder *builder);
+  ForceResetStorageRes() = default;
+  ForceResetStorageRes(bool success, uint64_t current_commit_timestamp)
+      : success(success), current_commit_timestamp(current_commit_timestamp) {}
+
+  bool success;
+  uint64_t current_commit_timestamp;
+};
+
+using ForceResetStorageRpc = rpc::RequestResponse<ForceResetStorageReq, ForceResetStorageRes>;
+
 }  // namespace memgraph::storage::replication
 
 // SLK serialization declarations
@@ -267,4 +297,12 @@ void Save(const memgraph::storage::SalientConfig &self, memgraph::slk::Builder *
 
 void Load(memgraph::storage::SalientConfig *self, memgraph::slk::Reader *reader);
 
+void Save(const memgraph::storage::replication::ForceResetStorageReq &self, memgraph::slk::Builder *builder);
+
+void Load(memgraph::storage::replication::ForceResetStorageReq *self, memgraph::slk::Reader *reader);
+
+void Save(const memgraph::storage::replication::ForceResetStorageRes &self, memgraph::slk::Builder *builder);
+
+void Load(memgraph::storage::replication::ForceResetStorageRes *self, memgraph::slk::Reader *reader);
+
 }  // namespace memgraph::slk
diff --git a/src/storage/v2/storage.hpp b/src/storage/v2/storage.hpp
index d2c42c33a..875d781b7 100644
--- a/src/storage/v2/storage.hpp
+++ b/src/storage/v2/storage.hpp
@@ -30,6 +30,7 @@
 #include "storage/v2/durability/paths.hpp"
 #include "storage/v2/durability/wal.hpp"
 #include "storage/v2/edge_accessor.hpp"
+#include "storage/v2/edges_iterable.hpp"
 #include "storage/v2/indices/indices.hpp"
 #include "storage/v2/mvcc.hpp"
 #include "storage/v2/replication/enums.hpp"
@@ -61,6 +62,7 @@ class EdgeAccessor;
 struct IndicesInfo {
   std::vector<LabelId> label;
   std::vector<std::pair<LabelId, PropertyId>> label_property;
+  std::vector<EdgeTypeId> edge_type;
 };
 
 struct ConstraintsInfo {
@@ -172,6 +174,8 @@ class Storage {
                                       const std::optional<utils::Bound<PropertyValue>> &lower_bound,
                                       const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) = 0;
 
+    virtual EdgesIterable Edges(EdgeTypeId edge_type, View view) = 0;
+
     virtual Result<std::optional<VertexAccessor>> DeleteVertex(VertexAccessor *vertex);
 
     virtual Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> DetachDeleteVertex(
@@ -192,6 +196,8 @@ class Storage {
                                             const std::optional<utils::Bound<PropertyValue>> &lower,
                                             const std::optional<utils::Bound<PropertyValue>> &upper) const = 0;
 
+    virtual uint64_t ApproximateEdgeCount(EdgeTypeId id) const = 0;
+
     virtual std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId &label) const = 0;
 
     virtual std::optional<storage::LabelPropertyIndexStats> GetIndexStats(
@@ -224,6 +230,8 @@ class Storage {
 
     virtual bool LabelPropertyIndexExists(LabelId label, PropertyId property) const = 0;
 
+    virtual bool EdgeTypeIndexExists(EdgeTypeId edge_type) const = 0;
+
     virtual IndicesInfo ListAllIndices() const = 0;
 
     virtual ConstraintsInfo ListAllConstraints() const = 0;
@@ -268,10 +276,14 @@ class Storage {
 
     virtual utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(LabelId label, PropertyId property) = 0;
 
+    virtual utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(EdgeTypeId edge_type) = 0;
+
     virtual utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label) = 0;
 
     virtual utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label, PropertyId property) = 0;
 
+    virtual utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(EdgeTypeId edge_type) = 0;
+
     virtual utils::BasicResult<StorageExistenceConstraintDefinitionError, void> CreateExistenceConstraint(
         LabelId label, PropertyId property) = 0;
 
@@ -285,6 +297,7 @@ class Storage {
                                                                    const std::set<PropertyId> &properties) = 0;
 
     virtual void DropGraph() = 0;
+    auto GetTransaction() -> Transaction * { return std::addressof(transaction_); }
 
    protected:
     Storage *storage_;
@@ -338,9 +351,15 @@ class Storage {
 
   StorageMode GetStorageMode() const noexcept;
 
-  virtual void FreeMemory(std::unique_lock<utils::ResourceLock> main_guard) = 0;
+  virtual void FreeMemory(std::unique_lock<utils::ResourceLock> main_guard, bool periodic) = 0;
 
-  void FreeMemory() { FreeMemory({}); }
+  void FreeMemory() {
+    if (storage_mode_ == StorageMode::IN_MEMORY_ANALYTICAL) {
+      FreeMemory(std::unique_lock{main_lock_}, false);
+    } else {
+      FreeMemory({}, false);
+    }
+  }
 
   virtual std::unique_ptr<Accessor> Access(memgraph::replication_coordination_glue::ReplicationRole replication_role,
                                            std::optional<IsolationLevel> override_isolation_level) = 0;
diff --git a/src/storage/v2/vertices_iterable.cpp b/src/storage/v2/vertices_iterable.cpp
index f6ff46da6..9753052ae 100644
--- a/src/storage/v2/vertices_iterable.cpp
+++ b/src/storage/v2/vertices_iterable.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -10,7 +10,6 @@
 // licenses/APL.txt.
 
 #include "storage/v2/vertices_iterable.hpp"
-
 namespace memgraph::storage {
 
 VerticesIterable::VerticesIterable(AllVerticesIterable vertices) : type_(Type::ALL) {
diff --git a/src/storage/v2/vertices_iterable.hpp b/src/storage/v2/vertices_iterable.hpp
index e057e8a38..6075a68a2 100644
--- a/src/storage/v2/vertices_iterable.hpp
+++ b/src/storage/v2/vertices_iterable.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt
index bac3e78f3..802b8ff6f 100644
--- a/src/utils/CMakeLists.txt
+++ b/src/utils/CMakeLists.txt
@@ -24,6 +24,7 @@ find_package(Threads REQUIRED)
 
 add_library(mg-utils STATIC ${utils_src_files})
 add_library(mg::utils ALIAS mg-utils)
+
 target_link_libraries(mg-utils PUBLIC Boost::headers fmt::fmt spdlog::spdlog json)
 target_link_libraries(mg-utils PRIVATE librdtsc stdc++fs Threads::Threads gflags uuid rt)
 
diff --git a/src/utils/atomic_memory_block.hpp b/src/utils/atomic_memory_block.hpp
index c15424549..31a3cf3a9 100644
--- a/src/utils/atomic_memory_block.hpp
+++ b/src/utils/atomic_memory_block.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/src/utils/disk_utils.hpp b/src/utils/disk_utils.hpp
index c4b9accd6..29e0c9893 100644
--- a/src/utils/disk_utils.hpp
+++ b/src/utils/disk_utils.hpp
@@ -21,7 +21,7 @@ inline std::optional<std::string> GetOldDiskKeyOrNull(storage::Delta *head) {
     head = head->next;
   }
   if (head->action == storage::Delta::Action::DELETE_DESERIALIZED_OBJECT) {
-    return head->old_disk_key.value;
+    return head->old_disk_key.value.as_opt_str();
   }
   return std::nullopt;
 }
diff --git a/src/utils/event_counter.cpp b/src/utils/event_counter.cpp
index a7f4d30fb..2d526936c 100644
--- a/src/utils/event_counter.cpp
+++ b/src/utils/event_counter.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -26,6 +26,7 @@
   M(ScanAllByLabelPropertyValueOperator, Operator, "Number of times ScanAllByLabelPropertyValue operator was used.") \
   M(ScanAllByLabelPropertyOperator, Operator, "Number of times ScanAllByLabelProperty operator was used.")           \
   M(ScanAllByIdOperator, Operator, "Number of times ScanAllById operator was used.")                                 \
+  M(ScanAllByEdgeTypeOperator, Operator, "Number of times ScanAllByEdgeTypeOperator operator was used.")             \
   M(ExpandOperator, Operator, "Number of times Expand operator was used.")                                           \
   M(ExpandVariableOperator, Operator, "Number of times ExpandVariable operator was used.")                           \
   M(ConstructNamedPathOperator, Operator, "Number of times ConstructNamedPath operator was used.")                   \
diff --git a/src/utils/functional.hpp b/src/utils/functional.hpp
index e0714de2a..fe60edc5c 100644
--- a/src/utils/functional.hpp
+++ b/src/utils/functional.hpp
@@ -18,8 +18,11 @@
 
 namespace memgraph::utils {
 
-template <class F, class T, class R = typename std::invoke_result<F, T>::type>
-auto fmap(F &&f, std::vector<T> const &v) -> std::vector<R> {
+template <template <typename, typename...> class Container, typename T, typename Allocator = std::allocator<T>,
+          typename F = std::identity, typename R = std::decay_t<std::invoke_result_t<F, T>>>
+requires ranges::range<Container<T, Allocator>> &&
+    (!std::same_as<Container<T, Allocator>, std::string>)auto fmap(const Container<T, Allocator> &v, F &&f = {})
+        -> std::vector<R> {
   return v | ranges::views::transform(std::forward<F>(f)) | ranges::to<std::vector<R>>();
 }
 
diff --git a/src/utils/resource_lock.hpp b/src/utils/resource_lock.hpp
index 7a3ef9444..1a812935a 100644
--- a/src/utils/resource_lock.hpp
+++ b/src/utils/resource_lock.hpp
@@ -66,6 +66,17 @@ struct ResourceLock {
     }
     return false;
   }
+
+  template <typename Rep, typename Period>
+  bool try_lock_shared_for(std::chrono::duration<Rep, Period> const &time) {
+    auto lock = std::unique_lock{mtx};
+    // block until available
+    if (!cv.wait_for(lock, time, [this] { return state != UNIQUE; })) return false;
+    state = SHARED;
+    ++count;
+    return true;
+  }
+
   void unlock() {
     auto lock = std::unique_lock{mtx};
     state = UNLOCKED;
diff --git a/src/utils/scheduler.hpp b/src/utils/scheduler.hpp
index 742271a95..45b2c8b04 100644
--- a/src/utils/scheduler.hpp
+++ b/src/utils/scheduler.hpp
@@ -57,7 +57,7 @@ class Scheduler {
         // program and there is probably no work to do in scheduled function at
         // the start of the program. Since Server will log some messages on
         // the program start we let him log first and we make sure by first
-        // waiting that funcion f will not log before it.
+        // waiting that function f will not log before it.
         // Check for pause also.
         std::unique_lock<std::mutex> lk(mutex_);
         auto now = std::chrono::system_clock::now();
diff --git a/src/utils/settings.cpp b/src/utils/settings.cpp
index 4768edc42..5e0954b4b 100644
--- a/src/utils/settings.cpp
+++ b/src/utils/settings.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/src/utils/string.hpp b/src/utils/string.hpp
index e5c4c4f3c..26709439f 100644
--- a/src/utils/string.hpp
+++ b/src/utils/string.hpp
@@ -229,6 +229,13 @@ inline std::vector<std::string> Split(const std::string_view src, const std::str
   return res;
 }
 
+inline std::vector<std::string_view> SplitView(const std::string_view src, const std::string_view delimiter,
+                                               int splits = -1) {
+  std::vector<std::string_view> res;
+  Split(&res, src, delimiter, splits);
+  return res;
+}
+
 /**
  * Split a string by whitespace into a vector.
  * Runs of consecutive whitespace are regarded as a single delimiter.
@@ -242,7 +249,7 @@ std::vector<TString, TAllocator> *Split(std::vector<TString, TAllocator> *out, c
   if (src.empty()) return out;
   // TODO: Investigate how much regex allocate and perhaps replace with custom
   // solution doing no allocations.
-  std::regex not_whitespace("[^\\s]+");
+  static std::regex not_whitespace("[^\\s]+");
   auto matches_begin = std::cregex_iterator(src.data(), src.data() + src.size(), not_whitespace);
   auto matches_end = std::cregex_iterator();
   out->reserve(std::distance(matches_begin, matches_end));
diff --git a/src/utils/typeinfo.hpp b/src/utils/typeinfo.hpp
index 8ee7cdc33..77129f5db 100644
--- a/src/utils/typeinfo.hpp
+++ b/src/utils/typeinfo.hpp
@@ -32,6 +32,7 @@ enum class TypeId : uint64_t {
   SCAN_ALL_BY_LABEL_PROPERTY_VALUE,
   SCAN_ALL_BY_LABEL_PROPERTY,
   SCAN_ALL_BY_ID,
+  SCAN_ALL_BY_EDGE_TYPE,
   EXPAND_COMMON,
   EXPAND,
   EXPANSION_LAMBDA,
@@ -68,6 +69,7 @@ enum class TypeId : uint64_t {
   APPLY,
   INDEXED_JOIN,
   HASH_JOIN,
+  ROLLUP_APPLY,
 
   // Replication
   // NOTE: these NEED to be stable in the 2000+ range (see rpc version)
@@ -99,6 +101,8 @@ enum class TypeId : uint64_t {
   REP_DROP_AUTH_DATA_RES,
   REP_TRY_SET_MAIN_UUID_REQ,
   REP_TRY_SET_MAIN_UUID_RES,
+  REP_FORCE_RESET_STORAGE_REQ,
+  REP_FORCE_RESET_STORAGE_RES,
 
   // Coordinator
   COORD_FAILOVER_REQ,
@@ -114,6 +118,8 @@ enum class TypeId : uint64_t {
 
   COORD_GET_UUID_REQ,
   COORD_GET_UUID_RES,
+  COORD_GET_INSTANCE_DATABASES_REQ,
+  COORD_GET_INSTANCE_DATABASES_RES,
 
   // AST
   AST_LABELIX = 3000,
@@ -180,6 +186,7 @@ enum class TypeId : uint64_t {
   AST_EXPLAIN_QUERY,
   AST_PROFILE_QUERY,
   AST_INDEX_QUERY,
+  AST_EDGE_INDEX_QUERY,
   AST_CREATE,
   AST_CALL_PROCEDURE,
   AST_MATCH,
diff --git a/src/utils/uuid.cpp b/src/utils/uuid.cpp
index fbcf662de..d55cfaba3 100644
--- a/src/utils/uuid.cpp
+++ b/src/utils/uuid.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -10,7 +10,7 @@
 // licenses/APL.txt.
 
 #include "utils/uuid.hpp"
-#include <uuid/uuid.h>
+
 #include "slk/serialization.hpp"
 
 namespace memgraph::utils {
diff --git a/src/utils/uuid.hpp b/src/utils/uuid.hpp
index bca55d73b..bbfec6228 100644
--- a/src/utils/uuid.hpp
+++ b/src/utils/uuid.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -12,6 +12,7 @@
 #pragma once
 
 #include <uuid/uuid.h>
+
 #include <array>
 #include <json/json.hpp>
 #include <string>
@@ -39,9 +40,10 @@ struct UUID {
 
   UUID() { uuid_generate(uuid.data()); }
   explicit operator std::string() const {
-    auto decoded = std::array<char, UUID_STR_LEN>{};
+    // Note not using UUID_STR_LEN so we can build with older libuuid
+    auto decoded = std::array<char, 37 /*UUID_STR_LEN*/>{};
     uuid_unparse(uuid.data(), decoded.data());
-    return std::string{decoded.data(), UUID_STR_LEN - 1};
+    return std::string{decoded.data(), 37 /*UUID_STR_LEN*/ - 1};
   }
 
   explicit operator arr_t() const { return uuid; }
diff --git a/tests/e2e/high_availability/coord_cluster_registration.py b/tests/e2e/high_availability/coord_cluster_registration.py
index 68a387281..774c6dca1 100644
--- a/tests/e2e/high_availability/coord_cluster_registration.py
+++ b/tests/e2e/high_availability/coord_cluster_registration.py
@@ -133,12 +133,12 @@ def test_register_repl_instances_then_coordinators():
         return sorted(list(execute_and_fetch_all(coordinator3_cursor, "SHOW INSTANCES")))
 
     expected_cluster_coord3 = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_cluster_coord3, check_coordinator3)
 
@@ -147,21 +147,23 @@ def test_register_repl_instances_then_coordinators():
     def check_coordinator1():
         return sorted(list(execute_and_fetch_all(coordinator1_cursor, "SHOW INSTANCES")))
 
-    # TODO: (andi) This should be solved eventually
-    expected_cluster_not_shared = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "replica"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),
     ]
 
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
 
     coordinator2_cursor = connect(host="localhost", port=7691).cursor()
 
     def check_coordinator2():
         return sorted(list(execute_and_fetch_all(coordinator2_cursor, "SHOW INSTANCES")))
 
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator2)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
 
 
 def test_register_coordinator_then_repl_instances():
@@ -187,12 +189,12 @@ def test_register_coordinator_then_repl_instances():
         return sorted(list(execute_and_fetch_all(coordinator3_cursor, "SHOW INSTANCES")))
 
     expected_cluster_coord3 = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_cluster_coord3, check_coordinator3)
 
@@ -201,21 +203,23 @@ def test_register_coordinator_then_repl_instances():
     def check_coordinator1():
         return sorted(list(execute_and_fetch_all(coordinator1_cursor, "SHOW INSTANCES")))
 
-    # TODO: (andi) This should be solved eventually
-    expected_cluster_not_shared = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "replica"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),
     ]
 
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
 
     coordinator2_cursor = connect(host="localhost", port=7691).cursor()
 
     def check_coordinator2():
         return sorted(list(execute_and_fetch_all(coordinator2_cursor, "SHOW INSTANCES")))
 
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator2)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
 
 
 def test_coordinators_communication_with_restarts():
@@ -237,10 +241,13 @@ def test_coordinators_communication_with_restarts():
     )
     execute_and_fetch_all(coordinator3_cursor, "SET INSTANCE instance_3 TO MAIN")
 
-    expected_cluster_not_shared = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "replica"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),
     ]
 
     coordinator1_cursor = connect(host="localhost", port=7690).cursor()
@@ -248,20 +255,20 @@ def test_coordinators_communication_with_restarts():
     def check_coordinator1():
         return sorted(list(execute_and_fetch_all(coordinator1_cursor, "SHOW INSTANCES")))
 
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
 
     coordinator2_cursor = connect(host="localhost", port=7691).cursor()
 
     def check_coordinator2():
         return sorted(list(execute_and_fetch_all(coordinator2_cursor, "SHOW INSTANCES")))
 
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator2)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
 
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "coordinator_1")
     interactive_mg_runner.start(MEMGRAPH_INSTANCES_DESCRIPTION, "coordinator_1")
     coordinator1_cursor = connect(host="localhost", port=7690).cursor()
 
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
 
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "coordinator_1")
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "coordinator_2")
@@ -271,11 +278,11 @@ def test_coordinators_communication_with_restarts():
     coordinator1_cursor = connect(host="localhost", port=7690).cursor()
     coordinator2_cursor = connect(host="localhost", port=7691).cursor()
 
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator1)
-    mg_sleep_and_assert(expected_cluster_not_shared, check_coordinator2)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
 
 
-# TODO: (andi) Test when dealing with distributed coordinators that you can register on one coordinator and unregister from any other coordinator
+# # TODO: (andi) Test when dealing with distributed coordinators that you can register on one coordinator and unregister from any other coordinator
 @pytest.mark.parametrize(
     "kill_instance",
     [True, False],
@@ -284,7 +291,12 @@ def test_unregister_replicas(kill_instance):
     safe_execute(shutil.rmtree, TEMP_DIR)
     interactive_mg_runner.start_all(MEMGRAPH_INSTANCES_DESCRIPTION)
 
+    coordinator1_cursor = connect(host="localhost", port=7690).cursor()
+    coordinator2_cursor = connect(host="localhost", port=7691).cursor()
     coordinator3_cursor = connect(host="localhost", port=7692).cursor()
+
+    assert add_coordinator(coordinator3_cursor, "ADD COORDINATOR 1 ON '127.0.0.1:10111'")
+    assert add_coordinator(coordinator3_cursor, "ADD COORDINATOR 2 ON '127.0.0.1:10112'")
     execute_and_fetch_all(
         coordinator3_cursor, "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001'"
     )
@@ -296,6 +308,12 @@ def test_unregister_replicas(kill_instance):
     )
     execute_and_fetch_all(coordinator3_cursor, "SET INSTANCE instance_3 TO MAIN")
 
+    def check_coordinator1():
+        return sorted(list(execute_and_fetch_all(coordinator1_cursor, "SHOW INSTANCES")))
+
+    def check_coordinator2():
+        return sorted(list(execute_and_fetch_all(coordinator2_cursor, "SHOW INSTANCES")))
+
     def check_coordinator3():
         return sorted(list(execute_and_fetch_all(coordinator3_cursor, "SHOW INSTANCES")))
 
@@ -305,10 +323,21 @@ def test_unregister_replicas(kill_instance):
         return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS")))
 
     expected_cluster = [
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+    ]
+
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "replica"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),
     ]
 
     expected_replicas = [
@@ -328,6 +357,8 @@ def test_unregister_replicas(kill_instance):
         ),
     ]
 
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
     mg_sleep_and_assert(expected_cluster, check_coordinator3)
     mg_sleep_and_assert(expected_replicas, check_main)
 
@@ -336,9 +367,19 @@ def test_unregister_replicas(kill_instance):
     execute_and_fetch_all(coordinator3_cursor, "UNREGISTER INSTANCE instance_1")
 
     expected_cluster = [
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+    ]
+
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),
     ]
 
     expected_replicas = [
@@ -351,6 +392,8 @@ def test_unregister_replicas(kill_instance):
         ),
     ]
 
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
     mg_sleep_and_assert(expected_cluster, check_coordinator3)
     mg_sleep_and_assert(expected_replicas, check_main)
 
@@ -359,11 +402,22 @@ def test_unregister_replicas(kill_instance):
     execute_and_fetch_all(coordinator3_cursor, "UNREGISTER INSTANCE instance_2")
 
     expected_cluster = [
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+    ]
+
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_3", "", "", "unknown", "main"),
     ]
     expected_replicas = []
 
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
     mg_sleep_and_assert(expected_cluster, check_coordinator3)
     mg_sleep_and_assert(expected_replicas, check_main)
 
@@ -372,7 +426,11 @@ def test_unregister_main():
     safe_execute(shutil.rmtree, TEMP_DIR)
     interactive_mg_runner.start_all(MEMGRAPH_INSTANCES_DESCRIPTION)
 
+    coordinator1_cursor = connect(host="localhost", port=7690).cursor()
+    coordinator2_cursor = connect(host="localhost", port=7691).cursor()
     coordinator3_cursor = connect(host="localhost", port=7692).cursor()
+    assert add_coordinator(coordinator3_cursor, "ADD COORDINATOR 1 ON '127.0.0.1:10111'")
+    assert add_coordinator(coordinator3_cursor, "ADD COORDINATOR 2 ON '127.0.0.1:10112'")
     execute_and_fetch_all(
         coordinator3_cursor, "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001'"
     )
@@ -384,16 +442,35 @@ def test_unregister_main():
     )
     execute_and_fetch_all(coordinator3_cursor, "SET INSTANCE instance_3 TO MAIN")
 
+    def check_coordinator1():
+        return sorted(list(execute_and_fetch_all(coordinator1_cursor, "SHOW INSTANCES")))
+
+    def check_coordinator2():
+        return sorted(list(execute_and_fetch_all(coordinator2_cursor, "SHOW INSTANCES")))
+
     def check_coordinator3():
         return sorted(list(execute_and_fetch_all(coordinator3_cursor, "SHOW INSTANCES")))
 
     expected_cluster = [
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
 
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "replica"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),
+    ]
+
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
     mg_sleep_and_assert(expected_cluster, check_coordinator3)
 
     try:
@@ -407,20 +484,43 @@ def test_unregister_main():
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_3")
 
     expected_cluster = [
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "main"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", False, "unknown"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
     ]
 
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "main"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),
+    ]
+
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
     mg_sleep_and_assert(expected_cluster, check_coordinator3)
 
     execute_and_fetch_all(coordinator3_cursor, "UNREGISTER INSTANCE instance_3")
 
     expected_cluster = [
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "main"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+    ]
+
+    expected_cluster_shared = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "main"),
+        ("instance_2", "", "", "unknown", "replica"),
     ]
 
     expected_replicas = [
@@ -438,6 +538,8 @@ def test_unregister_main():
     def check_main():
         return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS")))
 
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator1)
+    mg_sleep_and_assert(expected_cluster_shared, check_coordinator2)
     mg_sleep_and_assert(expected_cluster, check_coordinator3)
     mg_sleep_and_assert(expected_replicas, check_main)
 
diff --git a/tests/e2e/high_availability/coordinator.py b/tests/e2e/high_availability/coordinator.py
index 4330c2194..ed55dff9e 100644
--- a/tests/e2e/high_availability/coordinator.py
+++ b/tests/e2e/high_availability/coordinator.py
@@ -44,10 +44,10 @@ def test_coordinator_show_instances():
         return sorted(list(execute_and_fetch_all(cursor, "SHOW INSTANCES;")))
 
     expected_data = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_data, retrieve_data)
 
diff --git a/tests/e2e/high_availability/disable_writing_on_main_after_restart.py b/tests/e2e/high_availability/disable_writing_on_main_after_restart.py
index 53d570a6d..363ce1c41 100644
--- a/tests/e2e/high_availability/disable_writing_on_main_after_restart.py
+++ b/tests/e2e/high_availability/disable_writing_on_main_after_restart.py
@@ -143,20 +143,20 @@ def test_writing_disabled_on_main_restart():
         return sorted(list(execute_and_fetch_all(coordinator3_cursor, "SHOW INSTANCES")))
 
     expected_cluster_coord3 = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_cluster_coord3, check_coordinator3)
 
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_3")
 
     expected_cluster_coord3 = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_3", "", "127.0.0.1:10013", False, "unknown"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
     ]
 
     mg_sleep_and_assert(expected_cluster_coord3, check_coordinator3)
@@ -173,10 +173,10 @@ def test_writing_disabled_on_main_restart():
         )
 
     expected_cluster_coord3 = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
 
     mg_sleep_and_assert(expected_cluster_coord3, check_coordinator3)
diff --git a/tests/e2e/high_availability/distributed_coords.py b/tests/e2e/high_availability/distributed_coords.py
index 07b6eefe0..7dc3ef238 100644
--- a/tests/e2e/high_availability/distributed_coords.py
+++ b/tests/e2e/high_availability/distributed_coords.py
@@ -17,7 +17,11 @@ import tempfile
 import interactive_mg_runner
 import pytest
 from common import connect, execute_and_fetch_all, safe_execute
-from mg_utils import mg_sleep_and_assert, mg_sleep_and_assert_collection
+from mg_utils import (
+    mg_sleep_and_assert,
+    mg_sleep_and_assert_any_function,
+    mg_sleep_and_assert_collection,
+)
 
 interactive_mg_runner.SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
 interactive_mg_runner.PROJECT_DIR = os.path.normpath(
@@ -117,6 +121,205 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
 }
 
 
+def get_instances_description_no_setup():
+    return {
+        "instance_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7687",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10011",
+            ],
+            "log_file": "instance_1.log",
+            "data_directory": f"{TEMP_DIR}/instance_1",
+            "setup_queries": [],
+        },
+        "instance_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7688",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10012",
+            ],
+            "log_file": "instance_2.log",
+            "data_directory": f"{TEMP_DIR}/instance_2",
+            "setup_queries": [],
+        },
+        "instance_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7689",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10013",
+            ],
+            "log_file": "instance_3.log",
+            "data_directory": f"{TEMP_DIR}/instance_3",
+            "setup_queries": [],
+        },
+        "coordinator_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7690",
+                "--log-level=TRACE",
+                "--raft-server-id=1",
+                "--raft-server-port=10111",
+            ],
+            "log_file": "coordinator1.log",
+            "data_directory": f"{TEMP_DIR}/coordinator_1",
+            "setup_queries": [],
+        },
+        "coordinator_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7691",
+                "--log-level=TRACE",
+                "--raft-server-id=2",
+                "--raft-server-port=10112",
+            ],
+            "log_file": "coordinator2.log",
+            "data_directory": f"{TEMP_DIR}/coordinator_2",
+            "setup_queries": [],
+        },
+        "coordinator_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7692",
+                "--log-level=TRACE",
+                "--raft-server-id=3",
+                "--raft-server-port=10113",
+            ],
+            "log_file": "coordinator3.log",
+            "data_directory": f"{TEMP_DIR}/coordinator_3",
+            "setup_queries": [],
+        },
+    }
+
+
+def test_old_main_comes_back_on_new_leader_as_replica():
+    # 1. Start all instances.
+    # 2. Kill the main instance
+    # 3. Kill the leader
+    # 4. Start the old main instance
+    # 5. Run SHOW INSTANCES on the new leader and check that the old main instance is registered as a replica
+    # 6. Start again previous leader
+
+    safe_execute(shutil.rmtree, TEMP_DIR)
+    inner_instances_description = get_instances_description_no_setup()
+
+    interactive_mg_runner.start_all(inner_instances_description)
+
+    setup_queries = [
+        "ADD COORDINATOR 1 ON '127.0.0.1:10111'",
+        "ADD COORDINATOR 2 ON '127.0.0.1:10112'",
+        "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001'",
+        "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002'",
+        "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003'",
+        "SET INSTANCE instance_3 TO MAIN",
+    ]
+    coord_cursor_3 = connect(host="localhost", port=7692).cursor()
+    for query in setup_queries:
+        execute_and_fetch_all(coord_cursor_3, query)
+
+    interactive_mg_runner.kill(inner_instances_description, "coordinator_3")
+    interactive_mg_runner.kill(inner_instances_description, "instance_3")
+
+    coord_cursor_1 = connect(host="localhost", port=7690).cursor()
+
+    def show_instances_coord1():
+        return sorted(list(execute_and_fetch_all(coord_cursor_1, "SHOW INSTANCES;")))
+
+    coord_cursor_2 = connect(host="localhost", port=7691).cursor()
+
+    def show_instances_coord2():
+        return sorted(list(execute_and_fetch_all(coord_cursor_2, "SHOW INSTANCES;")))
+
+    leader_data = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+    ]
+    mg_sleep_and_assert_any_function(leader_data, [show_instances_coord1, show_instances_coord2])
+
+    follower_data = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "main"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),  # TODO: (andi) Will become unknown.
+    ]
+    mg_sleep_and_assert_any_function(leader_data, [show_instances_coord1, show_instances_coord2])
+    mg_sleep_and_assert_any_function(follower_data, [show_instances_coord1, show_instances_coord2])
+
+    interactive_mg_runner.start(inner_instances_description, "instance_3")
+
+    leader_data = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "replica"),
+    ]
+    mg_sleep_and_assert_any_function(leader_data, [show_instances_coord1, show_instances_coord2])
+
+    new_main_cursor = connect(host="localhost", port=7687).cursor()
+
+    def show_replicas():
+        return sorted(list(execute_and_fetch_all(new_main_cursor, "SHOW REPLICAS;")))
+
+    replicas = [
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+        ),
+        (
+            "instance_3",
+            "127.0.0.1:10003",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+        ),
+    ]
+    mg_sleep_and_assert_collection(replicas, show_replicas)
+
+    execute_and_fetch_all(new_main_cursor, "CREATE (n:Node {name: 'node'})")
+
+    replica_2_cursor = connect(host="localhost", port=7688).cursor()
+
+    def get_vertex_count():
+        return execute_and_fetch_all(replica_2_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(1, get_vertex_count)
+
+    replica_3_cursor = connect(host="localhost", port=7689).cursor()
+
+    def get_vertex_count():
+        return execute_and_fetch_all(replica_3_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(1, get_vertex_count)
+
+    interactive_mg_runner.start(inner_instances_description, "coordinator_3")
+
+
 def test_distributed_automatic_failover():
     safe_execute(shutil.rmtree, TEMP_DIR)
     interactive_mg_runner.start_all(MEMGRAPH_INSTANCES_DESCRIPTION)
@@ -138,8 +341,11 @@ def test_distributed_automatic_failover():
             {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
         ),
     ]
-    actual_data_on_main = sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
-    assert actual_data_on_main == sorted(expected_data_on_main)
+
+    def retrieve_data_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    mg_sleep_and_assert_collection(expected_data_on_main, retrieve_data_show_replicas)
 
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_3")
 
@@ -149,13 +355,14 @@ def test_distributed_automatic_failover():
         return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_on_coord = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("coordinator_2", "127.0.0.1:10112", "", True, "coordinator"),
-        ("coordinator_3", "127.0.0.1:10113", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "main"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", False, "unknown"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
     ]
+
     mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_repl_cluster)
 
     new_main_cursor = connect(host="localhost", port=7687).cursor()
@@ -202,5 +409,845 @@ def test_distributed_automatic_failover():
     mg_sleep_and_assert_collection(expected_data_on_new_main_old_alive, retrieve_data_show_replicas)
 
 
+def test_distributed_automatic_failover_with_leadership_change():
+    safe_execute(shutil.rmtree, TEMP_DIR)
+    inner_instances_description = get_instances_description_no_setup()
+
+    interactive_mg_runner.start_all(inner_instances_description)
+
+    setup_queries = [
+        "ADD COORDINATOR 1 ON '127.0.0.1:10111'",
+        "ADD COORDINATOR 2 ON '127.0.0.1:10112'",
+        "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001'",
+        "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002'",
+        "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003'",
+        "SET INSTANCE instance_3 TO MAIN",
+    ]
+    coord_cursor_3 = connect(host="localhost", port=7692).cursor()
+    for query in setup_queries:
+        execute_and_fetch_all(coord_cursor_3, query)
+
+    interactive_mg_runner.kill(inner_instances_description, "coordinator_3")
+    interactive_mg_runner.kill(inner_instances_description, "instance_3")
+
+    coord_cursor_1 = connect(host="localhost", port=7690).cursor()
+
+    def show_instances_coord1():
+        return sorted(list(execute_and_fetch_all(coord_cursor_1, "SHOW INSTANCES;")))
+
+    coord_cursor_2 = connect(host="localhost", port=7691).cursor()
+
+    def show_instances_coord2():
+        return sorted(list(execute_and_fetch_all(coord_cursor_2, "SHOW INSTANCES;")))
+
+    leader_data = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+    ]
+    mg_sleep_and_assert_any_function(leader_data, [show_instances_coord1, show_instances_coord2])
+
+    follower_data = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "main"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),  # TODO: (andi) Will become unknown.
+    ]
+    mg_sleep_and_assert_any_function(leader_data, [show_instances_coord1, show_instances_coord2])
+    mg_sleep_and_assert_any_function(follower_data, [show_instances_coord1, show_instances_coord2])
+
+    new_main_cursor = connect(host="localhost", port=7687).cursor()
+
+    def retrieve_data_show_replicas():
+        return sorted(list(execute_and_fetch_all(new_main_cursor, "SHOW REPLICAS;")))
+
+    expected_data_on_new_main = [
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+        ),
+        (
+            "instance_3",
+            "127.0.0.1:10003",
+            "sync",
+            {"ts": 0, "behind": None, "status": "invalid"},
+            {"memgraph": {"ts": 0, "behind": 0, "status": "invalid"}},
+        ),
+    ]
+    mg_sleep_and_assert_collection(expected_data_on_new_main, retrieve_data_show_replicas)
+
+    interactive_mg_runner.start(inner_instances_description, "instance_3")
+    expected_data_on_new_main_old_alive = [
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+        ),
+        (
+            "instance_3",
+            "127.0.0.1:10003",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+        ),
+    ]
+
+    mg_sleep_and_assert_collection(expected_data_on_new_main_old_alive, retrieve_data_show_replicas)
+
+    interactive_mg_runner.start(inner_instances_description, "coordinator_3")
+
+
+def test_no_leader_after_leader_and_follower_die():
+    # 1. Register all but one replication instnce on the first leader.
+    # 2. Kill the leader and a follower.
+    # 3. Check that the remaining follower is not promoted to leader by trying to register remaining replication instance.
+
+    safe_execute(shutil.rmtree, TEMP_DIR)
+
+    interactive_mg_runner.start_all(MEMGRAPH_INSTANCES_DESCRIPTION)
+
+    interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "coordinator_3")
+    interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "coordinator_2")
+
+    coord_cursor_1 = connect(host="localhost", port=7690).cursor()
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(coord_cursor_1, "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.10001'")
+        assert str(e) == "Couldn't register replica instance since coordinator is not a leader!"
+
+
+def test_old_main_comes_back_on_new_leader_as_main():
+    # 1. Start all instances.
+    # 2. Kill all instances
+    # 3. Kill the leader
+    # 4. Start the old main instance
+    # 5. Run SHOW INSTANCES on the new leader and check that the old main instance is main once again
+
+    safe_execute(shutil.rmtree, TEMP_DIR)
+
+    inner_memgraph_instances = get_instances_description_no_setup()
+    interactive_mg_runner.start_all(inner_memgraph_instances)
+
+    coord_cursor_3 = connect(host="localhost", port=7692).cursor()
+
+    setup_queries = [
+        "ADD COORDINATOR 1 ON '127.0.0.1:10111'",
+        "ADD COORDINATOR 2 ON '127.0.0.1:10112'",
+        "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001'",
+        "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002'",
+        "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003'",
+        "SET INSTANCE instance_3 TO MAIN",
+    ]
+
+    for query in setup_queries:
+        execute_and_fetch_all(coord_cursor_3, query)
+
+    interactive_mg_runner.kill(inner_memgraph_instances, "instance_1")
+    interactive_mg_runner.kill(inner_memgraph_instances, "instance_2")
+    interactive_mg_runner.kill(inner_memgraph_instances, "instance_3")
+    interactive_mg_runner.kill(inner_memgraph_instances, "coordinator_3")
+
+    coord_cursor_1 = connect(host="localhost", port=7690).cursor()
+
+    def show_instances_coord1():
+        return sorted(list(execute_and_fetch_all(coord_cursor_1, "SHOW INSTANCES;")))
+
+    coord_cursor_2 = connect(host="localhost", port=7691).cursor()
+
+    def show_instances_coord2():
+        return sorted(list(execute_and_fetch_all(coord_cursor_2, "SHOW INSTANCES;")))
+
+    interactive_mg_runner.start(inner_memgraph_instances, "instance_3")
+
+    leader_data = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+    ]
+
+    follower_data = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("instance_1", "", "", "unknown", "replica"),
+        ("instance_2", "", "", "unknown", "replica"),
+        ("instance_3", "", "", "unknown", "main"),
+    ]
+    mg_sleep_and_assert_any_function(leader_data, [show_instances_coord1, show_instances_coord2])
+    mg_sleep_and_assert_any_function(follower_data, [show_instances_coord1, show_instances_coord2])
+
+    interactive_mg_runner.start(inner_memgraph_instances, "instance_1")
+    interactive_mg_runner.start(inner_memgraph_instances, "instance_2")
+
+    new_main_cursor = connect(host="localhost", port=7689).cursor()
+
+    def show_replicas():
+        return sorted(list(execute_and_fetch_all(new_main_cursor, "SHOW REPLICAS;")))
+
+    replicas = [
+        (
+            "instance_1",
+            "127.0.0.1:10001",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+        ),
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+        ),
+    ]
+    mg_sleep_and_assert_collection(replicas, show_replicas)
+
+    execute_and_fetch_all(new_main_cursor, "CREATE (n:Node {name: 'node'})")
+
+    replica_1_cursor = connect(host="localhost", port=7687).cursor()
+    assert len(execute_and_fetch_all(replica_1_cursor, "MATCH (n) RETURN n;")) == 1
+
+    replica_2_cursor = connect(host="localhost", port=7688).cursor()
+    assert len(execute_and_fetch_all(replica_2_cursor, "MATCH (n) RETURN n;")) == 1
+
+    interactive_mg_runner.start(inner_memgraph_instances, "coordinator_3")
+
+
+def test_registering_4_coords():
+    # Goal of this test is to assure registering of multiple coordinators in row works
+    safe_execute(shutil.rmtree, TEMP_DIR)
+    INSTANCES_DESCRIPTION = {
+        "instance_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7687",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10011",
+            ],
+            "log_file": "instance_1.log",
+            "data_directory": f"{TEMP_DIR}/instance_1",
+            "setup_queries": [],
+        },
+        "instance_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7688",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10012",
+            ],
+            "log_file": "instance_2.log",
+            "data_directory": f"{TEMP_DIR}/instance_2",
+            "setup_queries": [],
+        },
+        "instance_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7689",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10013",
+            ],
+            "log_file": "instance_3.log",
+            "data_directory": f"{TEMP_DIR}/instance_3",
+            "setup_queries": [],
+        },
+        "coordinator_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7690",
+                "--log-level=TRACE",
+                "--raft-server-id=1",
+                "--raft-server-port=10111",
+            ],
+            "log_file": "coordinator1.log",
+            "setup_queries": [],
+        },
+        "coordinator_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7691",
+                "--log-level=TRACE",
+                "--raft-server-id=2",
+                "--raft-server-port=10112",
+            ],
+            "log_file": "coordinator2.log",
+            "setup_queries": [],
+        },
+        "coordinator_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7692",
+                "--log-level=TRACE",
+                "--raft-server-id=3",
+                "--raft-server-port=10113",
+            ],
+            "log_file": "coordinator3.log",
+            "setup_queries": [],
+        },
+        "coordinator_4": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7693",
+                "--log-level=TRACE",
+                "--raft-server-id=4",
+                "--raft-server-port=10114",
+            ],
+            "log_file": "coordinator4.log",
+            "setup_queries": [
+                "ADD COORDINATOR 1 ON '127.0.0.1:10111';",
+                "ADD COORDINATOR 2 ON '127.0.0.1:10112';",
+                "ADD COORDINATOR 3 ON '127.0.0.1:10113';",
+                "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001'",
+                "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002'",
+                "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003'",
+                "SET INSTANCE instance_3 TO MAIN",
+            ],
+        },
+    }
+
+    interactive_mg_runner.start_all(INSTANCES_DESCRIPTION)
+
+    coord_cursor = connect(host="localhost", port=7693).cursor()
+
+    def retrieve_data_show_repl_cluster():
+        return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("coordinator_4", "127.0.0.1:10114", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_repl_cluster)
+
+
+def test_registering_coord_log_store():
+    # Goal of this test is to assure registering a bunch of instances and de-registering works properly
+    # w.r.t nuRaft log
+    # 1. Start basic instances # 3 logs
+    # 2. Check all is there
+    # 3. Create 3 additional instances and add them to cluster # 3 logs -> 1st snapshot
+    # 4. Check everything is there
+    # 5. Set main # 1 log
+    # 6. Check correct state
+    # 7. Drop 2 new instances # 2 logs
+    # 8. Check correct state
+    # 9. Drop 1 new instance # 1 log -> 2nd snapshot
+    # 10. Check correct state
+    safe_execute(shutil.rmtree, TEMP_DIR)
+
+    INSTANCES_DESCRIPTION = {
+        "instance_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7687",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10011",
+            ],
+            "log_file": "instance_1.log",
+            "data_directory": f"{TEMP_DIR}/instance_1",
+            "setup_queries": [],
+        },
+        "instance_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7688",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10012",
+            ],
+            "log_file": "instance_2.log",
+            "data_directory": f"{TEMP_DIR}/instance_2",
+            "setup_queries": [],
+        },
+        "instance_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7689",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10013",
+            ],
+            "log_file": "instance_3.log",
+            "data_directory": f"{TEMP_DIR}/instance_3",
+            "setup_queries": [],
+        },
+        "coordinator_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7690",
+                "--log-level=TRACE",
+                "--raft-server-id=1",
+                "--raft-server-port=10111",
+            ],
+            "log_file": "coordinator1.log",
+            "setup_queries": [],
+        },
+        "coordinator_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7691",
+                "--log-level=TRACE",
+                "--raft-server-id=2",
+                "--raft-server-port=10112",
+            ],
+            "log_file": "coordinator2.log",
+            "setup_queries": [],
+        },
+        "coordinator_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7692",
+                "--log-level=TRACE",
+                "--raft-server-id=3",
+                "--raft-server-port=10113",
+            ],
+            "log_file": "coordinator3.log",
+            "setup_queries": [],
+        },
+        "coordinator_4": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7693",
+                "--log-level=TRACE",
+                "--raft-server-id=4",
+                "--raft-server-port=10114",
+            ],
+            "log_file": "coordinator4.log",
+            "setup_queries": [
+                "ADD COORDINATOR 1 ON '127.0.0.1:10111';",
+                "ADD COORDINATOR 2 ON '127.0.0.1:10112';",
+                "ADD COORDINATOR 3 ON '127.0.0.1:10113';",
+                "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001'",
+                "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002'",
+                "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003'",
+            ],
+        },
+    }
+    assert "SET INSTANCE instance_3 TO MAIN" not in INSTANCES_DESCRIPTION["coordinator_4"]["setup_queries"]
+
+    # 1
+    interactive_mg_runner.start_all(INSTANCES_DESCRIPTION)
+
+    # 2
+    coord_cursor = connect(host="localhost", port=7693).cursor()
+
+    def retrieve_data_show_repl_cluster():
+        return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
+
+    coordinators = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+        ("coordinator_4", "127.0.0.1:10114", "", "unknown", "coordinator"),
+    ]
+
+    basic_instances = [
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "replica"),
+    ]
+
+    expected_data_on_coord = []
+    expected_data_on_coord.extend(coordinators)
+    expected_data_on_coord.extend(basic_instances)
+
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_repl_cluster)
+
+    # 3
+    instances_ports_added = [10011, 10012, 10013]
+    bolt_port_id = 7700
+    coord_port_id = 10014
+
+    additional_instances = []
+    for i in range(4, 7):
+        instance_name = f"instance_{i}"
+        args_desc = [
+            "--experimental-enabled=high-availability",
+            "--log-level=TRACE",
+        ]
+
+        bolt_port = f"--bolt-port={bolt_port_id}"
+
+        coord_server_port = f"--coordinator-server-port={coord_port_id}"
+
+        args_desc.append(bolt_port)
+        args_desc.append(coord_server_port)
+
+        instance_description = {
+            "args": args_desc,
+            "log_file": f"instance_{i}.log",
+            "data_directory": f"{TEMP_DIR}/instance_{i}",
+            "setup_queries": [],
+        }
+
+        full_instance_desc = {instance_name: instance_description}
+        interactive_mg_runner.start(full_instance_desc, instance_name)
+        repl_port_id = coord_port_id - 10
+        assert repl_port_id < 10011, "Wrong test setup, repl port must be smaller than smallest coord port id"
+
+        execute_and_fetch_all(
+            coord_cursor,
+            f"REGISTER INSTANCE {instance_name} ON '127.0.0.1:{coord_port_id}' WITH '127.0.0.1:{repl_port_id}'",
+        )
+
+        additional_instances.append((f"{instance_name}", "", f"127.0.0.1:{coord_port_id}", "up", "replica"))
+        instances_ports_added.append(coord_port_id)
+        coord_port_id += 1
+        bolt_port_id += 1
+
+    # 4
+    expected_data_on_coord.extend(additional_instances)
+
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_repl_cluster)
+
+    # 5
+    execute_and_fetch_all(coord_cursor, "SET INSTANCE instance_3 TO MAIN")
+
+    # 6
+    basic_instances.pop()
+    basic_instances.append(("instance_3", "", "127.0.0.1:10013", "up", "main"))
+
+    new_expected_data_on_coordinator = []
+
+    new_expected_data_on_coordinator.extend(coordinators)
+    new_expected_data_on_coordinator.extend(basic_instances)
+    new_expected_data_on_coordinator.extend(additional_instances)
+
+    mg_sleep_and_assert(new_expected_data_on_coordinator, retrieve_data_show_repl_cluster)
+
+    # 7
+    for i in range(6, 4, -1):
+        execute_and_fetch_all(coord_cursor, f"UNREGISTER INSTANCE instance_{i};")
+        additional_instances.pop()
+
+    new_expected_data_on_coordinator = []
+    new_expected_data_on_coordinator.extend(coordinators)
+    new_expected_data_on_coordinator.extend(basic_instances)
+    new_expected_data_on_coordinator.extend(additional_instances)
+
+    # 8
+    mg_sleep_and_assert(new_expected_data_on_coordinator, retrieve_data_show_repl_cluster)
+
+    # 9
+
+    new_expected_data_on_coordinator = []
+    new_expected_data_on_coordinator.extend(coordinators)
+    new_expected_data_on_coordinator.extend(basic_instances)
+
+    execute_and_fetch_all(coord_cursor, f"UNREGISTER INSTANCE instance_4;")
+
+    # 10
+    mg_sleep_and_assert(new_expected_data_on_coordinator, retrieve_data_show_repl_cluster)
+
+
+def test_multiple_failovers_in_row_no_leadership_change():
+    # Goal of this test is to assure multiple failovers in row work without leadership change
+    # 1. Start basic instances
+    # 2. Check all is there
+    # 3. Kill MAIN (instance_3)
+    # 4. Expect failover (instance_1)
+    # 5. Kill instance_1
+    # 6. Expect failover instance_2
+    # 7. Start instance_3
+    # 8. Expect instance_3 and instance_2 (MAIN) up
+    # 9. Kill instance_2
+    # 10. Expect instance_3 MAIN
+    # 11. Write some data on instance_3
+    # 12. Start instance_2 and instance_1
+    # 13. Expect instance_1 and instance2 to be up and cluster to have correct state
+    # 13. Expect data to be replicated
+
+    # 1
+    inner_memgraph_instances = get_instances_description_no_setup()
+    interactive_mg_runner.start_all(inner_memgraph_instances, keep_directories=False)
+
+    coord_cursor_3 = connect(host="localhost", port=7692).cursor()
+
+    setup_queries = [
+        "ADD COORDINATOR 1 ON '127.0.0.1:10111'",
+        "ADD COORDINATOR 2 ON '127.0.0.1:10112'",
+        "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001'",
+        "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002'",
+        "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003'",
+        "SET INSTANCE instance_3 TO MAIN",
+    ]
+
+    for query in setup_queries:
+        execute_and_fetch_all(coord_cursor_3, query)
+
+    # 2
+
+    def get_func_show_instances(cursor):
+        def show_instances_follower_coord():
+            return sorted(list(execute_and_fetch_all(cursor, "SHOW INSTANCES;")))
+
+        return show_instances_follower_coord
+
+    coordinator_data = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("coordinator_2", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("coordinator_3", "127.0.0.1:10113", "", "unknown", "coordinator"),
+    ]
+
+    leader_data = []
+    leader_data.extend(coordinator_data)
+    leader_data.extend(
+        [
+            ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+            ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+            ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+        ]
+    )
+
+    follower_data = []
+    follower_data.extend(coordinator_data)
+    follower_data.extend(
+        [
+            ("instance_1", "", "", "unknown", "replica"),
+            ("instance_2", "", "", "unknown", "replica"),
+            ("instance_3", "", "", "unknown", "main"),
+        ]
+    )
+
+    coord_cursor_1 = connect(host="localhost", port=7690).cursor()
+    coord_cursor_2 = connect(host="localhost", port=7691).cursor()
+
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_1))
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_2))
+    mg_sleep_and_assert_collection(leader_data, get_func_show_instances(coord_cursor_3))
+
+    # 3
+
+    interactive_mg_runner.kill(inner_memgraph_instances, "instance_3")
+
+    # 4
+
+    leader_data = []
+    leader_data.extend(coordinator_data)
+    leader_data.extend(
+        [
+            ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+            ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+            ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ]
+    )
+
+    follower_data = []
+    follower_data.extend(coordinator_data)
+    follower_data.extend(
+        [
+            ("instance_1", "", "", "unknown", "main"),
+            ("instance_2", "", "", "unknown", "replica"),
+            (
+                "instance_3",
+                "",
+                "",
+                "unknown",
+                "main",
+            ),  # TODO(antoniofilipovic) change to unknown after PR with transitions
+        ]
+    )
+
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_1))
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_2))
+    mg_sleep_and_assert_collection(leader_data, get_func_show_instances(coord_cursor_3))
+
+    # 5
+    interactive_mg_runner.kill(inner_memgraph_instances, "instance_1")
+
+    # 6
+    leader_data = []
+    leader_data.extend(coordinator_data)
+    leader_data.extend(
+        [
+            ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+            ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+            ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ]
+    )
+
+    follower_data = []
+    follower_data.extend(coordinator_data)
+    follower_data.extend(
+        [
+            ("instance_1", "", "", "unknown", "main"),
+            ("instance_2", "", "", "unknown", "main"),  # TODO(antoniofilipovic) change to unknown
+            ("instance_3", "", "", "unknown", "main"),  # TODO(antoniofilipovic) change to unknown
+        ]
+    )
+
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_1))
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_2))
+    mg_sleep_and_assert_collection(leader_data, get_func_show_instances(coord_cursor_3))
+
+    # 7
+
+    interactive_mg_runner.start(inner_memgraph_instances, "instance_3")
+
+    # 8
+
+    leader_data = []
+    leader_data.extend(coordinator_data)
+    leader_data.extend(
+        [
+            ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+            ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+            ("instance_3", "", "127.0.0.1:10013", "up", "replica"),
+        ]
+    )
+
+    follower_data = []
+    follower_data.extend(coordinator_data)
+    follower_data.extend(
+        [
+            ("instance_1", "", "", "unknown", "main"),  # TODO(antoniofilipovic) change to unknown
+            ("instance_2", "", "", "unknown", "main"),
+            ("instance_3", "", "", "unknown", "replica"),
+        ]
+    )
+
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_1))
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_2))
+    mg_sleep_and_assert_collection(leader_data, get_func_show_instances(coord_cursor_3))
+
+    # 9
+    interactive_mg_runner.kill(inner_memgraph_instances, "instance_2")
+
+    # 10
+    leader_data = []
+    leader_data.extend(coordinator_data)
+    leader_data.extend(
+        [
+            ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+            ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+            ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+        ]
+    )
+
+    follower_data = []
+    follower_data.extend(coordinator_data)
+    follower_data.extend(
+        [
+            ("instance_1", "", "", "unknown", "main"),  # TODO(antoniofilipovic) change to unknown
+            ("instance_2", "", "", "unknown", "main"),  # TODO(antoniofilipovic) change to unknown
+            ("instance_3", "", "", "unknown", "main"),
+        ]
+    )
+
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_1))
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_2))
+    mg_sleep_and_assert_collection(leader_data, get_func_show_instances(coord_cursor_3))
+
+    # 11
+
+    instance_3_cursor = connect(port=7689, host="localhost").cursor()
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(instance_3_cursor, "CREATE ();")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    # 12
+    interactive_mg_runner.start(inner_memgraph_instances, "instance_1")
+    interactive_mg_runner.start(inner_memgraph_instances, "instance_2")
+
+    # 13
+    leader_data = []
+    leader_data.extend(coordinator_data)
+    leader_data.extend(
+        [
+            ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+            ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+            ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+        ]
+    )
+
+    follower_data = []
+    follower_data.extend(coordinator_data)
+    follower_data.extend(
+        [
+            ("instance_1", "", "", "unknown", "replica"),
+            ("instance_2", "", "", "unknown", "replica"),
+            ("instance_3", "", "", "unknown", "main"),
+        ]
+    )
+
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_1))
+    mg_sleep_and_assert_collection(follower_data, get_func_show_instances(coord_cursor_2))
+    mg_sleep_and_assert_collection(leader_data, get_func_show_instances(coord_cursor_3))
+
+    # 14.
+
+    def show_replicas():
+        return sorted(list(execute_and_fetch_all(instance_3_cursor, "SHOW REPLICAS;")))
+
+    replicas = [
+        (
+            "instance_1",
+            "127.0.0.1:10001",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 2, "behind": 0, "status": "ready"}},
+        ),
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"ts": 0, "behind": None, "status": "ready"},
+            {"memgraph": {"ts": 2, "behind": 0, "status": "ready"}},
+        ),
+    ]
+    mg_sleep_and_assert_collection(replicas, show_replicas)
+
+    def get_vertex_count_func(cursor):
+        def get_vertex_count():
+            return execute_and_fetch_all(cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+        return get_vertex_count
+
+    mg_sleep_and_assert(1, get_vertex_count_func(connect(port=7687, host="localhost").cursor()))
+
+    mg_sleep_and_assert(1, get_vertex_count_func(connect(port=7688, host="localhost").cursor()))
+
+
 if __name__ == "__main__":
     sys.exit(pytest.main([__file__, "-rA"]))
diff --git a/tests/e2e/high_availability/not_replicate_from_old_main.py b/tests/e2e/high_availability/not_replicate_from_old_main.py
index c2cc93cb1..7ffffc04a 100644
--- a/tests/e2e/high_availability/not_replicate_from_old_main.py
+++ b/tests/e2e/high_availability/not_replicate_from_old_main.py
@@ -75,8 +75,11 @@ def test_replication_works_on_failover():
             {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
         ),
     ]
-    actual_data_on_main = sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
-    assert actual_data_on_main == expected_data_on_main
+
+    def retrieve_data_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    mg_sleep_and_assert(expected_data_on_main, retrieve_data_show_replicas)
 
     # 3
     interactive_mg_runner.start_all_keep_others(MEMGRAPH_SECOND_CLUSTER_DESCRIPTION)
@@ -200,9 +203,9 @@ def test_not_replicate_old_main_register_new_cluster():
         return sorted(list(execute_and_fetch_all(first_cluster_coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_up_first_cluster = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_2", "", "127.0.0.1:10012", True, "main"),
-        ("shared_instance", "", "127.0.0.1:10011", True, "replica"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+        ("shared_instance", "", "127.0.0.1:10011", "up", "replica"),
     ]
 
     mg_sleep_and_assert(expected_data_up_first_cluster, show_repl_cluster)
@@ -254,9 +257,9 @@ def test_not_replicate_old_main_register_new_cluster():
         return sorted(list(execute_and_fetch_all(second_cluster_coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_up_second_cluster = [
-        ("coordinator_1", "127.0.0.1:10112", "", True, "coordinator"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
-        ("shared_instance", "", "127.0.0.1:10011", True, "replica"),
+        ("coordinator_1", "127.0.0.1:10112", "", "unknown", "coordinator"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+        ("shared_instance", "", "127.0.0.1:10011", "up", "replica"),
     ]
 
     mg_sleep_and_assert(expected_data_up_second_cluster, show_repl_cluster)
diff --git a/tests/e2e/high_availability/single_coordinator.py b/tests/e2e/high_availability/single_coordinator.py
index ecf063092..7335d2847 100644
--- a/tests/e2e/high_availability/single_coordinator.py
+++ b/tests/e2e/high_availability/single_coordinator.py
@@ -37,6 +37,9 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
             "TRACE",
             "--coordinator-server-port",
             "10011",
+            "--replication-restore-state-on-startup=true",
+            "--storage-recover-on-startup=false",
+            "--data-recovery-on-startup=false",
         ],
         "log_file": "instance_1.log",
         "data_directory": f"{TEMP_DIR}/instance_1",
@@ -51,6 +54,9 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
             "TRACE",
             "--coordinator-server-port",
             "10012",
+            "--replication-restore-state-on-startup=true",
+            "--storage-recover-on-startup=false",
+            "--data-recovery-on-startup=false",
         ],
         "log_file": "instance_2.log",
         "data_directory": f"{TEMP_DIR}/instance_2",
@@ -65,6 +71,9 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
             "TRACE",
             "--coordinator-server-port",
             "10013",
+            "--replication-restore-state-on-startup=true",
+            "--storage-recover-on-startup=false",
+            "--data-recovery-on-startup=false",
         ],
         "log_file": "instance_3.log",
         "data_directory": f"{TEMP_DIR}/instance_3",
@@ -90,14 +99,1058 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
 }
 
 
-def test_replication_works_on_failover():
+@pytest.mark.parametrize("data_recovery", ["false", "true"])
+def test_replication_works_on_failover_replica_1_epoch_2_commits_away(data_recovery):
     # Goal of this test is to check the replication works after failover command.
-    # 1. We start all replicas, main and coordinator manually: we want to be able to kill them ourselves without relying on external tooling to kill processes.
+    # 1. We start all replicas, main and coordinator manually
+    # 2. We check that main has correct state
+    # 3. Create initial data on MAIN
+    # 4. Expect data to be copied on all replicas
+    # 5. Kill instance_1 (replica 1)
+    # 6. Create data on MAIN and expect to be copied to only one replica (instance_2)
+    # 7. Kill main
+    # 8. Instance_2 new MAIN
+    # 9. Create vertex on instance 2
+    # 10. Start instance_1(it should have one commit on old epoch and new epoch with new commit shouldn't be replicated)
+    # 11. Expect data to be copied on instance_1
+    # 12. Start old MAIN (instance_3)
+    # 13. Expect data to be copied to instance_3
+
+    temp_dir = tempfile.TemporaryDirectory().name
+
+    MEMGRAPH_INNER_INSTANCES_DESCRIPTION = {
+        "instance_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7688",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10011",
+                "--replication-restore-state-on-startup",
+                "true",
+                f"--data-recovery-on-startup={data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_1.log",
+            "data_directory": f"{temp_dir}/instance_1",
+            "setup_queries": [],
+        },
+        "instance_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7689",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10012",
+                "--replication-restore-state-on-startup",
+                "true",
+                f"--data-recovery-on-startup={data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_2.log",
+            "data_directory": f"{temp_dir}/instance_2",
+            "setup_queries": [],
+        },
+        "instance_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7687",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10013",
+                "--replication-restore-state-on-startup",
+                "true",
+                "--data-recovery-on-startup",
+                f"{data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_3.log",
+            "data_directory": f"{temp_dir}/instance_3",
+            "setup_queries": [],
+        },
+        "coordinator": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7690",
+                "--log-level=TRACE",
+                "--raft-server-id=1",
+                "--raft-server-port=10111",
+            ],
+            "log_file": "coordinator.log",
+            "setup_queries": [
+                "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001';",
+                "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002';",
+                "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003';",
+                "SET INSTANCE instance_3 TO MAIN",
+            ],
+        },
+    }
+
+    # 1
+    interactive_mg_runner.start_all(MEMGRAPH_INNER_INSTANCES_DESCRIPTION)
+
+    # 2
+    main_cursor = connect(host="localhost", port=7687).cursor()
+
+    def retrieve_data_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    expected_data_on_main = [
+        (
+            "instance_1",
+            "127.0.0.1:10001",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+    ]
+    mg_sleep_and_assert_collection(expected_data_on_main, retrieve_data_show_replicas)
+
+    # 3
+    execute_and_fetch_all(main_cursor, "CREATE (:EpochVertex1 {prop:1});")
+
+    # 4
+
+    instance_1_cursor = connect(host="localhost", port=7688).cursor()
+    instance_2_cursor = connect(host="localhost", port=7689).cursor()
+
+    assert execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n);")[0][0] == 1
+    assert execute_and_fetch_all(instance_2_cursor, "MATCH (n) RETURN count(n);")[0][0] == 1
+
+    # 5
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_1")
+
+    # 6
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(main_cursor, "CREATE (:EpochVertex1 {prop:2});")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    assert execute_and_fetch_all(instance_2_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+
+    # 7
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_3")
+
+    # 8.
+    coord_cursor = connect(host="localhost", port=7690).cursor()
+
+    def retrieve_data_show_instances():
+        return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 9
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(instance_2_cursor, "CREATE (:Epoch3 {prop:3});")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    # 10
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_1")
+
+    new_expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+    ]
+    mg_sleep_and_assert(new_expected_data_on_coord, retrieve_data_show_instances)
+
+    # 11
+    instance_1_cursor = connect(host="localhost", port=7688).cursor()
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(3, get_vertex_count)
+
+    # 12
+
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_3")
+
+    new_expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "replica"),
+    ]
+    mg_sleep_and_assert(new_expected_data_on_coord, retrieve_data_show_instances)
+
+    # 13
+
+    instance_3_cursor = connect(host="localhost", port=7687).cursor()
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_3_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(3, get_vertex_count)
+
+
+@pytest.mark.parametrize("data_recovery", ["false", "true"])
+def test_replication_works_on_failover_replica_2_epochs_more_commits_away(data_recovery):
+    # Goal of this test is to check the replication works after failover command if one
+    # instance missed couple of epochs but data is still available on one of the instances
+
+    # 1. We start all replicas, main and coordinator manually
+    # 2. Main does commit
+    # 3. instance_2 down
+    # 4. Main commits more
+    # 5. Main down
+    # 6. Instance_1 new main
+    # 7. Instance 1 commits
+    # 8. Instance 4 gets data
+    # 9. Instance 1 dies
+    # 10. Instance 4 new main
+    # 11. Instance 4 commits
+    # 12. Instance 2 wakes up
+    # 13. Instance 2 gets data from old epochs
+    # 14. All other instances wake up
+    # 15. Everything is replicated
+
+    temp_dir = tempfile.TemporaryDirectory().name
+
+    MEMGRAPH_INNER_INSTANCES_DESCRIPTION = {
+        "instance_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7688",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10011",
+                "--replication-restore-state-on-startup",
+                "true",
+                f"--data-recovery-on-startup={data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_1.log",
+            "data_directory": f"{temp_dir}/instance_1",
+            "setup_queries": [],
+        },
+        "instance_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7689",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10012",
+                "--replication-restore-state-on-startup",
+                "true",
+                f"--data-recovery-on-startup={data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_2.log",
+            "data_directory": f"{temp_dir}/instance_2",
+            "setup_queries": [],
+        },
+        "instance_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7687",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10013",
+                "--replication-restore-state-on-startup",
+                "true",
+                "--data-recovery-on-startup",
+                f"{data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_3.log",
+            "data_directory": f"{temp_dir}/instance_3",
+            "setup_queries": [],
+        },
+        "instance_4": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7691",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10014",
+                "--replication-restore-state-on-startup",
+                "true",
+                "--data-recovery-on-startup",
+                f"{data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_4.log",
+            "data_directory": f"{temp_dir}/instance_4",
+            "setup_queries": [],
+        },
+        "coordinator": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7690",
+                "--log-level=TRACE",
+                "--raft-server-id=1",
+                "--raft-server-port=10111",
+            ],
+            "log_file": "coordinator.log",
+            "setup_queries": [
+                "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001';",
+                "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002';",
+                "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003';",
+                "REGISTER INSTANCE instance_4 ON '127.0.0.1:10014' WITH '127.0.0.1:10004';",
+                "SET INSTANCE instance_3 TO MAIN",
+            ],
+        },
+    }
+
+    # 1
+
+    interactive_mg_runner.start_all(MEMGRAPH_INNER_INSTANCES_DESCRIPTION)
+
+    expected_data_on_main = [
+        (
+            "instance_1",
+            "127.0.0.1:10001",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+        (
+            "instance_4",
+            "127.0.0.1:10004",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+    ]
+
+    main_cursor = connect(host="localhost", port=7687).cursor()
+
+    def retrieve_data_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    mg_sleep_and_assert_collection(expected_data_on_main, retrieve_data_show_replicas)
+
+    # 2
+
+    execute_and_fetch_all(main_cursor, "CREATE (:EpochVertex1 {prop:1});")
+    execute_and_fetch_all(main_cursor, "CREATE (:EpochVertex1 {prop:2});")
+
+    instance_1_cursor = connect(host="localhost", port=7688).cursor()
+    instance_2_cursor = connect(host="localhost", port=7689).cursor()
+    instance_4_cursor = connect(host="localhost", port=7691).cursor()
+
+    assert execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+    assert execute_and_fetch_all(instance_2_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+    assert execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+
+    # 3
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_2")
+
+    coord_cursor = connect(host="localhost", port=7690).cursor()
+
+    def retrieve_data_show_instances():
+        return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "replica"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 4
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(main_cursor, "CREATE (:EpochVertex1 {prop:1});")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    assert execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n);")[0][0] == 3
+    assert execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n);")[0][0] == 3
+
+    # 5
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_3")
+
+    # 6
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "replica"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 7
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(instance_1_cursor, "CREATE (:Epoch2Vertex {prop:1});")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    # 8
+
+    assert execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n);")[0][0] == 4
+
+    # 9
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_1")
+
+    # 10
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "main"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 11
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(instance_4_cursor, "CREATE (:Epoch3Vertex {prop:1});")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    # 12
+
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_2")
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "main"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 13
+
+    instance_2_cursor = connect(host="localhost", port=7689).cursor()
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_2_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(5, get_vertex_count)
+
+    # 14
+
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_1")
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_3")
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "replica"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "main"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 15
+    instance_1_cursor = connect(host="localhost", port=7688).cursor()
+    instance_4_cursor = connect(host="localhost", port=7691).cursor()
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(5, get_vertex_count)
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(5, get_vertex_count)
+
+
+@pytest.mark.parametrize("data_recovery", ["true"])
+def test_replication_forcefully_works_on_failover_replica_misses_epoch(data_recovery):
+    # Goal of this test is to check the replication works forcefully if replica misses epoch
+    # 1. We start all replicas, main and coordinator manually
+    # 2. We check that main has correct state
+    # 3. Create initial data on MAIN
+    # 4. Expect data to be copied on all replicas
+    # 5. Kill instance_1 ( this one will miss complete epoch)
+    # 6. Kill main (instance_3)
+    # 7. Instance_2
+    # 8. Instance_2 commits
+    # 9. Instance_2 down
+    # 10. instance_4 down
+    # 11. Instance 1 up (missed epoch)
+    # 12 Instance 1 new main
+    # 13 instance 2 up
+    # 14 Force data from instance 1 to instance 2
+
+    temp_dir = tempfile.TemporaryDirectory().name
+
+    MEMGRAPH_INNER_INSTANCES_DESCRIPTION = {
+        "instance_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7688",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10011",
+                "--replication-restore-state-on-startup",
+                "true",
+                f"--data-recovery-on-startup={data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_1.log",
+            "data_directory": f"{temp_dir}/instance_1",
+            "setup_queries": [],
+        },
+        "instance_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7689",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10012",
+                "--replication-restore-state-on-startup",
+                "true",
+                f"--data-recovery-on-startup={data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_2.log",
+            "data_directory": f"{temp_dir}/instance_2",
+            "setup_queries": [],
+        },
+        "instance_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7687",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10013",
+                "--replication-restore-state-on-startup",
+                "true",
+                "--data-recovery-on-startup",
+                f"{data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_3.log",
+            "data_directory": f"{temp_dir}/instance_3",
+            "setup_queries": [],
+        },
+        "instance_4": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7691",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10014",
+                "--replication-restore-state-on-startup",
+                "true",
+                "--data-recovery-on-startup",
+                f"{data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_4.log",
+            "data_directory": f"{temp_dir}/instance_4",
+            "setup_queries": [],
+        },
+        "coordinator": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7690",
+                "--log-level=TRACE",
+                "--raft-server-id=1",
+                "--raft-server-port=10111",
+            ],
+            "log_file": "coordinator.log",
+            "setup_queries": [
+                "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001';",
+                "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002';",
+                "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003';",
+                "REGISTER INSTANCE instance_4 ON '127.0.0.1:10014' WITH '127.0.0.1:10004';",
+                "SET INSTANCE instance_3 TO MAIN",
+            ],
+        },
+    }
+
+    # 1
+
+    interactive_mg_runner.start_all(MEMGRAPH_INNER_INSTANCES_DESCRIPTION)
+
+    # 2
+
+    main_cursor = connect(host="localhost", port=7687).cursor()
+    expected_data_on_main = [
+        (
+            "instance_1",
+            "127.0.0.1:10001",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+        (
+            "instance_4",
+            "127.0.0.1:10004",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+    ]
+
+    main_cursor = connect(host="localhost", port=7687).cursor()
+
+    def retrieve_data_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    mg_sleep_and_assert_collection(expected_data_on_main, retrieve_data_show_replicas)
+
+    coord_cursor = connect(host="localhost", port=7690).cursor()
+
+    def retrieve_data_show_instances():
+        return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "replica"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 3
+
+    execute_and_fetch_all(main_cursor, "CREATE (:Epoch1Vertex {prop:1});")
+    execute_and_fetch_all(main_cursor, "CREATE (:Epoch1Vertex {prop:2});")
+
+    # 4
+    instance_1_cursor = connect(host="localhost", port=7688).cursor()
+    instance_2_cursor = connect(host="localhost", port=7689).cursor()
+    instance_4_cursor = connect(host="localhost", port=7691).cursor()
+
+    assert execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+    assert execute_and_fetch_all(instance_2_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+    assert execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+
+    # 5
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_1")
+
+    # 6
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_3")
+
+    # 7
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "replica"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 8
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(instance_2_cursor, "CREATE (:Epoch2Vertex {prop:1});")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(3, get_vertex_count)
+
+    assert execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n);")[0][0] == 3
+
+    # 9
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_2")
+
+    # 10
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_4")
+
+    # 11
+
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_1")
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "down", "unknown"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 12
+
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_2")
+
+    # 13
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "down", "unknown"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 12
+    instance_1_cursor = connect(host="localhost", port=7688).cursor()
+    instance_2_cursor = connect(host="localhost", port=7689).cursor()
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(2, get_vertex_count)
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_2_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(2, get_vertex_count)
+
+    # 13
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(instance_1_cursor, "CREATE (:Epoch3Vertex {prop:1});")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    # 14
+
+    def get_vertex_objects_func_creator(cursor):
+        def get_vertex_objects():
+            return list(
+                execute_and_fetch_all(
+                    cursor, "MATCH (n) " "WITH labels(n) as labels, properties(n) as props " "RETURN labels[0], props;"
+                )
+            )
+
+        return get_vertex_objects
+
+    vertex_objects = [("Epoch1Vertex", {"prop": 1}), ("Epoch1Vertex", {"prop": 2}), ("Epoch3Vertex", {"prop": 1})]
+
+    mg_sleep_and_assert_collection(vertex_objects, get_vertex_objects_func_creator(instance_1_cursor))
+
+    mg_sleep_and_assert_collection(vertex_objects, get_vertex_objects_func_creator(instance_2_cursor))
+
+    # 15
+
+
+@pytest.mark.parametrize("data_recovery", ["false", "true"])
+def test_replication_correct_replica_chosen_up_to_date_data(data_recovery):
+    # Goal of this test is to check that correct replica instance as new MAIN is chosen
+    # 1. We start all replicas, main and coordinator manually
+    # 2. We check that main has correct state
+    # 3. Create initial data on MAIN
+    # 4. Expect data to be copied on all replicas
+    # 5. Kill instance_1 ( this one will miss complete epoch)
+    # 6. Kill main (instance_3)
+    # 7. Instance_2 new MAIN
+    # 8. Instance_2 commits and replicates data
+    # 9. Instance_4 down (not main)
+    # 10. instance_2 down (MAIN), instance 1 up (missed epoch),
+    # instance 4 up (In this case we should always choose instance_4 because it has up-to-date data)
+    # 11 Instance 4 new main
+    # 12 instance_1 gets up-to-date data, instance_4 has all data
+
+    temp_dir = tempfile.TemporaryDirectory().name
+
+    MEMGRAPH_INNER_INSTANCES_DESCRIPTION = {
+        "instance_1": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7688",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10011",
+                "--replication-restore-state-on-startup",
+                "true",
+                f"--data-recovery-on-startup={data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_1.log",
+            "data_directory": f"{temp_dir}/instance_1",
+            "setup_queries": [],
+        },
+        "instance_2": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7689",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10012",
+                "--replication-restore-state-on-startup",
+                "true",
+                f"--data-recovery-on-startup={data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_2.log",
+            "data_directory": f"{temp_dir}/instance_2",
+            "setup_queries": [],
+        },
+        "instance_3": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7687",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10013",
+                "--replication-restore-state-on-startup",
+                "true",
+                "--data-recovery-on-startup",
+                f"{data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_3.log",
+            "data_directory": f"{temp_dir}/instance_3",
+            "setup_queries": [],
+        },
+        "instance_4": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7691",
+                "--log-level",
+                "TRACE",
+                "--coordinator-server-port",
+                "10014",
+                "--replication-restore-state-on-startup",
+                "true",
+                "--data-recovery-on-startup",
+                f"{data_recovery}",
+                "--storage-recover-on-startup=false",
+            ],
+            "log_file": "instance_4.log",
+            "data_directory": f"{temp_dir}/instance_4",
+            "setup_queries": [],
+        },
+        "coordinator": {
+            "args": [
+                "--experimental-enabled=high-availability",
+                "--bolt-port",
+                "7690",
+                "--log-level=TRACE",
+                "--raft-server-id=1",
+                "--raft-server-port=10111",
+            ],
+            "log_file": "coordinator.log",
+            "setup_queries": [
+                "REGISTER INSTANCE instance_1 ON '127.0.0.1:10011' WITH '127.0.0.1:10001';",
+                "REGISTER INSTANCE instance_2 ON '127.0.0.1:10012' WITH '127.0.0.1:10002';",
+                "REGISTER INSTANCE instance_3 ON '127.0.0.1:10013' WITH '127.0.0.1:10003';",
+                "REGISTER INSTANCE instance_4 ON '127.0.0.1:10014' WITH '127.0.0.1:10004';",
+                "SET INSTANCE instance_3 TO MAIN",
+            ],
+        },
+    }
+
+    # 1
+
+    interactive_mg_runner.start_all(MEMGRAPH_INNER_INSTANCES_DESCRIPTION)
+
+    # 2
+
+    main_cursor = connect(host="localhost", port=7687).cursor()
+    expected_data_on_main = [
+        (
+            "instance_1",
+            "127.0.0.1:10001",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+        (
+            "instance_2",
+            "127.0.0.1:10002",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+        (
+            "instance_4",
+            "127.0.0.1:10004",
+            "sync",
+            {"behind": None, "status": "ready", "ts": 0},
+            {"memgraph": {"behind": 0, "status": "ready", "ts": 0}},
+        ),
+    ]
+
+    main_cursor = connect(host="localhost", port=7687).cursor()
+
+    def retrieve_data_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    mg_sleep_and_assert_collection(expected_data_on_main, retrieve_data_show_replicas)
+
+    coord_cursor = connect(host="localhost", port=7690).cursor()
+
+    def retrieve_data_show_instances():
+        return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
+
+    # TODO(antoniofilipovic) Before fixing durability, if this is removed we also have an issue. Check after fix
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "replica"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 3
+
+    execute_and_fetch_all(main_cursor, "CREATE (:Epoch1Vertex {prop:1});")
+    execute_and_fetch_all(main_cursor, "CREATE (:Epoch1Vertex {prop:2});")
+
+    # 4
+    instance_1_cursor = connect(host="localhost", port=7688).cursor()
+    instance_2_cursor = connect(host="localhost", port=7689).cursor()
+    instance_4_cursor = connect(host="localhost", port=7691).cursor()
+
+    assert execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+    assert execute_and_fetch_all(instance_2_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+    assert execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n);")[0][0] == 2
+
+    # 5
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_1")
+
+    # 6
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_3")
+
+    # 7
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "replica"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 8
+
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(instance_2_cursor, "CREATE (:Epoch2Vertex {prop:1});")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(3, get_vertex_count)
+
+    assert execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n);")[0][0] == 3
+
+    # 9
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_4")
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "main"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "down", "unknown"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 10
+
+    interactive_mg_runner.kill(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_2")
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_1")
+    interactive_mg_runner.start(MEMGRAPH_INNER_INSTANCES_DESCRIPTION, "instance_4")
+
+    # 11
+
+    expected_data_on_coord = [
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
+        ("instance_4", "", "127.0.0.1:10014", "up", "main"),
+    ]
+    mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_instances)
+
+    # 12
+    instance_1_cursor = connect(host="localhost", port=7688).cursor()
+    instance_4_cursor = connect(host="localhost", port=7691).cursor()
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_1_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(3, get_vertex_count)
+
+    def get_vertex_count():
+        return execute_and_fetch_all(instance_4_cursor, "MATCH (n) RETURN count(n)")[0][0]
+
+    mg_sleep_and_assert(3, get_vertex_count)
+
+
+def test_replication_works_on_failover_simple():
+    # Goal of this test is to check the replication works after failover command.
+    # 1. We start all replicas, main and coordinator manually
     # 2. We check that main has correct state
     # 3. We kill main
     # 4. We check that coordinator and new main have correct state
     # 5. We insert one vertex on new main
     # 6. We check that vertex appears on new replica
+    # 7. We bring back main up
+    # 8. Expect data to be copied to main
     safe_execute(shutil.rmtree, TEMP_DIR)
 
     # 1
@@ -121,8 +1174,11 @@ def test_replication_works_on_failover():
             {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
         ),
     ]
-    actual_data_on_main = sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
-    assert actual_data_on_main == expected_data_on_main
+
+    def main_cursor_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    mg_sleep_and_assert_collection(expected_data_on_main, main_cursor_show_replicas)
 
     # 3
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_3")
@@ -134,10 +1190,10 @@ def test_replication_works_on_failover():
         return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_on_coord = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "main"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", False, "unknown"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
     ]
     mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_repl_cluster)
 
@@ -164,33 +1220,48 @@ def test_replication_works_on_failover():
     ]
     mg_sleep_and_assert_collection(expected_data_on_new_main, retrieve_data_show_replicas)
 
+    # 5
+    with pytest.raises(Exception) as e:
+        execute_and_fetch_all(new_main_cursor, "CREATE ();")
+    assert "At least one SYNC replica has not confirmed committing last transaction." in str(e.value)
+    # 6
+    alive_replica_cursor = connect(host="localhost", port=7689).cursor()
+    res = execute_and_fetch_all(alive_replica_cursor, "MATCH (n) RETURN count(n) as count;")[0][0]
+    assert res == 1, "Vertex should be replicated"
+
+    # 7
     interactive_mg_runner.start(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_3")
+
+    def retrieve_data_show_replicas():
+        return sorted(list(execute_and_fetch_all(new_main_cursor, "SHOW REPLICAS;")))
+
+    new_main_cursor = connect(host="localhost", port=7688).cursor()
+
     expected_data_on_new_main = [
         (
             "instance_2",
             "127.0.0.1:10002",
             "sync",
             {"ts": 0, "behind": None, "status": "ready"},
-            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+            {"memgraph": {"ts": 2, "behind": 0, "status": "ready"}},
         ),
         (
             "instance_3",
             "127.0.0.1:10003",
             "sync",
             {"ts": 0, "behind": None, "status": "ready"},
-            {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
+            {"memgraph": {"ts": 2, "behind": 0, "status": "ready"}},
         ),
     ]
-    mg_sleep_and_assert_collection(expected_data_on_new_main, retrieve_data_show_replicas)
+    mg_sleep_and_assert(expected_data_on_new_main, retrieve_data_show_replicas)
 
-    # 5
-    execute_and_fetch_all(new_main_cursor, "CREATE ();")
+    # 8
+    alive_main = connect(host="localhost", port=7687).cursor()
 
-    # 6
-    alive_replica_cursror = connect(host="localhost", port=7689).cursor()
-    res = execute_and_fetch_all(alive_replica_cursror, "MATCH (n) RETURN count(n) as count;")[0][0]
-    assert res == 1, "Vertex should be replicated"
-    interactive_mg_runner.stop_all(MEMGRAPH_INSTANCES_DESCRIPTION)
+    def retrieve_vertices_count():
+        return execute_and_fetch_all(alive_main, "MATCH (n) RETURN count(n) as count;")[0][0]
+
+    mg_sleep_and_assert(1, retrieve_vertices_count)
 
 
 def test_replication_works_on_replica_instance_restart():
@@ -224,8 +1295,11 @@ def test_replication_works_on_replica_instance_restart():
             {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
         ),
     ]
-    actual_data_on_main = sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
-    assert actual_data_on_main == expected_data_on_main
+
+    def main_cursor_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    mg_sleep_and_assert_collection(expected_data_on_main, main_cursor_show_replicas)
 
     # 3
     coord_cursor = connect(host="localhost", port=7690).cursor()
@@ -236,10 +1310,10 @@ def test_replication_works_on_replica_instance_restart():
         return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_on_coord = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", False, "unknown"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert_collection(expected_data_on_coord, retrieve_data_show_repl_cluster)
 
@@ -302,10 +1376,10 @@ def test_replication_works_on_replica_instance_restart():
         return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_on_coord = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_repl_cluster)
 
@@ -350,10 +1424,10 @@ def test_show_instances():
         return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
 
     expected_data = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_data, show_repl_cluster)
 
@@ -373,20 +1447,20 @@ def test_show_instances():
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_1")
 
     expected_data = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", False, "unknown"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_data, show_repl_cluster)
 
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_2")
 
     expected_data = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", False, "unknown"),
-        ("instance_2", "", "127.0.0.1:10012", False, "unknown"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_data, show_repl_cluster)
 
@@ -412,8 +1486,11 @@ def test_simple_automatic_failover():
             {"memgraph": {"ts": 0, "behind": 0, "status": "ready"}},
         ),
     ]
-    actual_data_on_main = sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
-    assert actual_data_on_main == sorted(expected_data_on_main)
+
+    def main_cursor_show_replicas():
+        return sorted(list(execute_and_fetch_all(main_cursor, "SHOW REPLICAS;")))
+
+    mg_sleep_and_assert_collection(expected_data_on_main, main_cursor_show_replicas)
 
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_3")
 
@@ -423,10 +1500,10 @@ def test_simple_automatic_failover():
         return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_on_coord = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "main"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", False, "unknown"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
     ]
     mg_sleep_and_assert(expected_data_on_coord, retrieve_data_show_repl_cluster)
 
@@ -498,7 +1575,10 @@ def test_registering_replica_fails_endpoint_exists():
             coord_cursor,
             "REGISTER INSTANCE instance_5 ON '127.0.0.1:10011' WITH '127.0.0.1:10005';",
         )
-    assert str(e.value) == "Couldn't register replica instance since instance with such endpoint already exists!"
+    assert (
+        str(e.value)
+        == "Couldn't register replica instance since instance with such coordinator endpoint already exists!"
+    )
 
 
 def test_replica_instance_restarts():
@@ -511,20 +1591,20 @@ def test_replica_instance_restarts():
         return sorted(list(execute_and_fetch_all(cursor, "SHOW INSTANCES;")))
 
     expected_data_up = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_data_up, show_repl_cluster)
 
     interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_1")
 
     expected_data_down = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", False, "unknown"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_data_down, show_repl_cluster)
 
@@ -553,18 +1633,18 @@ def test_automatic_failover_main_back_as_replica():
         return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_after_failover = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "main"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", False, "unknown"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
     ]
     mg_sleep_and_assert(expected_data_after_failover, retrieve_data_show_repl_cluster)
 
     expected_data_after_main_coming_back = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "main"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "replica"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "main"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "replica"),
     ]
 
     interactive_mg_runner.start(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_3")
@@ -592,20 +1672,20 @@ def test_automatic_failover_main_back_as_main():
         return sorted(list(execute_and_fetch_all(coord_cursor, "SHOW INSTANCES;")))
 
     expected_data_all_down = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", False, "unknown"),
-        ("instance_2", "", "127.0.0.1:10012", False, "unknown"),
-        ("instance_3", "", "127.0.0.1:10013", False, "unknown"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "down", "unknown"),
     ]
 
     mg_sleep_and_assert(expected_data_all_down, retrieve_data_show_repl_cluster)
 
     interactive_mg_runner.start(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_3")
     expected_data_main_back = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", False, "unknown"),
-        ("instance_2", "", "127.0.0.1:10012", False, "unknown"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "down", "unknown"),
+        ("instance_2", "", "127.0.0.1:10012", "down", "unknown"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
     mg_sleep_and_assert(expected_data_main_back, retrieve_data_show_repl_cluster)
 
@@ -620,10 +1700,10 @@ def test_automatic_failover_main_back_as_main():
     interactive_mg_runner.start(MEMGRAPH_INSTANCES_DESCRIPTION, "instance_2")
 
     expected_data_replicas_back = [
-        ("coordinator_1", "127.0.0.1:10111", "", True, "coordinator"),
-        ("instance_1", "", "127.0.0.1:10011", True, "replica"),
-        ("instance_2", "", "127.0.0.1:10012", True, "replica"),
-        ("instance_3", "", "127.0.0.1:10013", True, "main"),
+        ("coordinator_1", "127.0.0.1:10111", "", "unknown", "coordinator"),
+        ("instance_1", "", "127.0.0.1:10011", "up", "replica"),
+        ("instance_2", "", "127.0.0.1:10012", "up", "replica"),
+        ("instance_3", "", "127.0.0.1:10013", "up", "main"),
     ]
 
     mg_sleep_and_assert(expected_data_replicas_back, retrieve_data_show_repl_cluster)
diff --git a/tests/e2e/mg_utils.py b/tests/e2e/mg_utils.py
index 3a475bf3c..7279f25f2 100644
--- a/tests/e2e/mg_utils.py
+++ b/tests/e2e/mg_utils.py
@@ -17,6 +17,28 @@ def mg_sleep_and_assert(expected_value, function_to_retrieve_data, max_duration=
     return result
 
 
+def mg_sleep_and_assert_any_function(
+    expected_value, functions_to_retrieve_data, max_duration=20, time_between_attempt=0.2
+):
+    result = [f() for f in functions_to_retrieve_data]
+    if any((x == expected_value for x in result)):
+        return result
+    start_time = time.time()
+    while result != expected_value:
+        duration = time.time() - start_time
+        if duration > max_duration:
+            assert (
+                False
+            ), f" mg_sleep_and_assert has tried for too long and did not get the expected result! Last result was: {result}"
+
+        time.sleep(time_between_attempt)
+        result = [f() for f in functions_to_retrieve_data]
+        if any((x == expected_value for x in result)):
+            return result
+
+    return result
+
+
 def mg_sleep_and_assert_collection(
     expected_value, function_to_retrieve_data, max_duration=20, time_between_attempt=0.2
 ):
diff --git a/tests/e2e/query_modules/schema_test.py b/tests/e2e/query_modules/schema_test.py
index fbb376a22..e819a430e 100644
--- a/tests/e2e/query_modules/schema_test.py
+++ b/tests/e2e/query_modules/schema_test.py
@@ -431,7 +431,7 @@ def test_node_type_properties1():
             f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
         )[0]
     )
-    assert (result) == [":`Activity`", ["Activity"], "location", "String", False]
+    assert (result) == [":`Activity`", ["Activity"], "location", ["String"], True]
 
     result = list(
         execute_and_fetch_all(
@@ -439,7 +439,7 @@ def test_node_type_properties1():
             f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
         )[1]
     )
-    assert (result) == [":`Activity`", ["Activity"], "name", "String", False]
+    assert (result) == [":`Activity`", ["Activity"], "name", ["String"], True]
 
     result = list(
         execute_and_fetch_all(
@@ -447,7 +447,7 @@ def test_node_type_properties1():
             f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
         )[2]
     )
-    assert (result) == [":`Dog`", ["Dog"], "name", "String", False]
+    assert (result) == [":`Dog`", ["Dog"], "name", ["String"], True]
 
     result = list(
         execute_and_fetch_all(
@@ -455,7 +455,7 @@ def test_node_type_properties1():
             f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
         )[3]
     )
-    assert (result) == [":`Dog`", ["Dog"], "owner", "String", False]
+    assert (result) == [":`Dog`", ["Dog"], "owner", ["String"], True]
 
 
 def test_node_type_properties2():
@@ -471,7 +471,8 @@ def test_node_type_properties2():
         cursor,
         f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
     )
-    assert (list(result[0])) == [":`MyNode`", ["MyNode"], "", "", False]
+
+    assert (list(result[0])) == [":`MyNode`", ["MyNode"], "", [], False]
     assert (result.__len__()) == 1
 
 
@@ -489,8 +490,8 @@ def test_node_type_properties3():
         f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
     )
 
-    assert (list(result[0])) == [":`Dog`", ["Dog"], "name", "String", False]
-    assert (list(result[1])) == [":`Dog`", ["Dog"], "owner", "String", False]
+    assert (list(result[0])) == [":`Dog`", ["Dog"], "name", ["String"], False]
+    assert (list(result[1])) == [":`Dog`", ["Dog"], "owner", ["String"], False]
     assert (result.__len__()) == 2
 
 
@@ -509,9 +510,9 @@ def test_node_type_properties4():
             f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
         )
     )
-    assert (list(result[0])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property1", "String", False]
-    assert (list(result[1])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property2", "String", False]
-    assert (list(result[2])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property3", "String", False]
+    assert (list(result[0])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property1", ["String"], False]
+    assert (list(result[1])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property2", ["String"], False]
+    assert (list(result[2])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property3", ["String"], False]
     assert (result.__len__()) == 3
 
 
@@ -528,7 +529,49 @@ def test_node_type_properties5():
         f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
     )
 
-    assert (list(result[0])) == [":`Dog`", ["Dog"], "name", "String", True]
+    assert (list(result[0])) == [":`Dog`", ["Dog"], "name", ["String"], True]
+    assert (result.__len__()) == 1
+
+
+def test_node_type_properties6():
+    cursor = connect().cursor()
+    execute_and_fetch_all(
+        cursor,
+        """
+        CREATE (d:Dog {name: 'Rex'})
+        CREATE (n:Dog {name: 'Simba', owner: 'Lucy'})
+        """,
+    )
+    result = execute_and_fetch_all(
+        cursor,
+        f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
+    )
+
+    assert (list(result[0])) == [":`Dog`", ["Dog"], "name", ["String"], True]
+    assert (list(result[1])) == [":`Dog`", ["Dog"], "owner", ["String"], False]
+    assert (result.__len__()) == 2
+
+
+def test_node_type_properties_multiple_property_types():
+    cursor = connect().cursor()
+    execute_and_fetch_all(
+        cursor,
+        """
+        CREATE (n:Node {prop1: 1})
+        CREATE (m:Node {prop1: '1'})
+        """,
+    )
+    result = execute_and_fetch_all(
+        cursor,
+        f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];",
+    )
+    assert (list(result[0])) == [":`Node`", ["Node"], "prop1", ["Int", "String"], True] or (list(result[0])) == [
+        ":`Node`",
+        ["Node"],
+        "prop1",
+        ["String", "Int"],
+        True,
+    ]
     assert (result.__len__()) == 1
 
 
@@ -544,7 +587,7 @@ def test_rel_type_properties1():
             f"CALL libschema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory;",
         )[0]
     )
-    assert (result) == [":`LOVES`", "", "", False]
+    assert (result) == [":`LOVES`", "", [], False]
 
 
 def test_rel_type_properties2():
@@ -560,7 +603,7 @@ def test_rel_type_properties2():
         cursor,
         f"CALL libschema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory;",
     )
-    assert (list(result[0])) == [":`LOVES`", "duration", "Int", False]
+    assert (list(result[0])) == [":`LOVES`", "duration", ["Int"], False]
     assert (result.__len__()) == 1
 
 
@@ -576,7 +619,47 @@ def test_rel_type_properties3():
         cursor,
         f"CALL libschema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory;",
     )
-    assert (list(result[0])) == [":`LOVES`", "duration", "Int", True]
+    assert (list(result[0])) == [":`LOVES`", "duration", ["Int"], True]
+    assert (result.__len__()) == 1
+
+
+def test_rel_type_properties4():
+    cursor = connect().cursor()
+    execute_and_fetch_all(
+        cursor,
+        """
+        CREATE (n:Dog {name: 'Simba', owner: 'Lucy'})-[j:LOVES {duration: 30}]->(a:Activity {name: 'Running', location: 'Zadar'})
+        CREATE (m:Dog {name: 'Rex', owner: 'Lucy'})-[r:LOVES {duration: 30, weather: 'sunny'}]->(b:Activity {name: 'Running', location: 'Zadar'})
+        """,
+    )
+    result = execute_and_fetch_all(
+        cursor,
+        f"CALL libschema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory;",
+    )
+    assert (list(result[0])) == [":`LOVES`", "weather", ["String"], False]
+    assert (list(result[1])) == [":`LOVES`", "duration", ["Int"], True]
+    assert (result.__len__()) == 2
+
+
+def test_rel_type_properties_multiple_property_types():
+    cursor = connect().cursor()
+    execute_and_fetch_all(
+        cursor,
+        """
+        CREATE (n:Dog {name: 'Simba', owner: 'Lucy'})-[j:LOVES {duration: 30}]->(a:Activity {name: 'Running', location: 'Zadar'})
+        CREATE (m:Dog {name: 'Rex', owner: 'Lucy'})-[r:LOVES {duration: "30"}]->(b:Activity {name: 'Running', location: 'Zadar'})
+        """,
+    )
+    result = execute_and_fetch_all(
+        cursor,
+        f"CALL libschema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory;",
+    )
+    assert (list(result[0])) == [":`LOVES`", "duration", ["Int", "String"], True] or (list(result[0])) == [
+        ":`LOVES`",
+        "duration",
+        ["String", "Int"],
+        True,
+    ]
     assert (result.__len__()) == 1
 
 
diff --git a/tests/e2e/replication/common.hpp b/tests/e2e/replication/common.hpp
index f5113ac37..1938eb0f3 100644
--- a/tests/e2e/replication/common.hpp
+++ b/tests/e2e/replication/common.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -34,12 +34,13 @@ DEFINE_double(reads_duration_limit, 10.0, "How long should the client perform re
 namespace mg::e2e::replication {
 
 auto ParseDatabaseEndpoints(const std::string &database_endpoints_str) {
-  const auto db_endpoints_strs = memgraph::utils::Split(database_endpoints_str, ",");
+  const auto db_endpoints_strs = memgraph::utils::SplitView(database_endpoints_str, ",");
   std::vector<memgraph::io::network::Endpoint> database_endpoints;
   for (const auto &db_endpoint_str : db_endpoints_strs) {
-    const auto maybe_host_port = memgraph::io::network::Endpoint::ParseSocketOrIpAddress(db_endpoint_str, 7687);
+    const auto maybe_host_port = memgraph::io::network::Endpoint::ParseSocketOrAddress(db_endpoint_str, 7687);
     MG_ASSERT(maybe_host_port);
-    database_endpoints.emplace_back(maybe_host_port->first, maybe_host_port->second);
+    auto const [ip, port] = *maybe_host_port;
+    database_endpoints.emplace_back(std::string(ip), port);
   }
   return database_endpoints;
 }
diff --git a/tests/e2e/replication/show_while_creating_invalid_state.py b/tests/e2e/replication/show_while_creating_invalid_state.py
index be7cd2b54..963aad7fd 100644
--- a/tests/e2e/replication/show_while_creating_invalid_state.py
+++ b/tests/e2e/replication/show_while_creating_invalid_state.py
@@ -923,7 +923,7 @@ def test_replication_role_recovery(connection):
                 "--log-level=TRACE",
                 "--replication-restore-state-on-startup",
                 "true",
-                "--storage-recover-on-startup",
+                "--data-recovery-on-startup",
                 "false",
             ],
             "log_file": "replica.log",
@@ -934,7 +934,7 @@ def test_replication_role_recovery(connection):
                 "--bolt-port",
                 "7687",
                 "--log-level=TRACE",
-                "--storage-recover-on-startup=true",
+                "--data-recovery-on-startup=true",
                 "--replication-restore-state-on-startup=true",
             ],
             "log_file": "main.log",
@@ -1105,7 +1105,7 @@ def test_basic_recovery_when_replica_is_kill_when_main_is_down():
                 "--bolt-port",
                 "7687",
                 "--log-level=TRACE",
-                "--storage-recover-on-startup=true",
+                "--data-recovery-on-startup=true",
                 "--replication-restore-state-on-startup=true",
             ],
             "log_file": "main.log",
@@ -1201,7 +1201,7 @@ def test_async_replication_when_main_is_killed():
                 "data_directory": f"{data_directory_replica.name}",
             },
             "main": {
-                "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+                "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
                 "log_file": "main.log",
                 "setup_queries": [],
                 "data_directory": f"{data_directory_main.name}",
@@ -1284,7 +1284,7 @@ def test_sync_replication_when_main_is_killed():
                 "data_directory": f"{data_directory_replica.name}",
             },
             "main": {
-                "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+                "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
                 "log_file": "main.log",
                 "setup_queries": [],
                 "data_directory": f"{data_directory_main.name}",
@@ -1340,7 +1340,7 @@ def test_attempt_to_write_data_on_main_when_async_replica_is_down():
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
             "log_file": "main.log",
             "setup_queries": [
                 "REGISTER REPLICA async_replica1 ASYNC TO '127.0.0.1:10001';",
@@ -1443,7 +1443,7 @@ def test_attempt_to_write_data_on_main_when_sync_replica_is_down(connection):
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup", "true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup", "true"],
             "log_file": "main.log",
             # need to do it manually
             "setup_queries": [],
@@ -1572,7 +1572,7 @@ def test_attempt_to_create_indexes_on_main_when_async_replica_is_down():
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
             "log_file": "main.log",
             "setup_queries": [
                 "REGISTER REPLICA async_replica1 ASYNC TO '127.0.0.1:10001';",
@@ -1673,7 +1673,7 @@ def test_attempt_to_create_indexes_on_main_when_sync_replica_is_down(connection)
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
             "log_file": "main.log",
             # Need to do it manually
             "setup_queries": [],
@@ -1818,7 +1818,7 @@ def test_trigger_on_create_before_commit_with_offline_sync_replica(connection):
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
             "log_file": "main.log",
             # Need to do it manually since we kill replica
             "setup_queries": [],
@@ -1937,7 +1937,7 @@ def test_trigger_on_update_before_commit_with_offline_sync_replica(connection):
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
             "log_file": "main.log",
             "setup_queries": [],
         },
@@ -2060,7 +2060,7 @@ def test_trigger_on_delete_before_commit_with_offline_sync_replica(connection):
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
             "log_file": "main.log",
             "setup_queries": [],
         },
@@ -2187,7 +2187,7 @@ def test_trigger_on_create_before_and_after_commit_with_offline_sync_replica(con
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
             "log_file": "main.log",
             "setup_queries": [],
         },
@@ -2310,7 +2310,7 @@ def test_triggers_on_create_before_commit_with_offline_sync_replica(connection):
             "setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
         },
         "main": {
-            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
+            "args": ["--bolt-port", "7687", "--log-level=TRACE", "--data-recovery-on-startup=true"],
             "log_file": "main.log",
             "setup_queries": [],
         },
diff --git a/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature b/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature
index 8c5538d6b..a6a4b15d2 100644
--- a/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature
+++ b/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature
@@ -291,3 +291,45 @@ Feature: List operators
 #        Then the result should be:
 #            | years                 |
 #            | [2021,2003,2003,1999] |
+
+     Scenario: Multiple entries with list pattern comprehension
+        Given graph "graph_keanu"
+        When executing query:
+            """
+            MATCH (n:Person)
+            RETURN n.name, [(n)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | b.released] AS years
+            """
+        Then an error should be raised
+
+     Scenario: Multiple list pattern comprehensions in Return
+        Given graph "graph_keanu"
+        When executing query:
+            """
+            MATCH (n:Person)
+            RETURN n.name,
+                [(n)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | b.released] AS years,
+                [(n)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | b.title] AS titles
+            """
+        Then an error should be raised
+
+     Scenario: Function inside pattern comprehension's expression
+        Given graph "graph_keanu"
+        When executing query:
+            """
+            MATCH (keanu:Person {name: 'Keanu Reeves'})
+            RETURN [p = (keanu)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | size(nodes(p))] AS nodes
+            """
+        Then an error should be raised
+
+     Scenario: Multiple list pattern comprehensions in With
+        Given graph "graph_keanu"
+        When executing query:
+            """
+            MATCH (n) WHERE size(n.name) > 5
+            WITH
+                n AS actor,
+                [(n)-->(m) WHERE m.released > 2000 | m.title] AS titles,
+                [(n)-->(m) WHERE m.released > 2000 | m.released] AS years
+            RETURN actor.name, years, titles;
+            """
+        Then an error should be raised
diff --git a/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher b/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher
index a7a72aced..98f48c3c1 100644
--- a/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher
+++ b/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher
@@ -1,5 +1,7 @@
 CREATE
   (keanu:Person {name: 'Keanu Reeves'}),
+  (trinity:Person {name: 'Carrie-Anne Moss'}),
+  (morpheus:Person {name: 'Laurence Fishburne'}),
   (johnnyMnemonic:Movie {title: 'Johnny Mnemonic', released: 1995}),
   (theMatrixRevolutions:Movie {title: 'The Matrix Revolutions', released: 2003}),
   (theMatrixReloaded:Movie {title: 'The Matrix Reloaded', released: 2003}),
@@ -13,4 +15,7 @@ CREATE
   (keanu)-[:ACTED_IN]->(theReplacements),
   (keanu)-[:ACTED_IN]->(theMatrix),
   (keanu)-[:ACTED_IN]->(theDevilsAdvocate),
-  (keanu)-[:ACTED_IN]->(theMatrixResurrections);
+  (keanu)-[:ACTED_IN]->(theMatrixResurrections),
+  (trinity)-[:ACTED_IN]->(theMatrix),
+  (trinity)-[:ACTED_IN]->(theMatrixReloaded),
+  (morpheus)-[:ACTED_IN]->(theMatrix);
diff --git a/tests/integration/durability/tests/v17/test_all/create_dataset.cypher b/tests/integration/durability/tests/v17/test_all/create_dataset.cypher
new file mode 100644
index 000000000..9ee350d9a
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_all/create_dataset.cypher
@@ -0,0 +1,22 @@
+// --storage-items-per-batch is set to 10
+CREATE INDEX ON :`label2`(`prop2`);
+CREATE INDEX ON :`label2`(`prop`);
+CREATE INDEX ON :`label`;
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE EDGE INDEX ON :`edge_type`;
+CREATE (:`edge_index_from`), (:`edge_index_to`);
+MATCH (n:`edge_index_from`), (m:`edge_index_to`) CREATE (n)-[r:`edge_type`]->(m);
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 0, `prop2`: ["kaj", 2, Null, {`prop4`: -1.341}], `ext`: 2, `prop`: "joj"});
+CREATE (:__mg_vertex__:`label2`:`label` {__mg_id__: 1, `ext`: 2, `prop`: "joj"});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 2, `prop2`: 2, `prop`: 1});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 3, `prop2`: 2, `prop`: 2});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 0 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 1 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 2 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 3 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`prop2`, u.`prop` IS UNIQUE;
+ANALYZE GRAPH;
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+DROP EDGE INDEX ON :`edge_type`;
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_all/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_all/expected_snapshot.cypher
new file mode 100644
index 000000000..fb2d74667
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_all/expected_snapshot.cypher
@@ -0,0 +1,19 @@
+    CREATE (:__mg_vertex__:`edge_index_from` {__mg_id__: 0});
+    CREATE (:__mg_vertex__:`edge_index_to` {__mg_id__: 1});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 2, `prop2`: ["kaj", 2, Null, {`prop4`: -1.341}], `ext`: 2, `prop`: "joj"});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop2`: 2, `prop`: 1});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 5, `prop2`: 2, `prop`: 2});
+    CREATE (:__mg_vertex__:`label`:`label2` {__mg_id__: 3, `ext`: 2, `prop`: "joj"});
+    CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`prop2`, u.`prop` IS UNIQUE;
+    CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+    CREATE INDEX ON :__mg_vertex__(__mg_id__);
+    CREATE INDEX ON :`label2`(`prop2`);
+    CREATE INDEX ON :`label2`(`prop`);
+    CREATE INDEX ON :`label`;
+    DROP INDEX ON :__mg_vertex__(__mg_id__);
+    MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge_type`]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 2 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 3 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 4 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 5 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
diff --git a/tests/integration/durability/tests/v17/test_all/expected_wal.cypher b/tests/integration/durability/tests/v17/test_all/expected_wal.cypher
new file mode 100644
index 000000000..33efec9e2
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_all/expected_wal.cypher
@@ -0,0 +1,19 @@
+    CREATE (:__mg_vertex__:`edge_index_from` {__mg_id__: 0});
+    CREATE (:__mg_vertex__:`edge_index_to` {__mg_id__: 1});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 2, `prop2`: ["kaj", 2, Null, {`prop4`: -1.341}], `prop`: "joj", `ext`: 2});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop2`: 2, `prop`: 1});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 5, `prop2`: 2, `prop`: 2});
+    CREATE (:__mg_vertex__:`label`:`label2` {__mg_id__: 3, `prop`: "joj", `ext`: 2});
+    CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`prop2`, u.`prop` IS UNIQUE;
+    CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+    CREATE INDEX ON :__mg_vertex__(__mg_id__);
+    CREATE INDEX ON :`label2`(`prop2`);
+    CREATE INDEX ON :`label2`(`prop`);
+    CREATE INDEX ON :`label`;
+    DROP INDEX ON :__mg_vertex__(__mg_id__);
+    MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge_type`]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 2 CREATE (u)-[:`link` {`prop`: -1, `ext`: [false, {`k`: "l"}]}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 3 CREATE (u)-[:`link` {`prop`: -1, `ext`: [false, {`k`: "l"}]}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 4 CREATE (u)-[:`link` {`prop`: -1, `ext`: [false, {`k`: "l"}]}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 5 CREATE (u)-[:`link` {`prop`: -1, `ext`: [false, {`k`: "l"}]}]->(v);
diff --git a/tests/integration/durability/tests/v17/test_all/snapshot.bin b/tests/integration/durability/tests/v17/test_all/snapshot.bin
new file mode 100644
index 000000000..9cc54b480
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_all/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v17/test_all/wal.bin b/tests/integration/durability/tests/v17/test_all/wal.bin
new file mode 100644
index 000000000..61a33372d
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_all/wal.bin differ
diff --git a/tests/integration/durability/tests/v17/test_constraints/create_dataset.cypher b/tests/integration/durability/tests/v17/test_constraints/create_dataset.cypher
new file mode 100644
index 000000000..96bb4bac4
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_constraints/create_dataset.cypher
@@ -0,0 +1,6 @@
+CREATE CONSTRAINT ON (u:`label2`) ASSERT EXISTS (u.`ext2`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`a` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`b` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`c` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`a`, u.`b` IS UNIQUE;
diff --git a/tests/integration/durability/tests/v17/test_constraints/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_constraints/expected_snapshot.cypher
new file mode 100644
index 000000000..fbe2c28ab
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_constraints/expected_snapshot.cypher
@@ -0,0 +1,6 @@
+CREATE CONSTRAINT ON (u:`label2`) ASSERT EXISTS (u.`ext2`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`c` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`b` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`a` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`b`, u.`a` IS UNIQUE;
diff --git a/tests/integration/durability/tests/v17/test_constraints/expected_wal.cypher b/tests/integration/durability/tests/v17/test_constraints/expected_wal.cypher
new file mode 100644
index 000000000..9260455ed
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_constraints/expected_wal.cypher
@@ -0,0 +1,6 @@
+CREATE CONSTRAINT ON (u:`label2`) ASSERT EXISTS (u.`ext2`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`a`, u.`b` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`a` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`b` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`c` IS UNIQUE;
diff --git a/tests/integration/durability/tests/v17/test_constraints/snapshot.bin b/tests/integration/durability/tests/v17/test_constraints/snapshot.bin
new file mode 100644
index 000000000..76986ab9a
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_constraints/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v17/test_constraints/wal.bin b/tests/integration/durability/tests/v17/test_constraints/wal.bin
new file mode 100644
index 000000000..f2d54e5fd
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_constraints/wal.bin differ
diff --git a/tests/integration/durability/tests/v17/test_edges/create_dataset.cypher b/tests/integration/durability/tests/v17/test_edges/create_dataset.cypher
new file mode 100644
index 000000000..ab3b3af6d
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_edges/create_dataset.cypher
@@ -0,0 +1,60 @@
+// --storage-items-per-batch is set to 7
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__ {__mg_id__: 4});
+CREATE (:__mg_vertex__ {__mg_id__: 5});
+CREATE (:__mg_vertex__ {__mg_id__: 6});
+CREATE (:__mg_vertex__ {__mg_id__: 7});
+CREATE (:__mg_vertex__ {__mg_id__: 8});
+CREATE (:__mg_vertex__ {__mg_id__: 9});
+CREATE (:__mg_vertex__ {__mg_id__: 10});
+CREATE (:__mg_vertex__ {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 14});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 15});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:`edge` {`prop`: 11}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:`edge` {`prop`: true}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:`edge2`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 9 CREATE (u)-[:`edge2` {`prop`: -3.141}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:`edgelink` {`prop`: {`prop`: 1, `prop2`: {`prop4`: 9}}}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:`edgelink` {`prop`: [1, Null, false, "\n\n\n\n\\\"\"\n\t"]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+ANALYZE GRAPH;
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_edges/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_edges/expected_snapshot.cypher
new file mode 100644
index 000000000..596753ba5
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_edges/expected_snapshot.cypher
@@ -0,0 +1,58 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__ {__mg_id__: 4});
+CREATE (:__mg_vertex__ {__mg_id__: 5});
+CREATE (:__mg_vertex__ {__mg_id__: 6});
+CREATE (:__mg_vertex__ {__mg_id__: 7});
+CREATE (:__mg_vertex__ {__mg_id__: 8});
+CREATE (:__mg_vertex__ {__mg_id__: 9});
+CREATE (:__mg_vertex__ {__mg_id__: 10});
+CREATE (:__mg_vertex__ {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 14});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 15});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:`edge` {`prop`: 11}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:`edge` {`prop`: true}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:`edge2`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 9 CREATE (u)-[:`edge2` {`prop`: -3.141}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:`edgelink` {`prop`: {`prop`: 1, `prop2`: {`prop4`: 9}}}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:`edgelink` {`prop`: [1, Null, false, "\n\n\n\n\\\"\"\n\t"]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_edges/expected_wal.cypher b/tests/integration/durability/tests/v17/test_edges/expected_wal.cypher
new file mode 100644
index 000000000..596753ba5
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_edges/expected_wal.cypher
@@ -0,0 +1,58 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__ {__mg_id__: 4});
+CREATE (:__mg_vertex__ {__mg_id__: 5});
+CREATE (:__mg_vertex__ {__mg_id__: 6});
+CREATE (:__mg_vertex__ {__mg_id__: 7});
+CREATE (:__mg_vertex__ {__mg_id__: 8});
+CREATE (:__mg_vertex__ {__mg_id__: 9});
+CREATE (:__mg_vertex__ {__mg_id__: 10});
+CREATE (:__mg_vertex__ {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 14});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 15});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:`edge` {`prop`: 11}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:`edge` {`prop`: true}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:`edge2`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 9 CREATE (u)-[:`edge2` {`prop`: -3.141}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:`edgelink` {`prop`: {`prop`: 1, `prop2`: {`prop4`: 9}}}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:`edgelink` {`prop`: [1, Null, false, "\n\n\n\n\\\"\"\n\t"]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_edges/snapshot.bin b/tests/integration/durability/tests/v17/test_edges/snapshot.bin
new file mode 100644
index 000000000..070bbe530
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_edges/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v17/test_edges/wal.bin b/tests/integration/durability/tests/v17/test_edges/wal.bin
new file mode 100644
index 000000000..914f49154
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_edges/wal.bin differ
diff --git a/tests/integration/durability/tests/v17/test_indices/create_dataset.cypher b/tests/integration/durability/tests/v17/test_indices/create_dataset.cypher
new file mode 100644
index 000000000..739062f19
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_indices/create_dataset.cypher
@@ -0,0 +1,6 @@
+CREATE INDEX ON :`label2`;
+CREATE INDEX ON :`label2`(`prop2`);
+CREATE INDEX ON :`label`(`prop2`);
+CREATE INDEX ON :`label`(`prop`);
+CREATE EDGE INDEX ON :`edgetype`;
+ANALYZE GRAPH;
diff --git a/tests/integration/durability/tests/v17/test_indices/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_indices/expected_snapshot.cypher
new file mode 100644
index 000000000..1e930697a
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_indices/expected_snapshot.cypher
@@ -0,0 +1,5 @@
+CREATE INDEX ON :`label2`;
+CREATE INDEX ON :`label`(`prop`);
+CREATE INDEX ON :`label`(`prop2`);
+CREATE INDEX ON :`label2`(`prop2`);
+CREATE EDGE INDEX ON :`edgetype`;
diff --git a/tests/integration/durability/tests/v17/test_indices/expected_wal.cypher b/tests/integration/durability/tests/v17/test_indices/expected_wal.cypher
new file mode 100644
index 000000000..bfae88b0b
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_indices/expected_wal.cypher
@@ -0,0 +1,5 @@
+CREATE INDEX ON :`label2`;
+CREATE INDEX ON :`label2`(`prop2`);
+CREATE INDEX ON :`label`(`prop2`);
+CREATE INDEX ON :`label`(`prop`);
+CREATE EDGE INDEX ON :`edgetype`;
diff --git a/tests/integration/durability/tests/v17/test_indices/snapshot.bin b/tests/integration/durability/tests/v17/test_indices/snapshot.bin
new file mode 100644
index 000000000..99ad6e0ea
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_indices/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v17/test_indices/wal.bin b/tests/integration/durability/tests/v17/test_indices/wal.bin
new file mode 100644
index 000000000..661cba6c1
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_indices/wal.bin differ
diff --git a/tests/integration/durability/tests/v17/test_vertices/create_dataset.cypher b/tests/integration/durability/tests/v17/test_vertices/create_dataset.cypher
new file mode 100644
index 000000000..061df375b
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_vertices/create_dataset.cypher
@@ -0,0 +1,18 @@
+// --storage-items-per-batch is set to 5
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 2, `prop`: false});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 3, `prop`: true});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop`: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 5, `prop2`: 3.141});
+CREATE (:__mg_vertex__:`label6` {__mg_id__: 6, `prop3`: true, `prop2`: -314000000});
+CREATE (:__mg_vertex__:`label3`:`label1`:`label2` {__mg_id__: 7});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 8, `prop3`: "str", `prop2`: 2, `prop`: 1});
+CREATE (:__mg_vertex__:`label2`:`label1` {__mg_id__: 9, `prop`: {`prop_nes`: "kaj je"}});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 10, `prop_array`: [1, false, Null, "str", {`prop2`: 2}]});
+CREATE (:__mg_vertex__:`label3`:`label` {__mg_id__: 11, `prop`: {`prop`: [1, false], `prop2`: {}, `prop3`: "test2", `prop4`: "test"}});
+CREATE (:__mg_vertex__ {__mg_id__: 12, `prop`: " \n\"\'\t\\%"});
+ANALYZE GRAPH;
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_vertices/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_vertices/expected_snapshot.cypher
new file mode 100644
index 000000000..ecdc1229e
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_vertices/expected_snapshot.cypher
@@ -0,0 +1,16 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 2, `prop`: false});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 3, `prop`: true});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop`: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 5, `prop2`: 3.141});
+CREATE (:__mg_vertex__:`label6` {__mg_id__: 6, `prop3`: true, `prop2`: -314000000});
+CREATE (:__mg_vertex__:`label2`:`label3`:`label1` {__mg_id__: 7});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 8, `prop3`: "str", `prop2`: 2, `prop`: 1});
+CREATE (:__mg_vertex__:`label1`:`label2` {__mg_id__: 9, `prop`: {`prop_nes`: "kaj je"}});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 10, `prop_array`: [1, false, Null, "str", {`prop2`: 2}]});
+CREATE (:__mg_vertex__:`label`:`label3` {__mg_id__: 11, `prop`: {`prop`: [1, false], `prop2`: {}, `prop3`: "test2", `prop4`: "test"}});
+CREATE (:__mg_vertex__ {__mg_id__: 12, `prop`: " \n\"\'\t\\%"});
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_vertices/expected_wal.cypher b/tests/integration/durability/tests/v17/test_vertices/expected_wal.cypher
new file mode 100644
index 000000000..d8f758737
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_vertices/expected_wal.cypher
@@ -0,0 +1,16 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 2, `prop`: false});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 3, `prop`: true});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop`: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 5, `prop2`: 3.141});
+CREATE (:__mg_vertex__:`label6` {__mg_id__: 6, `prop2`: -314000000, `prop3`: true});
+CREATE (:__mg_vertex__:`label2`:`label3`:`label1` {__mg_id__: 7});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 8, `prop`: 1, `prop2`: 2, `prop3`: "str"});
+CREATE (:__mg_vertex__:`label1`:`label2` {__mg_id__: 9, `prop`: {`prop_nes`: "kaj je"}});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 10, `prop_array`: [1, false, Null, "str", {`prop2`: 2}]});
+CREATE (:__mg_vertex__:`label`:`label3` {__mg_id__: 11, `prop`: {`prop`: [1, false], `prop2`: {}, `prop3`: "test2", `prop4`: "test"}});
+CREATE (:__mg_vertex__ {__mg_id__: 12, `prop`: " \n\"\'\t\\%"});
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_vertices/snapshot.bin b/tests/integration/durability/tests/v17/test_vertices/snapshot.bin
new file mode 100644
index 000000000..8a67d9a7d
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_vertices/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v17/test_vertices/wal.bin b/tests/integration/durability/tests/v17/test_vertices/wal.bin
new file mode 100644
index 000000000..304db455f
Binary files /dev/null and b/tests/integration/durability/tests/v17/test_vertices/wal.bin differ
diff --git a/tests/manual/interactive_planning.cpp b/tests/manual/interactive_planning.cpp
index 3f64c4f37..f0f60ba91 100644
--- a/tests/manual/interactive_planning.cpp
+++ b/tests/manual/interactive_planning.cpp
@@ -214,6 +214,8 @@ class InteractiveDbAccessor {
     return label_property_index_.at(key);
   }
 
+  bool EdgeTypeIndexExists(memgraph::storage::EdgeTypeId edge_type) { return true; }
+
   std::optional<memgraph::storage::LabelIndexStats> GetIndexStats(const memgraph::storage::LabelId label) const {
     return dba_->GetIndexStats(label);
   }
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 6f7b3bbef..44b24b6f6 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -430,3 +430,25 @@ target_include_directories(${test_prefix}distributed_lamport_clock PRIVATE ${CMA
 
 add_unit_test(query_hint_provider.cpp)
 target_link_libraries(${test_prefix}query_hint_provider mg-query mg-glue)
+
+
+# Test coordination
+if(MG_ENTERPRISE)
+add_unit_test(coordination_utils.cpp)
+target_link_libraries(${test_prefix}coordination_utils gflags mg-coordination mg-repl_coord_glue)
+target_include_directories(${test_prefix}coordination_utils PRIVATE ${CMAKE_SOURCE_DIR}/include)
+endif()
+
+# Test Raft log serialization
+if(MG_ENTERPRISE)
+add_unit_test(raft_log_serialization.cpp)
+target_link_libraries(${test_prefix}raft_log_serialization gflags mg-coordination mg-repl_coord_glue)
+target_include_directories(${test_prefix}raft_log_serialization PRIVATE ${CMAKE_SOURCE_DIR}/include)
+endif()
+
+# Test Raft log serialization
+if(MG_ENTERPRISE)
+add_unit_test(coordinator_cluster_state.cpp)
+target_link_libraries(${test_prefix}coordinator_cluster_state gflags mg-coordination mg-repl_coord_glue)
+target_include_directories(${test_prefix}coordinator_cluster_state PRIVATE ${CMAKE_SOURCE_DIR}/include)
+endif()
diff --git a/tests/unit/coordination_utils.cpp b/tests/unit/coordination_utils.cpp
new file mode 100644
index 000000000..7c77b4e68
--- /dev/null
+++ b/tests/unit/coordination_utils.cpp
@@ -0,0 +1,236 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include <gflags/gflags.h>
+#include <gtest/gtest.h>
+#include "coordination/coordinator_instance.hpp"
+#include "dbms/constants.hpp"
+#include "replication_coordination_glue/common.hpp"
+#include "utils/functional.hpp"
+
+class CoordinationUtils : public ::testing::Test {
+ protected:
+  void SetUp() override {}
+
+  void TearDown() override {}
+
+  std::filesystem::path test_folder_{std::filesystem::temp_directory_path() / "MG_tests_unit_coordination"};
+};
+
+TEST_F(CoordinationUtils, MemgraphDbHistorySimple) {
+  // Choose any if everything is same
+  // X = dead
+  // Main      : A(24)  B(36)  C(48) D(50) E(51) X
+  // replica  1: A(24)  B(36)  C(48) D(50) E(51)
+  // replica  2: A(24)  B(36)  C(48) D(50) E(51)
+  // replica  3: A(24)  B(36)  C(48) D(50) E(51)
+  std::vector<std::pair<std::string, memgraph::replication_coordination_glue::DatabaseHistories>>
+      instance_database_histories;
+
+  std::vector<std::pair<memgraph::utils::UUID, uint64_t>> histories;
+  histories.emplace_back(memgraph::utils::UUID{}, 24);
+  histories.emplace_back(memgraph::utils::UUID{}, 36);
+  histories.emplace_back(memgraph::utils::UUID{}, 48);
+  histories.emplace_back(memgraph::utils::UUID{}, 50);
+  histories.emplace_back(memgraph::utils::UUID{}, 51);
+
+  memgraph::utils::UUID db_uuid;
+  std::string default_name = std::string(memgraph::dbms::kDefaultDB);
+
+  auto db_histories = memgraph::utils::fmap(histories, [](const std::pair<memgraph::utils::UUID, uint64_t> &pair) {
+    return std::make_pair(std::string(pair.first), pair.second);
+  });
+
+  memgraph::replication_coordination_glue::DatabaseHistory history{
+      .db_uuid = db_uuid, .history = db_histories, .name = default_name};
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_1_db_histories_{history};
+  instance_database_histories.emplace_back("instance_1", instance_1_db_histories_);
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_2_db_histories_{history};
+  instance_database_histories.emplace_back("instance_2", instance_2_db_histories_);
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_3_db_histories_{history};
+  instance_database_histories.emplace_back("instance_3", instance_3_db_histories_);
+  memgraph::coordination::CoordinatorInstance instance;
+
+  auto [instance_name, latest_epoch, latest_commit_timestamp] =
+      instance.ChooseMostUpToDateInstance(instance_database_histories);
+  ASSERT_TRUE(instance_name == "instance_1" || instance_name == "instance_2" || instance_name == "instance_3");
+  ASSERT_TRUE(latest_epoch == db_histories.back().first);
+  ASSERT_TRUE(latest_commit_timestamp == db_histories.back().second);
+}
+
+TEST_F(CoordinationUtils, MemgraphDbHistoryLastEpochDifferent) {
+  // Prioritize one with the biggest last commit timestamp on last epoch
+  // X = dead
+  // Main      : A(24)  B(36)  C(48) D(50) E(59) X
+  // replica  1: A(24)  B(12)  C(15) D(17) E(51)
+  // replica  2: A(24)  B(12)  C(15) D(17) E(57)
+  // replica  3: A(24)  B(12)  C(15) D(17) E(59)
+  std::vector<std::pair<std::string, memgraph::replication_coordination_glue::DatabaseHistories>>
+      instance_database_histories;
+
+  std::vector<std::pair<memgraph::utils::UUID, uint64_t>> histories;
+  histories.emplace_back(memgraph::utils::UUID{}, 24);
+  histories.emplace_back(memgraph::utils::UUID{}, 36);
+  histories.emplace_back(memgraph::utils::UUID{}, 48);
+  histories.emplace_back(memgraph::utils::UUID{}, 50);
+  histories.emplace_back(memgraph::utils::UUID{}, 59);
+
+  memgraph::utils::UUID db_uuid;
+  std::string default_name = std::string(memgraph::dbms::kDefaultDB);
+
+  auto db_histories = memgraph::utils::fmap(histories, [](const std::pair<memgraph::utils::UUID, uint64_t> &pair) {
+    return std::make_pair(std::string(pair.first), pair.second);
+  });
+
+  db_histories.back().second = 51;
+  memgraph::replication_coordination_glue::DatabaseHistory history1{
+      .db_uuid = db_uuid, .history = db_histories, .name = default_name};
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_1_db_histories_{history1};
+  instance_database_histories.emplace_back("instance_1", instance_1_db_histories_);
+
+  db_histories.back().second = 57;
+  memgraph::replication_coordination_glue::DatabaseHistory history2{
+      .db_uuid = db_uuid, .history = db_histories, .name = default_name};
+  memgraph::replication_coordination_glue::DatabaseHistories instance_2_db_histories_{history2};
+  instance_database_histories.emplace_back("instance_2", instance_2_db_histories_);
+
+  db_histories.back().second = 59;
+  memgraph::replication_coordination_glue::DatabaseHistory history3{
+      .db_uuid = db_uuid, .history = db_histories, .name = default_name};
+  memgraph::replication_coordination_glue::DatabaseHistories instance_3_db_histories_{history3};
+  instance_database_histories.emplace_back("instance_3", instance_3_db_histories_);
+
+  memgraph::coordination::CoordinatorInstance instance;
+  auto [instance_name, latest_epoch, latest_commit_timestamp] =
+      instance.ChooseMostUpToDateInstance(instance_database_histories);
+
+  ASSERT_TRUE(instance_name == "instance_3");
+  ASSERT_TRUE(latest_epoch == db_histories.back().first);
+  ASSERT_TRUE(latest_commit_timestamp == db_histories.back().second);
+}
+
+TEST_F(CoordinationUtils, MemgraphDbHistoryOneInstanceAheadFewEpochs) {
+  // Prioritize one biggest commit timestamp
+  // X = dead
+  // Main      : A(24)  B(36)  C(48) D(50) E(51)   X    X     X  X
+  // replica  1: A(24)  B(36)  C(48) D(50) E(51) F(60) G(65)  X  up
+  // replica  2: A(24)  B(36)  C(48) D(50) E(51)  X     X     X  up
+  // replica  3: A(24)  B(36)  C(48) D(50) E(51)  X     X     X  up
+  std::vector<std::pair<std::string, memgraph::replication_coordination_glue::DatabaseHistories>>
+      instance_database_histories;
+
+  std::vector<std::pair<memgraph::utils::UUID, uint64_t>> histories;
+  histories.emplace_back(memgraph::utils::UUID{}, 24);
+  histories.emplace_back(memgraph::utils::UUID{}, 36);
+  histories.emplace_back(memgraph::utils::UUID{}, 48);
+  histories.emplace_back(memgraph::utils::UUID{}, 50);
+  histories.emplace_back(memgraph::utils::UUID{}, 51);
+
+  memgraph::utils::UUID db_uuid;
+  std::string default_name = std::string(memgraph::dbms::kDefaultDB);
+
+  auto db_histories = memgraph::utils::fmap(histories, [](const std::pair<memgraph::utils::UUID, uint64_t> &pair) {
+    return std::make_pair(std::string(pair.first), pair.second);
+  });
+
+  memgraph::replication_coordination_glue::DatabaseHistory history{
+      .db_uuid = db_uuid, .history = db_histories, .name = default_name};
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_1_db_histories_{history};
+  instance_database_histories.emplace_back("instance_1", instance_1_db_histories_);
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_2_db_histories_{history};
+  instance_database_histories.emplace_back("instance_2", instance_2_db_histories_);
+
+  histories.emplace_back(memgraph::utils::UUID{}, 60);
+  histories.emplace_back(memgraph::utils::UUID{}, 65);
+  auto db_histories_longest =
+      memgraph::utils::fmap(histories, [](const std::pair<memgraph::utils::UUID, uint64_t> &pair) {
+        return std::make_pair(std::string(pair.first), pair.second);
+      });
+
+  memgraph::replication_coordination_glue::DatabaseHistory history_longest{
+      .db_uuid = db_uuid, .history = db_histories_longest, .name = default_name};
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_3_db_histories_{history_longest};
+  instance_database_histories.emplace_back("instance_3", instance_3_db_histories_);
+
+  memgraph::coordination::CoordinatorInstance instance;
+  auto [instance_name, latest_epoch, latest_commit_timestamp] =
+      instance.ChooseMostUpToDateInstance(instance_database_histories);
+
+  ASSERT_TRUE(instance_name == "instance_3");
+  ASSERT_TRUE(latest_epoch == db_histories_longest.back().first);
+  ASSERT_TRUE(latest_commit_timestamp == db_histories_longest.back().second);
+}
+
+TEST_F(CoordinationUtils, MemgraphDbHistoryInstancesHistoryDiverged) {
+  // When history diverged, also prioritize one with biggest last commit timestamp
+  // Main      : A(1)  B(2)   C(3)    X
+  // replica  1: A(1)  B(2)   C(3)    X     X up
+  // replica  2: A(1)  B(2)    X     D(5)   X up
+  // replica  3: A(1)  B(2)    X     D(4)   X up
+  std::vector<std::pair<std::string, memgraph::replication_coordination_glue::DatabaseHistories>>
+      instance_database_histories;
+
+  std::vector<std::pair<memgraph::utils::UUID, uint64_t>> histories;
+  histories.emplace_back(memgraph::utils::UUID{}, 1);
+  histories.emplace_back(memgraph::utils::UUID{}, 2);
+  histories.emplace_back(memgraph::utils::UUID{}, 3);
+
+  memgraph::utils::UUID db_uuid;
+  std::string default_name = std::string(memgraph::dbms::kDefaultDB);
+
+  auto db_histories = memgraph::utils::fmap(histories, [](const std::pair<memgraph::utils::UUID, uint64_t> &pair) {
+    return std::make_pair(std::string(pair.first), pair.second);
+  });
+
+  memgraph::replication_coordination_glue::DatabaseHistory history{
+      .db_uuid = db_uuid, .history = db_histories, .name = default_name};
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_1_db_histories_{history};
+  instance_database_histories.emplace_back("instance_1", instance_1_db_histories_);
+
+  db_histories.pop_back();
+
+  auto oldest_commit_timestamp{5};
+  auto newest_different_epoch = memgraph::utils::UUID{};
+  histories.emplace_back(newest_different_epoch, oldest_commit_timestamp);
+  auto db_histories_different =
+      memgraph::utils::fmap(histories, [](const std::pair<memgraph::utils::UUID, uint64_t> &pair) {
+        return std::make_pair(std::string(pair.first), pair.second);
+      });
+
+  memgraph::replication_coordination_glue::DatabaseHistory history_3{
+      .db_uuid = db_uuid, .history = db_histories_different, .name = default_name};
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_3_db_histories_{history_3};
+  instance_database_histories.emplace_back("instance_3", instance_3_db_histories_);
+
+  db_histories_different.back().second = 4;
+  memgraph::replication_coordination_glue::DatabaseHistory history_2{
+      .db_uuid = db_uuid, .history = db_histories_different, .name = default_name};
+
+  memgraph::replication_coordination_glue::DatabaseHistories instance_2_db_histories_{history_2};
+  instance_database_histories.emplace_back("instance_2", instance_2_db_histories_);
+
+  memgraph::coordination::CoordinatorInstance instance;
+  auto [instance_name, latest_epoch, latest_commit_timestamp] =
+      instance.ChooseMostUpToDateInstance(instance_database_histories);
+
+  ASSERT_TRUE(instance_name == "instance_3");
+  ASSERT_TRUE(latest_epoch == std::string(newest_different_epoch));
+  ASSERT_TRUE(latest_commit_timestamp == oldest_commit_timestamp);
+}
diff --git a/tests/unit/coordinator_cluster_state.cpp b/tests/unit/coordinator_cluster_state.cpp
new file mode 100644
index 000000000..8df2797f2
--- /dev/null
+++ b/tests/unit/coordinator_cluster_state.cpp
@@ -0,0 +1,163 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include "nuraft/coordinator_cluster_state.hpp"
+#include "nuraft/coordinator_state_machine.hpp"
+#include "replication_coordination_glue/role.hpp"
+
+#include "utils/file.hpp"
+
+#include <gflags/gflags.h>
+#include <gtest/gtest.h>
+#include "json/json.hpp"
+
+#include "libnuraft/nuraft.hxx"
+
+using memgraph::coordination::CoordinatorClientConfig;
+using memgraph::coordination::CoordinatorClusterState;
+using memgraph::coordination::CoordinatorStateMachine;
+using memgraph::coordination::InstanceState;
+using memgraph::coordination::RaftLogAction;
+using memgraph::replication_coordination_glue::ReplicationMode;
+using memgraph::replication_coordination_glue::ReplicationRole;
+using nuraft::buffer;
+using nuraft::buffer_serializer;
+using nuraft::ptr;
+
+class CoordinatorClusterStateTest : public ::testing::Test {
+ protected:
+  void SetUp() override {}
+
+  void TearDown() override {}
+
+  std::filesystem::path test_folder_{std::filesystem::temp_directory_path() /
+                                     "MG_tests_unit_coordinator_cluster_state"};
+};
+
+TEST_F(CoordinatorClusterStateTest, InstanceStateSerialization) {
+  InstanceState instance_state{
+      CoordinatorClientConfig{"instance3",
+                              "127.0.0.1",
+                              10112,
+                              std::chrono::seconds{1},
+                              std::chrono::seconds{5},
+                              std::chrono::seconds{10},
+                              {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10001},
+                              .ssl = std::nullopt},
+      ReplicationRole::MAIN};
+
+  nlohmann::json j = instance_state;
+  InstanceState deserialized_instance_state = j.get<InstanceState>();
+
+  EXPECT_EQ(instance_state.config, deserialized_instance_state.config);
+  EXPECT_EQ(instance_state.status, deserialized_instance_state.status);
+}
+
+TEST_F(CoordinatorClusterStateTest, DoActionRegisterInstances) {
+  auto coordinator_cluster_state = memgraph::coordination::CoordinatorClusterState{};
+
+  {
+    CoordinatorClientConfig config{"instance1",
+                                   "127.0.0.1",
+                                   10111,
+                                   std::chrono::seconds{1},
+                                   std::chrono::seconds{5},
+                                   std::chrono::seconds{10},
+                                   {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10001},
+                                   .ssl = std::nullopt};
+
+    auto buffer = CoordinatorStateMachine::SerializeRegisterInstance(config);
+    auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+
+    coordinator_cluster_state.DoAction(payload, action);
+  }
+  {
+    CoordinatorClientConfig config{"instance2",
+                                   "127.0.0.1",
+                                   10112,
+                                   std::chrono::seconds{1},
+                                   std::chrono::seconds{5},
+                                   std::chrono::seconds{10},
+                                   {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10002},
+                                   .ssl = std::nullopt};
+
+    auto buffer = CoordinatorStateMachine::SerializeRegisterInstance(config);
+    auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+
+    coordinator_cluster_state.DoAction(payload, action);
+  }
+  {
+    CoordinatorClientConfig config{"instance3",
+                                   "127.0.0.1",
+                                   10113,
+                                   std::chrono::seconds{1},
+                                   std::chrono::seconds{5},
+                                   std::chrono::seconds{10},
+                                   {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10003},
+                                   .ssl = std::nullopt};
+
+    auto buffer = CoordinatorStateMachine::SerializeRegisterInstance(config);
+    auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+
+    coordinator_cluster_state.DoAction(payload, action);
+  }
+  {
+    CoordinatorClientConfig config{"instance4",
+                                   "127.0.0.1",
+                                   10114,
+                                   std::chrono::seconds{1},
+                                   std::chrono::seconds{5},
+                                   std::chrono::seconds{10},
+                                   {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10004},
+                                   .ssl = std::nullopt};
+
+    auto buffer = CoordinatorStateMachine::SerializeRegisterInstance(config);
+    auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+
+    coordinator_cluster_state.DoAction(payload, action);
+  }
+  {
+    CoordinatorClientConfig config{"instance5",
+                                   "127.0.0.1",
+                                   10115,
+                                   std::chrono::seconds{1},
+                                   std::chrono::seconds{5},
+                                   std::chrono::seconds{10},
+                                   {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10005},
+                                   .ssl = std::nullopt};
+
+    auto buffer = CoordinatorStateMachine::SerializeRegisterInstance(config);
+    auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+
+    coordinator_cluster_state.DoAction(payload, action);
+  }
+  {
+    CoordinatorClientConfig config{"instance6",
+                                   "127.0.0.1",
+                                   10116,
+                                   std::chrono::seconds{1},
+                                   std::chrono::seconds{5},
+                                   std::chrono::seconds{10},
+                                   {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10006},
+                                   .ssl = std::nullopt};
+
+    auto buffer = CoordinatorStateMachine::SerializeRegisterInstance(config);
+    auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+
+    coordinator_cluster_state.DoAction(payload, action);
+  }
+
+  ptr<buffer> data;
+  coordinator_cluster_state.Serialize(data);
+
+  auto deserialized_coordinator_cluster_state = CoordinatorClusterState::Deserialize(*data);
+  ASSERT_EQ(coordinator_cluster_state.GetInstances(), deserialized_coordinator_cluster_state.GetInstances());
+}
diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp
index 63cca3aa4..bcc6767f4 100644
--- a/tests/unit/cypher_main_visitor.cpp
+++ b/tests/unit/cypher_main_visitor.cpp
@@ -4624,3 +4624,101 @@ TEST_P(CypherMainVisitorTest, CallSubquery) {
     ASSERT_TRUE(nested_match);
   }
 }
+
+TEST_P(CypherMainVisitorTest, PatternComprehension) {
+  auto &ast_generator = *GetParam();
+  {
+    const auto *query =
+        dynamic_cast<CypherQuery *>(ast_generator.ParseQuery("MATCH (n) RETURN [(n)-->(b) | b.val] AS res;"));
+    const auto *ret = dynamic_cast<Return *>(query->single_query_->clauses_[1]);
+
+    const auto *pc = dynamic_cast<PatternComprehension *>(ret->body_.named_expressions[0]->expression_);
+    ASSERT_TRUE(pc);
+
+    // Check for variable_
+    EXPECT_EQ(pc->variable_, nullptr);
+
+    // Check for pattern_
+    const auto pattern = pc->pattern_;
+    ASSERT_TRUE(pattern->atoms_.size() == 3);
+
+    const auto *node1 = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
+    const auto *edge = dynamic_cast<EdgeAtom *>(pattern->atoms_[1]);
+    const auto *node2 = dynamic_cast<NodeAtom *>(pattern->atoms_[2]);
+
+    ASSERT_TRUE(node1);
+    ASSERT_TRUE(edge);
+    ASSERT_TRUE(node2);
+
+    // Check for filter_
+    EXPECT_EQ(pc->filter_, nullptr);
+
+    // Check for resultExpr_
+    const auto *result_expr = pc->resultExpr_;
+    ASSERT_TRUE(result_expr);
+  }
+  {
+    const auto *query = dynamic_cast<CypherQuery *>(
+        ast_generator.ParseQuery("MATCH (n) RETURN [(n)-->(b) WHERE b.id=1 | b.val] AS res;"));
+    const auto *ret = dynamic_cast<Return *>(query->single_query_->clauses_[1]);
+
+    const auto *pc = dynamic_cast<PatternComprehension *>(ret->body_.named_expressions[0]->expression_);
+    ASSERT_TRUE(pc);
+
+    // Check for variable_
+    EXPECT_EQ(pc->variable_, nullptr);
+
+    // Check for pattern_
+    const auto pattern = pc->pattern_;
+    ASSERT_TRUE(pattern->atoms_.size() == 3);
+
+    const auto *node1 = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
+    const auto *edge = dynamic_cast<EdgeAtom *>(pattern->atoms_[1]);
+    const auto *node2 = dynamic_cast<NodeAtom *>(pattern->atoms_[2]);
+
+    ASSERT_TRUE(node1);
+    ASSERT_TRUE(edge);
+    ASSERT_TRUE(node2);
+
+    // Check for filter_
+    const auto *filter = pc->filter_;
+    ASSERT_TRUE(filter);
+    ASSERT_TRUE(filter->expression_);
+
+    // Check for resultExpr_
+    const auto *result_expr = pc->resultExpr_;
+    ASSERT_TRUE(result_expr);
+  }
+  {
+    const auto *query = dynamic_cast<CypherQuery *>(
+        ast_generator.ParseQuery("MATCH (n) RETURN [p = (n)-->(b) WHERE b.id=1 | b.val] AS res;"));
+    const auto *ret = dynamic_cast<Return *>(query->single_query_->clauses_[1]);
+
+    const auto *pc = dynamic_cast<PatternComprehension *>(ret->body_.named_expressions[0]->expression_);
+    ASSERT_TRUE(pc);
+
+    // Check for variable_
+    ASSERT_TRUE(pc->variable_);
+
+    // Check for pattern_
+    const auto pattern = pc->pattern_;
+    ASSERT_TRUE(pattern->atoms_.size() == 3);
+
+    const auto *node1 = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
+    const auto *edge = dynamic_cast<EdgeAtom *>(pattern->atoms_[1]);
+    const auto *node2 = dynamic_cast<NodeAtom *>(pattern->atoms_[2]);
+
+    ASSERT_TRUE(node1);
+    ASSERT_TRUE(edge);
+    ASSERT_TRUE(node2);
+
+    // Check for filter_
+    const auto *filter = pc->filter_;
+    ASSERT_TRUE(filter);
+    ASSERT_TRUE(filter->expression_);
+
+    // Check for resultExpr_
+    const auto *result_expr = pc->resultExpr_;
+    ASSERT_TRUE(result_expr);
+  }
+}
diff --git a/tests/unit/dbms_database.cpp b/tests/unit/dbms_database.cpp
index 535c0c055..0fded2324 100644
--- a/tests/unit/dbms_database.cpp
+++ b/tests/unit/dbms_database.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
diff --git a/tests/unit/query_common.hpp b/tests/unit/query_common.hpp
index a14ef2d30..c18e06abf 100644
--- a/tests/unit/query_common.hpp
+++ b/tests/unit/query_common.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -635,3 +635,5 @@ auto GetForeach(AstStorage &storage, NamedExpression *named_expr, const std::vec
 #define DROP_USER(usernames) storage.Create<memgraph::query::DropUser>((usernames))
 #define CALL_PROCEDURE(...) memgraph::query::test_common::GetCallProcedure(storage, __VA_ARGS__)
 #define CALL_SUBQUERY(...) memgraph::query::test_common::GetCallSubquery(this->storage, __VA_ARGS__)
+#define PATTERN_COMPREHENSION(variable, pattern, filter, resultExpr) \
+  this->storage.template Create<memgraph::query::PatternComprehension>(variable, pattern, filter, resultExpr)
diff --git a/tests/unit/query_plan.cpp b/tests/unit/query_plan.cpp
index bc4b2660c..5b574c1ff 100644
--- a/tests/unit/query_plan.cpp
+++ b/tests/unit/query_plan.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -808,13 +808,68 @@ TYPED_TEST(TestPlanner, MatchWhereBeforeExpand) {
   CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectFilter(), ExpectExpand(), ExpectProduce());
 }
 
+TYPED_TEST(TestPlanner, MatchEdgeTypeIndex) {
+  FakeDbAccessor dba;
+  auto indexed_edge_type = dba.EdgeType("indexed_edgetype");
+  dba.SetIndexCount(indexed_edge_type, 1);
+  {
+    // Test MATCH ()-[r:indexed_edgetype]->() RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(PATTERN(NODE("anon1"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"indexed_edgetype"}),
+                      NODE("anon2"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAllByEdgeType(), ExpectProduce());
+  }
+  {
+    // Test MATCH (a)-[r:indexed_edgetype]->() RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(PATTERN(NODE("a"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"indexed_edgetype"}),
+                      NODE("anon2"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectExpand(), ExpectProduce());
+  }
+  {
+    // Test MATCH ()-[r:indexed_edgetype]->(b) RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(PATTERN(NODE("anon1"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"indexed_edgetype"}),
+                      NODE("b"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectExpand(), ExpectProduce());
+  }
+  {
+    // Test MATCH (a)-[r:indexed_edgetype]->(b) RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(
+            PATTERN(NODE("a"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"indexed_edgetype"}), NODE("b"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectExpand(), ExpectProduce());
+  }
+  {
+    // Test MATCH ()-[r:not_indexed_edgetype]->() RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(PATTERN(NODE("anon1"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"not_indexed_edgetype"}),
+                      NODE("anon2"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectExpand(), ExpectProduce());
+  }
+}
+
 TYPED_TEST(TestPlanner, MatchFilterPropIsNotNull) {
   FakeDbAccessor dba;
   auto label = dba.Label("label");
   auto prop = PROPERTY_PAIR(dba, "prop");
   dba.SetIndexCount(label, 1);
   dba.SetIndexCount(label, prop.second, 1);
-
   {
     // Test MATCH (n :label) -[r]- (m) WHERE n.prop IS NOT NULL RETURN n
     auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n", "label"), EDGE("r"), NODE("m"))),
diff --git a/tests/unit/query_plan_checker.hpp b/tests/unit/query_plan_checker.hpp
index 92089eb82..6eef3841a 100644
--- a/tests/unit/query_plan_checker.hpp
+++ b/tests/unit/query_plan_checker.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -65,6 +65,7 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor {
   PRE_VISIT(ScanAllByLabelPropertyValue);
   PRE_VISIT(ScanAllByLabelPropertyRange);
   PRE_VISIT(ScanAllByLabelProperty);
+  PRE_VISIT(ScanAllByEdgeType);
   PRE_VISIT(ScanAllById);
   PRE_VISIT(Expand);
   PRE_VISIT(ExpandVariable);
@@ -170,6 +171,7 @@ using ExpectCreateExpand = OpChecker<CreateExpand>;
 using ExpectDelete = OpChecker<Delete>;
 using ExpectScanAll = OpChecker<ScanAll>;
 using ExpectScanAllByLabel = OpChecker<ScanAllByLabel>;
+using ExpectScanAllByEdgeType = OpChecker<ScanAllByEdgeType>;
 using ExpectScanAllById = OpChecker<ScanAllById>;
 using ExpectExpand = OpChecker<Expand>;
 using ExpectConstructNamedPath = OpChecker<ConstructNamedPath>;
@@ -560,6 +562,12 @@ class FakeDbAccessor {
     return 0;
   }
 
+  int64_t EdgesCount(memgraph::storage::EdgeTypeId edge_type) const {
+    auto found = edge_type_index_.find(edge_type);
+    if (found != edge_type_index_.end()) return found->second;
+    return 0;
+  }
+
   bool LabelIndexExists(memgraph::storage::LabelId label) const {
     return label_index_.find(label) != label_index_.end();
   }
@@ -573,6 +581,10 @@ class FakeDbAccessor {
     return false;
   }
 
+  bool EdgeTypeIndexExists(memgraph::storage::EdgeTypeId edge_type) const {
+    return edge_type_index_.find(edge_type) != edge_type_index_.end();
+  }
+
   std::optional<memgraph::storage::LabelPropertyIndexStats> GetIndexStats(
       const memgraph::storage::LabelId label, const memgraph::storage::PropertyId property) const {
     return memgraph::storage::LabelPropertyIndexStats{.statistic = 0, .avg_group_size = 1};  // unique id
@@ -594,6 +606,8 @@ class FakeDbAccessor {
     label_property_index_.emplace_back(label, property, count);
   }
 
+  void SetIndexCount(memgraph::storage::EdgeTypeId edge_type, int64_t count) { edge_type_index_[edge_type] = count; }
+
   memgraph::storage::LabelId NameToLabel(const std::string &name) {
     auto found = labels_.find(name);
     if (found != labels_.end()) return found->second;
@@ -608,6 +622,8 @@ class FakeDbAccessor {
     return edge_types_.emplace(name, memgraph::storage::EdgeTypeId::FromUint(edge_types_.size())).first->second;
   }
 
+  memgraph::storage::EdgeTypeId EdgeType(const std::string &name) { return NameToEdgeType(name); }
+
   memgraph::storage::PropertyId NameToProperty(const std::string &name) {
     auto found = properties_.find(name);
     if (found != properties_.end()) return found->second;
@@ -632,6 +648,7 @@ class FakeDbAccessor {
 
   std::unordered_map<memgraph::storage::LabelId, int64_t> label_index_;
   std::vector<std::tuple<memgraph::storage::LabelId, memgraph::storage::PropertyId, int64_t>> label_property_index_;
+  std::unordered_map<memgraph::storage::EdgeTypeId, int64_t> edge_type_index_;
 };
 
 }  // namespace memgraph::query::plan
diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp
index c4bb966eb..50a52c828 100644
--- a/tests/unit/query_semantic.cpp
+++ b/tests/unit/query_semantic.cpp
@@ -1442,3 +1442,27 @@ TYPED_TEST(TestSymbolGenerator, PropertyCachingMixedLookups2) {
   ASSERT_TRUE(prop3_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
   ASSERT_TRUE(prop4_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
 }
+
+TYPED_TEST(TestSymbolGenerator, PatternComprehension) {
+  auto prop = this->dba.NameToProperty("prop");
+
+  // MATCH (n) RETURN [(n)-[edge]->(m) | m.prop] AS alias
+  auto query = QUERY(SINGLE_QUERY(
+      MATCH(PATTERN(NODE("n"))),
+      RETURN(NEXPR("alias", PATTERN_COMPREHENSION(nullptr,
+                                                  PATTERN(NODE("n"), EDGE("edge", EdgeAtom::Direction::BOTH, {}, false),
+                                                          NODE("m", std::nullopt, false)),
+                                                  nullptr, PROPERTY_LOOKUP(this->dba, "m", prop))))));
+
+  auto symbol_table = MakeSymbolTable(query);
+  ASSERT_EQ(symbol_table.max_position(), 7);
+
+  memgraph::query::plan::UsedSymbolsCollector collector(symbol_table);
+  auto *ret = dynamic_cast<Return *>(query->single_query_->clauses_[1]);
+  auto *pc = dynamic_cast<PatternComprehension *>(ret->body_.named_expressions[0]->expression_);
+
+  pc->Accept(collector);
+
+  // n, edge, m, Path
+  ASSERT_EQ(collector.symbols_.size(), 4);
+}
diff --git a/tests/unit/raft_log_serialization.cpp b/tests/unit/raft_log_serialization.cpp
new file mode 100644
index 000000000..8550cf5b8
--- /dev/null
+++ b/tests/unit/raft_log_serialization.cpp
@@ -0,0 +1,151 @@
+// Copyright 2024 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include "coordination/coordinator_config.hpp"
+#include "nuraft/coordinator_state_machine.hpp"
+#include "nuraft/raft_log_action.hpp"
+#include "utils/file.hpp"
+#include "utils/uuid.hpp"
+
+#include <gflags/gflags.h>
+#include <gtest/gtest.h>
+#include "json/json.hpp"
+
+using memgraph::coordination::CoordinatorClientConfig;
+using memgraph::coordination::CoordinatorStateMachine;
+using memgraph::coordination::RaftLogAction;
+using memgraph::coordination::ReplClientInfo;
+using memgraph::replication_coordination_glue::ReplicationMode;
+using memgraph::utils::UUID;
+
+class RaftLogSerialization : public ::testing::Test {
+ protected:
+  void SetUp() override {}
+
+  void TearDown() override {}
+
+  std::filesystem::path test_folder_{std::filesystem::temp_directory_path() / "MG_tests_unit_raft_log_serialization"};
+};
+
+TEST_F(RaftLogSerialization, ReplClientInfo) {
+  ReplClientInfo info{"instance_name", ReplicationMode::SYNC, "127.0.0.1", 10111};
+
+  nlohmann::json j = info;
+  ReplClientInfo info2 = j.get<memgraph::coordination::ReplClientInfo>();
+
+  ASSERT_EQ(info, info2);
+}
+
+TEST_F(RaftLogSerialization, CoordinatorClientConfig) {
+  CoordinatorClientConfig config{"instance3",
+                                 "127.0.0.1",
+                                 10112,
+                                 std::chrono::seconds{1},
+                                 std::chrono::seconds{5},
+                                 std::chrono::seconds{10},
+                                 {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10001},
+                                 .ssl = std::nullopt};
+
+  nlohmann::json j = config;
+  CoordinatorClientConfig config2 = j.get<memgraph::coordination::CoordinatorClientConfig>();
+
+  ASSERT_EQ(config, config2);
+}
+
+TEST_F(RaftLogSerialization, RaftLogActionRegister) {
+  auto action = RaftLogAction::REGISTER_REPLICATION_INSTANCE;
+
+  nlohmann::json j = action;
+  RaftLogAction action2 = j.get<memgraph::coordination::RaftLogAction>();
+
+  ASSERT_EQ(action, action2);
+}
+
+TEST_F(RaftLogSerialization, RaftLogActionUnregister) {
+  auto action = RaftLogAction::UNREGISTER_REPLICATION_INSTANCE;
+
+  nlohmann::json j = action;
+  RaftLogAction action2 = j.get<memgraph::coordination::RaftLogAction>();
+
+  ASSERT_EQ(action, action2);
+}
+
+TEST_F(RaftLogSerialization, RaftLogActionPromote) {
+  auto action = RaftLogAction::SET_INSTANCE_AS_MAIN;
+
+  nlohmann::json j = action;
+  RaftLogAction action2 = j.get<memgraph::coordination::RaftLogAction>();
+
+  ASSERT_EQ(action, action2);
+}
+
+TEST_F(RaftLogSerialization, RaftLogActionDemote) {
+  auto action = RaftLogAction::SET_INSTANCE_AS_REPLICA;
+
+  nlohmann::json j = action;
+  RaftLogAction action2 = j.get<memgraph::coordination::RaftLogAction>();
+
+  ASSERT_EQ(action, action2);
+}
+
+TEST_F(RaftLogSerialization, RaftLogActionUpdateUUID) {
+  auto action = RaftLogAction::UPDATE_UUID;
+
+  nlohmann::json j = action;
+  RaftLogAction action2 = j.get<memgraph::coordination::RaftLogAction>();
+
+  ASSERT_EQ(action, action2);
+}
+
+TEST_F(RaftLogSerialization, RegisterInstance) {
+  CoordinatorClientConfig config{"instance3",
+                                 "127.0.0.1",
+                                 10112,
+                                 std::chrono::seconds{1},
+                                 std::chrono::seconds{5},
+                                 std::chrono::seconds{10},
+                                 {"instance_name", ReplicationMode::ASYNC, "replication_ip_address", 10001},
+                                 .ssl = std::nullopt};
+
+  auto buffer = CoordinatorStateMachine::SerializeRegisterInstance(config);
+  auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+  ASSERT_EQ(action, RaftLogAction::REGISTER_REPLICATION_INSTANCE);
+  ASSERT_EQ(config, std::get<CoordinatorClientConfig>(payload));
+}
+
+TEST_F(RaftLogSerialization, UnregisterInstance) {
+  auto buffer = CoordinatorStateMachine::SerializeUnregisterInstance("instance3");
+  auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+  ASSERT_EQ(action, RaftLogAction::UNREGISTER_REPLICATION_INSTANCE);
+  ASSERT_EQ("instance3", std::get<std::string>(payload));
+}
+
+TEST_F(RaftLogSerialization, SetInstanceAsMain) {
+  auto buffer = CoordinatorStateMachine::SerializeSetInstanceAsMain("instance3");
+  auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+  ASSERT_EQ(action, RaftLogAction::SET_INSTANCE_AS_MAIN);
+  ASSERT_EQ("instance3", std::get<std::string>(payload));
+}
+
+TEST_F(RaftLogSerialization, SetInstanceAsReplica) {
+  auto buffer = CoordinatorStateMachine::SerializeSetInstanceAsReplica("instance3");
+  auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+  ASSERT_EQ(action, RaftLogAction::SET_INSTANCE_AS_REPLICA);
+  ASSERT_EQ("instance3", std::get<std::string>(payload));
+}
+
+TEST_F(RaftLogSerialization, UpdateUUID) {
+  UUID uuid;
+  auto buffer = CoordinatorStateMachine::SerializeUpdateUUID(uuid);
+  auto [payload, action] = CoordinatorStateMachine::DecodeLog(*buffer);
+  ASSERT_EQ(action, RaftLogAction::UPDATE_UUID);
+  ASSERT_EQ(uuid, std::get<UUID>(payload));
+}
diff --git a/tests/unit/storage_v2_decoder_encoder.cpp b/tests/unit/storage_v2_decoder_encoder.cpp
index 9b627cb77..15db49b1c 100644
--- a/tests/unit/storage_v2_decoder_encoder.cpp
+++ b/tests/unit/storage_v2_decoder_encoder.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -337,6 +337,7 @@ TEST_F(DecoderEncoderTest, PropertyValueInvalidMarker) {
         case memgraph::storage::durability::Marker::SECTION_CONSTRAINTS:
         case memgraph::storage::durability::Marker::SECTION_DELTA:
         case memgraph::storage::durability::Marker::SECTION_EPOCH_HISTORY:
+        case memgraph::storage::durability::Marker::SECTION_EDGE_INDICES:
         case memgraph::storage::durability::Marker::SECTION_OFFSETS:
         case memgraph::storage::durability::Marker::DELTA_VERTEX_CREATE:
         case memgraph::storage::durability::Marker::DELTA_VERTEX_DELETE:
@@ -355,6 +356,8 @@ TEST_F(DecoderEncoderTest, PropertyValueInvalidMarker) {
         case memgraph::storage::durability::Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
         case memgraph::storage::durability::Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_SET:
         case memgraph::storage::durability::Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR:
+        case memgraph::storage::durability::Marker::DELTA_EDGE_TYPE_INDEX_CREATE:
+        case memgraph::storage::durability::Marker::DELTA_EDGE_TYPE_INDEX_DROP:
         case memgraph::storage::durability::Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
         case memgraph::storage::durability::Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
         case memgraph::storage::durability::Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
diff --git a/tests/unit/storage_v2_durability_inmemory.cpp b/tests/unit/storage_v2_durability_inmemory.cpp
index 54671077f..7794f2ab9 100644
--- a/tests/unit/storage_v2_durability_inmemory.cpp
+++ b/tests/unit/storage_v2_durability_inmemory.cpp
@@ -69,6 +69,7 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
     ONLY_EXTENDED,
     ONLY_EXTENDED_WITH_BASE_INDICES_AND_CONSTRAINTS,
     BASE_WITH_EXTENDED,
+    BASE_WITH_EDGE_TYPE_INDEXED,
   };
 
  public:
@@ -270,6 +271,15 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
     if (single_transaction) ASSERT_FALSE(acc->Commit().HasError());
   }
 
+  void CreateEdgeIndex(memgraph::storage::Storage *store, memgraph::storage::EdgeTypeId edge_type) {
+    {
+      // Create edge-type index.
+      auto unique_acc = store->UniqueAccess(ReplicationRole::MAIN);
+      ASSERT_FALSE(unique_acc->CreateIndex(edge_type).HasError());
+      ASSERT_FALSE(unique_acc->Commit().HasError());
+    }
+  }
+
   void VerifyDataset(memgraph::storage::Storage *store, DatasetType type, bool properties_on_edges,
                      bool verify_info = true) {
     auto base_label_indexed = store->NameToLabel("base_indexed");
@@ -310,13 +320,19 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
                       UnorderedElementsAre(std::make_pair(base_label_indexed, property_id),
                                            std::make_pair(extended_label_indexed, property_count)));
           break;
+        case DatasetType::BASE_WITH_EDGE_TYPE_INDEXED:
+          ASSERT_THAT(info.label, UnorderedElementsAre(base_label_unindexed));
+          ASSERT_THAT(info.label_property, UnorderedElementsAre(std::make_pair(base_label_indexed, property_id)));
+          ASSERT_THAT(info.edge_type, UnorderedElementsAre(et1));
+          break;
       }
     }
 
     // Verify index statistics
     {
       switch (type) {
-        case DatasetType::ONLY_BASE: {
+        case DatasetType::ONLY_BASE:
+        case DatasetType::BASE_WITH_EDGE_TYPE_INDEXED: {
           const auto l_stats = acc->GetIndexStats(base_label_unindexed);
           ASSERT_TRUE(l_stats);
           ASSERT_EQ(l_stats->count, 1);
@@ -379,6 +395,7 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
       auto info = acc->ListAllConstraints();
       switch (type) {
         case DatasetType::ONLY_BASE:
+        case DatasetType::BASE_WITH_EDGE_TYPE_INDEXED:
           ASSERT_THAT(info.existence, UnorderedElementsAre(std::make_pair(base_label_unindexed, property_id)));
           ASSERT_THAT(info.unique, UnorderedElementsAre(
                                        std::make_pair(base_label_unindexed, std::set{property_id, property_extra})));
@@ -402,6 +419,7 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
 
     bool have_base_dataset = false;
     bool have_extended_dataset = false;
+    bool have_edge_type_indexed_dataset = false;
     switch (type) {
       case DatasetType::ONLY_BASE:
       case DatasetType::ONLY_BASE_WITH_EXTENDED_INDICES_AND_CONSTRAINTS:
@@ -415,6 +433,9 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
         have_base_dataset = true;
         have_extended_dataset = true;
         break;
+      case DatasetType::BASE_WITH_EDGE_TYPE_INDEXED:
+        have_base_dataset = true;
+        have_edge_type_indexed_dataset = true;
     }
 
     // Verify base dataset.
@@ -675,6 +696,19 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
       }
     }
 
+    if (have_edge_type_indexed_dataset) {
+      MG_ASSERT(properties_on_edges, "Edge-type indexing needs --properties-on-edges!");
+      // Verify edge-type indices.
+      {
+        std::vector<memgraph::storage::EdgeAccessor> edges;
+        edges.reserve(kNumBaseEdges / 2);
+        for (auto edge : acc->Edges(et1, memgraph::storage::View::OLD)) {
+          edges.push_back(edge);
+        }
+        ASSERT_EQ(edges.size(), kNumBaseEdges / 2);
+      }
+    }
+
     if (verify_info) {
       auto info = store->GetBaseInfo();
       if (have_base_dataset) {
@@ -2972,3 +3006,42 @@ TEST_P(DurabilityTest, ConstraintsRecoveryFunctionSetting) {
           &variant_existence_constraint_creation_func);
   MG_ASSERT(pval_existence, "Chose wrong type of function for recovery of existence constraint data");
 }
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_P(DurabilityTest, EdgeTypeIndexRecovered) {
+  if (GetParam() == false) {
+    return;
+  }
+  // Create snapshot.
+  {
+    memgraph::storage::Config config{.salient.items = {.properties_on_edges = GetParam()},
+                                     .durability = {.storage_directory = storage_directory, .snapshot_on_exit = true}};
+    memgraph::replication::ReplicationState repl_state{memgraph::storage::ReplicationStateRootPath(config)};
+    memgraph::dbms::Database db{config, repl_state};
+    CreateBaseDataset(db.storage(), GetParam());
+    VerifyDataset(db.storage(), DatasetType::ONLY_BASE, GetParam());
+    CreateEdgeIndex(db.storage(), db.storage()->NameToEdgeType("base_et1"));
+    VerifyDataset(db.storage(), DatasetType::BASE_WITH_EDGE_TYPE_INDEXED, GetParam());
+  }
+
+  ASSERT_EQ(GetSnapshotsList().size(), 1);
+  ASSERT_EQ(GetBackupSnapshotsList().size(), 0);
+  ASSERT_EQ(GetWalsList().size(), 0);
+  ASSERT_EQ(GetBackupWalsList().size(), 0);
+
+  // Recover snapshot.
+  memgraph::storage::Config config{.salient.items = {.properties_on_edges = GetParam()},
+                                   .durability = {.storage_directory = storage_directory, .recover_on_startup = true}};
+  memgraph::replication::ReplicationState repl_state{memgraph::storage::ReplicationStateRootPath(config)};
+  memgraph::dbms::Database db{config, repl_state};
+  VerifyDataset(db.storage(), DatasetType::BASE_WITH_EDGE_TYPE_INDEXED, GetParam());
+
+  // Try to use the storage.
+  {
+    auto acc = db.Access();
+    auto vertex = acc->CreateVertex();
+    auto edge = acc->CreateEdge(&vertex, &vertex, db.storage()->NameToEdgeType("et"));
+    ASSERT_TRUE(edge.HasValue());
+    ASSERT_FALSE(acc->Commit().HasError());
+  }
+}
diff --git a/tests/unit/storage_v2_indices.cpp b/tests/unit/storage_v2_indices.cpp
index 8ee053087..23c82313d 100644
--- a/tests/unit/storage_v2_indices.cpp
+++ b/tests/unit/storage_v2_indices.cpp
@@ -44,6 +44,8 @@ class IndexTest : public testing::Test {
     this->prop_val = acc->NameToProperty("val");
     this->label1 = acc->NameToLabel("label1");
     this->label2 = acc->NameToLabel("label2");
+    this->edge_type_id1 = acc->NameToEdgeType("edge_type_1");
+    this->edge_type_id2 = acc->NameToEdgeType("edge_type_2");
     vertex_id = 0;
   }
 
@@ -61,6 +63,8 @@ class IndexTest : public testing::Test {
   PropertyId prop_val;
   LabelId label1;
   LabelId label2;
+  EdgeTypeId edge_type_id1;
+  EdgeTypeId edge_type_id2;
 
   VertexAccessor CreateVertex(Storage::Accessor *accessor) {
     VertexAccessor vertex = accessor->CreateVertex();
@@ -68,11 +72,23 @@ class IndexTest : public testing::Test {
     return vertex;
   }
 
+  VertexAccessor CreateVertexWithoutProperties(Storage::Accessor *accessor) {
+    VertexAccessor vertex = accessor->CreateVertex();
+    return vertex;
+  }
+
+  EdgeAccessor CreateEdge(VertexAccessor *from, VertexAccessor *to, EdgeTypeId edge_type, Storage::Accessor *accessor) {
+    auto edge = accessor->CreateEdge(from, to, edge_type);
+    MG_ASSERT(!edge.HasError());
+    MG_ASSERT(!edge->SetProperty(this->prop_id, PropertyValue(vertex_id++)).HasError());
+    return edge.GetValue();
+  }
+
   template <class TIterable>
   std::vector<int64_t> GetIds(TIterable iterable, View view = View::OLD) {
     std::vector<int64_t> ret;
-    for (auto vertex : iterable) {
-      ret.push_back(vertex.GetProperty(this->prop_id, view)->ValueInt());
+    for (auto item : iterable) {
+      ret.push_back(item.GetProperty(this->prop_id, view)->ValueInt());
     }
     return ret;
   }
@@ -1292,3 +1308,368 @@ TYPED_TEST(IndexTest, LabelPropertyIndexClearOldDataFromDisk) {
     ASSERT_EQ(disk_test_utils::GetRealNumberOfEntriesInRocksDB(tx_db), 1);
   }
 }
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexCreate) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_FALSE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_EQ(acc->ListAllIndices().edge_type.size(), 0);
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 0; i < 10; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 10; i < 20; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+      acc->AdvanceCommand();
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+      acc->Abort();
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 10; i < 20; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+      acc->AdvanceCommand();
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+      acc->AdvanceCommand();
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexDrop) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_FALSE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_EQ(acc->ListAllIndices().edge_type.size(), 0);
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 0; i < 10; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->DropIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_FALSE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_EQ(acc->ListAllIndices().label.size(), 0);
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_TRUE(unique_acc->DropIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_FALSE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_EQ(acc->ListAllIndices().label.size(), 0);
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 10; i < 20; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_TRUE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_THAT(acc->ListAllIndices().edge_type, UnorderedElementsAre(this->edge_type_id1));
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+      acc->AdvanceCommand();
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+    }
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexBasic) {
+  // The following steps are performed and index correctness is validated after
+  // each step:
+  // 1. Create 10 edges numbered from 0 to 9.
+  // 2. Add EdgeType1 to odd numbered, and EdgeType2 to even numbered edges.
+  // 3. Delete even numbered edges.
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id2).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    auto acc = this->storage->Access(ReplicationRole::MAIN);
+    EXPECT_THAT(acc->ListAllIndices().edge_type, UnorderedElementsAre(this->edge_type_id1, this->edge_type_id2));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW), IsEmpty());
+
+    for (int i = 0; i < 10; ++i) {
+      auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+      auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+      this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+    }
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 2, 4, 6, 8));
+
+    acc->AdvanceCommand();
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD),
+                UnorderedElementsAre(0, 2, 4, 6, 8));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 2, 4, 6, 8));
+
+    for (auto vertex : acc->Vertices(View::OLD)) {
+      auto edges = vertex.OutEdges(View::OLD)->edges;
+      for (auto &edge : edges) {
+        int64_t id = edge.GetProperty(this->prop_id, View::OLD)->ValueInt();
+        if (id % 2 == 0) {
+          ASSERT_NO_ERROR(acc->DetachDelete({}, {&edge}, false));
+        }
+      }
+    }
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD),
+                UnorderedElementsAre(0, 2, 4, 6, 8));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW), IsEmpty());
+
+    acc->AdvanceCommand();
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW), IsEmpty());
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexTransactionalIsolation) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    // Check that transactions only see entries they are supposed to see.
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id2).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    auto acc_before = this->storage->Access(ReplicationRole::MAIN);
+    auto acc = this->storage->Access(ReplicationRole::MAIN);
+    auto acc_after = this->storage->Access(ReplicationRole::MAIN);
+
+    for (int i = 0; i < 5; ++i) {
+      auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+      auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+      this->CreateEdge(&vertex_from, &vertex_to, this->edge_type_id1, acc.get());
+    }
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 1, 2, 3, 4));
+
+    EXPECT_THAT(this->GetIds(acc_before->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+
+    EXPECT_THAT(this->GetIds(acc_after->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+
+    ASSERT_NO_ERROR(acc->Commit());
+
+    auto acc_after_commit = this->storage->Access(ReplicationRole::MAIN);
+
+    EXPECT_THAT(this->GetIds(acc_before->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+
+    EXPECT_THAT(this->GetIds(acc_after->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+
+    EXPECT_THAT(this->GetIds(acc_after_commit->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 1, 2, 3, 4));
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexCountEstimate) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id2).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    auto acc = this->storage->Access(ReplicationRole::MAIN);
+    for (int i = 0; i < 20; ++i) {
+      auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+      auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+      this->CreateEdge(&vertex_from, &vertex_to, i % 3 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+    }
+
+    EXPECT_EQ(acc->ApproximateEdgeCount(this->edge_type_id1), 13);
+    EXPECT_EQ(acc->ApproximateEdgeCount(this->edge_type_id2), 7);
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexRepeatingEdgeTypesBetweenSameVertices) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    auto acc = this->storage->Access(ReplicationRole::MAIN);
+    auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+    auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+
+    for (int i = 0; i < 5; ++i) {
+      this->CreateEdge(&vertex_from, &vertex_to, this->edge_type_id1, acc.get());
+    }
+
+    EXPECT_EQ(acc->ApproximateEdgeCount(this->edge_type_id1), 5);
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 1, 2, 3, 4));
+  }
+}
diff --git a/tests/unit/storage_v2_wal_file.cpp b/tests/unit/storage_v2_wal_file.cpp
index 07a35d754..dcb7d3326 100644
--- a/tests/unit/storage_v2_wal_file.cpp
+++ b/tests/unit/storage_v2_wal_file.cpp
@@ -37,6 +37,10 @@ memgraph::storage::durability::WalDeltaData::Type StorageMetadataOperationToWalD
       return memgraph::storage::durability::WalDeltaData::Type::LABEL_INDEX_CREATE;
     case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_DROP:
       return memgraph::storage::durability::WalDeltaData::Type::LABEL_INDEX_DROP;
+    case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+      return memgraph::storage::durability::WalDeltaData::Type::EDGE_INDEX_CREATE;
+    case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_DROP:
+      return memgraph::storage::durability::WalDeltaData::Type::EDGE_INDEX_DROP;
     case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_STATS_SET:
       return memgraph::storage::durability::WalDeltaData::Type::LABEL_INDEX_STATS_SET;
     case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_STATS_CLEAR:
@@ -280,6 +284,41 @@ class DeltaGenerator final {
         case memgraph::storage::durability::StorageMetadataOperation::UNIQUE_CONSTRAINT_DROP:
           data.operation_label_properties.label = label;
           data.operation_label_properties.properties = properties;
+          break;
+        case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_DROP:
+          MG_ASSERT(false, "Invalid function call!");
+      }
+      data_.emplace_back(timestamp_, data);
+    }
+  }
+
+  void AppendEdgeTypeOperation(memgraph::storage::durability::StorageMetadataOperation operation,
+                               const std::string &edge_type) {
+    auto edge_type_id = memgraph::storage::EdgeTypeId::FromUint(mapper_.NameToId(edge_type));
+    wal_file_.AppendOperation(operation, edge_type_id, timestamp_);
+    if (valid_) {
+      UpdateStats(timestamp_, 1);
+      memgraph::storage::durability::WalDeltaData data;
+      data.type = StorageMetadataOperationToWalDeltaDataType(operation);
+      switch (operation) {
+        case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_DROP:
+          data.operation_edge_type.edge_type = edge_type;
+          break;
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_DROP:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_STATS_CLEAR:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_CLEAR:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_STATS_SET:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_DROP:
+        case memgraph::storage::durability::StorageMetadataOperation::EXISTENCE_CONSTRAINT_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::EXISTENCE_CONSTRAINT_DROP:;
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_SET:
+        case memgraph::storage::durability::StorageMetadataOperation::UNIQUE_CONSTRAINT_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::UNIQUE_CONSTRAINT_DROP:
+          MG_ASSERT(false, "Invalid function call!");
       }
       data_.emplace_back(timestamp_, data);
     }