diff --git a/src/storage/v2/name_id_mapper.hpp b/src/storage/v2/name_id_mapper.hpp
new file mode 100644
index 000000000..e447213ef
--- /dev/null
+++ b/src/storage/v2/name_id_mapper.hpp
@@ -0,0 +1,81 @@
+#pragma once
+
+#include <atomic>
+#include <string>
+
+#include "utils/skip_list.hpp"
+
+namespace storage {
+
+class NameIdMapper final {
+ private:
+  struct MapNameToId {
+    std::string name;
+    uint64_t id;
+
+    bool operator<(const MapNameToId &other) { return name < other.name; }
+    bool operator==(const MapNameToId &other) { return name == other.name; }
+
+    bool operator<(const std::string &other) { return name < other; }
+    bool operator==(const std::string &other) { return name == other; }
+  };
+
+  struct MapIdToName {
+    uint64_t id;
+    std::string name;
+
+    bool operator<(const MapIdToName &other) { return id < other.id; }
+    bool operator==(const MapIdToName &other) { return id == other.id; }
+
+    bool operator<(uint64_t other) { return id < other; }
+    bool operator==(uint64_t other) { return id == other; }
+  };
+
+ public:
+  uint64_t NameToId(const std::string &name) {
+    auto name_to_id_acc = name_to_id_.access();
+    auto found = name_to_id_acc.find(name);
+    uint64_t id;
+    if (found == name_to_id_acc.end()) {
+      uint64_t new_id = counter_.fetch_add(1, std::memory_order_acq_rel);
+      // Try to insert the mapping with the `new_id`, but use the id that is in
+      // the object itself. The object that cointains the mapping is designed to
+      // be a map, so that if the inserted name already exists `insert` will
+      // return an iterator to the existing item. This prevents assignment of
+      // two IDs to the same name when the mapping is being inserted
+      // concurrently from two threads. One ID is wasted in that case, though.
+      id = name_to_id_acc.insert({name, new_id}).first->id;
+    } else {
+      id = found->id;
+    }
+    auto id_to_name_acc = id_to_name_.access();
+    // We have to try to insert the ID to name mapping even if we are not the
+    // one who assigned the ID because we have to make sure that after this
+    // method returns that both mappings exist.
+    id_to_name_acc.insert({id, name});
+    return id;
+  }
+
+  // NOTE: Currently this function returns a `const std::string &` instead of a
+  // `std::string` to avoid making unnecessary copies of the string.
+  // Usually, this wouldn't be correct because the accessor to the
+  // `utils::SkipList` is destroyed in this function and that removes the
+  // guarantee that the reference to the value contained in the list will be
+  // valid.
+  // Currently, we never delete anything from the `utils::SkipList` so the
+  // references will always be valid. If you change this class to remove unused
+  // names, be sure to change the signature of this function.
+  const std::string &IdToName(uint64_t id) {
+    auto id_to_name_acc = id_to_name_.access();
+    auto result = id_to_name_acc.find(id);
+    CHECK(result != id_to_name_acc.end())
+        << "Trying to get a name for an invalid ID!";
+    return result->name;
+  }
+
+ private:
+  std::atomic<uint64_t> counter_{0};
+  utils::SkipList<MapNameToId> name_to_id_;
+  utils::SkipList<MapIdToName> id_to_name_;
+};
+}  // namespace storage
diff --git a/src/storage/v2/storage.cpp b/src/storage/v2/storage.cpp
index fe2130b7b..f711fa029 100644
--- a/src/storage/v2/storage.cpp
+++ b/src/storage/v2/storage.cpp
@@ -274,6 +274,26 @@ Result<bool> Storage::Accessor::DeleteEdge(EdgeAccessor *edge) {
   return Result<bool>{true};
 }
 
+const std::string &Storage::Accessor::LabelToName(uint64_t label) {
+  return storage_->name_id_mapper_.IdToName(label);
+}
+const std::string &Storage::Accessor::PropertyToName(uint64_t property) {
+  return storage_->name_id_mapper_.IdToName(property);
+}
+const std::string &Storage::Accessor::EdgeTypeToName(uint64_t edge_type) {
+  return storage_->name_id_mapper_.IdToName(edge_type);
+}
+
+uint64_t Storage::Accessor::NameToLabel(const std::string &name) {
+  return storage_->name_id_mapper_.NameToId(name);
+}
+uint64_t Storage::Accessor::NameToProperty(const std::string &name) {
+  return storage_->name_id_mapper_.NameToId(name);
+}
+uint64_t Storage::Accessor::NameToEdgeType(const std::string &name) {
+  return storage_->name_id_mapper_.NameToId(name);
+}
+
 void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; }
 
 void Storage::Accessor::Commit() {
diff --git a/src/storage/v2/storage.hpp b/src/storage/v2/storage.hpp
index fe5207018..37e2d4a80 100644
--- a/src/storage/v2/storage.hpp
+++ b/src/storage/v2/storage.hpp
@@ -6,6 +6,7 @@
 #include "storage/v2/edge.hpp"
 #include "storage/v2/edge_accessor.hpp"
 #include "storage/v2/mvcc.hpp"
+#include "storage/v2/name_id_mapper.hpp"
 #include "storage/v2/result.hpp"
 #include "storage/v2/transaction.hpp"
 #include "storage/v2/vertex.hpp"
@@ -79,6 +80,14 @@ class Storage final {
 
     Result<bool> DeleteEdge(EdgeAccessor *edge);
 
+    const std::string &LabelToName(uint64_t label);
+    const std::string &PropertyToName(uint64_t property);
+    const std::string &EdgeTypeToName(uint64_t edge_type);
+
+    uint64_t NameToLabel(const std::string &name);
+    uint64_t NameToProperty(const std::string &name);
+    uint64_t NameToEdgeType(const std::string &name);
+
     void AdvanceCommand();
 
     void Commit();
@@ -113,6 +122,8 @@ class Storage final {
   // whatever.
   CommitLog commit_log_;
 
+  NameIdMapper name_id_mapper_;
+
   utils::SpinLock committed_transactions_lock_;
   std::list<Transaction> committed_transactions_;
 
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 4d04f2f2c..34916a69f 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -316,6 +316,9 @@ target_link_libraries(${test_prefix}storage_v2 mg-storage-v2)
 add_unit_test(storage_v2_gc.cpp)
 target_link_libraries(${test_prefix}storage_v2_gc mg-storage-v2)
 
+add_unit_test(storage_v2_name_id_mapper.cpp)
+target_link_libraries(${test_prefix}storage_v2_name_id_mapper mg-storage-v2)
+
 # Test LCP
 
 add_custom_command(
diff --git a/tests/unit/storage_v2_name_id_mapper.cpp b/tests/unit/storage_v2_name_id_mapper.cpp
new file mode 100644
index 000000000..e2353db07
--- /dev/null
+++ b/tests/unit/storage_v2_name_id_mapper.cpp
@@ -0,0 +1,36 @@
+#include <gtest/gtest.h>
+
+#include "storage/v2/name_id_mapper.hpp"
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(NameIdMapper, Basic) {
+  storage::NameIdMapper mapper;
+
+  ASSERT_EQ(mapper.NameToId("n1"), 0);
+  ASSERT_EQ(mapper.NameToId("n2"), 1);
+  ASSERT_EQ(mapper.NameToId("n1"), 0);
+  ASSERT_EQ(mapper.NameToId("n2"), 1);
+  ASSERT_EQ(mapper.NameToId("n3"), 2);
+
+  ASSERT_EQ(mapper.IdToName(0), "n1");
+  ASSERT_EQ(mapper.IdToName(1), "n2");
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(NameIdMapper, Correctness) {
+  storage::NameIdMapper mapper;
+
+  ASSERT_DEATH(mapper.IdToName(0), "");
+  ASSERT_EQ(mapper.NameToId("n1"), 0);
+  ASSERT_EQ(mapper.IdToName(0), "n1");
+
+  ASSERT_DEATH(mapper.IdToName(1), "");
+  ASSERT_EQ(mapper.NameToId("n2"), 1);
+  ASSERT_EQ(mapper.IdToName(1), "n2");
+
+  ASSERT_EQ(mapper.NameToId("n1"), 0);
+  ASSERT_EQ(mapper.NameToId("n2"), 1);
+
+  ASSERT_EQ(mapper.IdToName(1), "n2");
+  ASSERT_EQ(mapper.IdToName(0), "n1");
+}